SlaneYang

SlaneYang 查看完整档案

重庆编辑重庆邮电大学  |  计算机科学与技术学院 编辑MY  |  前端工程师 编辑 www.slane.cn 编辑
编辑

神秘的前端工程师
微博: @SlaneYang
个人网站: www.slane.cn

个人动态

SlaneYang 赞了回答 · 2020-08-10

解决CSS3 Transform 引起的 z-index "失效"

这里涉及到一个stacking context(有人翻译为层叠上下文)的概念。

给元素设置transform属性会创建一个新的stacking context

请看下面的具体讲解:

注:以下两个例子最好先想象一下预览效果,再查看结果预览页面。

先上一个小例子:

http://jsfiddle.net/2tss9rp3/embedded/html,css,result

上面这个例子中,两个div都没有设置任何position,如果没有给test-1添加transform属性的话,第二个div将会覆盖第一个div。但是如果设置了transform的话呢?由于transform会创建一个新的stacking context。在层级关系上就要比test-2高一级,因此,显示在上面。

再来一个例子,

http://jsfiddle.net/2tss9rp3/1/embedded/html,css,result

这个例子了是对上面那个例子作了个简单的修改。我们给test-1添加了一个position: relativetest-2没有任何position属性,只是添加了一个transform的属性。如果不看预览页面的话,可能会以为test-1会显示在test-2上方,其实不然。由于transform会创建新的stacking context,同时test-2在文档中又处于test-1的后面,所以最终的效果是test-2显示在test-1的上方。

那么问题来了,哪些情况下会创建新的stacking context呢?

MDN上有相关的介绍:

  • the root element (HTML),
  • positioned (absolutely or relatively) with a z-index value other than "auto",
  • a flex item with a z-index value other than "auto",
  • elements with an opacity value less than 1,
  • elements with a transform value other than "none",
  • elements with a mix-blend-mode value other than "normal",
  • elements with isolation set to "isolate", on mobile WebKit and Chrome 22+, position: fixed always creates a new stacking context, even when z-index is "auto",
  • specifing any attribute above in will-change even you don't write themselves directly

其中,第二条是我们平时最常见的,另外几条加粗的会随着CSS3的普及越来越常见。令我感到惊奇是,opacity竟然也会创建新的stacking context,你可以试着将上面两个例子中的transform换成opacity,会得到同样的效果。

值得注意的是,介绍stacking context的文章显然不像介绍CSS中另外一个“上下文”——Block formatting context(块级格式上下文)的文章多,原因可能是,我们在平常很少遇到stacking context相关的问题,但是随着CSS3的普及,这方面的问题可能会多起来的。

这也算是CSS中一个比较有趣而且有用的知识点,之前在工作中遇到过一次,正好此处有人问到,特整理了一下,供参考。


补充

说了这么多,回到你这个具体的问题上来,由于你没有提供具体的代码,不好说出具体的问题所在,但可以推测出你的代码中可能有类似下面这个例子中的结构。你给.child设置再大的z-index都没有用。但是如果将.innertransform去掉就不一样了。试试看。

http://jsfiddle.net/2tss9rp3/4/embedded/html,css,result

参考链接

关注 22 回答 5

SlaneYang 赞了回答 · 2019-11-13

解决img标签插入图片返回403,浏览器可以直接打开

最后在html中加了 <meta name="referrer" content="no-referrer" /> 解决了

关注 3 回答 3

SlaneYang 赞了文章 · 2019-10-29

你想知道的关于 Refs 的知识都在这了

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

Refs 使用场景

在某些情况下,我们需要在典型数据流之外强制修改子组件,被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素,例如:

  • 管理焦点,文本选择或媒体播放。
  • 触发强制动画。
  • 集成第三方 DOM 库。

设置 Refs

1. createRef

支持在函数组件和类组件内部使用

createRef 是 React16.3 版本中引入的。

创建 Refs

使用 React.createRef() 创建 Refs,并通过 ref 属性附加至 React 元素上。通常在构造函数中,将 Refs 分配给实例属性,以便在整个组件中引用。

访问 Refs

ref 被传递给 render 中的元素时,对该节点的引用可以在 refcurrent 属性中访问。

import React from 'react';
export default class MyInput extends React.Component {
    constructor(props) {
        super(props);
        //分配给实例属性
        this.inputRef = React.createRef(null);
    }

    componentDidMount() {
        //通过 this.inputRef.current 获取对该节点的引用
        this.inputRef && this.inputRef.current.focus();
    }

    render() {
        //把 <input> ref 关联到构造函数中创建的 `inputRef` 上
        return (
            <input type="text" ref={this.inputRef}/>
        )
    }
}

ref 的值根据节点的类型而有所不同:

  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
  • ref 属性用于自定义的 class 组件时, ref 对象接收组件的挂载实例作为其 current 属性。
  • 不能在函数组件上使用 ref 属性,因为函数组件没有实例。

总结:为 DOM 添加 ref,那么我们就可以通过 ref 获取到对该DOM节点的引用。而给React组件添加 ref,那么我们可以通过 ref 获取到该组件的实例【不能在函数组件上使用 ref 属性,因为函数组件没有实例】。

2. useRef

仅限于在函数组件内使用

useRef 是 React16.8 中引入的,只能在函数组件中使用。

创建 Refs

使用 React.useRef() 创建 Refs,并通过 ref 属性附加至 React 元素上。

const refContainer = useRef(initialValue);

useRef 返回的 ref 对象在组件的整个生命周期内保持不变

访问 Refs

ref 被传递给 React 元素时,对该节点的引用可以在 refcurrent 属性中访问。

import React from 'react';

export default function MyInput(props) {
    const inputRef = React.useRef(null);
    React.useEffect(() => {
        inputRef.current.focus();
    });
    return (
        <input type="text" ref={inputRef} />
    )
}

关于 React.useRef() 返回的 ref 对象在组件的整个生命周期内保持不变,我们来和 React.createRef() 来做一个对比,代码如下:

import React, { useRef, useEffect, createRef, useState } from 'react';
function MyInput() {
    let [count, setCount] = useState(0);

    const myRef = createRef(null);
    const inputRef = useRef(null);
    //仅执行一次
    useEffect(() => {
        inputRef.current.focus();
        window.myRef = myRef;
        window.inputRef = inputRef;
    }, []);
    
    useEffect(() => {
        //除了第一次为true, 其它每次都是 false 【createRef】
        console.log('myRef === window.myRef', myRef === window.myRef);
        //始终为true 【useRef】
        console.log('inputRef === window.inputRef', inputRef === window.inputRef);
    })
    return (
        <>
            <input type="text" ref={inputRef}/>
            <button onClick={() => setCount(count+1)}>{count}</button>
        </>
    )
}

3. 回调 Refs

支持在函数组件和类组件内部使用

React 支持 回调 refs 的方式设置 Refs。这种方式可以帮助我们更精细的控制何时 Refs 被设置和解除。

使用 回调 refs 需要将回调函数传递给 React元素ref 属性。这个函数接受 React 组件实例 或 HTML DOM 元素作为参数,将其挂载到实例属性上,如下所示:

import React from 'react';

