GithubHelp home page GithubHelp logo

myblogs's People

Watchers

Jeffersondyj avatar

myblogs's Issues

随便割2个题

二叉数bfs

是否存在一条路径sum,令其可匹配到target值

`

const findPath = (root, targetValue) => {
    const queue = [{node: root, value: root.value, parent: -1}];
    let head = 0;
    let tail = 0;
    const stack = [];

    while (head <= tail && !stack.length) {
        const {node: currentNode, value: currentValue, parent} = queue[head];
        if (currentValue === targetValue) {
            stack.unshift(currentNode.value);
            let traceItem = queue[parent];
            while (traceItem) {
                stack.unshift(traceItem.node.value);
                traceItem = queue[traceItem.parent];
            }
        }
        if (currentNode.children && currentNode.children.length) {
            currentNode.children.forEach(child => {
                queue[++tail] = {node: child, value: currentValue + child.value, parent: head};
            });
        }
        ++head;
    }
    return stack.length ? stack.join('+') : 'No Solution';
}

class TreeNode {
    constructor(value, children) {
        this.value = value;
        this.children = children;
    }
}

const tree = new TreeNode(
    1,
    [
        new TreeNode(
            2,
            [
                new TreeNode(4),
                new TreeNode(7)
            ]
        ),
        new TreeNode(
            3,
            [
                new TreeNode(5),
                new TreeNode(
                    7,
                    [
                        new TreeNode(2)
                    ]
                )
            ]
        )
    ]
);

for (let i = 1; i < 15; ++i) {
    console.log(i + '=' + findPath(tree, i));
}

`

窃以为现在真实需求里面不会让你搞这种TreeNode的class,更多可能是后端同学给你一个数组[{id, text, children}]或者[{value, children}],用数组描述该数据结构也没问题,写出的代码应该也会比上面扩展性更好(可用于多叉树,一级节点允许是森林)。教条地套算法题而不切合需求的,都是耍流氓!

`

const findPath = (forest, targetValue) => {
    const arr = [].concat(forest);
    const stack = [];
    for (let i = 0; i < arr.length; ++i) {
        if (stack.length) {
            break;
        }
        let {value, sumValue, children, parent = -1} = arr[i];
        sumValue = sumValue || value;
        if (sumValue === targetValue) {
            stack.push(value);
            let traceItem = arr[parent];
            while (traceItem) {
                stack.unshift(traceItem.value);
                traceItem = arr[traceItem.parent];
            }
        }
        children && children.forEach(child => arr.push({
            ...child,
            sumValue: sumValue + child.value,
            parent: i
        }));
    }
    return stack.length ? stack.join('+') : 'No Solution';
}

const forest = [
    {
        value: 1, children: [
            {
                value: 2,
                children: [
                    {value: 4}
                ]
            },
            {
                value: 3,
                children: [
                    {value: 5},
                    {value: 7, children: [{value: 2}]}
                ]
            }
        ]
    }
];

for (let i = 1; i < 15; ++i) {
    console.log(i + '=' + findPath(forest, i));
}

`

currying

用sum举例,实现currying函数:

固定参数的curry化,而且是单参数调用

拿4个参数举例,
const add4 = (a, b, c, d) => (a + b + c + d);
const sum = currying(add4);
sum(1)(2)(3)(4); // 10

`

const currying = fn => {
    const curry = (length, args) => {
        return length === 0 ? fn(...args) : x => curry(length - 1, args.concat(x));
    };
    return curry(fn.length, []);
};

`
虽然烧脑,但细细分解看下来还好:把原本是4个参数的函数放进去递归,每递归一次减1,然后把参数按顺序concat到空数组里面,最后还原成为执行那个4个参数的add4。

But,我如果想这样调用呢?
sum(1,2)(3,4); // 就不是10了

固定参数的curry化,但可能有多参数调用

`

const currying = fn => {
    const curry = (length, args) => {
        return length === 0 ? fn(...args) : (...inner) => curry(length - inner.length, args.concat(inner));
    };
    return curry(fn.length, []);
};

`

就把x => curry(length - 1, args.concat(x)) 改成了
(...inner) => curry(length - inner.length, args.concat(inner)),很好理解

But,我如果想这样调用呢?
sum(100, 300); // 返回函数,等价sum(400)
sum(100)(50)(20, 90); // 返回函数,等价sum(260)
sum(200)(50)(); // 250

wtf...

非固定参数的curry化,除非碰到空调用才执行

分析发现,非固定参数时length这个变量被弱化了,是看inner的length决定fn(...args),所以——

`

const currying = fn => {
    const curry = (args = []) => (...inner) => inner.length ? curry(args.concat(inner)) : fn(...args);
    return curry();
}
const sum = currying((...args) => args.reduce((a, b) => a + b, 0));

`
注意currying里面执行的函数,不能限定参数了,需要用array.reduce。
纯烧脑,估计需求里面不会用到吧,curry化有短板的童鞋练练概念倒是真的真的很不错。

JS之私有——一个世界性的难题

JS之私有

Jefferson其实蛮早就研究过了这个topic并写了sample。今天只不过整理出文字来而已。

一般说来,如果想原型中实现私有方法,我们约定俗成地加一个_:

SomeClass.prototype._somePrivateMethod = function () {
    // TODO
};

但是老道发话了(引用原话):

Do not use _ (underbar) as the first character of a name. It is sometimes used to indicate privacy, but it does not actually provide privacy. If privacy is important, use the forms that provide private members. Avoid conventions that demonstrate a lack of competence.

和私有方法类似,下例是一个加_的特权属性:

function Car() {
    this._mile = 0;
}

Car.prototype.getMile = function () {
    return this._mile;
};

但很遗憾,依然无法实现真正的私有,我们可以简单地: var car = new Car(); car._mile = 1;

改进1.0版

var Car = (function () {
    var privateStore = {};
    var uid = 0;
    
    function Car() {
        privateStore[this.id = uid++] = {};
        privateStore[this.id].mile = 0;
    }
    
    Car.prototype.getMile = function () {
        return privateStore[this.id].mile;
    };
}());

用uid生成递增的Car的this.id,所有的setter和getter都通过privateStore,这样无法直接写car.mile = 1了。实现了私有保护!

貌似有用诶~不过这是终极方案了吗?No,原因如下:

1 额外代码茫茫多,业务一旦复杂了,FE还要写一堆privateStore神马的,会精神崩溃直至跳楼;
2 this.id被占用,如果某个业务类也需要this.id,就傻眼了;
3 内存泄露——privateStore会对每个实例有引用,这些实例不会被回收。

改进思路

1 声明私有的途径,好歹应该快捷方便些;
2 私有方法可以调用共有方法;
3 不能占用this.id;不能有内存泄露问题。

改进2.0版

设计一个util函数,PrivateParts.create。传入privateMethods,其中:
var privateMethods = Object.create(A.prototype); // A为类名

而var _ = PrivateParts.create(privateMethods);
之后所有的私有变量、私有方法,都通过:_(this).*** 来获取句柄。

function isType(type) {
    return function (obj) {
        return Object.prototype.toString.call(obj) === "[object " + type + "]";
    };
}
var isFunction = isType('Function');

var PrivateParts = {
    create: function (methods) {
        var store = {};
        return function (obj) {
            for (var k in methods) {
                if (isFunction(methods[k]) && !store[k]) {
                    (function (_k) {
                        store[_k] = function () {
                            return methods[_k].apply(obj, arguments);
                        };
                    })(k);
                }
            }
            return store;
        };
    }
};

// 以下是sample
var A = function (opt) {
    this.getAttr = function () {
        return _(this).attr;
    };
    this.getPrivateMethod = function (key) {
        return _(this)[key];
    };
    
    var privateMethods = Object.create(A.prototype);
    privateMethods.invokePublic = function (str) {
        str = str || '1';
        return this.getNameAndAttr() + str;
    };
    privateMethods.invokePrivate = function (str) {
        str = str || '2';
        return _(this).invokePublic() + str;
    };
    privateMethods.getParams = function (str) {
        str = str || '3';
        return _(this).attr + this.name + str;
    };
    
    var _ = PrivateParts.create(privateMethods);
    opt = opt || {};
    this.name = opt.name || 'name';
    _(this).attr = opt.attr || 'attr';
};

A.prototype.getName = function () {
    return this.name;
};
A.prototype.getNameAndAttr = function () {
    return this.getName() + this.getAttr();
};

var a1 = new A();
function process(obj, str) {
    function printcall(method) {
        console.log(method ? method(str) : 'cannot access');
    }
    console.log(obj.name, obj.attr, obj.getAttr());
    printcall(obj.invokePublic);
    printcall(obj.getPrivateMethod('invokePublic'));
    printcall(obj.invokePrivate);
    printcall(obj.getPrivateMethod('invokePrivate'));
    printcall(obj.getParams);
    printcall(obj.getPrivateMethod('getParams'));
}
process(a1);

End

上述依然远非最终版本,只是自己设计的一个2.0版本而已。

司徒正美的《JS框架设计》里面chapter5也有很多不错的类工厂范例,值得推荐!

So, 欢迎大家提出建议,Jefferson Deng waiting for you.

最近的一些编程实践-react列表篇

是的,列表篇,前面的兄弟篇—表单篇在此

本篇可能没有上面formik讲那么多api,主要是讲自己在antd的table和一些其它组件的结合用法,用法也比较常见,自己觉得还算一个不错的tutorial,纯属抛砖引玉,大家看了之后,也可以见仁见智~

需求

呵呵嗒,还是ue图起跳:

image

image

image

一个带分页,页码,多选批量操作,行内编辑的一个列表。我们准备用antd的table来实现。

实现

首先来看看我设计的react-redux的store:

`
const initialConditions = {
    pageNo: 1,
    // 行内编辑相关
    isEditing: '', // LEVEL, AUTH_TYPE
    currentRowIndex: -1, // 鼠标hover着的rowIndex
    selectedRows: [] // 勾选上的rows,注意并非rowKey
};

const initialState = {
    ...initialConditions,
    loading: false, // true就要加一个loading的Spin
    list: [], // 数据
    total: 0,
    pageSize: 10
};
`

注意到还拆了一层initialConditions出来,是考虑到刷新的时候(比如delete了列表里面某些行,点击了分页等),需要重新请求新鲜的数据,此时只需要udpate某些状态到initialConditions,再配上当前实际的pageNo即可得到预期的请求参数。

来看看sagas:

`
function* fetchListSaga() {
    yield put(update({loading: true}));
    const result = yield call(authApi.search, {
        pageNo: yield select(makeSelector('pageNo')),
        pageSize: yield select(makeSelector('pageSize'))
    });
    yield put(update({
        loading: false,
        list: result.result,
        total: result.totalCount
    }));
}

const [tableChange, waitTableChange] = createSaga(
    'tableChange',
    function* ({payload: {selectedRowKeys}}) {
        const param = {
            ...initialConditions,
            pageNo: selectedRowKeys.current,
            pageSize: selectedRowKeys.pageSize
        };
        yield put(update(param));
        yield call(fetchListSaga);
    }
);

const [currentRowIndexChange, waitCurrentRowIndexChange] = createSaga(
    'currentRowIndexChange',
    function* ({payload: currentRowIndex}) {
        const isEditing = yield select(makeSelector('isEditing'));
        const modalVisible = yield select(makeSelector('inlineModalVisible'));
        if (!isEditing && !modalVisible) { // 非编辑态时,才update
            yield put(update({currentRowIndex}));
        }
    }
);
`

currentRowIndexChange里面需要注意,如果当前已经在行内编辑,就别更新了,如果此时不慎去更新了currentRowIndex,那Overlay明明还悬浮着,别的行的行内编辑入口却偷偷摸摸出来了。

editingChange(更新isEditing)和selectedRowsChange(更新selectedRows)不说了,就是纯update store而已,没有别的逻辑。

好了,我们来看看UI里面怎么用的table:

`
class AuthList extends PureComponent {

    getRowKey = item => item.userId;

    onVisibleChange = key => visible => {
        visible && this.props.editingChange({isEditing: key});
    }

    onRowChange = (record, index) => {
        const currentRowIndexChange = this.props.currentRowIndexChange;
        return {
            onMouseEnter: () => currentRowIndexChange(index),
            onMouseLeave: () => currentRowIndexChange(-1)
        };
    }
}
`

2个方法也很好理解,一个是将来Dropdown组件的onVisibleChange的回调方法(表示我现在正在行内编辑第hoverIndex行的isEditing这个key的cell),由于Dropdown可以指定overlay,这个overlay我们就可以写成一个业务组件了,传值也有上面model的list[currentRowIndex],完全不成问题。而当你要关闭overlay时,isEditing置为''就可以了。

另外一个方法当然就是table里面row变化的回调,不用多解释,代码很清晰了。

来看看render方法里面antd table的使用:

`
<Table
    loading={loading}
    columns={columns}
    pagination={pagination}
    dataSource={list}
    rowKey={this.getRowKey}
    onChange={tableChange}
    onRow={this.onRowChange}
    rowSelection={{
        selectedRowKeys: selectedRows.map(({userId}) => userId),
        onChange(selectedRowKeys, selectedRows) {
             selectedRowsChange(selectedRows);
        }
    }}
/>
`

pagination不说了哈,主要讲讲columns里面行内编辑怎么配的,这里前面抖过的onVisibleChange包袱终于登场了~

`
const entryProps = {isEditing, currentRowIndex, onVisibleChange: this.onVisibleChange};

const genEditEntry = ({rowIndex, currentRowIndex, overlay, isEditing, onVisibleChange, key}) => (
    <Fragment>
        {
            rowIndex === currentRowIndex && <Dropdown
                overlay={overlay}
                trigger={['click']}
                visible={isEditing === key}
                onVisibleChange={onVisibleChange(key)}
                placement="bottomCenter"
            >
                <Icon type="edit" />
            </Dropdown>
        }
    </Fragment>
);

return [
    {
        title: '授权级别',
        width: 120,
        dataIndex: 'level',
        key: 'level',
        render(text, {level}, rowIndex) {
            return (
                <div>
                    LEVEL文案
                    {
                        genEditEntry({
                            ...entryProps,
                            rowIndex,
                            key: 'LEVEL',
                            overlay: <Level />
                        })
                    }
                </div>
            );
        }
    },
    {
        title: '允许编辑',
        width: 120,
        dataIndex: 'authType',
        key: 'authType',
        render(text, {authType}, rowIndex) {
            return (
                <div>
                    authType文案
                    {
                        genEditEntry({
                            ...entryProps,
                            rowIndex,
                            key: 'AUTH_TYPE',
                            overlay: <AuthType />
                        })
                    }
                </div>
            );
        }
    }
];
`

genEditEntry很巧妙:只有当前hover着的row才出该行的行内编辑entry。然后点击entry时激发的Dropdown的onVisibleChange传key,让key对应的这个column的Dropdown的state变可见。具体你可以看看Dropdown的api,这是一个结合使用的范例~

至于具体的各个Overlay本身,就是另外的业务逻辑了(相当于微型表单),这里不做赘述了。

简单吗?其实很简单,你甚至可以把这些代码封装成套路(list-ria),后续复用起来更加无脑流水线化。另外这个套路依然可以扩展为更加复杂的形态,毕竟表头还是很简单的,antdtable的表头还可以支持筛选,排序等工作,这里ue需求没有提到,都是Jefferson后面待做的功课~

其它

啥都不多说了,antd还是非常强大的,继续学习api吧~

antd Table

prefetch prerender和预加载

前言

有了浏览器缓存了,为何还有预加载?

  • 用户可能首次访问,无缓存
  • 用户自己清了缓存
  • 缓存已经过期

机理

先了解一下浏览器显示页面的过程: 首先是DNS解析,然后建立TCP连接,接着下载HTML内容以及资源文件,最后才是整个页面的渲染。如图:
LOGO
图片来源:https://docs.google.com/presentation/d/18zlAdKAxnc51y_kj-6sWLmnjl6TLnaru_WH0LJTjP-o/present?slide=id.gc03305a_0106

这四个阶段必须是串行的,任何一步的延迟都会最终影响到页面加载时间。但浏览器在这方面已经做了很多优化,例如它会猜测你将要打开的页面,并预先解析DNS甚至直接下载它们。但浏览器猜测的能力是有限的,作为Web开发者我们可以通过dns-prefetch, subresource, prefetch, prerender等指令来帮助浏览器优化性能。图中4个节点:

  • preresolve dns预解析:提前解析主机地址,减少dns延迟
  • preconnect tcp预连接:提前连接到目标服务器,减少tcp握手
  • prefetcting 资源预加载:提前加载核心资源
  • prerendering 页面预渲染:提前获取目标页面和全部相关资源

你可以进入chrome浏览器,输入:

:::javascript
Chrome://dns
Chrome://predictors

可以看到浏览器有做了一些努力,来预测用户行为。

dns-prefetch

可以指示浏览器去预先解析DNS域名。这样可以减少将要打开页面的延迟:

:::javascript
<link rel='dns-prefetch' href='example.com'>

prefetch

用来初始化对后续导航中资源的获取。prefetch指定的资源获取优先级是最低的。

:::javascript
<link rel="prefetch" href="checkout.html">

场景:比如下载apk的Download按钮。

subresource

用来标识出重要的资源,浏览器会在当前访问页面时立即下载它们。

:::javascript
<link rel="subresource" href="critical/app.js">

subresource的语义是当前页面的子资源,浏览器会立即下载它们。
subresource的优先级高于prefetch。

prerender

让浏览器在后台事先渲染好整个页面:

:::javascript
<link rel="prerender" href="checkout.html">

因为要渲染整个页面,所以它需要的所有资源也会被全部下载。如果里面的JS需要在页面显示时运行,可以通过页面可见性API来实现。当然只有GET才是可以预先渲染的,预渲染POST当然是不安全的。
场景:比如淘宝列表页中的某些热门详情页。

总结

这些特性真心不错,且用且珍惜。

React组件规范

前言

基于ES6的React组件规范
文中可能会出现:MUST SHOULD MAY 表示肯定,语气强到弱
MAY NOT SHOULD NOT MUST NOT 表示否定,语气弱到强

命名&声明

文件名:MUST 使用大驼峰,如 MyComponent.js;
组件命名:组件名称 MUST 和文件名一致,如 MyComponent.js 里的组件名 MUST 是 MyComponent;一个目录的根组件 SHOULD 使用 index.js 命名,MUST 以目录名称作为组件名称;

// bad 
export default React.createClass({
    displayName: 'ReservationCard' 
});

// good 
const ReservationCard = React.createClass({
});

export default ReservationCard;

引号引用

// bad
<Foo bar='bar' />
// good
<Foo bar="bar" />

// bad
<Foo style={{ left: "20px" }} />
// good
<Foo style={{ left: '20px' }} />

属性书写

属性较少时 MAY 行内排列;
属性较多时 MUST 每行一个属性,闭合标签单独成行。

// bad
<input type="text" value={this.state.newDinosaurName} onChange={this.inputHandler.bind(this, 'newDinosaurName')} />
// bad
<input type="text" value={this.state.newDinosaurName}
   onChange={this.inputHandler.bind(this, 'newDinosaurName')} />

// good
<input
    type="text"
    value={this.state.newDinosaurName}
    onChange={this.inputHandler.bind(this, 'newDinosaurName')}
/>

方法

SHOULD NOT 使用下划线前缀命名 React 组件的方法;
MUST 按照生命周期组顺序织组件的方法、属性;
方法之间 MUST 空一行;
render MUST 始终放在最后;
自定义方法 SHOULD 放在 React API 方法之后,render之前。

// React 组件中按照以下顺序组织代码
class extends React.Component {
    getDefaultProps() {
    }

    getInitialState() {
    }

    componentWillMount() {
    }

    componentDidMount() {
        // do something: add DOM event listener, etc.
    }

    componentWillReceiveProps() {
    }

    shouldComponentUpdate() {
    }

    componentWillUpdate() {
    }

    componentDidUpdate() {
    }

    componentWillUnmount() {
        // do something: remove DOM event listener. etc.
    }

    // getter methods for render like getSelectReason() or getFooterContent()

    // Optional render methods like renderNavigation() or renderProfilePicture()

    render() {
        // ...
    }
}

render

render方法里面 SHOULD 以<开头,不应该存在if else分支,视情况返回不同的JSX。相同的组件 SHOULD 返回相同的顶级元素容器。

// bad
render() {
    if (this.state.a) {
        return <strong>222</strong>
    } else {
        return <div>222</div>
    }
}

ref

ref SHOULD NOT 用字符串形式,因为字符串形式的ref会自始至终将字符串放在refs对象中,会有泄露的问题。

// bad
<Foo
    ref="myRef"
/>