export default class MyInput extends React.Component {
    constructor(props) {
        super(props);
        this.inputRef = null;
        this.setTextInputRef = (ele) => {
            this.inputRef = ele;
        }
    }

    componentDidMount() {
        this.inputRef && this.inputRef.focus();
    }
    render() {
        return (
            <input type="text" ref={this.setTextInputRef}/>
        )
    }
}

React 会在组件挂载时,调用 ref 回调函数并传入 DOM元素(或React实例),当卸载时调用它并传入 null。在 componentDidMountcomponentDidUpdate 触发前,React 会保证 Refs 一定是最新的。

可以在组件间传递回调形式的 refs.
import React from 'react';

export default function Form() {
    let ref = null;
    React.useEffect(() => {
        //ref 即是 MyInput 中的 input 节点
        ref.focus();
    }, [ref]);

    return (
        <>
            <MyInput inputRef={ele => ref = ele} />
            {/** other code */}

        </>
    )
}

function MyInput (props) {
    return (
        <input type="text" ref={props.inputRef}/>
    )
}

4. 字符串 Refs(过时API)

函数组件内部不支持使用 字符串 refs [支持 createRef | useRef | 回调 Ref]
function MyInput() {
    return (
        <>
            <input type='text' ref={'inputRef'} />
        </>
    )
}

16dfbb1de78766f8.jpg

类组件

通过 this.refs.XXX 获取 React 元素。

class MyInput extends React.Component {
    componentDidMount() {
        this.refs.inputRef.focus();
    }
    render() {
        return (
            <input type='text' ref={'inputRef'} />
        )
    }
}

Ref 传递

在 Hook 之前,高阶组件(HOC) 和 render props 是 React 中复用组件逻辑的主要手段。

尽管高阶组件的约定是将所有的 props 传递给被包装组件,但是 refs 是不会被传递的,事实上, ref 并不是一个 prop,和 key 一样,它由 React 专门处理。

这个问题可以通过 React.forwardRef (React 16.3中新增)来解决。在 React.forwardRef 之前,这个问题,我们可以通过给容器组件添加 forwardedRef (prop的名字自行确定,不过不能是 ref 或者是 key).

React.forwardRef 之前
import React from 'react';
import hoistNonReactStatic from 'hoist-non-react-statics';

const withData = (WrappedComponent) => {
    class ProxyComponent extends React.Component {
        componentDidMount() {
            //code
        }
        //这里有个注意点就是使用时,我们需要知道这个组件是被包装之后的组件
        //将ref值传递给 forwardedRef 的 prop
        render() {
            const {forwardedRef, ...remainingProps} = this.props;
            return (
                <WrappedComponent ref={forwardedRef} {...remainingProps}/>
            )
        }
    }
    //指定 displayName.   未复制静态方法(重点不是为了讲 HOC)
    ProxyComponent.displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
    //复制非 React 静态方法
    hoistNonReactStatic(ProxyComponent, WrappedComponent);
    return ProxyComponent;
}

这个示例中,我们将 ref 的属性值通过 forwardedRefprop,传递给被包装的组件,使用:

class MyInput extends React.Component {
    render() {
        return (
            <input type="text" {...this.props} />
        )
    }
}

MyInput = withData(MyInput);
function Form(props) {
    const inputRef = React.useRef(null);
    React.useEffect(() => {
        console.log(inputRef.current)
    })
    //我们在使用 MyInput 时,需要区分其是否是包装过的组件,以确定是指定 ref 还是 forwardedRef
    return (
        <MyInput forwardedRef={inputRef} />
    )
}
React.forwardRef

Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧,其允许某些组件接收 ref,并将其向下传递给子组件。

转发 ref 到DOM中:

import React from 'react';

const MyInput = React.forwardRef((props, ref) => {
    return (
        <input type="text" ref={ref} {...props} />
    )
});
function Form() {
    const inputRef = React.useRef(null);
    React.useEffect(() => {
        console.log(inputRef.current);//input节点
    })
    return (
        <MyInput ref={inputRef} />
    )
}
  1. 调用 React.useRef 创建了一个 React ref 并将其赋值给 ref 变量。
  2. 指定 ref 为JSX属性,并向下传递 <MyInput ref={inputRef}>
  3. React 传递 refforwardRef 内函数 (props, ref) => ... 作为其第二个参数。
  4. 向下转发该 ref 参数到 <button ref={ref}>,将其指定为JSX属性
  5. ref 挂载完成,inputRef.current 指向 input DOM节点

注意

第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref

React.forwardRef 之前,我们如果想传递 ref 属性给子组件,需要区分出是否是被HOC包装之后的组件,对使用来说,造成了一定的不便。我们来使用 React.forwardRef 重构。

import React from 'react';
import hoistNonReactStatic from 'hoist-non-react-statics';

function withData(WrappedComponent) {
    class ProxyComponent extends React.Component {
        componentDidMount() {
            //code
        }
        render() {
            const {forwardedRef, ...remainingProps} = this.props;
            return (
                <WrappedComponent ref={forwardedRef} {...remainingProps}/>
            )
        }
    }
    
    //我们在使用被withData包装过的组件时,只需要传 ref 即可
    const forwardRef = React.forwardRef((props, ref) => (
        <ProxyComponent {...props} forwardedRef={ref} />
    ));
    //指定 displayName.
    forwardRef.displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
    return hoistNonReactStatic(forwardRef, WrappedComponent);
}
class MyInput extends React.Component {
    render() {
        return (
            <input type="text" {...this.props} />
        )
    }
}
MyInput.getName = function() {
    console.log('name');
}
MyInput = withData(MyInput);
console.log(MyInput.getName); //测试静态方法拷贝是否正常


function Form(props) {
    const inputRef = React.useRef(null);
    React.useEffect(() => {
        console.log(inputRef.current);//被包装组件MyInput
    })
    //在使用时,传递 ref 即可
    return (
        <MyInput ref={inputRef} />
    )
}

react-redux 中获取子组件(被包装的木偶组件)的实例

旧版本中(V4 / V5)

我们知道,connect 有四个参数,如果我们想要在父组件中子组件(木偶组件)的实例,那么需要设置第四个参数 optionswithReftrue。随后可以在父组件中通过容器组件实例的 getWrappedInstance() 方法获取到木偶组件(被包装的组件)的实例,如下所示:

//MyInput.js
import React from 'react';
import { connect } from 'react-redux';

class MyInput extends React.Component {
    render() {
        return (
            <input type="text" />
        )
    }
}
export default connect(null, null, null, { withRef: true })(MyInput);
//index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import MyInput from './MyInput';

function reducer(state, action) {
    return state;
}
const store = createStore(reducer);

function Main() {
    let ref = React.createRef();
    React.useEffect(() => {
        console.log(ref.current.getWrappedInstance());
    })
    return (
        <Provider store={store}>
            <MyInput ref={ref} />
        </Provider>
    )
}

ReactDOM.render(<Main />, document.getElementById("root"));

这里需要注意的是:MyInput 必须是类组件,而函数组件没有实例,自然也无法通过 ref 获取其实例。react-redux 源码中,通过给被包装组件增加 ref 属性,getWrappedInstance 返回的是该实例 this.refs.wrappedInstance