// ok
<Foo
    ref={(ref) => { this.myRef = ref; }}
/>

上面的方法之所以是ok,而不是good,是因为我们在查看组件时,很难察觉到你在JSX里偷偷为组件添加了一个新属性。组件所有用到的属性,应该都能在constructor或defaultProps中找到。

setState死循环

MUST NOT 在componentWillUpdate/componentDidUpdate/render中执行setState,可能导致死循环。

bind

SHOULD NOT 在JSX中使用bind方法绑定组件实例,可能导致性能问题。

// bad
class extends React.Component {
    onClickDiv() {
    }

    render() {
        return <div onClick={this.onClickDiv.bind(this)} />;
    }
}

// good
class extends React.Component {
    constructor(props) {
        super(props);
        this.onClickDiv = this.onClickDiv.bind(this);
    }

    onClickDiv() {
    }

    render() {
        return <div onClick={this.onClickDiv} />;
    }
}

其他

SHOULD NOT 使用cloneElement,createElement。
cloneElement可能会导致_owner丢失。_owner丢失会导致ref失效。

MAY NOT 使用createClass,mixin,PropTypes(它们已经被移出核心库,被逐渐边缘化)。

单个组件里面的DOM循环逻辑 MUST NOT 超过3层嵌套。
整个系统里面的组件的循环逻辑 MUST NOT 超过9层嵌套。

先写到这里,后续慢慢补充。

性能指标衡量体系

背景

维护网站的过程中,我们会非常关注网站的性能。使用过各种节点来衡量网站性能,如白屏时间,首屏加载时间,用户可交互时间等。社区和实际使用过程中,也运用过更多的指标来表达这些节点,如熟知的TTFB,Load,FP,FCP,TTI,FPS等概念

当用户可以开始操作网站了,我们会认为可以开始使用了。我厂大商业各大网站大多采用ITTI作为衡量指标。

什么是ITTI

是百度 weirwood 提出的一个理念,它的统计口径见:

图片

可以试着分析几个timeline,并提出问题:ITTI能否完全反映客户性能体验?

场景分析

来看这样一个timeline,基于我们的框架(ve-ria,bat-ria等)我们把XHR分为3类:block,critical,非critical

图片

如果长任务已经较早结束,那我们的ITTI根据上面的统计口径,即还是根据最后一个非critical请求(XHR资源)加载完成的时间来算的。这会有什么问题?

假设有这样一个场景:我们rd优化了列表api,将其拆分为一个耗时短的critical api(涵盖了80%+的信息量),以及另一个耗时基本相同的非critical api(全部信息),此时ITTI显然还会维持不变。这合理吗?

可见ITTI在此场景下,并不能完全反映客户性能体验
客户是有感知,并能觉得变快的(毕竟渲染出table主体的时间大大提前)

SI

这里重点说一下SI模型,这个模型对于ITTI有比较好的弥补作用:https://docs.webpagetest.org/metrics/speedindex/

由此引发一个问题,如何有效地衡量性能体验?这里涉及的其实是一个建模问题:随着时间推移,用户能看到的页面信息量,我们令其根据时间轴进行积分
(左:进行过api拆分;右:原来方案,下图只是一个描述性sample)

图片

理论上说,在一定时间轴内面积越大,表示客户能看到的加权信息量越多,体验就越好。
在这个模型下,我们推动 rd 进行 api 拆分并先行渲染 critical 部分就很有意义~

附录

各指标概念图示:

图片

文档加载

简称 全称 解释 来源
TTFB Time To First Byte 浏览器从请求页面开始到接收第一字节的时间 web标准
DCL DomContentLoaded DomContentLoaded 事件被触发时的时间 web标准
L Load onLoad 事件触发的时间 web标准

TTFB

浏览器从请求页面开始到接收第一字节的时间,这个时间段内包括 DNS 查找、TCP 连接和 SSL 连接。TTFB可以衡量文档加载的情况,此时页面从白屏转变成有第一个字节渲染到屏幕,它非常适合衡量白屏情况。但此时还完全不能让用户操作网站。

DCL

当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完全加载。

对应流程图上的Processing阶段结束时间点,也就是说此时网站页面信息已经被成功读取了,比TTFB多了很多工作,但在页面展示上,可能依然还是空白。DCL仍然适合衡量白屏时间,不适合衡量用户对网站的可交互情况。

L

页面所有资源都加载完毕后(比如图片,CSS),onLoad 事件才被触发。

对应流程图上的Load阶段的结束时间点,此时网站所需的初始资源都已被浏览器拿到,比 DCL 的资源更丰富,但依然不能保证页面已经渲染到位,时间点仍然早了。

内容展示

简称 全称 解释 来源
FP First Paint 从开始加载到浏览器首次绘制像素到屏幕上的时间 web标准
FCP First Contentful Paint 浏览器首次绘制来自 DOM 的内容的时间 web标准
FMP First Meaningful Paint 页面的主要内容绘制到屏幕上的时间 Google
LCP Largest Contentful Paint 可视区域中最大的内容元素呈现到屏幕上的时间 Web 孵化器社区组(WICG)
SI Speed Index 计算页面可见区域内容显示的平均时间 Google
FSP First Screen Paint 页面从开始加载到首屏内容全部绘制完成的时间 web标准提案

FP

从开始加载到浏览器首次绘制像素到屏幕上的时间,也就是页面在屏幕上首次发生视觉变化的时间。

这个时候页面可能开始不再白屏,根据具体实现不同,可能出现背景色了,页面头部导航等等可见内容,也有可能渲染的是不可见元素。

FCP

浏览器首次绘制来自 DOM 的内容的时间,内容必须是文本、图片(包含背景图)、非白色的 canvas 或 SVG,也包括带有正在加载中的 Web 字体的文本。

相比FP,这个指标确保页面渲染出来的是可见的内容,它的时间会大于FP,尤其是如果定义了自定义字体,需要加载字体大文件,会拉长FCP指标的时间。FCP确保页面可见了,但此时的内容却不一定可交互,还需要进一步寻找更合适的指标。

FMP

页面的重要内容绘制到屏幕上的时间,相比FCP,它应该可以更好的衡量网站体验情况。

FMP的计算和标准比较复杂一些,根据 Time to First Meaningful Paint: a layout-based approach 的解释,不同页面的重要性内容有所不同,对于博客页面来说,标题和摘要是首要需要呈现的内容;对于搜索引擎结果页来说,需要出来结果页面;而电商页面,图片是重要的展示内容。而FMP的计算方式是取得渲染布局对象最多的时刻,那么不同页面因为内容差异,得到的时间点并不适合放在一起比较。FMP担任不了统一标准,实际情况可能会更推荐与它相似的LCP。

LCP

可视区域中最大的内容元素呈现到屏幕上的时间,用以估算页面的主要内容对用户可见时间。

这样的内容元素一定程度上可以覆盖所需的内容,一般会将

FSP

页面从开始加载到首屏内容全部绘制完成的时间,用户可以看到首屏的全部内容。

相比LCP,FSP可以看到首屏中的完整内容,包括可视范围外的。如果页面不涉及交互,只需要纯展示的话,它是一个衡量页面完成情况的合适指标。但如果实现上在可视范围外渲染了过多内容,会很影响FSP的衡量准确性,时间点会延后。此外,FSP最终没有成为标准,在实际实现上也没有得到一个统一的官方标准,使用起来有些风险。

交互响应

简称 全称 解释 来源
FCI First CPU Idle 页面第一次可以响应用户输入的时间 WICG,Google
TTI Time To Interactive 网页第一次完全达到可交互状态的时间 Web 孵化器社区组(WICG)
FID First Input Delay 从用户第一次与页面交互(例如单击链接、点击按钮等)到浏览器实际能够响应该交互的时间 Web 孵化器社区组(WICG)
FPS Frames Per Second 每秒可以重新绘制的帧数 Web标准

FCI

页面第一次可以响应用户输入的时间。有一种典型的场景是长任务结束后,在随后的 5 秒内网络和主线程是空闲的,第一次出现这种情况的长任务结束点就是FCI。

TTI

表示网页第一次 完全达到可交互状态 的时间点,浏览器已经可以持续性的响应用户的输入。TTI就是可持续版本的FCI了。

根据TTI的定义,完全达到可交互状态的时间点是在最后一个长任务(Long Task)完成的时间, 并且在随后的 5 秒内网络和主线程是空闲的。

TTI不在标准内,但不少工具已经实现了它,我们接入的Weirwood也实现了TTI定义,并在此基础上衍生了ITTI,称为理想可交互时间点,用来更精确的衡量网站的可交互性。

加权指标的贡献度分解

背景

最近在效果广告搞性能优化,在拿结果的时候,我们经常会接触一系列值,然后做加权的操作
比如下面就是一个典型例子(数据为杜撰):

TTF(卡顿) 上周数据 上周采样 本周数据 本周采样
计划列表 369 80 382.3 70
单元列表 350 20 363.4 29
创意列表 300 20 313 24
关键词列表 402 80 504 85
列表总体 373.4 200 421.4 208

好,现在问题来了,我们希望找到卡顿指标退化 整整48ms 的主要原因~

这时候大家就说了:这数据不是一目了然么,关键词列表退化了100ms+,采样数又大,就是关键词列表拖累了大盘!
关键词列表(自己申辩):没错,主锅是我没得洗。但撇开事实不谈,你们别的模块难道就没有一点点责任么?~
这里就引发出俩个问题:

  • 比如数据不那么一目了然的情况下,肉眼无法看出谁是拖累大盘的主要原因(如下表,数据同样为杜撰)
  • 所以,我们可能希望更加量化(而非用肉眼去估摸)地计算出,哪个模块对总体指标贡献的最多
ITTI 上周数据 上周采样 本周数据 本周采样
列表 4200 80 4000 85
报告 5000 20 4800 20
其他模块 5500 40 5225 20
大盘总体 4685.71 140 4324 125

sample2:每个指标都优化了 200ms 或 275ms,但大盘优化了更多(361.7ms)。哪个指标贡献更大,肉眼能看出否?

指标的拆解和衡量

拆解

直接上结论:一个指标对大盘的影响,等于 值影响(Principle Term,TP) + 结构影响(Structure Effect,ES)

衡量

如何衡量,当然是控制变量法(有点 abtest 的味道在里面)

TP:今天相对与昨天的权重不变,单纯指标值产生的影响 + 昨天相对与今天的权重不变,单纯指标值产生的影响
说人话,即:(v2 - v1) * (w1 / 总w1) + (v2 - v1) * (w2 / 总w2)
我们应当要取一个平均,即 TP = (v2 - v1) * (w1 / 总w1 + w2 / 总w2) / 2

ES:今天相对与昨天的值不变,单纯权重产生的影响 + 昨天相对与今天的值不变,单纯权重产生的影响
说人话,即:(v1 - Avg1) * (w2 / 总w2 - w1 / 总w1) + (v2 - Avg2) * (w2 / 总w2 - w1 / 总w1)
我们应当要取一个平均,即 ES = (v1 + v2 - Avg1 - Avg2) * (w2 / 总w2 - w1 / 总w1) / 2

我提个问,这里为什么是(v1 - Avg1) ,后面是(v2- Avg2),直接就是v1 v2不行么

这个公式好不好,能不能经得起考验,拿刚刚这个例子溜一溜:

ITTI 上周数据 上周采样 本周数据 本周采样 TP ES Total
列表 4200 80 4000 85 -125.14 -43.96 -169.1
报告 5000 20 4800 20 -30.29 6.77 -23.51
其他模块 5500 40 5225 20 -61.29 -107.82 -169.1
大盘总体 4685.71 140 4324 125 -216.71 -145 -361.71

其中:(4000-4200) * (85/125 + 80/140) / 2 = -125.14
(4200+4000-4685.71-4324) * (85/125 - 80/140) / 2 = -43.96
可以看到:其他模块和列表的贡献度完全一样(并不是采样大就占尽优势,体现了ES的重要性)

ES有多重要,不妨再举个极端的例子(ITTI 软硬导航都退化200ms,但通过改善软硬导航比例,大盘反而优化)

ITTI 上周数据 上周采样 本周数据 本周采样 TP ES Total
硬导航 5800 175 6000 105 100 -225 -125
软导航 4000 105 4200 175 100 -225 -125
大盘总体 5125 280 4875 280 200 -450 -250

总结

  • 这是一种比较公认的加权平均类指标的定量分析方法,通过这种方法可以从指标值和结构两个维度解释数据波动:
    • 是值确实有变化
    • 还是大盘结构发生了变化
  • 提供了具体的量化依据,通过拆解更加精细化地:在知道了结果的情况下,反推 why
    • 场景不仅仅可以用做性能优化,对任何指标的分析都能套用,是一个完备的方法论
    • 知道了账户的数据(如消费,acp),也能通过下钻拆解到了细颗粒度的指标,就可以套用并计算分解的贡献度
    • 分流量、分行业、分运营单位、分样式,甚至是圈定一批客户名单作为分析,etc

报价相关问题的排查

背景

这次小同学做一个报价模块,踩了很多坑,也帮助他一一走出来,总结了好几个知识点:

Samesite

首先是一个iframe跨域问题,xxx.baidu.com 里面有一个 iframe 是 xxx.baidu-int.com ,导致了 chrome 和 safari 下,cookie无法植入,所以登录鉴权失败

Google 为了杜绝 CSRF(CSRF攻击参考:https://blog.csdn.net/freeking101/article/details/86537087 )的发生,Chrome会将没有声明SameSite值的cookie默认设置为SameSite=Lax

什么是SameSite

SameSite是Cookie中的一个属性,它用来标明这个 cookie 是个“同站 cookie”,“同站 cookie” 只能作为第一方cookie,不能作为第三方cookie,因此可以限制第三方Cookie,解决CSRF的问题。早在Chrome 51中就引入了这一属性,但是不会默认设置,所以相安无事。

第三方Cookie:由当前a.com页面发起的请求的 URL 不一定也是 a.com 上的,可能有 b.com 的,也可能有 c.com 的。我们把发送给 a.com 上的请求叫做第一方请求(first-party request),发送给 b.com 和 c.com 等的请求叫做第三方请求(third-party request),第三方请求和第一方请求一样,都会带上各自域名下的 cookie,所以就有了第一方cookie(first-party cookie)和第三方cookie(third-party cookie)的区别。上面提到的 CSRF 攻击,就是利用了第三方 cookie可以携带发送的特点 。

SameSite总共有三个值:Strict、Lax、None

  • Strict:最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。这个规则过于严格,可能造成非常不好的用户体验。比如一个人已经登陆了ERP环境,但在待办中,由于是不同的域,cookie带不过去,就等于一直没有登录状态,就会回到登录页。
  • Lax:规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。Chrome 80之后默认设置为该值。设置了Strict或Lax以后,基本就杜绝了CSRF攻击。
  • None:浏览器会在同站请求、跨站请求下继续发送cookies,不区分大小写。网站可以选择显式关闭 SameSite 属性,将其设为 None ,同时必须设置 Secure 属性(表示Cookie 只能通过 HTTPS 协议发送,HTTP协议不会发送),否则无效。

我们最后将子域升级为 https

cdn的坑

上面问题解决之后,又发现一个CORS问题:

xxx.baidu-int.com的页面里面的资源,xxx.baidu.com 也会用,我其实有把

 *.baidu-int.com
 *.baidu.com

都写到CORS 的 allow-origin配置里面去了,但访问 xxx.baidu.com 里面的资源,response的 allow-origin 竟然是返回了 xxx.baidu-int.com !

wtf?什么原因,原来是cdn的缓存坑

图片

如图,CDN 会缓存第一次 cors 请求的BOS响应(包括响应头),而第二次 cors 请求到达CDN后直接命中了前一次的缓存,使得虽然两次请求的 Origin 不同,但响应的 allow-origin 都是第一次请求的 origin(注:BOS是我们的存静态资源的物理系统)

怎么破:用Vary: Origin。具体可以狠狠点击:https://zhuanlan.zhihu.com/p/38972475

附带一句,对于 css、js 这样的静态资源,只要客户端支持 gzip,服务端应该总是启用它。
所以 Vary: Origin, Accept-Encoding 这样的响应头,除了可以规避上面描述的 cdn 问题,还能明确告知缓存服务器按照 Accept-Encoding 字段的内容,分别缓存不同的版本——是一个非常好的应用实践!

插曲:之后用了一下还是有点问题,会命中缓存,原因是这个Access-Control-Max-Age在捣鬼!众所周知,浏览器会发2次请求,第一次是浏览器使用OPTIONS方法发起一个预检请求,第二次才是真正的请求,第一次的预检请求获知服务器是否允许该请求:如果允许,才发起第二次真实的请求;如果不允许,则拦截第二次请求。
Access-Control-Max-Age用来指定本次预检请求的有效期,单位为秒,在此期间不用发出另一条预检请求。具体可以见:

https://stackoverflow.com/questions/42848208/cors-preflight-response-includes-varyorigin-and-access-control-max-age

Referrer Policy

最后碰到一个动态 iframe(没有src,里面内容是直接灌入的html),用 charles 抓包发现,ios 的 referrer 是空的,不清楚是不是和安卓有区分,怀疑是referer policy问题(IOS下,这个policy会变成no-referer),最后不得不把防盗链暂时关闭

https://baijiahao.baidu.com/s?id=1675887370329904484&wfr=spider&for=pc

解决前端请求后发先至的问题

接上一篇。我们在做web应用之时,经常会碰到后一次请求响应的数据先被渲染,待前一次请求响应回来,直接覆盖了后一次请求的渲染结果——这并非我们所期望看到的。

举例

我们封装一个fetchData函数,控制mock数据和timeout时间来做实验。

`
const fetchData = (data, timeout) => new Promise((resolve, reject) => {
    setTimeout(() => resolve({success: true, data}), timeout);
});

let result = null;
fetchData({t: 1}, 500).then(({data}) => result = data);
fetchData({t: 2}, 200).then(({data}) => result = data);
setTimeout(() => console.log(result), 1000); // {t: 1}
`

执行结果,打印的{t: 1}是前一次的请求response,并非我们的预期。

方案

在每次响应后且在渲染之前,判断当前响应是不是对应最新一次请求的。是,则渲染;不是,则不渲染。我们想到用计数器为每次请求标记一个flag,如果不是最新的,则数据不生效。来看代码——

`
const fetchData = (data, timeout) => new Promise((resolve, reject) => {
    setTimeout(() => resolve({success: true, data}), timeout);
});
let counter = 0;
let result = null;

const callFetchWithCounter = (data, timeout) => {
    let flag = ++counter;
    fetchData(data, timeout).then(({data}) => {
        if (flag !== counter) {
            return;
        }
        result = data;
    });
};

callFetchWithCounter({t: 1}, 500);
callFetchWithCounter({t: 2}, 200);
setTimeout(() => console.log(result), 1000); // {t: 2}
`

通过对api调用处的封装,实现了flag和计数器的判断,方案简单直观。但是,当我们要处理大量这类请求问题时,这类重复逻辑的代码将散落在各个地方,不是很优雅。正因为你把这些额外的处理逻辑放在了调用处——应该放在根源处,即封装IO请求,也就是例子的fetchData的那个函数里面。

解决

`
let counter = 0;
const fetchData = (data, timeout) => {
    let flag = ++counter;
    return new Promise((resolve, reject) => {
        setTimeout(() => { // 用reject来mock“检测出后发先至时序”
            flag === counter ? resolve({success: true, data}) : reject({success: false, data});
        }, timeout);
    });
};

let result = null;
fetchData({t: 1}, 500).then(({data}) => result = data, console.log);
fetchData({t: 2}, 200).then(({data}) => result = data, console.log);
setTimeout(() => console.log(result), 1000);
// {sucess: false, data: {t: 1}}
// {t: 2}
`

这样调用就和最开始那个举例一样了。

尾声

要不,读者你来,把这篇的成果和上一篇缓存结合起来?我相信你可以的,you can you up!

谈谈es6的Proxy

概述

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

image

基本语法/使用注意事项

var proxy = new Proxy(target, handler);

属性拦截

const employee = {
    firstName: 'Jefferson',
    lastName: 'Deng'
};
const p = new Proxy(employee, {
    get(target, propKey) {        
        if (propKey === 'fullName') {
            return `${target.firstName} ${target.lastName}`;
        }
        return target[propKey];
    }
});
console.log(p.fullName); // Jefferson Deng

拦截一览

下面是 Proxy 支持的拦截操作一览,一共 13 种:

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、* Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、* proxy.call(object, ...args)、proxy.apply(...)。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。

this指向

const target = new Date('2019-11-11');
const proxy = new Proxy(target, {});
proxy.getDate(); // uncaught TypeError
const handler = {
    get(target, propKey) {
        const result = Reflect.get(target, propKey);
        return typeof result === 'function' ? result.bind(target) : result;
    }
};
const proxy1 = new Proxy(target, handler);
proxy1.getDate(); // 11
proxy1.getDay(); // 1

这里又引入了另一个ES6的新增的对象Reflect,该对象就是用来获取对象中默认方法的。
具体可以参考大神的Reflect

应用场景

校验封装

{
    set(target, propKey, value) {
        if (propKey === 'age') {
            if (!Number.isInteger(value)) {
                throw new TypeError('Age must be an Integer');
            }
            if (value < 0) {
                throw new TypeError('Age cannot be negative');
            }
        }
        target[propKey] = value;
        return true; // 注意,严格模式下,set代理如果没有返回true,就会报错
    }
}

这样写当然已经不错。But,如果要直接为对象的所有属性开发校验器,很快就会让代码结构变得臃肿!你既然都使用 Proxy 了,则可以将校验器从核心逻辑分离出来自成一体:

function createValidator(target, validator) {  
    return new Proxy(target, {
        _validator: validator,
        set(target, key, value, proxy) {
            if (target.hasOwnProperty(key)) {
                let validator = this._validator[key];
                if (!!validator(value)) {
                    return Reflect.set(target, key, value, proxy);
                } else {
                    throw Error(`Cannot set ${key} to ${value}. Invalid`);
                }
            } else {
                throw Error(`${key} is not a valid property`);
            }
        }
    });
}

// sample
const personValidators = {  
    name(val) {
        // 你可以让这个返回,在正确时是true,错误时是报错文案。然后稍微改改createValidator即可
        return typeof val === 'string';
    },
    age(val) {
        return typeof age === 'number' && age > 0;
    }
}
class Person {  
    constructor(name, age) {
        this.name = name;
        this.age = age;
        return createValidator(this, personValidators);
    }
}
const bill = new Person('Bill', 25);
// 以下操作都会报错
bill.name = 0;
bill.age = 'Bill'; 
bill.age = -1;

链式取值,链式操作

先看看取值~开发中,链式取值是非常正常的操作,如:
res.data.productSetList[0].id

对于这种操作,会经常报出类似于Uncaught TypeError: Cannot read property 'id' of undefined 这种错误。如果说是res数据是前端自己定义,那么可控性会大一些,但是如果这些数据来自后端,那数据对于我们来说都是不可控的,因此为了保证程序能够正常运行下去,我们需要对此校验:

if (res.data.productSetList.length) {
    // your code
}
// 甚至还要判断得精细些,这就根本不能看了
if (res && res.data && res.data.productSetList && res.data.productSetList.length) {
}

optional chaining

这是一个出于stage 2的语法,目前已经有了babel的插件 babel-plugin-transform-optional-chaining,官方sample如下:

a?.b                          // undefined if `a` is null/undefined, `a.b` otherwise.
a == null ? undefined : a.b
a?.[x]                        // undefined if `a` is null/undefined, `a[x]` otherwise.
a == null ? undefined : a[x]
a?.b()                        // undefined if `a` is null/undefined
a == null ? undefined : a.b() // throws a TypeError if `a.b` is not a function// otherwise, evaluates to `a.b()`
a?.()                        // undefined if `a` is null/undefined
a == null ? undefined : a()  // throws a TypeError if `a` is neither null/undefined, nor a function// invokes the function `a` otherwise

函数解析字符串

我们可以通过函数解析字符串来解决这个问题,这种实现就是lodash的 _.get 方法

var object = {a: [{b: {c: 3}}]};
var result = _.get(object, 'a[0].b.c', 1);
console.log(result); // 3

实现我就不说了,看源码去吧~

使用解构

很好理解,我就不展开了,ps:层级深的话,有可能会被cr人员-2....

主角登场

const isNull = obj => obj === null || obj === undefined;
function pointer(obj, path = []) {
    return new Proxy(function () {}, { // 第一个参数必须弄一个空function,不然会报错
        get(target, propKey) {
            return pointer(obj, path.concat(propKey));
        },
        apply(target, self, args) {
            let val = obj;
            for (let i = 0; i < path.length; i++) {
                if (isNull(val)) {
                    break;
                }
                val = val[path[i]];
            }
            return isNull(val) ? args[0] : val; // 可以在最终调用时,手动设默认值
        }
    });
}
const c = {a: {b: [1, 2, 3]}};
pointer(c).a(); // {b: [1, 2, 3]}
pointer(c).a.b(); // [1, 2, 3]
pointer(c).a.b.d('defValue'); // defValue

有不错的可读性~

再看看链式操作

const pipe = (() => value => {
    const funcStack = [];
    const proxy = new Proxy({} , {
        get(target, fnName) {
            if (fnName === 'get') {
                return funcStack.reduce((val, fn) => fn(val), value);
            }
            funcStack.push(window[fnName]);
            return proxy;
        }
    });
    return proxy;
})();

var double = n => n * 2;
var square = n => n * n;
console.log(pipe(4).square.double.get); // 32

实现私有

{
    get(target, propKey){
        if (propKey.startsWith('_')) {
            console.log('私有变量不能被访问');
            return undefined;
        }
        return target[propKey];
    },
    set(target, propKey, value) {
        if (propKey.startsWith('_')) {
            console.log('私有变量不能被修改');
            return false;
        }
        target[propKey] = value;
        return true;
    }
} 

来看看之前我是怎么做的

设计一个util函数,PrivateParts.create。传入privateMethods,其中:
var privateMethods = Object.create(A.prototype); // A为类名

而var _ = PrivateParts.create(privateMethods);
之后所有的私有变量、私有方法,都通过:_(this).*** 来获取句柄。

function isType(type) {
    return function (obj) {
        return Object.prototype.toString.call(obj) === "[object " + type + "]";
    };
}
var isFunction = isType('Function');

var PrivateParts = {
    create: function (methods) {
        var store = {};
        return function (obj) {
            for (var k in methods) {
                if (isFunction(methods[k]) && !store[k]) {
                    (function (_k) {
                        store[_k] = function () {
                            return methods[_k].apply(obj, arguments);
                        };
                    })(k);
                }
            }
            return store;
        };
    }
};

// 以下是sample
var A = function (opt) {
    this.getAttr = function () {
        return _(this).attr;
    };
    this.getPrivateMethod = function (key) {
        return _(this)[key];
    };
    
    var privateMethods = Object.create(A.prototype);
    privateMethods.invokePublic = function (str) {
        str = str || '1';
        return this.getNameAndAttr() + str;
    };
    privateMethods.invokePrivate = function (str) {
        str = str || '2';
        return _(this).invokePublic() + str;
    };
    privateMethods.getParams = function (str) {
        str = str || '3';
        return _(this).attr + this.name + str;
    };
    
    var _ = PrivateParts.create(privateMethods);
    opt = opt || {};
    this.name = opt.name || 'name';
    _(this).attr = opt.attr || 'attr';
};

A.prototype.getName = function () {
    return this.name;
};
A.prototype.getNameAndAttr = function () {
    return this.getName() + this.getAttr();
};

var a1 = new A();
function process(obj, str) {
    function printcall(method) {
        console.log(method ? method(str) : 'cannot access');
    }
    console.log(obj.name, obj.attr, obj.getAttr());
    printcall(obj.invokePublic);
    printcall(obj.getPrivateMethod('invokePublic'));
    printcall(obj.invokePrivate);
    printcall(obj.getPrivateMethod('invokePrivate'));
    printcall(obj.getParams);
    printcall(obj.getPrivateMethod('getParams'));
}
process(a1);

数据结构桥接

项目中我们经常可以见到类似的数据结构,长得不尽相似,but各有不同:
{id, text, subModules: []} vs {name, value, children: []}

诉求:我们想只写一个函数,在函数中处理这不同的数据结构:

function bridge(map) {
    return obj => new Proxy(obj, {
        get(target, propKey) {
            if (Reflect.has(map, propKey)) {
                return Reflect.get(target, Reflect.get(map, propKey));
            }
            return Reflect.get(target, propKey);
        },
        set(target, propKey, value) {
            if (Reflect.has(map, propKey)) {
                Reflect.set(target, Reflect.get(map, propKey), value);
                return true;
            }
            Reflect.set(target, propKey, value);
            return true;
        }
    });
}
// 以下是sample
const print = ({id, text, children}) => console.log(id, text, children); // 标准格式:id, text, children
print(bridge({children: 'subModules'})({id: 1, text: 't1', subModules: []}));
print(bridge({id: 'value', text: 'name'})({value: 2, name: 'n2', children: ['c']}));

埋点/报警

其实这应该才是第一个被大家想到的:对对象的变更进行打日志监控,对使用者调用工具库里的过期api时,发警告。

相信你读完上面这些之后,就很好理解并实现这些feature了,这里篇幅所限,略去。

生成dom

const dom = new Proxy({}, {
    get(target, property) {
        return (attrs = {}, ...children) => {
            const el = document.createElement(property);
            for (let prop of Object.keys(attrs)) {
                el.setAttribute(prop, attrs[prop]);
            }
            for (let child of children) {
                if (typeof child === 'string') {
                    child = document.createTextNode(child);
                }
                el.appendChild(child);
            }
            return el;
        };
    }
});

const el = dom.div(
    {},
    'Hello, my name is ',
    dom.a({href: '//example.com'}, 'Jefferson'),
    '. I like:',
    dom.ul(
        {},
        dom.li({}, 'The web'),
        dom.li({}, 'The food')
    )
);
document.body.appendChild(el);

总结还是拆台?

Proxy是好货色,谁赞成,谁反对?

性能:我反对!

proxy到底tm有多慢!!??我刚刚已经弄挂了自己的浏览器....

var obj = {};
var _obj = {};
var proxy = new Proxy(_obj, {
    set: (target, prop, value) => {
         target[prop] = value;
    }
});
var printTimeConsumed = (target, repeatTimes) => {
    const t = new Date().getTime();
    for (var i = 0; i < repeatTimes; ++i) {
        target.a = 5;
    }
    console.log(new Date().getTime() - t);
};
printTimeConsumed(obj, 5e9); // 平均6s多
printTimeConsumed(proxy, 1e7); // 平均也是6s多

别问我为什么第二个参数是1e7,我本来也是5e9来着的....

参考资料

阮一峰大神Proxy
You don't know JS:元编程

前端引入es6

背景

什么是es6

ECMAScript是一种由Ecma国际(前身为欧洲计算机制造商协会,英文名称是European Computer Manufacturers Association)通过ECMA-262标准化的脚本程序设计语言。这种语言在万维网上应用广泛,它往往被称为JavaScript或JS。

实际上后两者是ECMA-262标准的实现和扩展。Javascript包含EcmaScript、DOM(Node,Element)、BOM(window,location),ECMA-262标准的发布过程:

  • ECMAScript 2.0 1998年6月发布
  • ECMAScript 3.0 1999年12月发布,成为JavaScript的通行标准,得到了广泛支持。
  • ECMAScript 4.0 2007年10月发布
  • ECMAScript 5.0 2009年12月发布
  • ECMAscript 5.1 2011年6月发布,并且成为ISO国际标准(ISO/IEC 16262:2011)
  • ECMAScript 6 2015年6月17日发布正式版本,即ECMAScript 2015

为何需要es6

语法糖

:::javascript
var array = [1, 2, 3];
array.forEach(v = > console.log(v));

增加程序可读性

减少程序代码出诡异错误的机会,例如:

  • 没有块级作用域 let 避免 var 的提升
  • 类写法比较怪异 彻底面向对象,可以写传统的类继承
  • 异步调用写法 yield 避免“回调地狱”
  • 没有模块管理 ......

符合未来趋势

如何引入es6

需要做什么

babel是一个JavaScript编译器,用于转化你的JS代码。
facebook也使用了es6 + babel编译。
baiju-web的系统中,我们需要:

  • 本地服务 在mockup环境下,edp-webserver可以利用babel将es6代码实时转es5
  • 线上打包 在build环境下,edp-build用babel将es6代码转为es5后,再模块化打包压缩

本地服务

新版的edp-webserver已经提供了babelHandler,注意到已经暴露了babel全局句柄,那我们只需要在edp-webserver-config.js里面,加上:

:::javascript
{
    location: /\.es6|\.jsx\.js/, // babel 同时对付es6和react
    handler: [
        babel({
            sourceMaps: 'both'
        }, { // forceTransform 无论是否有`define`都强制转成UMD/AMD模块
            forceTransform: true
        })
    ]
}

我现在是1.2.8版本,如果是老版本edp-webserver(无babel版),你可以:

:::javascript
npm uninstall -g edp-webserver
npm install -g edp-webserver

美中不足的是,原先启动webserver可以用edp ws start快捷启动,现在只能edp webserver start了。

线上打包

需要在edp-build-config.js里面的moduleCompiler之前加一个自定义Processor,命名为PaBabelProcessor。
也有参考过ecomfeedp-build的BabelProcessor,不过发现那里require的babeljs是5.*版的,最新的babeljs是6.18版,我们还是用更新的版本为宜,只是配置和官方Processor的源码略有不同而已。

:::javascript
/**
 * 初始化配置
 */
PaBabelProcessor.prototype.name = 'PaBabelProcessor';
PaBabelProcessor.prototype.files = ['**/*.es6.js', '**/*.jsx.js'];
PaBabelProcessor.prototype.babel = require('babel-core');
PaBabelProcessor.prototype.compileOptions = {
    compact: false,
    ast: false,
    presets: ['es2015']
};

/**
 * 构建处理
 *
 * @param {FileInfo} file 文件信息对象
 * @param {ProcessContext} processContext 构建环境对象
 * @param {Function} callback 处理完成回调函数
 */
PaBabelProcessor.prototype.process = function (file, processContext, callback) {
    var options = {};
    for (var attr in this.compileOptions) {
        if (this.compileOptions.hasOwnProperty(attr)) {
            options[attr] = this.compileOptions[attr];
        }
    }
    options.filename = file.fullPath;
    file.setData(this.babel.transform(file.data, options).code);
    callback();
};

这里edp-build不用换版本,维持你现在的环境即可。

引入import export特性

本地服务

考虑到es6也有强大的模块管理功能,拟将原来的define require写成import export形式

:::javascript
{
    location: /\.es6|\.jsx\.js/, // babel 同时对付es6和react
    handler: function (context) {
        var url = context.request.url;
        if (url.indexOf('\/output\/') >= 0) {
            file()(context);
            return;
        }
        var option = {sourceMaps: 'both'};
        url.indexOf('dsp') >= 0 || (option.modules = 'amd');
        babel(option, {
            // forceTransform 无论是否有`define`都强制转成UMD/AMD模块
            forceTransform: true
        })(context);
    }
}

意思也比较好理解:如果是build后的代码,则不管。如果是dsp下的代码(没来得及换成import),则用老办法转换(见上面代码)
否则则用babel5里面的配置:modules: 'amd'进行转换。这个是 babel5 ,倒是无需加plugin。

线上打包

需要在package.json里面加2个plugin:

"babel-plugin-add-module-exports": "^0.2.1"
"babel-plugin-transform-es2015-modules-amd": "^6.22.0"

然后BabelProcessor中的compileOptions配plugins: ['add-module-exports', 'transform-es2015-modules-amd']

第二个插件是转成amd模块,第一个插件是为了fix babel6编译对export default的支持。
鉴于伊始编译的代码死活没法正常工作,经过排查和跟踪,比对编译后的代码定位问题,所需的是类但得到的是{default: 类}:

在 babel5 时代,export default {}; 除了会被转译成 exports.default = {};,还会加一句 module.exports = exports.default(关键)
但在 babel6 时代做了一个区分,后面这句不再添加。在我看来,主要是为了区分 commonJS 和 es6 的模块定义,也就是 commonJS的 require 和 module.exports 搭配使用
这样一个相当于是 babel6 约定俗成的规范,导致了这个问题所在。

其他

踩过的坑

你当然可以在全局安装babel,但我没有这样做,而是选择在项目node_modules下局部安装。
注意光有babel-core,babel-preset-es2015依然要报错,还需要安装各个plugins。package.json下加了茫茫多这些:

:::javascript
"devDependencies": {
    "babel-core": "^6.18.0",
    "babel-plugin-add-module-exports": "^0.2.1",
    "babel-plugin-check-es2015-constants": "^6.8.0",
    "babel-plugin-transform-es2015-arrow-functions": "^6.8.0",
    "babel-plugin-transform-es2015-block-scoped-functions": "^6.8.0",
    "babel-plugin-transform-es2015-block-scoping": "^6.10.1",
    "babel-plugin-transform-es2015-classes": "^6.9.0",
    "babel-plugin-transform-es2015-computed-properties": "^6.8.0",
    "babel-plugin-transform-es2015-destructuring": "^6.9.0",
    "babel-plugin-transform-es2015-for-of": "^6.8.0",
    "babel-plugin-transform-es2015-function-name": "^6.9.0",
    "babel-plugin-transform-es2015-literals": "^6.8.0",
    "babel-plugin-transform-es2015-modules-amd": "^6.22.0",
    "babel-plugin-transform-es2015-object-super": "^6.8.0",
    "babel-plugin-transform-es2015-parameters": "^6.11.4",
    "babel-plugin-transform-es2015-shorthand-properties": "^6.8.0",
    "babel-plugin-transform-es2015-spread": "^6.8.0",
    "babel-plugin-transform-es2015-sticky-regex": "^6.8.0",
    "babel-plugin-transform-es2015-template-literals": "^6.8.0",
    "babel-plugin-transform-es2015-typeof-symbol": "^6.8.0",
    "babel-plugin-transform-es2015-unicode-regex": "^6.11.0",
    "babel-plugin-transform-proto-to-assign": "^6.9.0",
    "babel-preset-es2015": "*"
}

Jefferson估计你在这里直接npm install多半要挂,会报一个npm-cache文件夹下关于tar.gz包的.lock文件的错(wtf....这是什么鬼)。
看了一下各个plugin的依赖,发现很多plugin都依赖同一个lodash4.16.5版本的包,估计是解压时候读文件碰到死锁了。
解决方案嘛,一共20个左右的plugins,3个一组npm install装一下,然后再添3个,再继续npm install,亲测有效。

经过性能测试发现,PaBabelProcessor在初始化时会等待10s左右,然后单个读文件并转化会比较快(平均0.05s一个,应该还算是达到预期了)。node命令行下单独用babel-core转化多个文件,效果雷同。这样如果碰到一个30+个action(100个mvc文件左右)的项目,BabelProcessor耗时大约会在半分钟,占总耗时的40%-50%。

值得一提的是,在发现babel6对export default的支持不佳时,我试图将坑归结在er3的window.require上。
由于框架的er3是用window.require的导致得到的Action不再是类而是{default: 类},于是我试图用一段代码改er3来解决(其实也可以work,但改框架源码毕竟不好是吧):

:::javascript
else if (typeof SpecificAction['default'] === 'function') {
    // es6模块build时是babel6,而webServer下是babel5就ok
    SpecificAction = SpecificAction['default'];
}

呵呵,如果er3的代码有更新的es6版本,那估计就不会有这个“歧途”了,也不会有上面的,对babel5 babel6编译出来的结果所进行的差异化研究了。

附录

这次兼容es6升级所提交的cooder issue

好书1:EcmaScript 6入门
好书2:Babel 入门教程

任何问题和建议,Jefferson Deng waiting for you.

React嵌vue和vue嵌react

背景

调研@baidu/cube-sdk

调研纪要(占个坑)

背景是一个Table,里面每个cell都是一个reactNode,里面套vue
需要调研一下可行性,以及性能开销是否大

附录

vuera

漫谈Generator、yield、Thunk与co

Generator、yield

Generator是es2015(es6)的新特性,设计的初衷是为了能够简便地生成一系列对象,比如斐波那契数列(Fibonacci Sequence):

:::javascript
function* fibo() {
    let [a, b] = [1, 1];
    yield a;
    yield b;
    while (true) {
        [a, b] = [b, a + b];
        yield b;
    }
}
let gen = fibo();
for (var i = 0; i < 7; ++i) {
    console.log(gen.next().value); // 1 1 2 3 5 8 13
}

可能你会感到奇怪:为什么函数里面有死循环但没有导致死机?yield是神马鬼?我们给出解答:
生成器函数内的yield是一种新语法特性。类似return,但并非退出函数,而是切出生成器运行时。
比如这里while (true),可以看做一个长瑞士卷,yield就是切一刀,yield返回值就是其纹路。
每一次yield可以带出一个值(回到主线程);主线程也可以返回一个值去生成器运行时(协程),来看:

:::javascript
function* getNumbers() {
    var v1 = yield 0;
    console.log(v1);
    var v2 = yield 1;
    console.log(v2);
    return 5;
}
var nums = getNumbers();
console.log(nums.next(2)); // {value: 0, done: false},此时切出
console.log(nums.next(3)); // 先打印3,再{value: 1, done: false}
console.log(nums.next(4)); // 先打印4,再{value: 5, done: true}

这次是yield*的范例:

:::javascript
function* aGen(i) {
    yield i + 1;
    yield i + 3;
}
function* bGen(i) {
    yield i;
    yield* aGen(i + 4);
    yield i + 10;
}
var gen = bGen(10);
console.log(gen.next().value); // 10
console.log(gen.next().value); // 15
console.log(gen.next().value); // 17
console.log(gen.next().value); // 20

解决回调地狱

先看看噩梦般的金字塔(用sleep来模拟请求后端api):

:::javascript
function sleep(cb) {
    setTimeout(cb, 1000);
}
function getAuthor(cb) {
    sleep(function () { // () => {
        cb('dyj');
    });
}
function getBlog(cb, author) {
    sleep(function () { // () => {
        cb('github.com/Jefferson' + author);
    });
}
getAuthor(function (author) { // author => {
    console.log(author);
    getBlog(function (blog) { // blog => {
        console.log(blog);
    }, author);
});

这里已经是二层回调金字塔了,当异步操作越来越多时,嵌套变深,执行顺序不直观不利于维护。注:注释//后面的是es6语法糖

我们试着用Generator+yield旨在解决回调地狱的梗,把异步写成同步:

:::javascript
function co(gen) {
    const runner = gen(resume);
    function resume() {
        runner.next(...arguments);
    }
    runner.next();
}
co(function* (cb) {
    const author = yield getAuthor(cb);
    console.log(author);
    const blog = yield getBlog(cb, author);
    console.log(blog);
});

效果一样,猛一看大惊,匿名函数里面应该是异步执行的代码TM还真写成同步了。这正是依赖Generator的控制流特性:

  • 开始时 runner.next()
  • yield getAuthor(resume)
  • getAuthor(resume)
  • 异步操作完成,resume('dyj')
  • runner.next('dyj')
  • yield getBlog(resume, 'dyj')
  • getBlog(resume, 'dyj')
  • 异步操作完成,resume('github.com/Jeffersondyj')

可以看到,控制流运作完全符合预期。我们只需要关心逻辑顺序,而不用关注异步调用何时返回。resume保证流程推进。

美中不足的是,上述代码依然不够优雅,我们需要在Generator里面手动传递cb这个回调函数(该函数和业务无关)。

怎么破?于是乎,我们给出的办法就是——用thunk化来延迟求值。

Thunk与co

Thunk是一种“传名调用”的实现,将参数放到一个临时函数中去,将临时函数传入函数体,这个临时函数就叫Thunk函数。
在JS中,Thunk函数替换的不是表达式,而是一个多参数函数,替换成单参数函数,并只接受回调函数作为参数:

:::javascript
fs.readFile(fileName, callback); // 多参数
var readFileThunk = thunkify(fileName);
readFileThunk(callback); // 单参数
// 下面是这个特定case(读取文件)的thunkify简单实现
function thunkify(fileName) {
    return function (callback) {
        return fs.readFile(fileName, callback);
    }
}

任何函数只要参数有回调,就能写成Thunk形式,下面是一个thunkify简单转换器的实现:

:::javascript
function thunkify(fn) {
    return function () {
        var args = Array.prototype.slice.call(arguments);
        return function (callback) {
            args.push(callback); // 如果callback是第一个参数,就得unshift
            return fn.apply(this, args);
        };
    };
}
var readFileThunk = thunkify(fs.readFile);
readFileThunk(fileName)(callback);

最后结合上一节的Sample,得出终极解决方案:

:::javascript
function co(gen) {
    const runner = gen(resume);
    function resume() {
        const thunkcall = runner.next(...arguments);
        if (!thunkcall.done) {
            thunkcall.value(resume);
        }
    }
    resume();
}
co(function* () {
    const author = yield thunkify(getAuthor)();
    console.log(author);
    const blog = yield thunkify(getBlog)(author);
    console.log(blog);
});

注意:这里thunkify里面要用unshift。至此,异步调用被交到了resume中执行,不用在task中关注回调的问题。

你没看错,这个就是TJ大神开发的co基本原理,本文就是学习随笔+心得。

附录

es2015实战
Thunk函数的含义和用法
TJ大神github

任何问题和建议,Jefferson Deng waiting for you.

谈谈跨域请求(ZZ)

同源策略

同源策略的文档模型

同源策略(Same Origin policy,SOP),也称为单源策略(Single Origin policy),它是一种用于Web浏览器编程语言(如JavaScript和Ajax)的安全措施,以保护信息的保密性和完整性。

同源策略能阻止网站脚本访问其他站点使用的脚本,同时也阻止它与其他站点脚本交互。

一个实验

|* 原始资源 | 要访问的资源 | 非IE浏览器 | IE浏览器 *|
| http://example.com/a/ | http://example.com/b/ | 可以访问 | 可以访问 |
| http://example.com/ | http://www.example.com/ | 主机不匹配 | 主机不匹配 |
| http://example.com/a/ | https://example.com/a/ | 协议不匹配 | 协议不匹配 |
| http://example.com:81/ | http://example.com/ | 端口不匹配 | 可以访问 |

同源策略一开始是为了管理DOM之间的访问,后来逐渐扩展到Javascript对象,但并非是全部。

同源策略在提高了安全性,但同时也降低了灵活性。例如很难将login.example.com与payments.example.com两个域之间的数据可以方便的传送。

背景

XMLHttpRequest严格遵守同源策略,非同源不可请求。但是实践中经常会出现需要跨域请求资源的情况,例如某个子域名向负责进行用户验证的子域名请求用户信息等。

以往,有一种解决方案是利用JSONP进行跨域的资源请求,但这种方案存在着几点缺陷:

  • 1)只能进行GET请求
  • 2)要确定jsonp的请求是否失败并不容易,大多数框架的实现都是结合超时时间来判定