if (withRef) {
    this.renderedElement = createElement(WrappedComponent, {
        ...this.mergedProps,
        ref: 'wrappedInstance'
    })
}
新版本(V6 / V7)

react-redux新版本中使用了 React.forwardRef方法进行了 ref 转发。 自 V6 版本起,option 中的 withRef 已废弃,如果想要获取被包装组件的实例,那么需要指定 connect 的第四个参数 optionforwardReftrue,具体可见下面的示例:

//MyInput.js 文件
import React from 'react';
import { connect } from 'react-redux';

class MyInput extends React.Component {
    render() {
        return (
            <input type="text" />
        )
    }
}
export default connect(null, null, null, { forwardRef: true })(MyInput);

直接给被包装过的组件增加 ref,即可以获取到被包装组件的实例,如下所示:

//index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import MyInput from './MyInput';

function reducer(state, action) {
    return state;
}
const store = createStore(reducer);

function Main() {
    let ref = React.createRef();
    React.useEffect(() => {
        console.log(ref.current);
    })
    return (
        <Provider store={store}>
            <MyInput ref={ref} />
        </Provider>
    )
}


ReactDOM.render(<Main />, document.getElementById("root"));

同样,MyInput 必须是类组件,因为函数组件没有实例,自然也无法通过 ref 获取其实例。

react-redux 中将 ref 转发至 Connect 组件中。通过 forwardedRef 传递给被包装组件 WrappedComponentref

if (forwardRef) {
    const forwarded = React.forwardRef(function forwardConnectRef(
        props,
        ref
    ) {
        return <Connect {...props} forwardedRef={ref} />
    })

    forwarded.displayName = displayName
    forwarded.WrappedComponent = WrappedComponent
    return hoistStatics(forwarded, WrappedComponent)
}

//...
const { forwardedRef, ...wrapperProps } = props
const renderedWrappedComponent = useMemo(
    () => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
    [forwardedRef, WrappedComponent, actualChildProps]
)
参考链接:
查看原文

赞 25 收藏 17 评论 2

SlaneYang 赞了文章 · 2019-09-09

毕业3年,3份工作,3个国家,翻译运营市场到程序媛,我都经历了些什么

一个没有大英雄的故事

看多了牛逼轰轰的英雄贴,我来给大家添加点轻松的转码经历和两次被踢事件。预告,转码经历没有很苦逼。

自我介绍

现状:

目前在美国某二三线城市当地一大厂工作,初级程序媛,刚转码7个月,主要写前端,刚被拖过来临时写测试。事实上Segmentfault见证了我转码的整个阶段。一开始是用segmentfault做笔记,现在改用gitbook了。但是会经常上来刷文章。

转码经历

16年韩国某研究室毕业,专业是计算机学部下面的通信相关的研究室。所以跟计算机没有什么太大的关系,程序也就是会用个c语言的最基础的语法而已。毕业之后就知道自己绝对不会做本专业的事情,因为搞研究真的很枯燥~~
刚毕业的时候,拿到的offer都是看中我的语言能力,韩语高级英语交流也没有什么问题。毕业拿的offer有10k+的,但最后选择了北京的一家初创公司,当时就觉得面试的人看着面善就去了,完全没有职场规划。
到公司之后,由于大家都是年轻人,非常容易打成一片,慢慢得就把公司各个部门的职责职能了解得差不多了。我当时只知道自己不想做翻译,但是具体该干什么还是不是很清楚。于是我就准备了全部都试一试。在北京的一年,报班学习了市场营销(报的一个印度网校便宜非常多。。。),又自己开始学习最基础的web开发。
17年开始把自己的简历挂在韩国的招聘网站上求职,刚好有一家初创公司想招一枚市场人员顺带能帮忙做简单的美工,我就被招过去了。 在韩国的2年,手上偶尔会有一些需要写写网页的活儿,每天坚持每天学,自己做一些项目,渐渐地就积累下来了。
工作学习之余,我坚持每周出去旅游一天或者是约朋友,扩大自己的社交圈。18年结婚跟老公回了美国。
整个转码过程没有很苦逼,每天工作之余,坚持学习1-2小时,钱也赚了,恋爱也谈了,想出去看看,也出去看看了。转码准备期间,每周还都会出去短途旅游,家里还养了一只猫。转码成功的原因只有一个,坚持。做好计划,然后就不想了,按照计划每天坚持,2年下来就攒够了做为一个初级程序员的资本。 😄

转码期间最纠结的点

是该放下手上的工作,报个班或者是专注地学习,还是边工作边学习。这个就仁者见仁智者见智了,专注地学习肯定进度会快很多。以下是我做决定的心路历程:
图片描述
很多时候我考虑问题会因为某个点而钻牛角尖,大家经常说‘综合考虑’下决定。具体怎么个综合考虑呢,人家不肯说!我很多时候下决定就因为某个点纠结很久,最后以什么时候懒得纠结了随便就着某个点而匆忙地下了决定。所以,我后来遇到重大决定就把所有的点全部写下来形成面,一个一个考虑,然后下完决定就不再纠结了。因为这已经是当下最好的决定。

做程序员理由

爱好,只有搬砖的时候不打瞌睡,也最放松。做市场的时候,特别是小公司的市场,考虑的事情很多,盈利,流量,客户满意度,老板心情等等,但是实际上的实际产出远远比不上自己的野心,真的是得靠天时地利人和。而做技术就单纯太多了,付出了多少就会回馈多少。

聊聊做为程序员两次被踢的事儿

大中秋的,聊聊我的桑心往事,让大家乐呵乐呵。
我家还有个理念,在最好的日子整理一下不好的时光。一是笑看往事,二是助力奋斗未来。

第一次拿下项目并第一次被踢

由于我的美签面试地点选在了韩国,我在回国之后又专门回了一趟韩国呆了一个月等面试。期间的两周我找了一份在一个韩国小外包公司做网页开发,其实就是做几个静态页面。老板说要为一个游泳场做一个页面放到嵌入到本地旅游官网上,但是内容,设计,什么都没有给。我每天揪着老板问他到底要什么样的,他也说不上来。做了好几版,老板都不满意。后来过了很久突然想起来返回去看他们的成品,才发现他们用了官网的所有的设计代码代码,只是改了文案。。。。当初他们开始做的时候自己就根本不知道自己要什么,他们只是想有个人组织好文案,放到网页里。
如果有人跟我一样在韩国接这种外包的小项目,千万不要把他们的需求往高大上的方向想。我当时面试项目的时候,面了好几个,很多时候就是让你去打打杂的,具体杂物是什么得先搞清楚。
但素,他们给的时薪还都挺好的,虽然当时我的版本没有被采纳,时薪还是照样给了的,这点还是比较好。我记得是200软妹币左右一小时。

第二次被踢

在美国等上班期间,我有一个月比较空闲,就想着试试水看看跟国内的技术人员们合作一下是什么结果。组织的是一个知乎上技术圈里蛮有名气的年轻同学,能力非常的出众。当时他找一个人写前端VUE组件,我当时也刚学了VUE,想试试手,就互加了微信一起合作写组件。大概做了1周,互相都发现不合适。技术太菜再次被踢,当然没有给钱。。。
回头我就做了一件事情,拉黑他微信。😄,我相信他是大佬,也非常珍惜和他的合作机会,也希望以后能达到他要求,有机会可以和他合作。做出拉黑微信这个动作,我朋友们都很惊讶,说不至于拉黑吧,我不像随便拉黑别人的人啊,况且都是一个圈子的抬头不见低头见的。来再一次分析一下我为什么要这么做