因此在XMLHttpRequest v2标准下,提出了CORS(Cross Origin Resourse-Sharing)的模型,试图保证跨域请求的安全性。

基本模型

虽然说允许了XMLHttpRequest的跨域请求,但是这种许可并不是无条件的。

浏览器在进行请求时也会判断请求的合法性,验证是通过服务器的返回头来进行的,这时就涉及到了具体的请求类型,所以在标准中定义了简单跨域请求。

简单跨域请求就是满足下列条件的请求:

  • 1)请求方法为HEAD, GET或POST
  • 2)请求方法中没有设置请求头(Accept, Accept-Language, Content-Language, Content-Type除外)
  • 3)如果设置了Content-Type头,其值为application/x-www-form-urlencoded, multipart/form-data 或 text/plain

验证简单跨域请求与非简单跨域请求合法性的区别就在于,验证非简单跨域请求前,浏览器向服务器发送一个OPTIONS方法的预检请求来加以判断,如果预检失败,实际请求将被丢弃。而简单跨域请求浏览器会正常发送请求,再对返回头加以判断以检查请求的合法性。在检查失败时,浏览器将会阻止脚本对返回内容的访问。

不满足的,都是复杂跨域请求。它可能导致向服务端发送2次请求(比如contentType为“application/json”的跨域请求)。

通过setRequestHeader('X-Request-With', null)可以避免浏览器发送OPTIONS请求。

当使用cors发送跨域请求时失败时,后台是接收到了这次请求(可能也执行了数据查询操作),只是响应头部不合符要求,浏览器阻断了这次请求。

请求时发送的HTTP头

简单跨域请求并不会包含下面的HTTP头。而预检请求将会发送以下HTTP头:

  • 1)Origin: 普通的HTTP请求也会带有,在CORS中专门作为Origin信息供后端比对
  • 2)Access-Control-Request-Method: 接下来请求的方法,例如PUT, DELETE等等
  • 3)Access-Control-Request-Headers: 自定义的头部,所有用setRequestHeader方法设置的头部都将会以逗号隔开的形式包含在这个头中

其他头,例如实际请求的头部,Cookie头等都将不被包含在预检请求中。

返回的HTTP头

浏览器主要通过返回的这些HTTP头判断请求是否合法。值得注意的一点是,预检请求通过并不代表请求一定会成功,如果预检请求时服务器返回的HTTP头使浏览器判断请求合法,从而发出了实际请求,但是实际请求的返回头中含有的访问控制头显示请求不合法时,浏览器仍会判定请求不合法,从而向脚本隐藏返回的细节。

  • 1)Access-Control-Allow-Origin: 允许跨域访问的域,可以是域的列表,也可以是通配符"*"。Origin规则只对域名有效,并不会对子目录有效(不同子域名需要分开设置)
  • 2)Access-Control-Allow-Credentials: 是否允许请求带有验证信息,这部分将会在下面详细解释
  • 3)Access-Control-Expose-Headers: 允许脚本访问的返回头,请求成功后,脚本可以在XMLHttpRequest中访问这些头的信息(貌似webkit没有实现这个)
  • 4)Access-Control-Max-Age: 缓存此次请求的秒数。在这个时间范围内,所有同类型的请求都将不再发送预检请求而是直接使用此次返回的头作为判断依据(大幅优化请求次数)
  • 5)Access-Control-Allow-Methods: 允许使用的请求方法,以逗号隔开
  • 6)Access-Control-Allow-Headers: 允许自定义的头部,以逗号隔开,大小写不敏感

无论是预检请求或是实际请求,如果在Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Access-Control-Allow-Methods, Access-Control-Allow-Headers的检查失败,就会被视为请求失败。

Credentials

在跨域请求中,默认情况下,HTTP Authentication信息,Cookie头以及用户的SSL证书无论在预检请求中或是在实际请求都是不会被发送的。

但是,通过设置XMLHttpRequest的credentials为true,就会启用认证信息机制。

虽然简单请求还是不需要发送预检请求,但此时判断请求是否成功需要额外判断Access-Control-Allow-Credentials,如果Access-Control-Allow-Credentials为false,请求失败。

十分需要注意的的一点就是此时Access-Control-Allow-Origin不能为通配符_,如果Access-Control-Allow-Origin是通配符_的话,仍将认为请求失败。

即便是失败的请求,如果返回头中有Set-Cookie的头,浏览器还是会照常设置Cookie。

其他

安全性:最大的隐患就在于某些偷懒的程序员会将Access-Control-Allow-Origin设置为"*"。另外简单请求其实是发出的,这时候防止CSRF是有必要的。

参考文献

中英文都适用的MD5加密算法

一个字符串->MD5(32位加密)的算法(自己包了AMD模块化)。

适配中文,比起网上开源的算法,关键在于有一个utf8的converter:

:::javascript
define(function (require) {
return function (sMessage) {
    function rotateLeft(lValue, iShiftBits) {
        return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
    }

    function addUnsigned(lX, lY) {
        var lX8 = (lX & 0x80000000);
        var lY8 = (lY & 0x80000000);
        var lX4 = (lX & 0x40000000);
        var lY4 = (lY & 0x40000000);
        var lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);

        if (lX4 & lY4) {
            return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
        }
        if (lX4 | lY4) {
            if (lResult & 0x40000000) {
                return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
            } else {
                return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
            }
        } else {
            return (lResult ^ lX8 ^ lY8);
        }
    }

    function f(x, y, z) {
        return (x & y) | ((~x) & z);
    }

    function g(x, y, z) {
        return (x & z) | (y & (~z));
    }

    function h(x, y, z) {
        return (x ^ y ^ z);
    }

    function i(x, y, z) {
        return (y ^ (x | (~z)));
    }

    function ff(a, b, c, d, y, s, ac) {
        a = addUnsigned(a, addUnsigned(addUnsigned(f(b, c, d), y), ac));
        return addUnsigned(rotateLeft(a, s), b);
    }

    function gg(a, b, c, d, y, s, ac) {
        a = addUnsigned(a, addUnsigned(addUnsigned(g(b, c, d), y), ac));
        return addUnsigned(rotateLeft(a, s), b);
    }

    function hh(a, b, c, d, y, s, ac) {
        a = addUnsigned(a, addUnsigned(addUnsigned(h(b, c, d), y), ac));
        return addUnsigned(rotateLeft(a, s), b);
    }

    function ii(a, b, c, d, y, s, ac) {
        a = addUnsigned(a, addUnsigned(addUnsigned(i(b, c, d), y), ac));
        return addUnsigned(rotateLeft(a, s), b);
    }

    function convertToWordArray(sMessage) {
        var lWordCount;
        var lMessageLength = sMessage.length;
        var lNumberOfWordsTemp1 = lMessageLength + 8;
        var lNumberOfWordsTemp2
            = (lNumberOfWordsTemp1 - (lNumberOfWordsTemp1 % 64)) / 64;
        var lNumberOfWords = (lNumberOfWordsTemp2 + 1) * 16;
        var lWordArray = new Array(lNumberOfWords - 1);
        var lBytePosition = 0;
        var lByteCount = 0;
        while (lByteCount < lMessageLength) {
            lWordCount = (lByteCount - (lByteCount % 4)) / 4;
            lBytePosition = (lByteCount % 4) * 8;
            lWordArray[lWordCount] = (lWordArray[lWordCount]
                | (sMessage.charCodeAt(lByteCount) << lBytePosition));
            lByteCount++;
        }
        lWordCount = (lByteCount - (lByteCount % 4)) / 4;
        lBytePosition = (lByteCount % 4) * 8;
        lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
        lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
        lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
        return lWordArray;
    }

    function wordToHex(lValue) {
        var WordToHexValue = '';
        var WordToHexValueTemp = '';
        for (var lCount = 0; lCount <= 3; lCount++) {
            var lByte = (lValue >>> (lCount * 8)) & 255;
            WordToHexValueTemp = '0' + lByte.toString(16);
            WordToHexValue = WordToHexValue + WordToHexValueTemp
                .substr(WordToHexValueTemp.length - 2, 2);
        }
        return WordToHexValue;
    }

    function uTF8Encode(string) {
        string = string.replace(/\x0d\x0a/g, '\x0a');
        var output = '';
        for (var n = 0; n < string.length; n++) {
            var c = string.charCodeAt(n);
            if (c < 128) {
                output += String.fromCharCode(c);
            } else if ((c > 127) && (c < 2048)) {
                output += String.fromCharCode((c >> 6) | 192);
                output += String.fromCharCode((c & 63) | 128);
            } else {
                output += String.fromCharCode((c >> 12) | 224);
                output += String.fromCharCode(((c >> 6) & 63) | 128);
                output += String.fromCharCode((c & 63) | 128);
            }
        }
        return output;
    }

    var S11 = 7;
    var S12 = 12;
    var S13 = 17;
    var S14 = 22;
    var S21 = 5;
    var S22 = 9;
    var S23 = 14;
    var S24 = 20;
    var S31 = 4;
    var S32 = 11;
    var S33 = 16;
    var S34 = 23;
    var S41 = 6;
    var S42 = 10;
    var S43 = 15;
    var S44 = 21;
    // Steps 1 and 2. Append padding bits and length and convert to words
    sMessage = uTF8Encode(sMessage);
    var x = convertToWordArray(sMessage);
    // Step 3. Initialise
    var a = 0x67452301;
    var b = 0xEFCDAB89;
    var c = 0x98BADCFE;
    var d = 0x10325476;

    // Step 4. Process the message in 16-word blocks
    for (var k = 0; k < x.length; k += 16) {
        var AA = a;
        var BB = b;
        var CC = c;
        var DD = d;
        a = ff(a, b, c, d, x[k + 0], S11, 0xD76AA478);
        d = ff(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
        c = ff(c, d, a, b, x[k + 2], S13, 0x242070DB);
        b = ff(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
        a = ff(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
        d = ff(d, a, b, c, x[k + 5], S12, 0x4787C62A);
        c = ff(c, d, a, b, x[k + 6], S13, 0xA8304613);
        b = ff(b, c, d, a, x[k + 7], S14, 0xFD469501);
        a = ff(a, b, c, d, x[k + 8], S11, 0x698098D8);
        d = ff(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
        c = ff(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
        b = ff(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
        a = ff(a, b, c, d, x[k + 12], S11, 0x6B901122);
        d = ff(d, a, b, c, x[k + 13], S12, 0xFD987193);
        c = ff(c, d, a, b, x[k + 14], S13, 0xA679438E);
        b = ff(b, c, d, a, x[k + 15], S14, 0x49B40821);
        a = gg(a, b, c, d, x[k + 1], S21, 0xF61E2562);
        d = gg(d, a, b, c, x[k + 6], S22, 0xC040B340);
        c = gg(c, d, a, b, x[k + 11], S23, 0x265E5A51);
        b = gg(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
        a = gg(a, b, c, d, x[k + 5], S21, 0xD62F105D);
        d = gg(d, a, b, c, x[k + 10], S22, 0x2441453);
        c = gg(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
        b = gg(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
        a = gg(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
        d = gg(d, a, b, c, x[k + 14], S22, 0xC33707D6);
        c = gg(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
        b = gg(b, c, d, a, x[k + 8], S24, 0x455A14ED);
        a = gg(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
        d = gg(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
        c = gg(c, d, a, b, x[k + 7], S23, 0x676F02D9);
        b = gg(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
        a = hh(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
        d = hh(d, a, b, c, x[k + 8], S32, 0x8771F681);
        c = hh(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
        b = hh(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
        a = hh(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
        d = hh(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
        c = hh(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
        b = hh(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
        a = hh(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
        d = hh(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
        c = hh(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
        b = hh(b, c, d, a, x[k + 6], S34, 0x4881D05);
        a = hh(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
        d = hh(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
        c = hh(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
        b = hh(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
        a = ii(a, b, c, d, x[k + 0], S41, 0xF4292244);
        d = ii(d, a, b, c, x[k + 7], S42, 0x432AFF97);
        c = ii(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
        b = ii(b, c, d, a, x[k + 5], S44, 0xFC93A039);
        a = ii(a, b, c, d, x[k + 12], S41, 0x655B59C3);
        d = ii(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
        c = ii(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
        b = ii(b, c, d, a, x[k + 1], S44, 0x85845DD1);
        a = ii(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
        d = ii(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
        c = ii(c, d, a, b, x[k + 6], S43, 0xA3014314);
        b = ii(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
        a = ii(a, b, c, d, x[k + 4], S41, 0xF7537E82);
        d = ii(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
        c = ii(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
        b = ii(b, c, d, a, x[k + 9], S44, 0xEB86D391);
        a = addUnsigned(a, AA);
        b = addUnsigned(b, BB);
        c = addUnsigned(c, CC);
        d = addUnsigned(d, DD);
    }
    // Step 5. Output the 128 bit digest
    var temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);
    return temp.toLowerCase();
};

});

React-Redux之我见

UI和容器

将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)

UI 组件有以下几个特征:

  • 只负责 UI 的呈现,不带有任何业务逻辑
  • 没有状态(即不使用this.state这个变量)
  • 所有数据都由参数(this.props)提供
  • 不使用任何 Redux 的 API

因为不含有状态,UI 组件又称为"纯组件",即它纯函数一样,纯粹由参数决定它的值。

而容器组件恰恰相反:

  • 负责管理数据和业务逻辑,不负责 UI 的呈现
  • 带有内部状态
  • 使用 Redux 的 API

总之,只要记住一句话就可以了:UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。

你可能会问,如果一个组件既有 UI 又有业务逻辑,那怎么办?
回答是,将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI组件。前者负责与外部的通信,将数据传给后者,由后者渲染出视图。

怎么实现呢?

connect

用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。比如:

:::javascript
// 连接,生成容器
define(function (require) {
    const app = require('app');
    const UI = require('./MyUI.ui');
    return app.connect(
        ({prop1}, {prop2}) => ({
            prop1Config: prop1.mapA[prop1.key],
            prop2Config: prop2.mapB[prop2.key]
        }),
        {
            onKeyChange({dispatch}, key) {
                dispatch({
                    type: 'foo/modKey',
                    payload: {key}
                });
            },
            onMapAppend({dispatch}, map) {
                dispatch({
                    type: 'foo/modMap',
                    payload: {map}
                });
            }
        }
    )(UI);
});

// MyUI.ui.js,生成UI
define(function (require) {
    const React = require('react');
    return props => {
        const {prop1Config, prop2Config} = props;
        return (
            // 你的组件是一段jsx,可能是由prop1 prop2计算出来,里面有事件onKeyChange,onMapAppend
        );
    };
});

上面代码里面prop1,prop2可能是全局model里面的某个状态值,注意到这个容器就是一个所谓的“可预测状态容器”——我们通过状态渲染出UI。

我们的UI组件里面的事件onKeyChange,onMapAppend,一旦事件被激发,则就会去修改全局状态(怎么修改可以看下面的Reducers)。

状态和reducer

何为“状态”?那就要提到我们自己所维护的Model了。以下是Model以及Model内的细节:

:::javascript
// 全局Model,组合多个局部状态
define(function (require) {
    const prop1 = require('./prop1');
    const prop2 = require('./prop2');
    return [
        prop1,
        prop2
    ];
});

// prop1的Model,以及申明Reducers
define(function (require) {
    const initialState = {
        key: 'stateAA'
        mapA: {}
    };
    return {
        namespace: 'foo',
        state: initialState,
        reducers: {
            modKey(state, {payload: {key}}) {
                return {
                    ...state,
                    key
                };
            },
            modMap(state, {payload: {map}}) {
                return {
                    ...state,
                    mapA: map
                };
            }
        }
    };
});

可以看到,多个局部Model构成了全局的状态机。它是只读的!改变它的唯一方法是激发事件,事件通过reducer来改变状态机——分离了异步逻辑与状态的变更。

我们在Reducers里面的namespace下命名事件,就是前面(第一段代码)可供作dispatch的事件——可以用来修改这一局部的Model:可以注意到每次reducer都是在返回一个新对象!

react-redux的优势

现在SPA的组件众多,交互繁多。状态分散在模块间。跨模块交互越发重要。

react-redux对于数据流比较多(混乱),状态更改不稳定,刷新多异步多(对model的影响多),非常合适用它做选型。

总之一个宗旨,通过Model渲染UI,UI里面的事件又去激发了reducer,重新生成了新Model,导致了UI被重渲染——综上构成了完整的数据流。

redux的渲染流简单(单循环),数据流唯一,更多时候只需要关注业务,以及对应的数据走向。呵呵redux复杂?不存在的,复杂的只是你的业务逻辑!

在我们的SPA里面,它可以做更多的事情(然而这里没有写):同构补水,状态持久化,undo/redo,埋点分析行为。

End

好书:React-Redux 的用法,一峰大神

欢迎大家给出建议,Jefferson Deng waiting for you.

一个图片轮播控件

(function ($) {

/**
 * slider控件构造函数
 *
 * @param {Object} opt 参数
 * @param {number} opt.num 一共几张图片
 * @param {number} opt.per 每张图片的宽/高
 * @param {string} opt.outer 外容器query
 * @param {string} opt.inner 内容器query
 * @param {number=} opt.animInv 切换完成时间
 * @param {number=} opt.slideInv 切换间隔时间
 * @return {Object}
 */
window.slider = function (opt) {
    var hover = false;
    var per = opt.per;
    var container = $(opt.outer);
    var items = $(opt.inner);
    var animInv = opt.animInv || 500;
    var slideInv = opt.slideInv || 4000;
    var pid = 0;
    var imgNum = opt.num || 3;

    /**
     * 往左横移图片
     *
     * @param {boolean=} force 强制
     */
    function scrollToLeft(force) {
        if (force || !hover) {
            container.animate({
                scrollLeft: per
            }, animInv, 'linear', function () {
                items.append(items.children().first());
                container.scrollLeft(0);
            });
        }
    }

    /**
     * 往右横移图片
     *
     * @param {boolean=} force 强制
     */
    function scrollToRight(force) {
        if (force || !hover) {
            items.prepend(items.children().last());
            container.scrollLeft(per);
            container.animate({scrollLeft: 0}, animInv, 'linear');
        }
    }

    /**
     * 如果鼠标悬浮在某张图片上,则不再自动切换
     */
    function regMouse() {
        container.mouseover(function () {
            hover = true;
        }).mouseout(function () {
            hover = false;
        });
    }

    /**
     * 为容器自动加长宽属性css
     */
    function setCss() {
        container.css('overflow', 'hidden');
        container.css('width', per + 'px');
        items.css('width', (per * imgNum) + 'px');
        items.children().css('width', per + 'px');
    }

    /**
     * 注册控件事件
     */
    function regCtrls() {
        container.find('.prev').click(function () {
            scrollToRight(true);
        });
        container.find('.next').click(function () {
            scrollToLeft(true);
        });
    }

    /**
     * 渲染左右控件
     *
     * @param {string} style 样式
     */
    function renderCtrls(style) {
        container.css('position', 'relative');
        container.append([
            '<div class="ctrl">',
            '<span class="prev btn" style="' + style + '"></span>',
            '<span class="next btn" style="' + style + '"></span>',
            '</div>'
        ].join(''));
    }

    /**
     * 初始化
     *
     * @param {string} style 样式
     */
    function reg(style) {
        regMouse();
        setCss();
        renderCtrls(style);
        regCtrls();
    }

    return {
        start: function (style) {
            reg(style);
            if (items.width() > per) {
                pid = setInterval(scrollToLeft, slideInv);
            }
        },
        setPer: function (newPer) {
            per = newPer;
            setCss();
        },
        stop: function () {
            if (pid <= 0) {
                clearInterval(pid);
            }
        }
    };
};

})(jQuery);

效果:图片/任意div块轮播,自动添加左右<>滑动。
后期如果有点状的icon也可以迭代定制。

前端请求缓存机制

首先还是需求起步,首先还是想要实现这样一个功能,左侧Tree的选中状态有变化时,用不同的参数请求右侧商品数据。

显而易见的方案

`
const cache = {};
const fetchData = async (url, params) => {
    const key = url + JSON.stringify(params);
    cache[key] = cache[key] || (await fetch(url, params));
    return cache[key];
};
`

诚然可以解决大部分情况下的问题,但是在后端性能一般时,某一次请求的response还未返回时,说不定下一个一模一样的请求又发出去了....于是:

缓存promise

经过高人指点,有了promise方案——缓存的不是response的结果,而是promise:

`
const cachedPromise = {};
const fetchData = (url, params) => {
    const key = url + JSON.stringify(params);
    cachedPromise[key] = cachedPromise[key] || new Promise((resolve, reject) => {
        console.log(`fetching ${key}`);
        setTimeout(() => { // 用你封装的ajax代替,我这里就用setTimeout模拟一段逻辑了
            console.log(`fetched ${key}`);
            resolve({success: true, result: Date.now()});
        }, 1500);
    });
    return cachedPromise[key];
};

for (let i = 0; i < 4; ++i) {
    setTimeout(() => fetchData('/data/a', {}).then(({result}) => console.log(result)), i * 1e3);
}
for (let i = 0; i < 4; ++i) {
    setTimeout(() => fetchData('/data/a', {t: 1}).then(({result}) => console.log(result)), i * 1e3);
}
`

运行结果:
fetching /data/a{} // 瞬出
fetching /data/a{"t":1} // 瞬出
fetched /data/a{} // 1.5s
2次1562155692698 // 1.5s
fetched /data/a{"t":1} // 1.5s
2次1562155692699 // 1.5s
1562155692698 // 2s
1562155692699 // 2s
1562155692698 // 3s
1562155692699 // 3s

进阶问题

你还可以设置超时时间,就像我之前有一篇localStorage里面说的一样,
入cache时,它的数据结构可以是 = {key: {time: 请求返回的时间, promise对象, timeout}}
之后fetchData取cache时,先用Date.now()和cache[key].time+timeout比对,如果超时则先清除

好了,这个代码Jefferson就撒个懒,就由你来实现了~

最后一个问题可能和缓存话题无关,左侧交互先请求A,又切换了一次变成请求B,又回到请求A的状态。然而因为时序问题B后于第二个A返回回来。变成左侧是A的状态,右侧显示的是B的结果....
wtf....

So,敬请期待后一篇,如何解决前端请求的“后发先至”问题~

实现一个DomWeightMonitor

图片

在我们项目的开发环境上加这个,通过发现dom数的突增或者深度的突增,可帮助我们发现卡顿(TTF)

vue版本

<template>
<div>
    <div>Dom Count: {{ monitor.count }}</div>
    <div>Dom Depth: {{ monitor.depth }}</div>
</div>
</template>

<script>
import debounce from 'lodash/debounce';

const walk = (node, currentDepth) => {
    let count = 1;
    let depth = currentDepth;
    const children = [...node.children];
    children.forEach(subDom => {
        const result = walk(subDom, currentDepth + 1);
        count += result.count;
        depth = Math.max(depth, result.depth);
    });
    return {count, depth};
};

export default {
    name: 'dom-weight-monitor',
    data() {
        return {
            monitor: {count: 0, depth: 0}
        };
    },
    mounted() {
        this.__changeHandler = debounce(this.handleDomChange, 500);
        document.getElementById('app')?.addEventListener('DOMSubtreeModified', this.__changeHandler, false);
    },
    beforeDestroy() {
       document.getElementById('app')?.removeEventListener('DOMSubtreeModified', this.__changeHandler, false);
    },
    methods: {
        handleDomChange() {
            this.monitor = walk(document.getElementById('app'), 1);
        }
    }
};
</script>

React Hook 版本

import {useState, useEffect} from 'react';
import {useDebouncedCallback} from '@huse/debounce';

const walk = (node, currentDepth) => {
    let count = 1;
    let depth = currentDepth;
    const children = [...node.children];
    children.forEach(subDom => {
        const result = walk(subDom, currentDepth + 1);
        count += result.count;
        depth = Math.max(depth, result.depth);
    });
    return {count, depth};
};

export function useDomWeightMonitor({rootId = 'root', timer = 500} = {}) {
    const [depth, setDepth] = useState(0);
    const [count, setCount] = useState(0);
    const domChangedHandler = useDebouncedCallback(() => {
        const res = walk(document.getElementById(rootId), 1);
        setDepth(res.depth);
        setCount(res.count);
    }, timer);

    useEffect(() => {
        document.getElementById(rootId)?.addEventListener('DOMSubtreeModified', domChangedHandler, false);
        return () => {
            document.getElementById(rootId)?.removeEventListener('DOMSubtreeModified', domChangedHandler, false);
        };
    }, []);

    return {depth, count};
}

Charles 攻略

安装:https://www.charlesproxy.com/download/
完成之后,你可以用附录1前半部分的攻略,先破解好
注意此时你还无法抓https的包,需要用附录2的攻略(可能那篇文章有点旧)

图片

把这个弄成信任即可,然后按照附录1一开头的配置一下ssl proxy
图片
此时你就可以用附录3的rewrite了,也可以用附录1后半部分的手机抓包,大功告成

值得一提的是:如果是抓mac本机的包,charles会跟公司的自动代理配置 http://pac.internal.baidu.com/bdnew.pac
这个冲突,必须关掉公司的翻墙代理!抓手机的包无所谓~

附录

用Express搭建一个服务端

首先来看package.json,和express3不同的是,body-parser,errorhandler,express-session,method-override不再是内嵌的了,所以都需要引入——

"dependencies": {
    "body-parser": "^1.5.2",
    "ejs": "^2.6.1",
    "errorhandler": "^1.1.1",
    "express": "^4.8.0",
    "express-session": "^1.7.2",
    "ftp-get": "*",
    "jade": "^1.5.0",
    "method-override": "^2.1.2",
    "moment": "*",
    "morgan": "^1.2.2",
    "multer": "^0.1.3",
    "promise": "*",
    "serve-favicon": "^2.0.1",
    "underscore": "*",
    "util": "*"
}

然后是app.js,第一部分是环境注册:

var express = require('express');
// var favicon = require('serve-favicon');
var logger = require('morgan');
var methodOverride = require('method-override');
// var session = require('express-session');
var bodyParser = require('body-parser');
var multer = require('multer');
var errorHandler = require('errorhandler');

var path = require('path');
var fs = require('fs');
var _ = require('underscore');
var app = express();
var PORT = 3000;

// all environments
app.set('port', process.env.PORT || PORT);
app.set('views', path.join(__dirname, '../frontend/views'));
var ejs = require('ejs');
app.engine('html', ejs.__express);
app.set('view engine', 'html');
app.use(logger('dev'));
app.use(methodOverride());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(multer());
app.use(express.static(path.join(__dirname, '../frontend')));

注意到我们在../frontend/views里面放html模版,这些模版的后缀都是.html文件
于是我们又要引入ejs,注释掉的session等是觉得可能会有内存溢出,../frontend里面除了views,还有index.html

随后我们看看第二部分,错误处理,日志输出——

var currentEnv = app.get('env');
console.log('Environment: ' + currentEnv);
if (currentEnv === 'development') {
    app.use(errorHandler({
        dumpExceptions: true, showStack: true
    }));
} else if (currentEnv === 'production') {
    app.use(errorHandler());
}
process.on('uncaughtException', function (err) {
    console.log(err);
});

这里比较精细地区分了dev环境和生产环境,既然如此,我们就在package.json里面写scripts——

"scripts": {
    "dev": "export NODE_ENV=development && node app",
    "prod": "export NODE_ENV=production && node app"
}

运行时直接npm run dev或者npm run prod即可。
最后是app.js的最后一部分——

// 注册控制中心
_.each(fs.readdirSync('./controllers/'), function (n) {
    if (String(n).indexOf('.') === 0) {
        return true;
    } else {
        require('./controllers/' + n)(app);
    }
});

// 注册路由
_.each(fs.readdirSync('./routers/'), function (n) {
    if (String(n).indexOf('.') === 0) {
        return true;
    } else {
        var routerPath = n.substring(0, n.lastIndexOf('.'));
        var router = require('./routers/' + n);
        app.use('/' + routerPath, router);
    }
});

// 开始监听端口
app.listen(app.get('port'), function () {
    console.log('Express server listening on port ' + app.get('port'));
});

很好懂,注册controllers和routers,最后监听3000。
后面我们会对controllers和routers都给一个sample。

首先是controllers下的范例:

var YourController = function (app) {
    app.get('/data/模块1/list', function (req, res) {
        util.log(req.method + ' request to url : ' + req.route.path);
        res.json({/* 你的json数据 */});
    });
};
module.exports = YourController;

你可以在controllers文件夹下开多个文件,每个文件一个模块,放/data/模块*/list|add之类的请求,Method=get post等都能支持。

随后是routers下的范例sample.js:

var express = require('express');
var router = express.Router();

/* /sample */
router.get('/', function (req, res) {
    res.render('sample', {param1: '参数1'}); 
});

/* /sample/about */
router.get('/about', function (req, res) {
    res.render('about', {param2: '参数2'});
});

module.exports = router;

注意到app.js里面注册路由时是根据取到的文件名——

var routerPath = n.substring(0, n.lastIndexOf('.'));
var router = require('./routers/' + n);
app.use('/' + routerPath, router);

于是sample.js里面注册的都是/sample下属的路径 /sample /sample/about,而这里render时所取的tpl分别就是../frontend/views下的sample.html和about.html,里面可以支持写参数,格式如:<%= param1 %>

最后,你就可以在../frontend/index.html里面写你的业务js,验证路由,验证controllers下的请求~大功告成!

自己实现一个Promise

function final(status, value) {
    const promise = this;
    let fn;
    if (promise._status !== 'PENDING') {
        return;
    }
    
    setTimeout(() => {
        promise._status = status;
        const st = promise._status === 'FULFILLED';
        const queue = promise[st ? '_resolves' : '_rejects'];
        while (fn = queue.shift()) {
            value = fn.call(promise, value) || value;
        }
        promise[st ? '_value' : '_reason'] = value;
        promise['_resolves'] = promise['_rejects'] = undefined;
    });
}

export default function Promise(resolver) {
    if (!(typeof resolver === 'function')) {
        throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
    }
    // 如果不是promise实例,就new一个
    if (!(this instanceof Promise)) {
        return new Promise(resolver);
    }

    const promise = this;
    promise._status = 'PENDING';
    promise._resolves = [];
    promise._rejects = [];

    function resolve(value) {
        final.apply(promise, ['FULFILLED'].concat([value])); // value可能是数组
    }
    function reject(reason) {
        final.apply(promise, ['REJECTED'].concat([reason]));
    }
    resolver(resolve, reject);
}

Promise.prototype.then = function (onFulfilled, onRejected) {
    const promise = this;
    return new Promise((resolve, reject) => {
        function handle(value) {
            var ret = typeof onFulfilled === 'function' && onFulfilled(value) || value;
            if (ret && typeof ret['then'] === 'function') {
                ret.then(resolve, reject);
            } else {
                resolve(ret);
            }
        }

        function callback(reason) {
            reason = typeof onRejected === 'function' && onRejected(reason) || reason;
            reject(reason);
        }

        if (promise._status === 'PENDING') {
            promise._resolves.push(handle);
            promise._rejects.push(callback);
        } else if (promise._status === 'FULFILLED') { // 状态改变后的then操作,立刻执行
            handle(promise._value);
        } else if (promise._status === 'REJECTED') {
            callback(promise._reason);
        }
    });
};

Promise.resolve = function (...args) {
    return Promise(resolve => {
        resolve(...args);
    });
};

Promise.reject = function (...args) {
    return Promise((resolve, reject) => {
        reject(...args);
    });
};

Promise.all = function (promises) {
    if (!Array.isArray(promises)) {
        throw new TypeError('You must pass an array to Promise.all');
    }
    return Promise((resolve, reject) => {
        const result = [];
        let count = promises.length;

        // 这里与race中的函数相比,多了一层嵌套,要传入index
        function resolver(index) {
            return function (value) {
                resolveAll(index, value);
            };
        }
        function resolveAll(index, value) {
            result[index] = value;
            if (--count === 0) {
                resolve(result);
            }
        }

        for (let i = 0; i < promises.length; ++i) {
            promises[i].then(resolver(i), reject);
        }
    });
};

Promise.race = function (promises) {
    if (!Array.isArray(promises)) {
        throw new TypeError('You must pass an array to Promise.race');
    }
    return Promise((resolve, reject) => {
        for (let i = 0; i < promises.length; ++i) {
            promises[i].then(resolve, reject);
        }
    });
};

word-break:break-all和word-wrap:break-word的区别

word-break: normal / break-all / keep-all
normal: 使用默认的换行规则
break-all: 允许任意非CJK(Chinese/Japanese/Korean)文本间的单词断行
keep-all: 不允许CJK(Chinese/Japanese/Korean)文本中的单词换行,只能在半角空格或连字符处换行。非CJK文本的行为实际上和normal一致

word-wrap: normal / break-word
normal: 就是大家平常见得最多的正常的换行规则,文本会默认在空格之处换行,如果单个单词过长,而容器宽度不足够容纳这个单词,单词会溢出容器(非常坑)
break-word: 一行单词中实在没有其他靠谱的换行点的时候换行,在上面的基础上,会对过长的单词做词内断词处理,这样单词始终会在容器中,不会溢出容器

word-break:break-all(谐音:微博吧)正如其名字,所有的都换行。毫不留情,一点空隙都不放过
而word-wrap:break-word(谐音:我五百万)之所以换行,很可能是因为下面一行文字占满了整个容器的一行,于是就换行处理了

漫谈localStorage

在H5大行其道的今天,localStorage对每一个FE攻城师来说都不太陌生。
我们都知道localStorage是用于本地存储的,那么我们该如何用好这个特性呢?

容量

localStorage给我们带来了极大的便利,不同于cookies的小家子气,localStorage的存储量还是很可观的。
根据目前的市场上浏览器对loaclStorage的兼容性来看,最大容量最好是定为2560KB(safari),而chrome和firefox都是5120KB。这些数据可以通过逐步增大setItem来做实验得到。

注意:这里5120KB,是针对一个域名下(并非所有域名)。
知道了最大容量,如何知道当前域名下,剩余的localStorage容量呢?很简短——

::javascript
var size = 0;
for (var key in localStorage) {
    if (localStorage.hasOwnProperty(key)) {
        size += localStorage.getItem(key).length;
    }
}
return size;

时效

有人问:localStorage有失效时间没有?
这就好比问:「MySQL 怎么设置过期时间?D盘怎么设置过期时间?」
答案是:不能(我靠,如果这都能,码农分分钟要叛乱了)。
localStorage亦然。
有人不甘心:但是我用惯cookie了,真的很想设置,咋办?也可以——

 ::javascript
 function setLocalStorage(key, value) {
    localStorage.setItem(key, JSON.stringify({
        data: value, time: new Date().getTime()
    }));
}

function getLocalStorage(key, exp) {
    exp = exp || 1000 * 86400; // 默认一天过期
    var data = localStorage.getItem(key);
    if (data) {
        var obj = JSON.parse(data);
        if (new Date().getTime() - obj.time < exp) {
            return obj.data;
        }
        localStorage.removeItem(key);
    }
    return null;
}

经过了这样的封装,配套上述的get和set写法,就可以实现设置失效时间了。
but,你刚刚自己说的,localStorage有容量问题,现在就逃避话题了?
浏览器会告诉你:当容量超出时,不会存储数据,也不会覆盖现有数据,而是会引发 QUOTA_EXCEEDED_ERR 异常。

soga! so you guo duo ga! 那作为开发人员的我们该怎么办?有办法——

::javascript
function setLocalStorage(key, value) {
    try {
        localStorage.setItem(key, JSON.stringify({
            data: value, time: new Date().getTime()
        }));
    } catch (exception) {
        if (exception.name == 'QuotaExceededError') {
            localStorage.clear();
            setLocalStorage(key, value);
        }
    }
}

秋,秋德玛黛,一旦有exception你就直接clear()了,就删光了?这不是强奸用户吗?
应该找到最旧的那个,先removeItem这一个,然后再递归,发现如果还有exception,则继续删......
还有一个问题,比如value的长度太大,直接超过了2560KB(虽然不常见),如上机制会死循环,所以单个value的长度也是要判断的,出于健壮性。
好了,这段优化并不麻烦,就由读者你来完成吧......

事件

当localStorage或者sessionStorage中存储的值发生变化时,就会触发storage事件。
类似于click事件一样,其定义的方式也是一样,可以通过addEventListener来实现。
但是需要注意的是:在默认情况下storage事件的触发是发生在同源下的不同页面中的。
换句话说,如果我们修改localStorage中存储的值,然后在同一个页面设置storage事件,这样是无法触发的。
有人还是不甘心:我就想要在当前页面监听修改localStorage值的事件,该如何实现呢?

::javascript
function setLocalStorage(key, value) {
    var event = new Event('setItemEvent');
    event.newValue = value;
    window.dispatchEvent(event);
    localStorage.setItem(key, value);
};

window.addEventListener('setItemEvent', function (e) {
    console.log('修改后的值:' + e.newValue);
});

其他

我就直接ZZ了,鑫神这篇还是写的很细致的:清除各个浏览器中的数据研究

漏洞挖掘初探

首先我们普及一下CSS和CSRF。

XSS

XSS攻击全称跨站脚本攻击,是为不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS,XSS是一种在web应用中的计算机安全漏洞,它允许恶意web用户将代码植入到提供给其它用户使用的页面中。

总结下来就是:不被预期的跨站恶意脚本,在受害者浏览器环境中被执行。

反射型XSS

http://www.foo.com/index.php?query=<script>alert(1)</script>
典型的例子是,站内搜索的input框之类。

存储型XSS

提交的XSS代码已经入库到服务端(db,文件系统等),典型的例子是留言板。

DOM XSS

完全不经过服务端处理响应的,触发xss就是靠浏览器端的解析,比如:

http://www.foo.com/index.php#alert(1)
这个页面里面的js如果有写 eval(location.hash.substr(1)); 就sb了...

危害

挂马、盗取cookie、钓鱼等。

CSRF

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。

攻击通过在授权用户访问的页面中包含链接或者脚本的方式工作。比如编写一个银行站点上进行取款的form提交的链接(如果是Get请求那更简单),并将此链接作为图片src发到公共论坛上。如果该银行站点在cookie中保存客户的授权信息,并且受害人的此cookie没有过期,那么受害者的浏览器尝试load图片时将提交这个取款form和他的cookie。

总结下来这种攻击有3个关键点:跨站发出一个请求、无JS参与、请求得是身份认证过后的。

HTML CSRF

就是刚刚那个场景,img的src lowsrc dynsrc;iframe、frame、bgsound、video、audio、embed的src;css里面的background等。

JSON HiJacking

很多api的参数可以带callback(手百端能力就是一个典型的在api里面传callback的范例),则可以编写一个页面链接发到公共论坛,里面就是访问api,callback里面将服务器查询得到的json数据用image的src传到攻击者的服务器上,比如查询站内私信,联系人等——就让受害者的隐私一览无余。

Flash CSRF

危害

篡改目标站点的用户数据、盗取隐私、传播CSRF蠕虫等。

漏洞挖掘

重点其实在XSS上,CSRF的漏洞挖掘相对简单一些,你只需要确认:

  • 目标表单是否有有效的token随机串
  • 目标表单是否有验证码
  • 目标是否判断了referer
  • 网站根目录的crossdomain.xml的allow-access-from-domain是否是通配符
  • 目标api的JSON数据可以自定义callback函数

反射型XSS挖掘

一个普通的url:http://www.foo.com/product.php?id=1

攻击者会进行这些XSS测试,尝试以下payload:

<script>alert(1)</script>
"><script>alert(1)</script>
<img src=@ onerror=alert(1) />
" onmouseover=alert(1) x="
javascript:alert(1)//
</script><script>alert(1)//
";alert(1)//
}x:expression(alert(1))

无非针对的是以下几个输出点:

  • html标签之间
  • html标签之内
  • 成为js代码的值
  • 成为css代码的值

html标签之间

一般情况这里掠过,主要是<title> <textarea> <iframe> <plaintext>这些,里面如果出现<script>alert(1)</script>会弹出alert吗?不会!

这些无法执行脚本的标签,你可以将payload尝试变为:</title><script>alert(1)</script>即可。

html标签之内

最普通的场景还是input里面触发,<input type="text" value="Your XSS here" />。来看看:

" onmouseover=alert(1) x="
"><script>alert(1)</script>

前者闭合属性,on事件触发;后者强行闭合标签,直接执行。哪种更好呢?

刚刚是普通input,如果碰到<input type="hidden" value="Your XSS here" />呢?

只能强行闭合标签试试了,否则有hidden特性在,我们是触发不了xss的。

如果type写在后面,<input value="Your XSS here" type="hidden" />呢?

反而有机可乘:1" onmouseover=alert(1) type="text,这时候输出不再是一个hidden,而是一个text型的input!

我们得到一个小Tip:代码中写标签时,最好第一个属性就申明type,至少不要把value放在type之前。

成为js代码的值

比较少见,比如<script>a="Your Xss here";换行符;</script>

成为css代码的值

style中执行脚本是IE的独有特性,expression关键词注入,并适当闭合即可攻击。

存储型XSS挖掘

和上一节反射型差不多,无非是库里面的xss脚本最终显示在哪里,输出在哪里?一般有这几种:

  • 表单提交后跳转到到新页面
  • 表单所在页面就是输出点
  • 上述都没有,则需要去整个网站找目标输出点,这时候估计只能使用爬虫大法了....

DOM XSS挖掘

这个topic也很有意思,涉及到html与js的自解码机制。

自解码机制

例子1:<input type="button" value="exec" onclick="document.write('<img src=@ onerror=alert(123) />')" />

例子2:<input type="button" value="exec" onclick="document.write(htmlEscape('<img src=@ onerror=alert(123)) />')" />

例子3:<input type="button" value="exec" onclick="document.write('&lt;img src=@ onerror=alert(123) /&gt;')" />

在2和3中,document.write但值是一样的,实际结果?

原因:js是出现在html标签里面的,意味着这里的js可以进行html形式的编码,编码可以有以下2种:

  • 进制编码:&#xH(16进制),&#D(10进制)
  • HTML实体编码:即&lt;img src=@ onerror=alert(123) /&gt;

在js执行之前,html形式的编码会自动解码,所以例子1和3效果是一样的,而例子2进行的转义是有效果的。

刚刚是一段在html里的例子,如果是js环境里面:

$(**).onclick = function () {document.write('<img src=@ onerror=alert(123)) />')}

$(**).onclick = function () {document.write('&lt;img src=@ onerror=alert(123) /&gt;')}

前者是可以出现弹出alert的,后者不会是因为这段编码的内容在js执行之前并不做自动解码。此时这段内容要遵守js编码法则,具体有以下几种:

  • Unicode形式:\uH(16进制)
  • 普通16进制:\xH
  • 纯转义:' " < >

$(**).onclick = function () {document.write('\<img src=@ onerror=alert(123) \/\>')} 这种也是毫无作用的。

encode功能的标签

之前一节是理解domxss的基础,我们进一步来看看差异:

令一个textarea.innerHTML='<img src=@ onerror=alert(123) />';alert(textarea.innerHTML)

令一个div.innerHTML='<img src=@ onerror=alert(123) />';alert(div.innerHTML)

前者弹出&lt;img src=@ onerror=alert(123) /&gt;,后者弹出<img src="@" onerror="alert(123)">

这是由于textarea标签本身的性质决定的,类似的标签之前提到过,还有<title> <iframe> <plaintext>,还少了3个<noscript> <noframes> <xmp>。这里<plaintext>在火狐和chrome下有差异,火狐下不编码,而chrome下会。

URL编码

主要是跨浏览器的URL编码差异,这个简单说结论:

  • firefox:编码'"`<>
  • Chrome:编码"<>
  • IE:不做任何编码

如果服务端语言直接吐出url中的内容,则在IE中很可能会出XSS漏洞,Chrome可能小范围漏洞,火狐相对安全。

自动化挖掘

上面的只是初探,要做自动化的漏洞检测其实是很复杂的,难度也很高。效率和检出率是矛盾的,目前商业性质的漏洞平台也是只能在这之间做tradeoff。

漫谈React中的key

key和react中的diff

react中的key属性比较特殊,它不是给开发者用的(你给组件设置key之后,无法从props得到key),而是给react自己用的。
怎么用的呢?待我细细说来。

diff机制

React这个框架的核心**是,将页面分割成一个个组件,一个组件还可能嵌套更小的组件,每个组件有自己的数据(属性/状态)。当某个组件的数据发生变化时,更新该组件部分的视图。更新的过程是由数据驱动的,新的数据自该组件顶层向下流向子组件,每个组件调用自己的render方法得到新的视图,并与之前的视图作diff比较差异,完成更新。

React通过virtual dom来实现高效的视图更新。
基本原理是用纯js对象模拟dom树,每当更新时,根据组件们的render方法计算出新的虚拟dom树,并与此前的虚拟dom树作diff,最后映射到真实dom树上完成视图更新。
而两棵树的完全的diff算法是一个时间复杂度为 O(n^3) 的问题。但是在前端当中,很少出现跨越层级移动DOM元素的情况。
于是,React采用了简化的diff算法,只会对virtual dom中同一个层级的元素进行对比,这样算法复杂度就可以达到 O(n)。

key

react利用key来识别组件,它是一种身份标识标识。每个key对应一个组件,相同的key react认为是同一个组件,这样后续相同的key对应组件都不会被创建。
有了key属性后,就可以与组件建立了一种对应关系,react根据key来决定是销毁重新创建组件还是更新组件:

  • key相同,若组件属性有所变化,则react只更新组件对应的属性;没有变化则不更新。
  • key值不同,则react先销毁该组件(有状态组件的componentWillUnmount会执行),然后重新创建该组件(有状态组件的constructor和componentWillUnmount都会执行)

深入&&实践

来一段代码:

:::javascript
render() {
    const {users} = this.props;
    return (
        <div>
            <Info key={users} />
        </div>
    );
}

循环数组里面出现key,用来方便框架做diff,这个很好理解。而这里key是干啥的?
答案是users变了之后,强制刷新Info组件。但是如果传prop进去,当prop变更后也可以更新啊,这个例子里面没有为什么没有传prop呢?

这可能就要说到生命周期了,prop变了的话不会再走一遍生命周期,而key的更新会。来看:
比如碰到一个更新操作,你需要在componentWillReceiveProps做很多事情(比如状态复原,关闭一些和前users相关的模态对话框,注销响应事件),你的componentWillReceiveProps方法里会充斥着很多复杂的业务代码,最后重置state,几周后自己都看不懂代码。

解决方案是什么?就是用key属性。key属性一旦变化,React会自动销毁之前的组件,用一个全新的组件来渲染——
我们可以从容地在componentWillUnmount里做清理工作,至于重置state这些工作已经不需要做了,由于组件是销毁和重建,这些被天然完成。

这样做是否会影响性能?可能会的。但只要key的改变不很频繁,那对性能的影响是很小的。而上面的例子表明,此举带来的收益会非常大。
一个观点:为了组件内部逻辑的清晰,应该在复杂而有状态的组件上使用key属性(只要改变不很频繁),这样能在合适的时候触发组件的销毁与重建,给组件一个健康的生命周期。

其他&&注意事项

由数组创建的子组件必须有key属性,否则的话你可能见到下面这样的warning:

Warning: Each child in an array or iterator should have a unique "key" prop.

为什么只有数组中的元素需要有唯一的key,而其他的元素一般不需要写呢?
答案是:React有能力辨别出,更新前后元素的对应关系。这一点,也许直接看JSX不够明显,看Babel转换后的React.createElement则清晰很多:

:::javascript
// 转换前
const element = (
    <div>
        <h3>example</h3>
        {[<p key={1}>hello</p>, <p key={2}>world</p>]}
    </div>
);

// 转换后
"use strict";
var element = React.createElement(
    "div",
    null,
    React.createElement("h3",null,"example"),
    [
        React.createElement("p",{key:1},"hello"), 
        React.createElement("p",{key:2},"world")
    ]
);

不管props如何变化,数组外的每个元素始终出现在React.createElement()参数列表中的固定位置,这个位置就是天然的key。

用好key还是很有讲究的,最后我们列了几点注意事项:

  • index做为key——u.map(arr, (item, index) => {...}); 反面教材!涉及到arr的动态变更,例如数组新增元素、删除元素或者重新排序等,这时会导致展示错误的数据。
  • key要稳定唯一。但这是有范围的,即在数组生成的同级同类型的组件上要保持唯一,而不是所有组件的key都要保持唯一。
  • key属性是添加到自定义的子组件上,而不是子组件内部的顶层的组件上。
  • key值相同,表示同一个组件,react不会重新销毁创建组件实例,只可能更新;key不同,react会销毁已有的组件实例,重新创建组件新的实例。

欢迎大家给出建议,Jefferson Deng waiting for you.

自动获取npm包版本,并自增

有的时候,我们发 npm 包时,要手动维护 package.json 里面的版本,殊为不易,所以考虑用脚本:

const semver = require('semver/preload');

function getNextVersion(version, level, preid) {
  if (!preid) {
    return semver.inc(version, level);
  }
  if (version.includes('-')) {
    return semver.inc(version, 'prerelease', preid);
  }
  return semver.inc(version, `pre${level}`, preid);
}

这样,就免除了手动维护自增(而且后面还有alpha,beta等release阶段)的烦恼

附上 UT:

it('getNextVersion', () => {
    expect(getNextVersion('0.1.0', 'patch')).toBe('0.1.1');
    expect(getNextVersion('0.1.1', 'minor')).toBe('0.2.0');
    expect(getNextVersion('0.2.1', 'major')).toBe('1.0.0');

    expect(getNextVersion('0.1.0-alpha.0', 'patch')).toBe('0.1.0');
    expect(getNextVersion('0.1.0-alpha.0', 'minor')).toBe('0.1.0');
    expect(getNextVersion('0.1.0-alpha.0', 'major')).toBe('1.0.0');

    expect(getNextVersion('0.1.0-alpha.0', 'patch', 'beta')).toBe('0.1.0-beta.0');
    expect(getNextVersion('0.1.0-beta.0', 'patch', 'beta')).toBe('0.1.0-beta.1');
    expect(getNextVersion('0.1.0-beta.0', 'minor', 'beta')).toBe('0.1.0-beta.1');

    expect(getNextVersion('0.8.2', 'major', 'beta')).toBe('1.0.0-beta.0');
    expect(getNextVersion('0.8.2', 'patch', 'beta')).toBe('0.8.3-beta.0');
});

仔细的读者不禁会问:getNextVersion 的第一个参数 version 是怎么来的呢,难道要读 package.json 文件?

其实可以用 npm show 的方法:

function validatePackageName(packageName) {
    return packageName && /^(@\w+\/)?[\w-]+$/.test(packageName);
}

function getNpmLatestVersion(packageName, {preid, limitid}) {
  if (!validatePackageName(packageName)) {
    throw new Error('Illegal package name');
  }

  let stdout = execSync(`npm show ${packageName} versions --json`);
  let versions = JSON.parse(stdout.toString('utf8'));
  if (limitid) {
    versions = versions.filter(item => item < limitid);
  }
  versions = versions.reverse();
  if (preid) {
    return versions.find(item => item.includes(preid));
  }
  return versions[0];
}

在 getNpmLatestVersion 中我们可以限制最高版本(limitid),也能查找到某个 preid 的(preid是release阶段:alpha beta rc)版本,并返回符合条件下,最新的那个

后面应该不需要我再说了吧,调一下

fse.writeJSONSync(package.json路径,具体json)

大功告成!ps:感谢凯哥(hekai02)的指导~

附录

angularjs做图片轮播(无滑动效果,仅仅淡入淡出)

:::javascript
app.directive('slide', ['$rootScope', function ($rootScope) {
return {
    restrict: 'AE',
    templateUrl: './views/slide-tpl.html',
    replace: false,
    scope: {
        list: '=',
        index: '='
    },
    link: function ($scope, elem, attr) {
        $scope.prev = function () {
            if ($scope.index <= 0) {
                $scope.index = $scope.list.length - 1;
            } else {
                $scope.index--;
            }
        };

        $scope.next = function () {
            if ($scope.index >= ($scope.list.length - 1)) {
                $scope.index = 0;
            } else {
                $scope.index++;
            }
        };

        $scope.moveTo = function (index) {
            $scope.index = index;
        };

        var slide = {
            timer: null,
            box: elem[0],
            init: function () {
                this.addTimer();
                this.addEvent();
            },
            addTimer: function () {
                this.timer = setInterval(function () {
                    $scope.next();
                    $scope.$digest();
                }, 4000);
            },
            removeTimer: function () {
                clearInterval(this.timer);
                this.timer = null;
            },
            addEvent: function () {
                elem.on('mouseenter', function () {
                    slide.removeTimer();
                });
                elem.on('mouseleave', function () {
                    slide.addTimer();
                });
            }
        };
        slide.init();

        $rootScope.$on('$locationChangeStart', function () {
            slide.removeTimer();
        });
    }
};
}]);

slide-tpl.html:

:::javascript
<div class="banner animate-show" ng-repeat="item in list">
    <img class="animate-show" ng-src="{{item}}">
</div>
<div class="ctrl">
    <span class="prev btn" ng-click="prev()"></span>
    <span class="next btn" ng-click="next()"></span>
</div>
<div class="dot">
    <span ng-repeat="item in list" ng-class="{true: 'active', false: ''}[$index==index]"
        ng-click="moveTo($index)"></span>
</div>

css:
.animate-show.ng-hide-remove {
-webkit-transition: 500ms linear all;
-moz-transition: 500ms linear all;
-o-transition: 500ms linear all;
transition: 500ms linear all;
}

.animate-show.ng-hide-remove.ng-hide-remove-active {
opacity: 1;
}

.animate-show.ng-hide-remove {
opacity: 0;
}

.animate-show.ng-hide-add,
.animate-show.ng-hide-add.ng-hide-remove-active {
display: none;
}

从react render里面的箭头函数说开去

起因

组里面某同学在render里面写了一个类似这种组件:

`
const {onClick, value} = this.props;
return (
    <Comp onClick={() => onClick(value)} />
);
`

然后触发了我厂的代码规范,具体是这样写的:JS-998,JSX props should not use arrow functions

于是某些情况下,他可以这样写:

`
onClick = () => {
     const {onClick, value} = this.props;
     onClick(value);
}

render() {
    return (
         <Comp onClick={this.onClick} />
    );
}
`

不过他依旧对于要另外写一个类成员方法耿耿于怀。况且有时候是函数式定义的stateless组件,这样还不得不改成用Class方式定义,也显得特别sb。

另外一种苟且的法门是:某些网上帖子推崇的,“推荐使用提取子组件或在 HTML 元素中传递数据的方式来避免绑定。”那就要用data-set了,并操作dom了。这里代码过于苟且,就不贴sample了。

经过

做了一番实验后发现:箭头函数在每次 render 时都会重新分配(和使用 bind 的方式相同)。所以,尽管我将组件写为 PureComponent,如果组件的props里面有父组件给的箭头函数,会导致子组件为所有的实例传递了一个新的函数。所以每个用户实例都会重新 render。

结论:

避免在 render 中使用箭头函数和绑定。否则会打破 shouldComponentUpdate 和 PureComponent 的性能优化。

image

解决

该怎么整呢?我review了一下组内其他同学的code,发现这种困扰之前也有。有的甚至直接不用箭头函数了,而是另外写原生function...

我的建议是使用useMemo useCallback等新特性,顺便在组内分享时安利给了所有组员。

`
const {onClick, value} = this.props;
const onClickMemo = useCallback(() => onClick(value), [onClick, value]);
return (
    <Comp onClick={onClickMemo} />
);

const computedVal = useMemo(() => 
    props.a + props.b,
    [props.a, props.b]
);
return <div>{computedVal}</div>;
`

useCallback:把匿名回调“存”起来,考虑到匿名方法会被反复重新声明而无法被多次利用,然后容易造成component反复不必要的渲染。

useMemo:从component props中获得原始数据, 计算、转换格式后再显示是非常常见的需求。useMemo可以轻松防止不必要的重复计算。

这俩hook没出之前可以用memoize库,现在可以替换成这个react官方的了~

其他

React组件规范里面我其实也已经写了蛮多,后续因为react版本的升级等缘故,某些当时觉得ok的写法,现在不合适了,后续在那个issue里面再更新吧~

window.open被拦截的问题

背景:在chrome的安全机制里面,非用户触发的window.open方法,是会被拦截的
原因:非用户行为导致的新窗口打开会触发浏览器的安全拦截机制,需要用户授权不拦截才能打开
那么,具体哪些场景下,会被拦截或者不被拦截呢?我们来看——

场景篇

$('#btn').click(() => { window.open('不会拦截'); });

而ajax的回调,会被拦截

ajax.request(params).then(() => { window.open('惨遭拦截'); });

那么,我们就重点针对这种场景做方案。

尝试篇

function newWindow(url) { 
    var a = document.createElement('a'); 
    a.setAttribute('href', url); 
    a.setAttribute('target', '_blank');
    document.body.appendChild(a);
    a.click(); 
}

贴一段简短的代码,mock一个a标签,后续可以优化比如说removeChild
运行一下,结果不起作用。

ok,我们换一个思路:

var tempWindow = window.open('', '_blank', '');
tempWindow.location.href = 'http://www.baidu.com';

依然不行。我们得到启发,既然是ajax回调里面不能open,我放在ajax调用之前先open好window呢?

解决篇

var win = window.open('');
win.document.write('文件下载中,请耐心等待...');
ajax.request(params).then(res => {
    win.location.href = res.fileName;
}, () => {
    win.close();
    // 母页面报错TODO
});

然而有一个问题,打开的新页面在下载成功的情况下,不能自动关闭。如果直接在win.location.href后面直接win.close(),会导致下载不能正常进行。用win.onload也无效。

为了追求极致,思来想去,还是决定用iframe来解决~

function download(url) {
    var divId = '__DownloadContainer__';
    var iframeId = '__DownloadIframe__';
    var div = document.getElementById(divId);
    if (!div) {
        div = document.createElement('div');
        div.id = divId;
        div.innerHTML = '<iframe id="' + iframeId
            + '" name="' + iframeId + '"></iframe>';
        div.style.display = 'none';
        document.body.appendChild(div);
    }
    div.getElementsByTagName('iframe')[0].src = url;
}

ajax.request(params).then(res => {
    download(res.fileName);
}, () => {
    // 母页面报错TODO
});

测一下跨浏览器兼容性,无错!大功告成。本篇博客比较简短,可能读者你早就知道这个技巧了~

最近的一些编程实践-react表单篇

是的,表单篇,后续还会有姐妹篇-列表篇

本篇主要讲用formik+yup来解决表单中的大部分问题,减少工程师的代码量,让精力集中在业务上

需求

首先还是用需求来讲这个sample,需求本身不是很复杂,甚至是一个常见的简单需求:

image

点了第一个edit按钮时:

image

点了下面那个edit按钮时:

image

注意到联系人可以添加多个。而且2个提交都是提交整个的表单数据,即中心名称和联系人list。

formik初探

本来也可以用一个Input 一个TODOlist形式的组件来搞,然后维护state: {name, contactList: [{name, email}]}。每个input的onChange都去改state

不过当我看见formik的各种布道者安利说:“Build forms in React, without the tears”——“用formik做React表单不相信眼泪”时,有点心动了。于是我们来看:

`
import {compose, keys, pick} from 'ramda';
import {withFormik} from 'formik';
import * as yup from 'yup';
import * as validators from './validator';

const selectors = {
    contactList: contactSelector,
    name: makeMcSelector('name')
};
const formikOption = {
    mapPropsToValues: pick(keys(selectors)),
    validationSchema: yup.object().shape(validators),
    handleSubmit({name, contactList}, meta) {
        meta.props.save({name, contactList, meta});
    },
    enableReinitialize: true
};

const enhance = compose(
    connect(mapState, mapDispatch),
    withFormik(formikOption)
);
export default enhance(你的顶级组件);
`

其中validators专门另开一个文件:

`
import * as yup from 'yup';

export const name = yup
    .string()
    .max(40, '商品中心名称最多 40 个字符')
    .required('商品中心名称必填');

const contactName = yup
    .string()
    .matches(/^(?!.*;).*$/, '联系人名中不得带有“;”号')
    .required('联系人必填');

const email = yup
    .string()
    .email('必须输入正确的 Email 格式')
    .required('Email 地址必填');

export const contactList = yup.array().of(
    yup.object().shape({name: contactName, email})
);
`

有必要提一下注入Yup的好处:虽然formik已经极大的方便了表单的操作,但是我们还是可以看到在做表单验证的时候,写的代码还是比较繁琐,这个时候就需要yup出场了,yup的专长就是做规则校验。并且聪明的formik作者在设计formik的时候就让其很好的支持yup了。

可以看到使用Yup后,不管是从代码量,还是可读性来看都提交了很多,尤其是在表单非常复杂时,这之间的优化会非常的明显。Yup的操作非常的语义化,所以学习成本非常的低,你只需要知道它的api即可。

好了,让我们继续回头看看formikOption——

mapPropsToValues:也有initialValues。相应于表单初始字段值。Formik将使用这些值来生成例如props.values这样的方法组件。

即使你的表单默认情况下为空,你也必须使用初始值来初始化所有字段;否则,React会抛出异步,说你已经把一个输入字段从未控制(uncontrolled)状态改变成了可控制(controlled)状态。
initialValues并不适用于高阶组件(higher-order component);在高阶组件情况下,你需要mapPropsToValues。

enableReinitialize:默认值为false。此属性用于在initialValues变化时控制是否重置表单。

validate(values):其实本来上面yup的validationSchema在本Sample够用了,我主要说一些常见的特殊情况,比如一个互斥radio,选项A时需要inputA为非空,选项B时需要inputB为非空。
你这时候就需要写函数:

`
validate({radio, inputA, inputB}) {
    const errors = {};
    if (radio === 'A' && inputA === '') {
        errors.inputA = 'inputA不能为空';
    }
    if (radio === 'B' && inputB === '') {
        errors.inputB = 'inputB不能为空';
    }
    return errors;
}
`

其他的api,附录里面链接2讲的比较全哈~

实现

2个组件:

`
<Form>
    <FieldMcName name="name" />
    <FieldContact name="contactList" />
    <SubmitButtons />
</Form>
`

name必须和selectors对应,值需要正确初始化。

然后就是withFastField和withFieldArray登场了,这里是我们用formik的FastField和FieldArray进行的封装

`
const withFastField = Control => props => (
    <FastField {...props}>
        {fieldProps => <Control {...props} {...fieldProps} />}
    </FastField>
);
const withFieldArray = Control => props => (
    <FieldArray {...props}>
        {fieldProps => <Control {...props} {...fieldProps} />}
    </FieldArray>
);
`

随后我们就用这俩来包裹所开发的组件,组件里面props默认就有field和form

`
const FieldMcName = ({field, form}) => {
    return (
       .... 其它代码
       <Input
            name={field.name}
            value={field.value}
            onChange={field.onChange}
            onBlur={field.onBlur}
            placeholder="40 个字符以内"
        />
    );
};
export default withFastField(FieldMcName);

export default withFastArray(FieldContact); // 不再赘述
`

你看到这里可能会问:field是干啥的,form是干啥的?
field提供了一系列的方法onChange onBlur,一般情况下你只要像上面那个例子一样无脑用,就可以等同于把value记录在state里面(只不过这个state你没有维护而已!)

form.values就是这个form的state,field.value是这个组件的值,form.values是整个form的state值,有时候你可能用withFastField分拆了子组件,子组件有时候要看别的子组件的状态来做不同的展现,你就要用form.values。

form还有一些方法,比如antd的Input onChange传参数是e和field.onChange参数一致,但ant的Select Switch onChange传的是value/checked等,那这时候你Switch里面的onChange={field.onChange}就不对了,咋整呢?我们有:

`
const setFieldValue = form.setFieldValue;
const name = field.name;
const handleChange = useCallback(checked => setFieldValue(name, checked), [setFieldValue, name]);
`

formik还有一个api这里要用:formik.handleReset。把state里面的值重置为初始值。你看上面ue图,有一个取消按钮,如果点了的话就要调一下,不然你input输入了值,点击取消后,下次变为编辑态时,会看到上一次你自己输入的东西(和库里面的值不一致)。
handleSubmit很好理解,最上面的formikOption就有。

最后是提交按钮这个组件(formikConnect过):

`
export default connect(({handleCancel, formik: {errors, isSubmitting, handleSubmit, validateForm}}) => {
    useEffect(() => validateForm(), [validateForm]);
    const disabled = useMemo(() => isNotEmpty(errors), [errors]);
    return (
        <Fragment>
             <Button skin="important" disabled={disabled} onClick={handleSubmit} loading={isSubmitting}>
                确定
            </Button>
            <Button onClick={handleCancel} style={{marginLeft: '10px'}}>取消</Button>
        </Fragment>
    );
});
`

很贴心是吧,提交事件,disabled(校验不过的话),正在提交等状态都帮你想到了。就传一个取消事件进去就行了。

总结:子组件里面只需要formikConnect一下,就能有formik对象在props里面,可以用formik api。
子组件如果用FastField或者FieldArray包裹,就能在props里面有form和field对象,可以用field和form的api。
实在不行,你自己看文档,或者console打印一下,就ok啦~

附录

链接1
链接2

js合成简谱

window.AudioContext = window.AudioContext || window.webkitAudioContext;

(function () {
var timerId = -1;
var MUSIC_KEYS = [
    196.00, 220.00, 246.94, 261.63, 293.66, 329.63, 349.23,
    392.00, 440.00, 493.88, 523.25, 587.33, 659.25, 698.46,
    783.99, 880.00, 987.77, 1046.50
];

window.stopAudio = function () {
    if (timerId >= 0) {
        clearInterval(timerId);
        timerId = -1;
    }
};

function getMusic() {
    var arr = [
        '12315 66865 668 565 653531231  ', // 我去上学校
        '5353531 24325  5353531 24321  2244315 24325  5353531 24321  ', // 粉刷匠
        '3 1 331 33565  6665444 23212  3 1 3 1 33566  8 556 3 21235  8 556 3 21231  ', // 数鸭子
        '123 45432  32345  123 45432  34321 ', // 一朵玫瑰花
        '1 3 531 5321265 3 5 8a985565888 3 21235586532111 ', // 小母鸡
        '334554321123322 334554321123211', // 欢乐颂
        '5854321 112331345  5854352 43265 231', // 我爱北京***
        '89a58aa 989cccc 8788888 7878765 5566666 5353598 5aaabc88a9', // 学猫叫
        'a8966 acaca 67865  5 aca68 9998a 8 6' // 芒种
    ];
    return arr[Math.floor(Math.random() * arr.length)];
}

window.playAudio = function (options) {
    if (!window.AudioContext) {
        console.log('当前浏览器不支持Web Audio API');
        return;
    }
    options = options || {};

    var arrFrequency = [];
    var music = getMusic().split('');
    var offset = 6;
    for (var i = 0; i < music.length; ++i) {
        music[i] = parseInt(music[i], 16);
        music[i] > 11 && (offset = -1); // 音域太高,必须降一个key
    }
    for (var i = 0; i < music.length; ++i) {
        arrFrequency.push(isNaN(music[i]) ? ' ' : MUSIC_KEYS[music[i] + offset]);
    }

    // 创建新的音频上下文接口
    var audioCtx = new AudioContext();
    var start = 0;
    stopAudio();
    timerId = setInterval(function () {
        var frequency = arrFrequency[start]; // 当前频率
        if (!frequency) { // 播放结束
            if (!options.isCircle) { // 非循环播放就停止
                stopAudio();
                return;
            }
            start = 0; // 否则轮播
            frequency = arrFrequency[start];
        }
        start++;
        if (frequency === ' ') { // 空格表示节奏停顿
            return;
        }

        // 创建一个OscillatorNode,它表示一个周期性波形(振荡),基本上来说创造了一个音调
        var oscillator = audioCtx.createOscillator();
        // 创建一个GainNode,它可以控制音频的总音量
        var gainNode = audioCtx.createGain();
        // 把音量,音调和终节点进行关联
        oscillator.connect(gainNode);
        // audioCtx.destination返回AudioDestinationNode对象,表示当前audio context中所有节点的最终节点,一般表示音频渲染设备
        gainNode.connect(audioCtx.destination);
        // 指定音调的类型,single|square|triangle|sawtooth
        oscillator.type = options.playType || 'triangle';
        // 设置当前播放声音的频率,也就是最终播放声音的调调
        oscillator.frequency.value = frequency;
        // 当前时间设置音量为0
        gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
        // 0.01秒后音量为1
        gainNode.gain.linearRampToValueAtTime(1, audioCtx.currentTime + 0.01);
        // 音调从当前时间开始播放
        oscillator.start(audioCtx.currentTime);
        // 1秒内声音慢慢降低,是个不错的停止声音的方法
        gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1);
        // 1秒后完全停止声音
        oscillator.stop(audioCtx.currentTime + 1);
    }, options.interval || 250);
};
})();

// 调用
window.playAudio();
window.playAudio({isCircle: true}); // 循环播放
window.playAudio({interval: 167}); // 六分之一节拍

关于重试机制

比如我们做 Nodejs 服务的时候,或者业务端请求 API 的情况,经常会有偶发的错误,此种 case 往往重试一次即可成功,我们想封装这样一个方法

/**
 * 重试机制
 *
 * @param {Function} fn 该函数返回 Promise
 * @param {number=} retries 重试次数
 * @param {string=} errMsg 错误文案
 * @return {Promise}
 */
const retry = (fn, {retries = 2, errMsg = '操作失败'} = {}) => {
    return fn().catch(e => {
        if (--retries) {
            return retry(fn, {retries, errMsg});
        }
        return Promise.reject(`${errMsg},${e}`);
    });
};

如何测试:

test('retry', async () => {
    const promises = [Promise.resolve('OK'), Promise.reject(), Promise.reject()];
    let len = promises.length;
    const result = await util.retry(() => promises[--len], {retries: 3});
    expect(result).toEqual('OK');
});

你可以在 if (--retries) { 里面打印 console.log 看看是否执行2次失败,第三次才成功

甚至,我们还可以在 retry 源代码里面引入更多配置项比如 delay

const pause = duration => new Promise(res => setTimeout(res, duration));
const retryWithDelay = (fn, {retries = 2, errMsg = '操作失败', delay = 100} = {}) => {
    return fn().catch(e => {
        if (--retries) {
            return pause(delay).then(() => retryWithDelay(fn, {retries, errMsg, delay}));
        }
        return Promise.reject(`${errMsg},${e}`);
    });
};

这里我留个问题,如果把单元测试中的: let len = promises.length; 改成 let len = 2;
同时 {retries: 3} 也改成 {retries: 2},ut虽然也会执行成功,但是会报:

图片

读者可以想想是什么原因,这里我就先抛一下砖:

https://advancedweb.hu/how-to-avoid-uncaught-async-errors-in-javascript/

关于尾递归、栈和迭代

递归 vs 尾递归

递归

我们要先从递归讲起。首先话不多说来一个代码:

function fibonacci(n) {
    return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

调用栈应该是这样:

[fibonacci(5)]
[fibonacci(4) + fibonacci(3)]
[(fibonacci(3) + fibonacci(2)) + (fibonacci(2) + fibonacci(1))]
[((fibonacci(2) + fibonacci(1)) + (fibonacci(1) + fibonacci(0))) + ((fibonacci(1) + fibonacci(0)) + fibonacci(1))]
[fibonacci(1) + fibonacci(0) + fibonacci(1) + fibonacci(1) + fibonacci(0) + fibonacci(1) + fibonacci(0) + fibonacci(1)]

可以看到n=5时,调用栈长度已经是8了~
注意了,到这里为止,程序做的仅仅还只是展开而已,并没有运算真正做运算

我们普通递归的问题就在于此:展开的时候会产生非常大的中间缓存,而每一层的中间缓存都会占用我们宝贵的栈上空间
导致了当这个 n 很大的时候,栈上空间不足则会产生“爆栈”的情况

eg:把上面那段代码在浏览器里面执行:fibonacci(10000)
会报RangeError: Maximum call stack size exceeded

那有没有一种方法能够避免这样的情况呢?那当然是有的,尾递归粉墨登场~

尾递归

尾递归:
若函数在尾位置调用自身(或是一个尾调用本身的其他函数等等),则称这种情况为尾递归。尾递归也是递归的一种特殊情形。尾递归是一种特殊的尾调用,即在尾部直接调用自身的递归函数。对尾递归的优化也是关注尾调用的主要原因。尾调用不一定是递归调用,但是尾递归特别有用,也比较容易实现。
特点(维基百科尾调用词条):
尾递归在普通尾调用的基础上,多出了2个特征:

  1. 在尾部调用的是函数自身 (Self-called);
  2. 可通过优化,使得计算仅占用常量栈空间 (Stack Space)。

我们把上面那段改成尾递归:

function fibonacci2(n, a = 0, b = 1) {
    if (n === 0) {
        return a;
    }
    return fibonacci2(n - 1, b, a + b);
}

再分析一下调用栈:

fibonacci2(5)
fibonacci2(5, 0, 1)  
fibonacci2(4, 1, 1)  
fibonacci2(3, 1, 2)  
fibonacci2(2, 2, 3)  
fibonacci2(1, 3, 5)  
fibonacci2(0, 5, 8)

So,调用栈长度应该一直是1?

非也~尾递归函数依然还是递归函数,如果不优化依然跟普通递归函数一样会爆栈,该展开多少层依旧是展开多少层。不会爆栈是因为语言的编译器或者解释器所做了“尾递归优化”,才让它不会爆栈的。

你在nodejs环境执行:fibonacci2(10000),照样报:RangeError: Maximum call stack size exceeded
ps:node v6版本在加flag的情况下,已经可以做尾递归优化了。node --harmony-tailcalls test.js
but,在node的高版本又移除了,你执行这个flag,反而会报:bad option: --harmony-tailcalls

读者:读了你文章半天,合着尾递归优化还是镜花水月,无法落地?
非也!我们可以用栈和迭代嘛~

栈和迭代

栈的意义其实非常简单,五个字——保持入口环境。我们结合一段简单代码来展示一下:

function main() {
    foo();
    bar();
}
  • 首先建立一个函数栈。$
  • main 函数调用,将 main 函数压进函数栈里面。$ main
  • 调用 foo 函数,foo函数入栈。$ main foo
  • foo 函数返回并出栈。$ main
  • 调用 bar 函数,bar函数入栈。$ main bar
  • bar 函数返回并出栈。$ main
  • main 函数返回并出栈。$

这就是栈——这种”后入先出“的数据结构的意义所在——
可以看到第 4 和第 6 步的作用,让 foo 和 bar 函数执行完了以后能够在回到 main 函数调用他们的地方
既然是保持入口环境,那么在什么情况下可以把这个入口环境给优化掉?答案不言而喻,在入口环境没意义的情况下(即尾递归)

读者:你还是纸上谈兵地讲了些原理和意义,依然没讲尾递归优化!

用迭代手动优化尾递归

function fact(n, r) { // TODO这里把 n, r 作为迭代变量提出来
    if (n <= 0) {
        return r;
    } else {
        return fact(n - 1, r * n); // TODO用迭代函数替代 fact
    }
}

=>

function fact(_n, _r) { // _n, _r 用作初始化变量
    var n = _n;
    var r = _r; // 将原来的 n, r 变量提出来,成为迭代变量
    function _fact(_n, _r) { // 迭代函数非常简单,就是更新迭代变量而已
        n = _n;
        r = _r;
    }
    _fact_loop: while (true) { // 生成一个迭代循环
        if (n <= 0) {
            return r;
        } else {
            _fact(n - 1, r * n);
            continue _fact_loop; // 执行迭代函数,并且进入下一次迭代
        }
    }
}

可以在nodejs下测试:fact(1e5, 1),前者爆栈,后者可以返回Infinity

快速排序

function partition(arr, low, high) {
    let i = low;
    let j = high + 1;
    let k = arr[low]; // 尽量不要固定基准;如果基本有序,则随机基准;如果不确定也可以选择三数取中
    while (i < j) {
        while (arr[++i] < k && i < j);
        while (arr[--j] > k);
        if (i < j) {
            let temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    arr[low] = arr[j];
    arr[j] = k;
    return j;
}

function qsort(arr, low, high) {
    if (arguments.length === 1) {
        low = 0;
        high = arr.length - 1;
    }
    if (low < high) {
        const index = partition(arr, low, high);
        qsort(arr, low, index - 1);
        qsort(arr, index + 1, high);
    }
}

function qsort1(arr, low, high) {
    if (arguments.length === 1) {
        low = 0;
        high = arr.length - 1;
    }
    while (low < high) { // TODO如果high和low比较接近时,用插入排序
        const index = partition(arr, low, high);
        qsort1(arr, low, index - 1);
        low = index + 1; // 尾递归
    }
}

在极端情况下(倒序),前者(qsort)8000爆栈,后者(qsort1)14000爆栈

拗拗概念

while转递归

while (true) {
    console.log(1);
}

=>

function do() {
    console.log(1);
    do();
}

for转递归

function forEach(arr) {
    for (var i = 0; i < arr.length ; ++i) {
        console.log(arr[i]);
    }
}

=>

function forEach(arr) {
    var i = 0; // 循环变量丢外面
    function go() {
        console.log(arr[i]);
        if (++i < arr.length) {
            go();
        }
    }
    go();
}

or

function forEach(arr, i = 0) { // 循环变量传值
    console.log(arr[i]);
    if (++i < arr.length) {
        forEach(arr, i);
    }
}

TBD:迷宫问题

0代表墙,走过的路不可重复,期望从(1,1)走到(n,m)

22202
00202
22222
20202
20233

返回布尔(是否可以到达n,m)
返回途经的值最大的路径?

ts范式(不定期更新)

初级规范

1.任何类型声明都必须使用模块声明,不允许使用 declare 声明

// bad
declare type Foo = number[];
declare interface Global {
    foo: Foo;
}
// Good
import Foo from 'xxxx';
export interface Global {
    foo: Foo;
}

2.尽量避免 any

// Bad,实在不行尽量unknown,unknown不会具有传染特性,在使用前必须通过as类型断言成具体的类型
export function clone(data: any): any
// Good,泛型让返回类型得到正确的推导
export function clone<T>(data: T): T

// Bad
class LRUCache {
    add(key: string, value: any) {}
    get(key: string): any {}
}
// Good,容器类,工具类,尽可能考虑用泛型
class LRUCache<T> {
    add(key: string, value: T) {}
    get(key: string): T {}
}

3.尽量用 interface

基础类型、非对象类型,以及由对象字面量类型推导出的其他类型可以使用 type 声明。

// bad
type ENV = {
    type: 'logic' | 'view',
    platform: 'swan' | 'web' | 'talos',
    aa: string | undefined
}
// Good
interface ENV {
    type: 'logic' | 'view';
    platform?: 'swan' | 'web' | 'talos';
    aa?: string;
}
// 注意 interface 中,尽量用;而非, 
// 也尽量使用 ? 而非 |undefined 声明可缺省值

4.使用 as 而非 <> 形式的类型断言

// bad
const eventData = <EventData>JSON.parse(message);
// Good
const eventData = JSON.parse(message) as EventData;

5.使用 T[],而非 Array 声明数组

// bad
const arr: Array<number> = [1, 2, 3];
type loader = Array<Promise> => Array<unknown>;

// Good
const arr: number[] = [1, 2, 3];
type loader = Promise[] => unknown[];

// 当遇到复杂类型时,建议为数组子项类型定义别名,增加可读性
type EventListener = (string, unknown) => void;
type EventListeners = EventListener[];
// 相比于 type EventListeners = ((string, unknown) => void)[]; 更加清晰

6.禁止使用 Wrapper Classes

// bad
type request = (url: String) => Boolean;
// Good
type request = (url: string) => boolean;

7.基础类型的字面量赋值语句中,不需要添加类型声明

// bad
const os: string = 'android';
let length: number = 10;
// Good
const os = 'android';
let length = 10;

// 注意,此规则仅针对基础类型(primary type),如果变量是非基础类型,则需要根据实际情况选择
type OS = 'android' | 'ios';
let os: OS = 'android'; // 可变的 union type,需要申明
const os = 'android'; // 不可变的话,使用 const 就没必要申明

8.对于明确知道格式的 string 类型,可使用 Template Literal 精确定义类型

type RelativeApiPath = `/data/${string}`;
type AbsoluteApiUrl = `https://xx.com/data/${string}`;

function replaceApiUrl(path: RelativeApiPath): AbsoluteApiUrl {
  // 省略
}

9.给基础类型以语义

// Bad
const fill: string = 'red';
const stroke: string = 'black';
// Good
type Color = string;
const fill: Color = 'red';
const stroke: Color = 'black';

// 在未来如果想要对类型进行修改,比如颜色支持[r, g, b, a]这样的数组,我们可以非常方便的进行重构
type Color = string | number[];
// 你如果之前偷了懒,那后面就得面对几百个 : string 头大

高级用法

1.提取出公共的函数类型

// bad
function add(a:number,b:number){
  return a+b;
}
function sub(a:number,b:number){
  return a-b;
}
function mult(a:number,b:number){
  return a*b;
}
function div(a:number,b:number){
  return a/b;
}

// Good,提取出公共的函数类型,可简化如下
type Binary = (a:number,b:number) =>number;
const add: Binary = (a,b) => a+b;
const sub: Binary = (a,b) => a-b;
const mult: Binary = (a,b) => a*b;
const div: Binary= (a,b) => a/b;

2.重载

/**
 * 有一个函数:setMessage
 * 第一个参数值只能取init或update,如果是init,则第二个参数必须是{initData: 字符串},反之{updateData: 字符串},举个例子:
 * setMessage('init', {initData: 'aa'});
 * setMessage('update', {updateData: 'bb'});
 */
interface InitMessageParams {
    initData?: string;
}
interface UpdateMessageParams {
    updateData?: string;
}
export function setMessage(type: 'update', message: UpdateMessageParams): void
export function setMessage(type: 'init', message: InitMessageParams): void

3.上个例子更好的是泛型,适合扩展更多的枚举

interface MessageParamsMap {
    init: InitMessageParams;
    update: UpdateMessageParams;
}
type MessageType = keyof MessageParamsMap;
export function setMessage<T extends MessageType>(type: T, message: MessageParamsMap[T]): void

4.入参宽松和出参严格

// Good
interface ArrayLike<T> {
    readonly length: number;
    // 可以数字下标访问
    readonly [n: number]: T;
}
export function cloneArray<T>(arr: ArrayLike<T>): Array<T>
// 这个例子就同时体现了入参宽松,出参严格,入参可以接受普通的数组,也可以是Float32Array这样的静态类型数组,亦或者伪数组
// 因此入参类型定义成了拥有length属性,而且可以数字下标访问的对象。而出参因为都转成数组了,所以类型为Array<T>。

5.类型收紧

简单逻辑下,instanceof 和 typeof 也可以

// ok
declare const foo: number | string;
const bar = foo.toFixed(2); // 报错,因为 foo 有可能为字符串,不存在 toFixed 方法
if (typeof foo === 'number') {
    const bar = foo.toFixed(2);
}

6.(接上)建议使用 Union Of Interfaces 而非 Interface of Unions

// Bad
interface Point {
    type: 'string' | 'number';
    x: string | number;
    y: string | number;
}
// Good, 有利于收窄
interface StringPoint {
    type: 'string';
    x: string;
    y: string
}
interface NumberPoint {
    type: 'number';
    x: number;
    y: number;
}
type Point = StringPoint | NumberPoint;
function print(point: Point) {
    if (point.type === 'number') {
        console.log(point.x.toFixed(2)); // point 已被推为 NumberPoint,所以 toFixed 没问题
    }
}

7.如果类型可以通过其他类型推导出来,就不要重复定义

类型计算即我们俗称的类型体操。类型计算的方式多种多样,所以这是一个比较宽泛的规则。此处列举一些简单的常用操作:

// bad
interface StatusMap {
    loaded: boolean;
    created: boolean;
    attached: boolean;
}
function getStatus(name: 'loaded' | 'created' | 'attached'): boolean {
    return statusMap[name];
}
// Good,使用 keyof
type Status = keyof StatusMap;
function getStatus(name: Status): boolean {
    return statusMap[name];
}

// bad
type Lifecycle = 'launch' | 'show' | 'hide';
interface LifecycleEventMap {
  launch: {
    foo: string;
    bar: () => unknown;
  },
  show: {
    foo: string;
    bar: () => unknown;
  },
  hide: {
    foo: string;
    bar: () => unknown;
  }
}
// Good,使用映射类型(Mapped Type)定义新类型
interface LifecycleEventMap<Lifecycle> {
    [key in Lifecycle]: {
        foo: string;
        bar: () => unknown;
    };
}
// ps,补充于2021-11-24,上面疑似不太行,要下面这样子
type LifecycleEventMap = Record<Lifecycle, {
    foo: string;
    bar: () => unknown;
}>;

// Good,使用 Partial、Readonly、Pick、Omit 等定义新类型
interface PageInfo {
  pageConfig: PageConfig;
  type: PageType;
  pagePath: string;
  packagePath: string;
  routePath: string;
}
type PagePathInfo = Pick<PageInfo, 'pagePath' | 'packagePath' | 'routePath'>;

8.索引签名(index signature)

interface Map<T> {
  [index: string]: T;
}
let keys = keyof Map<string>; // string | number
// why?可以看:https://stackoverflow.com/questions/51808160/keyof-inferring-string-number-when-key-is-only-a-string/51808262#51808262
// 我想强行转成 string,咋整?
type Extract<T, U> = T extends U ? T : never;
let keys: Extract<keyof Map<string>, string>; // string
// 嵌套索引签名,尽量不要把字符串索引签名与有效变量混合使用
// Good
interface NestedCSS {
  color?: string;
  nest?: { // 另外开一个属性
    [selector: string]: NestedCSS;
  };
}
// bad
interface NestedCSS {
  color?: string;
  [selector: string]: string | NestedCSS;
}
const failsSilently: NestedCSS = {
  colour: 'red' // 'colour' 不会被捕捉到错误
};

附录

webpack编译dll后防止文件缓存

背景

有一个项目,把几个不常升级的包做成了dll打包,直接页面中用静态js引入
图片图片

然而有一天,dll所依赖的包升级了,文件里面的var lib_后面那串hash就变了,然而页面中引用的/store/dll/lib.js有缓存,直接就报错。

解决方案1

/store/dll/lib.js?r=xxx

这样每次都会把编译后的文件重新加载一遍,虽然是可以解决缓存的问题,但是会造成网络请求的浪费。

解决方案2

首先,dll的生成output改成[name]_[hash].js

output: {
    path: paths.dllPath,
    filename: '[name]_[hash].js', // 原来是[name].js
    library: '[name]_[hash]' 
}

此时我们如何在别的地方拿到这动态生成的hash值呢?用assets-webpack-plugin,需要在你打包dll的webpack.dll.config.js里面加入这个plugin:

const AssetsPlugin = require('assets-webpack-plugin');

plugins: [
    new AssetsPlugin({
        useCompilerPath: true,
        filename: 'webpack-dll-assets.js',
        processOutput: assets => {
            return 'window.WEBPACK_DLL_ASSETS=' + JSON.stringify(assets);
        }
    })
]

useCompilerPath: true,表示生成在output所配置的path下(也就是dll),打开这个文件夹,会发现里面是这些东西:

图片
打开webpack-dll-assets.js,发现:
图片

ok,我们接下来要改html里面对js的引用了,注意/store是publicPath,文件名字是上面配置的webpack-dll-assets.js,里面的window.WEBPACK_DLL_ASSETS.lib.js是文件的内容。

<script>
    document.write('<script type="text/javascript" src="/store/dll/webpack-dll-assets.js"><\/script>');    
</script>
<script>
    document.write('<script src="/store/dll/' + window.WEBPACK_DLL_ASSETS.lib.js + '"><\/script>');    
</script>

这样每次我们都会加载webpack-dll-assets.js。由于这个js很小,所以后面可以加?r=时间戳,防止这个文件出现缓存(会出大问题的!)。这样,根据这个小文件的内容拼装出要加载的js。大功告成!

谈谈浏览器和nodejs下的Event Loop

Event Loop是什么

网页中的main thread 是唯一的,即网页在单线程中运行,无论是 js 的运行,还是页面的渲染,还是 dom 的操作,都是在同一个 main thread 中操作的。代码的运行和页面的渲染顺序是严格定义的并且大多数情况下都是确定的。而这又要归功于 Event Loop。

Event Loop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。

image

浏览器的Event Loop是在html5的规范中明确定义
NodeJS的Event Loop是基于libuv实现的。可以参考Node的官方文档以及libuv的官方文档
libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商

宏任务&微任务

微任务 microtask(jobs): promise / ajax / process.nextTick() / Object.observe(该方法已废弃)
宏任务 macrotask(task): setTimeout / script / IO / UI Rendering / RAF(requestAnimationFrame)

浏览器下的事件循环是指:执行一个宏任务,然后执行清空微任务列表,循环再执行宏任务,再清微任务列表。
nodejs下的事件循环:timer 阶段;I/O 阶段;idle, prepare;poll ;check;close callbacks。

上面阐述只是比较表面的,用这种表面的解读,势必是无法让各位读者满意的,下面我们一一详解:

浏览器端详解

执行流程

  1. 执行全局Script同步代码,清空调用栈
  2. 清空微任务,注意清的过程中如果还产生新的微任务,则放到队尾继续清
  3. 清完微任务后,取出宏任务中位于队首的那个任务,放入Stack中执行
    重复2-3...

image
image

UI-render和RAF

有人会问UI-render啦,RAF啦,这些东西在哪里?其实这个是由浏览器自行判断决定的。

image

  1. 执行 UI render 操作:
    7.1-7.4. 判断 document 在此时间点渲染是否会『获益』。浏览器只需保证 60Hz 的刷新率即可(在机器负荷重时还会降低刷新率),若 eventloop 频率过高,即使渲染了浏览器也无法及时展示。
    7.5-7.9. 执行各种渲染所需工作,如 触发 resize、scroll 事件、建立媒体查询、运行 CSS 动画等等
    7.10. 执行 animation frame callbacks
    7.11. 执行 IntersectionObserver callback
    7.12. 渲染 UI

所以并不是每轮 eventloop 都会执行 UI Render。需要执行UI render时,它的时间节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render——就相当于又多加了一条支路,这条支路上有四个 task:

S:Style calculation,计算所有的 css 样式,每一个 element 应用什么 css。
L:Layout,布局会构建一个渲染树,包括当前页面的所有元素和它们所在的位置。
P:Pixel data,计算页面上的每个像素点数据。
RAF:告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用——加快了动画更新的响应速度。

image

注意:并不是所有的浏览器都像 Chrome 和 FireFox 将 RAF 放在 render 的第一步执行,有些浏览器将它放到了 Pixel Data 之后执行。虽然这样做是不精确的,因为已经把页面渲染完成了,再执行 RAF 函数那它的效果只能在下一桢看到了。

习题

好了,大道理大概念讲了这么多,图也上了这么多,你是不是被弄晕了或者忘了?我们实战一下:

习题1:

`
console.log(1);

setTimeout(() => {
    console.log(2);
    Promise.resolve().then(() => {
        console.log(3);
    });
});

new Promise((resolve, reject) => {
    console.log(4);
    resolve(5);
}).then(data => {
    console.log(data);
    Promise.resolve().then(() => {
        console.log(6);
    }).then(() => {
        console.log(7);
        setTimeout(() => {
            console.log(8);
        }, 0);
    });
});

setTimeout(() => {
    console.log(9);
}, 0);
console.log(10);
`

习题2:有一个页面,写了一个raf,每次把一个div移动1px,从屏幕左侧往右侧移1000px
另外一个页面,写了一个setTimeout(() => {把同样大小的div移动1px}, 0),递归调用也移1000px为止
假设系统没有别的进程,浏览器空闲。2个页面的现象分别是什么?哪个页面的div先完成动画?快多少倍?

nodejs端详解

各阶段任务&4宏2微

各个阶段执行的任务如下:

timers阶段:这个阶段执行setTimeout和setInterval预定的callback
I/O callback阶段:执行除了close事件的callbacks、被timers设定的callbacks、setImmediate()设定的callbacks这些之外的callbacks
idle, prepare阶段:仅node内部使用
poll阶段:获取新的I/O事件,适当的条件下node将阻塞在这里
check阶段:执行setImmediate()设定的callbacks
close callbacks阶段:执行socket.on('close', ....)这些callbacks

由上面的介绍可以看到,回调事件主要位于4个宏队列中:

Timers Queue
IO Callbacks Queue
Check Queue
Close Callbacks Queue

这4个都属于宏队列,在NodeJS中,不同的macrotask会被放置在不同的宏队列中。
之前在浏览器中,可以认为只有一个宏队列,所有的macrotask都会被加到这一个宏队列中。

NodeJS中微队列主要有2个:

Next Tick Queue:是放置process.nextTick(callback)的回调任务的
Other Micro Queue:放置其他microtask,比如Promise等

之前在浏览器中,也可以认为只有一个微队列,所有的microtask都会被加到这一个微队列中。
但是在NodeJS中,不同的microtask会被放置在不同的微队列中,其中Next Tick Queue优先。

执行流程

image

大体解释一下NodeJS的Event Loop过程:

  1. 执行全局Script的同步代码
  2. 执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务
    // 开始执行macrotask宏任务了
  3. 清空(注意nodejs是清空,浏览器是执行一个)Timers Queue -> 步骤2
  4. 清空 I/O Queue -> 步骤2
  5. 清空 Check Queue -> 步骤2
  6. 清空 Close Callback Queue -> 步骤2
  7. 循环步骤3-4-5-6

setTimeout 对比 setImmediate

setTimeout(fn, 0)在Timers阶段执行,并且是在poll阶段进行判断是否达到指定的timer时间才会执行
setImmediate(fn)在Check阶段执行
两者的执行顺序要根据当前的执行环境才能确定:
如果两者都在主模块调用,那么执行先后取决于进程性能,顺序随机;
如果两者都不在主模块调用,即在一个I/O Circle中调用,那么setImmediate的回调永远先执行,因为会先到Check阶段。

setImmediate 对比 process.nextTick

setImmediate(fn)的回调任务会插入到宏队列Check Queue中;
process.nextTick(fn)的回调任务会插入到微队列Next Tick Queue中;
process.nextTick(fn)调用深度有限制,上限是1000,而setImmedaite则没有。

习题

好了,我们继续实战一下(因为有执行环境的前提,我们假定习题都是在I/O Circle环境中):

习题1:

`
console.log('start');

setTimeout(() => {
    console.log(111);
    setTimeout(() => {
        console.log(222);
    }, 0);
    setImmediate(() => {
        console.log(333);
    });
    process.nextTick(() => {
        console.log(444);  
    });
}, 0);

setImmediate(() => {
    console.log(555);
    process.nextTick(() => {
        console.log(666);  
    });
});

setTimeout(() => {
    console.log(777);
    process.nextTick(() => {
        console.log(888);   
    });
}, 0);

process.nextTick(() => {
    console.log(999);  
});
console.log('end');
`

习题2:

`
console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    });
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5');
    });
}, 0);

new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8');
});
process.nextTick(function() {
    console.log('6');
});

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    });
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12');
    });
}, 0);
`

总结

  1. 浏览器可以理解成只有1个宏任务队列和1个微任务队列,先执行全局Script代码,执行完同步代码调用栈清空后,从微任务队列中依次取出所有的任务放入调用栈执行,微任务队列清空后,从宏任务队列中只取位于队首的任务放入调用栈执行,注意这里和Node的区别,只取一个,然后继续执行微队列中的所有任务,再去宏队列取一个,以此构成事件循环。
  2. 并不是每轮 eventloop 都会执行 UI Render。需要执行UI render时,它的时间节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。RAF虽然处于UI-render里,但的执行时机与浏览器的 render 实现策略有关,是黑箱的。
  3. NodeJS可以理解成有4个宏任务队列和2个微任务队列,但是执行宏任务时有6个阶段。先执行全局Script代码,执行完同步代码调用栈清空后,先从微任务队列Next Tick Queue中依次取出所有的任务放入调用栈中执行,再从微任务队列Other Microtask Queue中依次取出所有的任务放入调用栈中执行。然后开始宏任务的6个阶段,每个阶段都将该宏任务队列中的所有任务都取出来执行(注意,这里和浏览器不一样,浏览器只取一个),每个宏任务阶段执行完毕后,开始执行微任务,再开始执行下一阶段宏任务,以此构成事件循环。

参考资料

这篇摘录了很多英文版的标准

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.