图片描述

作为一个搞技术的优秀人才,是怎样提高自己的战斗力的呢?

首先我并不是一个‘优秀的技术人才‘,但综合实力来看我可能也许大概也算一个’搞技术的优秀人才‘,吧!

技术外技能值:韩语,英语,重庆话,普通话 4语,运营,市场,美工,翻译等多种职能自由切换。
软实力:学历够用,人缘好,实践能力强,敢想敢做,抗压能力强,皮实。

关于怎样提高自己的战斗力,我给以下几点建议:

1) 学会管理自己的心情,特别是在国内激进的氛围下。认清自己的价值,不要把别人对你的评价当作风向标,要以"自我为中心",活着自己满意的样子。
失去自我了,还谈什么战斗力呢
2) 坚持学习
看清自己的奋斗方向,做好规划,每天按部就班地走一定会达到目标的。毕竟你现在从小学开始学起,也会大学毕业的。
如果有人跟我一样多方面发展,时间管理加坚持特别重要,不然一天天就忙活叨叨的。
3)多在社区活动
这也是现在我正在做的事情,美国的线下线上技术活动我有机会都会去参加,看到很多前沿的技术思潮。最近看到的一个是谷歌的Bundle On Edge的,简要的就是他们在考虑实现前端网页上打包。
4)提前做好人生规划
如果你的人生规划里面有成家有孩子,那么多多物色一个理解你工作的人绝对是提高战斗力的重要因素。试想人到中年,职场冲刺阶段,有理解你支持你的家人胜过n点技能值。
刚毕业的同学会以为三十特别遥远,我曾经也是那么以为的。可是研究生一毕业三四年嗖一下就没了。眼看我也奔三了。-_-/// 想起初中同学当时说三十好老,她活到三十岁就自杀永葆青春。。。

送给即将成为程序员的经验

1)先关注自己,考虑清楚自己喜欢做什么。如果正处于迷茫期,那就努力做好眼前正在做的,并学习看起来有意思的,积累多了就不迷茫了。
2)弄清职场文化,越早看清越早适应。比如上司欣赏哪一类型的员工,合作的同事的处事方式等。
3) 多看别人的成果,少看别人的脸色。很多的答案,在别人的代码里面就会找到,比如团队的编程风格,习惯等。别人对你的批评,自己心里先过滤一下,吸收对你有利的部分,客观的评价接受,不客观的左耳进右耳出。
4)职场别抱怨,更别跟同事抱怨
抱怨花精力花时间,很多的抱怨有可能当时是觉得有个出口释放了,但后续的麻烦事情通常会更多,其中一个就是负面情绪持续积累。跟同事抱怨公司更是不可取,如果你不打算明天就走人的话。

如果还有一次选择职业的机会,你还会选程序员吗?为什么?

会啊!

为什么?

我已经选择了三次了,想尝试的事情通通都尝试了一遍,最终选择成为一名程序媛。每天上班都激情满满,下班有温馨的家庭,生活正是我想要的样子。

结语

本来是想打算分享一下,幸福的事儿。但是仔细想了想,决定把哪些经历过的被踢事件拿出来说说,因为毕竟这事儿一般人不好说出口,但!我是一般人么!不比别的,脸皮可着实比一般人厚几倍。


本文参与了 SegmentFault思否征文「一起分享你的故事」,欢迎正在阅读的你也加入,一起分享。

查看原文

赞 20 收藏 3 评论 14

SlaneYang 赞了文章 · 2019-05-30

一篇 NPM 常见问题小记

常见问题

1.npm 的package.json中的~和^

  • ~会匹配最近的小版本依赖包,比如~1.2.3会匹配所有1.2.x版本,但是不包括1.3.0
  • ^会匹配最新的大版本依赖包,比如^1.2.3会匹配所有1.x.x的包,包括1.3.0,但是不包括2.0.0

你也有可能会看见在 package.json 中模块的版本号前面既没有 ~ 也没有 ^ 就像下面那样:

"moment": "2.4.0"

上面的情况属于精确安装模块指定的版本号。通过命令参数-E,或者 --save-exact 来指定版本号安装的。上述模块对应的精确安装命令:npm install --save-exact moment@2.4.0

2.npm outdate与第三方工具npm-check

npm outdate此命令会列出项目中所有已经过时的包,像下面一样:

推荐使用检查依赖包更强大的一个工具 npm-check,更强大分析包的能力以及可以通过加上参数提供交互式更新方式,详情请参考其文档说明。安装完成后npm-check检查项目依赖包,展示像下面一样:

3.dependencies or devDependencies or optionalDependencies

dependencies(生产环境的依赖的包目录)devDependencies(开发环境的依赖包目录)

在项目的 package.json 使用命令 npm install --save moduel安装的模块会注册到 dependencies 目录中去(npm 5 开始 通过npm install不加--save 和npm install --save一样 都是局部安装并会把模块自动写入package.json中的dependencies里。)

npm install --dev module 并会安装模块并自动写入package.json中的 devDependencies 里。

当你 clone 下来了一个新项目在项目根目录下执行npm install的时候是会同时安装 dependencies 和 devDependencies中的所有依赖。当你的项目需要在生产环境中只需要安装 dependencies 中的依赖时,执行的是npm install --production命令。(如果配置了NODE_ENV环境变量为production,那么执行npm install就只安装dependencies里面的包。安装完后可以用npm ls查看安装的包的情况。)

With the --production flag (or when the NODE_ENV environment variable is set to production), npm will not install modules listed in devDependencies.

此外,你还有可能看到形如:

"optionalDependencies": {
    "gulp": "^3.9.1"
  }

optionalDependencies 是你在使用npm install npm install --save-optional gulp是所注册在可选依赖里的模块包,在项目执行npm install --no-optional就可以跳过可选包安装。

Tips : 在存在 package-lock.json 时npm install --no-optional你可能会发现依旧装上了可选模块,(它的issue页讨论),此时你可能还要加上另外一个参数执行npm install --no-optional --no-package-lock才能如愿。

4.npm config

查看和管理npm的基础配置。npm config ls -l

可以通过npm config get proxy查看你是否设置了npm 的代理。
npm config set registry https://registry.npm.taobao.org 改成更快的淘宝源,解决出现安装不了模块或者速度慢的问题。

5.npm cache

当你使用命令 npm config get cache命令时 你会得到你的本地 npm 缓存的完整路径,npm 缓存是什么呢,可以先从npm install的执行过程说起(此部分参考阮一峰老师的博客):

  1. 发出npm install命令
  2. npm 向 registry 查询模块压缩包的网址
  3. 下载压缩包,存放在~/.npm(本地NPM缓存路径)目录
  4. 解压压缩包到当前项目的node_modules目录

实际上说一个模块安装以后,本地其实保存了两份。一份是~/.npm目录下的压缩包,另一份是node_modules目录下解压后的代码。但是,运行npm install的时候,只会检查 node_modules 目录,而不会检查~/.npm目录。如果一个模块在~/.npm下有压缩包,但是没有安装在node_modules目录中,npm 依然会从远程仓库下载一次新的压缩包。

阮老师在当时文中提到的离线安装时所说的有很多弊端的npm install --cache-min命令已经在npm 5.0.0开始被 deprecated 了

--cache-min and --cache-max have been deprecated. (#15666)

我们想利用已经在缓存中之前已经备份的模块实现离线模块安装的的 cache 机制已经在V5的时候重写了,缓存将由 npm 来全局维护不再需要开发人员操心,离线安装时将不再尝试连接网络,而是降级尝试从缓存中读取,或直接失败。就是如果你 offline ,npm将无缝地使用您的缓存。以下新增参数命令翻译自npm v5.0.0的release

  • 一个新的--prefer-offline选项将使npm跳过任何有条件的请求(304检查)过时的缓存数据,并且只有在缓存中丢失了某些内容时才能访问网络
  • 一个新的--prefer-online选项,它将强制 npm 重新验证缓存数据(使用304次检查),忽略任何过时检查,并用重新验证的新数据刷新缓存。
  • 一个新的 --offline 选项将强制npm使用缓存或退出。如果试图安装的任何内容尚未存在于缓存中,它将报 ENOTCACHED错误。
  • 一个新的npm cache verify命令,它将对你的缓存进行辣鸡回收,减少不需要的东西占据的磁盘使用量,并且会对索引和内容进行全面的完整性验证。

6.package-lock.json 问题

项目中如果是用 npm V5 以上版本就会有这么一个详细记录安装模块的细节的文件。确定当前安装的包的依赖,以便后续重新安装的时候生成相同的依赖,而忽略项目开发过程中有些依赖已经发生的更新。

当你想通过搜索引擎了解package-lock.json 问题的时候,你可能会被“坑”字吸引过去,你可能会看到过“如果手动修改了 package.json 文件中已有模块的版本,直接执行npm install不会安装新指定的版本,只能通过npm install xxx@yy更新” ,不过这是 V5.0.0 的问题,现在V5.4.2版本后如果改了package.json,且package.json和lock文件不同,那么执行npm i时npm会根据package中的版本号以及语义含义去下载最新的包,并更新至lock。详见这个知乎回答

其余常见命令

  • npm ls 查看安装的模块
  • npm uninstall 卸载模块
  • npm update 更新模块
  • npm help 查看某条命令的详细帮助
  • npm version 查看模块版本
  • npm view 查看模块的注册信息
  • npm publish 发布模块
  • npm adduser 用户登录
  • npm init 初始化项目,并在项目文件夹中引导创建一个package.json文件
  • npm root 查看包的安装路径
查看原文

赞 6 收藏 9 评论 0

SlaneYang 赞了文章 · 2019-04-19

浏览器往返缓存(Back/Forward cache)问题的分析与解决

博客源地址:https://github.com/LeuisKen/l...
相关讨论还请到源 issue 下。

什么是往返缓存(Back/Forward cache)

往返缓存(Back/Forward cache,下文中简称bfcache)是浏览器为了在用户页面间执行前进后退操作时拥有更加流畅体验的一种策略。该策略具体表现为,当用户前往新页面时,将当前页面的浏览器DOM状态保存到bfcache中;当用户点击后退按钮的时候,将页面直接从bfcache中加载,节省了网络请求的时间。

但是bfcache的引入,导致了很多问题。下面,举一个我们遇到的场景:

sample

页面A是一个任务列表,用户从A页面选择了“任务1:看新闻”,点击“去完成”跳转到B页面。当用户进入B页面后,任务完成。此时用户点击回退按钮,会回退到A页面。此时的A页面“任务1:看新闻”的按钮,应该需要标记为“已完成”,由于bfcache的存在,当存入bfcache时,“任务1”的按钮是“去完成”,所以此时回来,按钮也是“去完成”,而不会标记为“已完成”。

既然bug产生了,我们该如何去解决它?很多文章都会提到unload事件,但是我们实际进行了测试发现并不好用。于是,为了解决问题,我们的bfcache探秘之旅开始了。

bfcache 探秘

在检索page cache in chromium的时候,我们发现了这个issue:https://bugs.chromium.org/p/c... 。里面提到 chromium(chrome的开源版本)在很久以前就已经将PageCache(即bfcache)这部分代码移除了。也就是说现在的chrome应该是没有这个东西的。可以确定的是,chrome以前的版本中,bfcache的实现是从webkit中拿来的,加上我们项目目前面向的用户主体就是 iOS + Android,iOS下是基于Webkit,Android基于chrome(且这部分功能也是源于webkit)。因此追溯这个问题,我们只要专注于研究webkitbfcache的逻辑即可。

同样通过上文中描述的commit记录,我们也很快定位到了PageCache相关逻辑在Webkit中的位置:webkit/Source/WebCore/history/PageCache.cpp

该文件中包含的两个方法引起了我们的注意:canCachePagecanCacheFrame。这里的Page即是我们通常理解中的“网页”,而我们也知道网页中可以嵌套<frame><iframe>等标签来置入其他页面。所以,PageFrame的概念就很明确了。而在canCachePage方法中,是调用了canCacheFrame的,如下:

// 给定 page 的 mainFrame 被传入了 canCacheFrame
bool isCacheable = canCacheFrame(page.mainFrame(), diagnosticLoggingClient, indentLevel + 1);

源代码链接:webkit/Source/WebCore/history/PageCache.cpp

因此,重头戏就在canCacheFrame了。

canCacheFrame方法返回的是一个布尔值,也就是其中变量isCacheable的值。那么,isCacheable的判断策略是什么?更重要的,这里面的策略,有哪些是我们能够利用到的。

注意到这里的代码:

Vector<ActiveDOMObject*> unsuspendableObjects;
if (frame.document() && !frame.document()->canSuspendActiveDOMObjectsForDocumentSuspension(&unsuspendableObjects)) {
    // do something...
    isCacheable = false;
}

源代码链接:webkit/Source/WebCore/history/PageCache.cpp

很明显canSuspendActiveDOMObjectsForDocumentSuspension是一个非常重要的方法,该方法中的重要信息见如下代码:

bool ScriptExecutionContext::canSuspendActiveDOMObjectsForDocumentSuspension(Vector<ActiveDOMObject*>* unsuspendableObjects)
{

    // something here...

    bool canSuspend = true;

    // something here...

    // We assume that m_activeDOMObjects will not change during iteration: canSuspend
    // functions should not add new active DOM objects, nor execute arbitrary JavaScript.
    // An ASSERT_WITH_SECURITY_IMPLICATION or RELEASE_ASSERT will fire if this happens, but it's important to code
    // canSuspend functions so it will not happen!
    ScriptDisallowedScope::InMainThread scriptDisallowedScope;
    for (auto* activeDOMObject : m_activeDOMObjects) {
        if (!activeDOMObject->canSuspendForDocumentSuspension()) {
            canSuspend = false;
            // someting here
        }
    }

    // something here...

    return canSuspend;
}

源代码链接:webkit/Source/WebCore/dom/ScriptExecutionContext.cpp

在这一部分,可以看到他调用每一个 ActiveDOMObjectcanSuspendForDocumentSuspension 方法,只要有一个返回了falsecanSuspend就会是false(Suspend这个单词是挂起的意思,也就是说存入bfcache对于浏览器来说就是把页面上的frame挂起了)。

接下来,关键的ActiveDOMObject定义在:webkit/Source/WebCore/dom/ActiveDOMObject.h ,该文件这部分注释,已经告诉了我们最想要的信息。

The canSuspendForDocumentSuspension() function is used by the caller if there is a choice between suspending and stopping. For example, a page won't be suspended and placed in the back/forward cache if it contains any objects that cannot be suspended.

canSuspendForDocumentSuspension 用于帮助函数调用者在“挂起(suspending)”与“停止”间做出选择。例如,一个页面如果包含任何不能被挂起的对象的话,那么它就不会被挂起并放到PageCache中。

接下来,我们要找的就是,哪些对象是不能被挂起的?在WebCore目录下,搜索包含canSuspendForDocumentSuspension() const关键字的.cpp文件,能找到48个结果。大概看了一下,最好用的objects that cannot be suspended应该就是Worker对象了,见代码:

bool Worker::canSuspendForDocumentSuspension() const
{
    // 这里其实是有一个 FIXME 的,看来 webkit 团队也觉得直接 return false 有点简单粗暴。
    // 不过还是等哪天他们真的修了再说吧
    // FIXME: It is not currently possible to suspend a worker, so pages with workers can not go into page cache.
    return false;
}

源代码链接:webkit/Source/WebCore/workers/Worker.cpp

解决方案

业务上添加如下代码:

// disable bfcache
try {
    var bfWorker = new Worker(window.URL.createObjectURL(new Blob(['1'])));
    window.addEventListener('unload', function () {
        // 这里绑个事件,构造一个闭包,以免 worker 被垃圾回收导致逻辑失效
        bfWorker.terminate();
    });
}
catch (e) {
    // if you want to do something here.
}

Thanks to

相关链接

查看原文

赞 13 收藏 11 评论 1

SlaneYang 赞了文章 · 2019-03-22

centos安装使用puppeteer和headless chrome

Google推出了无图形界面的headless Chrome之后,可以直接在远程服务器上直接跑一些测试脚本或者爬虫脚本了,猴开心!Google还附送了Puppeteer用于驱动没头的Chome。

阿里的Macaca也顺势写了Macaca-puppeteer,可以在Macaca上直接写通用的测试用例,在开发机上用图形界面看效果,上服务器走生产,岂不是美滋滋。

然鹅,可达鸭眉头一皱,发现事情并没有那么简单。

在阿里云的Centos 7.3上,安装puppeteer之后,会发现并不能启动官方的example:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

依赖安装

仔细看错误栈,核心的错误是如下一段:

...node_modules/puppeteer/.local-chromium/linux-496140/chrome-linux/chrome: error while loading shared libraries: libpangocairo-1.0.so.0: cannot open shared object file: No such file or directory

TROUBLESHOOTING: https://github.com/GoogleChro...

原来puppet虽然帮你下了一个Chromium,但并没有帮你把依赖都装好。于是你要自己把那些so都装好。

官方给的是Ubuntu版本的各个so包的apt-get安装方式,centos版本居然没有放!于是遍历了各个issue之后,终于发现还是有人给出了centos的库名,在一个看起来并不相关的issue里

#依赖库
yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y

#字体
yum install ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y

总算不用挨个去google了……

sandbox去沙箱

这时候你再去执行脚本,发现还是跑不起来。但是报错终于变了。这个时候变成了一个莫名其妙的错误:

(node:30559) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Failed to connect to chrome!
(node:30559) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

要疯掉了有没有,这啥玩意啊!!!!关键是这时候另外一个看起来是解决上面问题的issue,对这个错误进行了详细的讨论,然而直到今天(2017年9月27日)并没有讨论出什么结果。

网上很多讨论是说,直接调试那个Chrome。按照并不能解决问题的说法:直接去puppeteer的目录找到.local-chrome里面的Chromium执行文件,直接执行

./chrome -v --no-sandbox --disable-setuid-sandbox

(chrome:5333): Gtk-WARNING **: cannot open display: 

发现加上了--no-sanbox其实是能启动的,但是提示没有Gtk图形界面,那干脆加上--headless是不是就行了嘞?确实没有报错了。

回到puppeteer示例脚本,修改启动浏览器的代码,加上args:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

啊哈,终于执行成功了。下载下来了example.com的截图看了一眼,简直泪流满面。

回想一下,Puppet本身估计自带了--headless,所以如果直接去命令行执行chrome,还是要带上--headless。

终于搞定这一切发现Macaca顺便还提供了一个基于Ubuntu的Macaca-puppeteer的Docker,艾玛这方便太多了,早知道不自己折腾了。

查看原文

赞 43 收藏 29 评论 28

SlaneYang 关注了专栏 · 2018-11-15

我的前端探索

无量法门誓愿学

关注 54

SlaneYang 赞了文章 · 2018-11-15

【译】Node.js 中的队列

Node.js 中的队列

本文转载自:众成翻译
译者:文蔺
链接:http://www.zcfy.cc/article/662
原文:http://blog.yld.io/2016/05/10/introducing-queues/

Node.js 中的队列

这是深入探索 Node.js 中使用工作队列(work queues)管理异步工作流的系列文章的第一篇,来自the Node Patterns series

开始享受吧!


很常见的是,在应用程序流中,应用有着可以异步处理的工作负载。一个常见的例子是发送邮件。比方说,新用户注册时,可能需要给 Ta 发送一封确认邮件来确认用户刚刚输入的 email 地址是 Ta 自己的。这包括从模板中生成消息,向电子邮件服务提供商发送请求,解析结果,处理任何可能发送的最终错误,重试,等等…… 这个流程可能比较复杂,容易出错,或者在 HTTP 服务器的周期中花费太长时间。不过也有另外一个选择,可以向持久化存储中插入一个文档,该文档描述我们有一条待发送给这个用户的消息。另一个进程可能拿到这个文档,做一些比较重的工作:从模板生成消息,向服务器发送请求,解析错误,并在必要的情况下重排这个工作。

此外,系统需要和其他系统整合的情况也很常见。在我曾做过的一些项目中,需要不同的系统之间的进行用户配置文件的双向同步:当用户在一个系统中更新了个人资料,这些变化需要传递给其他系统,反之亦然。如果两个系统之间不需要很强的一致性,资料同步之间有一个小的延迟也许是可接受的,那这项工作就可以使用另一个进程异步处理。

更一般地说,在整个系统中有一个工作队列将工作生产者和消费者分开,这是一种常见的模式。生产者往工作队列中插入工作,消费者从队列中拿到工作并执行需要的任务。

工作队列

使用这样的拓扑结构有许多原因和优点,如:

  • 解耦工作生产者和消费者

  • 使重试逻辑更易于实现

  • 跨时间分配工作负载

  • 跨空间(nodes 节点)分配工作负载

  • 异步工作

  • 使外部系统更容易整合(最终的一致性)

让我们来分析一下其中的一些问题吧。

独立 (Isolate)

发送邮件是许多应用需要做的工作。一个例子是,用户修改了密码,一些应用很友好地发送邮件通知用户有人(最好不是其他人)修改了密码。现在发送邮件,通常是通过调用第三方邮件提供商提供的 HTTP API来完成的。如果服务缓慢或无法访问时候会怎样?你可不想就因为一封邮件发布出去就把密码给回滚了。当然,你也不想就因为在处理请求失败时碰到了工作中的一个非重要的部分,使得密码更改请求就这样崩掉了。密码修改后希望可以很快就发送出这封邮件,但不能有如此的代价。

重试 (Retry)

还有,修改密码意味着,你要为这个用户在两个系统中都做更改:一个中央用户数据库和一个遗留系统(legacy system)。(我知道这很恶心啊,不过我可不止见过一次 —— 现实就这么骨感。)假如第一个成功了、第二个失败了,咋办?

在这些情形下,你可以想一直重试直至成功:在遗留系统中更改密码是一个可以多次重复的结果相同的操作,而邮件也可以重复发送多次。

举例子,假如遗留系统修改密码了但未能成功返回通知,如果操作是幂等的,你可以稍后重试。

甚至,非幂等操作也可以从工作队列处理中尝到甜头。比如,你可以将一次货币交易插入到工作队列中 :给每次交易一个通用唯一标识符(UUID, universal unique identifier),稍后接收交易请求的系统可以保证不会发生重复交易。

在这个例子中,你基本只需要担心工作队列提供的必要的持久性保证:如果系统故障,你希望将交易丢失的风险降到最低。

分布及规模 (Distribute and scale)

另一个将工作生产者和消费者解耦的原因是,你可能想将工作集群规模化:如果任务消耗大量资源,如果任务是重 CPU 型的或者需要大量内存或操作系统资源,你可以将其与应用其他部分分离出来,放到工作队列中。

在任何应用中,一些操作比其他的要重。这可能会在整个节点引入有差异的工作负载:一个不幸的节点可能因处理太多的高并发业务而负担过重,而其它节点却被闲置。使用工作队列,将具体工作平均分配,可以将影响最小化。

工作队列的另一个效果是吸收工作峰(absorb work peaks):你可以为工作集群计划给定的最大容量,并确保容量永远不会超过。如果工作数量在短时间内急剧上升,工作队列完全可以解决,远离工作峰的压力。

系统监控在这里起到重要作用:你应当持续监控工作队列的长度,工作时间(完成一项任务的时间),工作占用,以及容量,以确定在高峰时间保证令人满意的操作时间需要的最佳、最小的资源。

防止崩溃 (Survive crashes)

如果你不需要以上任何一点东西,使用持久化工作队列的一个理由是防止崩溃。即使是同一个进程中的内存队列也能满足你的应用需求,持续的队列使你的应用在进程重启的时候更具弹性。

好了,理论讲得差不多了 —— 我们来看具体实现。

最简单的案例:内存工作队列(In-Memory Work Queue)

可以设计出的最简单的工作队列是一个内存队列。实现内存队列可能是个学校的练习(留给读者)。这里我们使用 Async 的 queue。

假设你在做的这个演示应用和一个控制你的房子的硬件单元相连接。你的 Node.js 应用和该单元通过一个串行端口对话,且有线协议只能同时接受一个挂起的命令。

这个协议被包装在我们的 domotic.js 模块中,模块暴露三个函数:

  • .connect() - 连接 domotic 模块

  • .command() - 发送命令,等待响应

  • .disconnect() - 切断与模块的连接

下面的代码模拟了这样一个模块:

domotic.js:

exports.connect = connect;  
exports.command = command;  
exports.disconnect = disconnect;

function connect(cb) {  
  setTimeout(cb, 100); // simulate connection
}

function command(cmd, options, cb) {  
  if (succeeds()) {
    setTimeout(cb, 100); // simulate command
  } else {
    setTimeout(function() {
      var err = Error('error connecting');
      err.code = 'ECONN';
      cb(err);
    }, 100);
  }

}

function disconnect(cb) {  
  if (cb) setTimeout(cb, 100); // simulate disconnection
}

function succeeds() {  
  return Math.random() > 0.5;
} 

注意我们并没有和任何 domotic 模块交互;我们只是假装,100 毫秒后成功调用回调函数。

同样, .command 函数模拟了连接错误: 如果 succeeds() 返回 false,连接失败,命令失败,这有 50% 的可能性(我们的 domotic 串行连接很容易出错)。这使我们能够测试在发生失败之后,我们的应用是否会成功重连并重试命令。

然后我们新建另一个模块,可以在队列后面发出命令。

domotic_queue.js:

var async = require('async');  
var Backoff = require('backoff');  
var domotic = require('./domotic');

var connected = false;

var queue = async.queue(work, 1);

function work(item, cb) {  
  ensureConnected(function() {
    domotic.command(item.command, item.options, callback);
  });

  function callback(err) {
    if (err && err.code == 'ECONN') {
      connected = false;
      work(item);
    } else cb(err);
  }
}

/// command

exports.command = pushCommand;

function pushCommand(command, options, cb) {  
  var work = {
    command: command,
    options: options
  };

  console.log('pushing command', work);

  queue.push(work, cb);
}

function ensureConnected(cb) {  
  if (connected) {
    return cb();
  } else {
    var backoff = Backoff.fibonacci();
    backoff.on('backoff', connect);
    backoff.backoff();
  }

  function connect() {
    domotic.connect(connected);
  }

  function connected(err) {
    if (err) {
      backoff.backoff();
    } else {
      connected = true;
      cb();
    }
  }
}

/// disconnect

exports.disconnect = disconnect;

function disconnect() {  
  if (! queue.length()) {
    domotic.disconnect();
  } else {
    console.log('waiting for the queue to drain before disonnecting');
    queue.drain = function() {
      console.log('disconnecting');
      domotic.disconnect();
    };
  }
} 

做了不少工作 —— 我们来一段段地分析。

var async = require('async');  
var Backoff = require('backoff');  
var domotic = require('./domotic'); 

这里我们引入了一些包:

  • async - 提供内存队列的实现

  • backoff - 让我们增加每一次失败后尝试重新连接的时间间隔

  • ./domotic - 模拟 domotic 的模块

我们的模块从连接断开状态开始启动:

`var connected = false;` 

建立我们的 async 队列:

`var queue = async.queue(work, 1);` 

这里提供一个叫做 worker 的工作函数(在代码中进一步定义的)和一个最大并发量 1。我们在这里强制设置,是因为我们定义了 domotic 模块协议一次只允许一个命令。

然后定义 worker 函数,它每次处理一个队列元素:

function work(item, cb) {  
  ensureConnected(function() {
    domotic.command(item.command, item.options, callback);
  });

  function callback(err) {
    if (err && err.code == 'ECONN') {
      connected = false;
      work(item);
    } else cb(err);
  }
} 

当我们的 async 队列加入另一个工作项目,会调用 work 函数,传递该工作项目和一个当工作完成时候为我们所调用的回调函数。

对每个工作项目来说,我们要确认已经连接了。一旦连接上,使用工作项目中会有的 commandoptions 属性,来用 domotic 模块来执行命令。传的最后一次参数是一个回调函数,当命令成功或失败之后会立即被调用。

回调函数中,我们明确地处理连接错误的情况,设置 connected 状态为 false,并再次调用 work重连。

如果没有发生错误,调用回调函数 cb 结束当前工作项目。

function ensureConnected(cb) {  
  if (connected) {
    return cb();
  } else {
    var backoff = Backoff.fibonacci();
    backoff.on('backoff', connect);
    backoff.backoff();
  }

  function connect() {
    domotic.connect(connected);
  }

  function connected(err) {
    if (err) {
      backoff.backoff();
    } else {
      connected = true;
      cb();
    }
  }
} 

ensureConnected 函数现在负责处于连接状态时调用回调或相反情况下尝试连接。尝试连接的时候,使用 backoff 增加每次重连的时间间隔。 每次 domotic.connect 函数带着错误被调用,在 backoff 事件触发之前增加间隔时间。触发 backoff 时,尝试连接。一旦连接成功,调用 cb 回调;否则保持重试。

这个模块暴露一个 .command 函数:

/// command

exports.command = pushCommand;

function pushCommand(command, options, cb) {  
  var work = {
    command: command,
    options: options
  };

  console.log('pushing command', work);

  queue.push(work, cb);
} 

这个命令简单的解析一个工作项目并将其推入队列。

最后,这个模块同样暴露出 .disconnect 函数。

/// disconnect

exports.disconnect = disconnect;

function disconnect() {  
  if (! queue.length()) {
    domotic.disconnect();
  } else {
    console.log('waiting for the queue to drain before disonnecting');
    queue.drain = function() {
      console.log('disconnecting');
      domotic.disconnect();
    };
  }
} 

这里我们只是确保在调用 domotic 模块的 disconnected 方法之前队列是空的。如果队列非空,在真正断开连接之前会等待其耗尽(drain)。

可选:在队列未被耗尽的情况下,您可以设置一个超时时间,然后强制断开连接。

然后我们来新建一个 domotic 客户端:

client.js:

var domotic = require('./domotic_queue');

for(var i = 0 ; i < 20; i ++) {  
  domotic.command('toggle light', i, function(err) {
    if (err) throw err;
    console.log('command finished');
  });
}

domotic.disconnect(); 

这里我们并行得向 domotic 模块添加了 20 个 settime 命令,同时传递了回调函数,当命令完成时就会被调用。如果有命令出错,简单地抛出错误并中断执行。

添加所有命令之后我们马上断开连接,不过模块会等待所有命令被执行之后才会真正将其断开。

让我们在命令行中试一下:

$ node client.js
pushing command { command: 'toggle light', options: 0 }  
pushing command { command: 'toggle light', options: 1 }  
pushing command { command: 'toggle light', options: 2 }  
pushing command { command: 'toggle light', options: 3 }  
pushing command { command: 'toggle light', options: 4 }  
pushing command { command: 'toggle light', options: 5 }  
pushing command { command: 'toggle light', options: 6 }  
pushing command { command: 'toggle light', options: 7 }  
pushing command { command: 'toggle light', options: 8 }  
pushing command { command: 'toggle light', options: 9 }  
pushing command { command: 'toggle light', options: 10 }  
pushing command { command: 'toggle light', options: 11 }  
pushing command { command: 'toggle light', options: 12 }  
pushing command { command: 'toggle light', options: 13 }  
pushing command { command: 'toggle light', options: 14 }  
pushing command { command: 'toggle light', options: 15 }  
pushing command { command: 'toggle light', options: 16 }  
pushing command { command: 'toggle light', options: 17 }  
pushing command { command: 'toggle light', options: 18 }  
pushing command { command: 'toggle light', options: 19 }  
waiting for the queue to drain before disonnecting  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
command finished  
disconnecting 

这里我们可以看到,所有命令被立即放到队列中,并且命令是被一些随机时间间隔着有序完成的。最后,所有命令完成之后连接切断。

下一篇文章

本系列的下一篇文章,我们将探索如何避免崩溃以及通过持久化工作项目来限制内存影响。

查看原文

赞 4 收藏 22 评论 0

SlaneYang 评论了文章 · 2018-10-31

git常用操作

git常用操作总结

仓库

  • 在当前目录新建一个Git代码库
    git init
  • 新建一个目录,将其初始化为Git代码库
    git init [project]
  • 下载一个项目
    git clone [url]

配置

  • 显示当前的Git配置
    git config --list
  • 设置提交代码时的用户信息
    git config [--global] user.name "[name]"
    git config [--global] user.email "[email address]"

文件操作

  • 添加指定文件到暂存区
    git add [file]
  • 添加指定目录到暂存区,包括子目录
    git add [dir]
  • 添加当前目录的所有文件到暂存区
    git add .
  • 删除工作区文件,并且将这次删除放入暂存区
    git rm [file]

代码提交

  • 提交暂存区到仓库区
    git commit -m [message]
  • 提交暂存区的指定文件到仓库区
    git commit [file] -m [message]

分支

  • 本地所有分支
    git branch
  • 远程所有分支
    git branch -r
  • 本地所有分支和远程所有分支
    git branch -a
  • 新建一个分支
    git branch [branch]
  • 新建一个分支,并且换到该分支
    git checkout -b [branch]
  • 切换回主分支
    git checkout master
  • 删除分支
    git branch -d [branch]
  • 删除远程分支
    push origin --delete [branch]
    git branch -dr [remote/branch]
  • 合并指定分支到当前分支
    git merge [branch]

远程

  • 查看远程分支
    git remote -v

更新与合并

  • 更新本地仓库至最新
    git pull
  • 提交本地所有改动到远程仓库(默认master分支)
    git push
  • 提交到远程指定分支
    git push origin [branch]
  • 本地已有项目与远程仓库连接
    git remote add origin [远程仓库地址]
  • 首次将本地代码提交到远程
    git push -u origin master

撤销

  • 恢复暂存区的指定文件到工作区
    git checkout [file]
  • 恢复暂存区的所有文件到工作区
    git checkout .
  • 重置暂存区与工作区,与上一次commit保持一致
    git reset --hard

查看信息

  • 查看有变更的文件
    git status
  • 查看当前分支的版本历史
    git log
  • 查看暂存区和工作区的差异
    git diff

历史版本

  • 切换回某个历史版本
    git checkout 历史版本号

错误解决方案

fatal: refusing to merge unrelated histories(拒绝合并不相关的历史)
  • 合并两个独立仓库历史
    git pull origin master –allow-unrelated-histories
  • 本地master分支提交到远程dev分支
    git push origin master:dev
error: failed to push some refs to 'https://github.com/......'
  • 移除远程连接
    git remote remove origin
  • 重新连接远程地址
    git remote add origin [远程仓库地址]
以上主要是总结git常用的操作,更多git操作请看git
查看原文

认证与成就

  • 获得 554 次点赞
  • 获得 14 枚徽章 获得 1 枚金徽章, 获得 1 枚银徽章, 获得 12 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-04-05
个人主页被 3.1k 人浏览