Code Monkey home page Code Monkey logo

blog's People

Contributors

func-star avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

limingziqiang

blog's Issues

Promise解析(一)

1

可以看到Promise其实是个构造函数,原型上挂着catchthen方法,自身拥有raceallrejectresolve等方法。

那既然是构造函数,那我们就直接new一个来看一下。

var promise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        console.log('第一个异步请求开始')
        resolve('请求返回');
    })
})
// 第一个异步请求开始

我们可以看到,Promise()接收一个包含两个参数的函数,两个参数分别为resolvereject,对应着成功和失败。而且在实例化一个Promise的时候,回调函数会马上执行。

function promise() {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log('第一个异步请求开始');
            resolve('成功返回');
        })
    })
}

通过这个函数,我们来看下面的demo

promise().then(function(data){
    console.log(data)
})
// 第一个异步请求开始
// 成功返回

第一个异步请求开始被打印出来肯定都清楚,可是成功返回也被打印出来了。前面介绍到 Promise 构造函数的原型上挂着两个 catchthen 两个方法,我们可以在 Promise 实例上链式调用.then()

.then() 可以接收两个回调函数作为参数,第一个对应 fullfiled(resoleved、成功) 执行,第二个对应 rejected(失败) 执行。并且这两个回调函数能拿到前面 resolve(arg)reject(arg) 传入的参数。

接下来reject同理:

function promise() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('第一个异步请求开始');
            reject('失败返回');
        })
    })
}
promise().then(function(data) {
    console.log(data)
}, function(data) {
    console.log(data)
})
// 第一个异步请求开始
// 失败返回

我们接着来看下面的demo

function promise() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('第一个异步请求开始');
            resolve('成功返回');
            reject('失败返回');
        }, 1000)
    })
}
promise().then(function(data) {
    console.log(data)
}, function(data) {
    console.log(data)
})
// 第一个异步请求开始
// 成功返回

我们可以看到这里的 reject('失败返回') 根本没有被触发。实际上,promise 的状态只会从 pendingresolved 或者 pendingrejected

简单的说就是一个处理成功的过程和一个处理失败的过程,而且一旦得到结果,状态就不能改变。

接下来我们来看一下 Promise的链式调用,如何将异步代码以同步模式组织起来:

function test1() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('异步操作1开始');
            resolve('成功返回test1');
        }, 1000)
    });
}

function test2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('异步操作2开始');
            resolve('成功返回test2');
        }, 1500)
    });
}

test1().then(function(data) {
    console.log(data)
    return '非Promise' //看这里
}).then(function(data) {
    console.log(data);
    return test2();
}).then(function(data) {
    console.log(data);
})
// 异步操作1开始
// 成功返回test1
// 非Promise
// 异步操作2开始
// 成功返回test2

这里我们可以发现 .then() 中如果不返回一个 Promise 实例而是返回一个值,也能被下一个 .then() 方法接收到。

.then()的原理(可以前往Promise解析(二)查看)

 this.then = function(onFulfilled) {
    return new Promise(function(resolve) {
        function handle() {

            var returnVal = isFn(onFulfilled) && onFulfilled(promise.value) || promise.value;
            // var returnVal = onFulfilled(promise.value);
            if (isFn(returnVal.then)) {
                returnVal.then(function(res) {
                    resolve(res)
                })
            } else {
                resolve(returnVal);
            }
        }
        if ('PENDING' === promise.status) {
            promise._resolveList.push(handle);
        } else if ('FULFILLED' === promise.status) {
            handle(promise.value)
        }
    })
}

除了 then, 在 Promise 的构造函数的原型中还有一个 catch() 方法,这个方法其实很容易理解,catch 会将 Promise 的执行报错和 resolve 执行过程中的报错呈现出来,而 reject 无法获取 resolve 执行过程中的报错。

接下来让我们来看下.all()的用法

function test1() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('异步操作1开始');
            resolve('成功返回1');
        }, 1000)
    });
}

function test2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('异步操作2开始');
            resolve('成功返回2');
        }, 1500)
    });
}

function test3() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('异步操作3开始');
            resolve('成功返回3');
        }, 1000)
    });
}
Promise.all([
    test1(),
    test2(),
    test3()
]).then(function(results) {
    console.log(results);
}).catch(function(reason) {
    console.log(reason);
})
// 异步操作1开始
// 异步操作2开始
// 异步操作3开始
// Array ["成功返回1", "成功返回2", "成功返回3"]

.all() 的作用相当于把三个异步操作并成一个来执行,等所有的异步操作都返回后,统一进入到 .then()

那么大家普遍都会有疑问,如果其中一个异步操作reject了呢,结果会怎样,现在我把第二个修改下

function test2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('异步操作2开始');
            reject('拒绝返回2')
        }, 1500)
    });
}
// 异步操作1开始
// 异步操作2开始
// 异步操作3开始
// 拒绝返回2

结果是 .then() 中的回调函数只接收了 rejected 状态的值而且不再是一个数组。

接下来来看一下 .race() 方法,这个方法的用法和 .all() 差不多,区别在于它不是当所有异步操作都结束后才进入 .then(),而 .race 是任意其中一个结束它就进入 .then() 方法。

function test1() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('异步操作1开始');
            resolve('成功返回1');
        }, 1000)
    });
}

function test2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('异步操作2开始');
            resolve('成功返回2');
        }, 1500)
    });
}
Promise.race([
    test1(),
    test2()
]).then(function(results) {
    console.log(results);
}).catch(function(reason) {
    console.log(reason);
})
// 异步操作1开始
// 成功返回1
// 异步操作2开始

自定义页面系统设计(四)

在介绍完摩羯系统的大致设计思路和实现思路之后,本章节来演示一下运营如何来简单的搭建一个页面。

在搭建页面之前,我们得先准备好几个可用的功能模块,这里我为大家准备了一个支持的跳转的图墙功能模块和一个文本功能模块

  • 创建一个页面
  1. 选择页面模版
  2. 填写页面标题
  3. 填写页面描述

页面创建演示

  • 添加功能模块
  1. 选择功能模块
  2. 填写功能模块数据配置信息
  3. 渲染

页面创建演示

自定义页面系统设计(三)

接着自定义页面系统设计(二)讲,这个章节主要会讲解如何解决下面几个点:

  • 模块信息配置
  • 最终页面的拼装

注:本章节暂先以命令行的方式来演示如何搭建一个页面,下一节再以可视化界面的形式来演示

模块信息配置

前面两节我们介绍,功能模块是需要被重复使用的,同一个模块module-a可能会被嵌入到page-apage-bpage-c等等页面中使用。
因此我们的功能模块需要支持可配置化,在不同的页面中具有不同的表现,这样才能最大程度的支持模块重用。

我们在「摩羯」(我们正在实现的系统)管理后台创建页面的操作步骤如下:

  1. 选择一个基础页面模板
  2. 选择需要的功能模块
  3. 为每一个功能模块填写配置信息
  4. 点击生成页面

我们可以看到在生成页面之前,我们需要为每一个功能模块准备一份配置信息,然后功能模块通过这个配置信息在不同的页面展现不同的效果。

生成的配置文件如下:

{
    "moduleName": "module-single-banner",
    "bannerUrl":
    {
        "name": "图片地址",
        "type": "string",
        "value": "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1962406471,854069461&fm=27&gp=0.jpg",
        "key": "bannerUrl"
    },
    "url":
    {
        "name": "跳转链接",
        "type": "string",
        "value": "http://mor.monajs.cn/",
        "key": "url"
    }
}

这份配置文件会被注入到页面中,并挂载在window上:

window.Capricorn.modules = {
    "module-text": [{
        "name": "module-text_0",
        "config": {
            "moduleName": "module-text",
            "bannerUrl": {
                "name": "图片地址",
                "type": "string",
                "value": "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1962406471,854069461&fm=27&gp=0.jpg",
                "key": "bannerUrl"
            },
            "url": {
                "name": "跳转链接",
                "type": "string",
                "value": "http://mor.monajs.cn/",
                "key": "url"
            }
        }
    }]
}
  • 注:每一个模块初始化创建的时候也会创建一份配置信息格式表

最终页面的拼装

当在「摩羯」管理后台点击页面生成的时候,我们会收集到以下信息:

  • 基础页面模板分支,自定义页面系统设计(二)中介绍我们通过分支来管理基础模板类型
  • 依赖的功能模块以及模块版本号
  • 各模块的配置信息
  • 页面的配置信息,包含titledescriptionicon

当这些信息都收集到之后,我们就可以进行页面生成了,分以下几步完成:

  1. 根据模板分支下载对应的页面模板代码
  2. 跟据功能模块的名称和版本,安装对应的依赖(之前讲到我们通过npm来管理我们的功能模块)
  3. 将功能模块的jscss脚本引入到页面模板中
  4. 将配置文件引入到页面模板中

页面创建演示

React简析之节点挂载

概述

React简析之实例化 中,我们将节点进行了实例化,得到了一个数据装载完整的 React 实例数据。这一节我们来介绍一下节点挂载的过程。
代码地址:v1.1

// 挂载节点
mount(parentNode) {
    if (this.nodeType === Constant.EMPTY_NODE) {
        return null
    }

    if (this.nodeType === Constant.REACT_NODE) {
        let componentObj = new this.currentElement.type(this.currentElement.props)
        // component实例
        this.componentObj = componentObj
        // 获取react render()出来的真实节点信息
        let componentElement = componentObj.render()
        if (!componentElement) {
            return null
        }
        this.componentElement = componentElement
        // 监听setState方法
        this.componentObj.__events.on('stateChange', () => {
            this.receiveComponent()
        })
        this.componentInstance = new ReactInstantiate(this.componentElement, null)
        let node = this.componentInstance.mount(parentNode)
        return node
    }

    if (!this.nativeNode) {
        this.nativeNode = reactDom.create(this.currentElement)
        // 事件绑定
        if (this.nodeType === Constant.NATIVE_NODE && this.currentElement.props) {
            reactEvents.register(this.nativeNode, this.currentElement.props)
        }
        if (parentNode) {
            this.parentNode = parentNode
            parentNode.appendChild(this.nativeNode)
        }
    }

    this.nativeNode['__reactInstance'] = {
        _currentElement: this.currentElement
    }

    this.mountChildren(this.nativeNode)
    return this.nativeNode
}

// 挂载子节点// 挂载节点
mount(parentNode) {
    if (this.nodeType === Constant.EMPTY_NODE) {
        return null
    }

    if (this.nodeType === Constant.REACT_NODE) {
        let componentObj = new this.currentElement.type(this.currentElement.props)
        // component实例
        this.componentObj = componentObj
        // 获取react render()出来的真实节点信息
        let componentElement = componentObj.render()
        if (!componentElement) {
            return null
        }
        this.componentElement = componentElement
        // 监听setState方法
        this.componentObj.__events.on('stateChange', () => {
            this.receiveComponent()
        })
        this.componentInstance = new ReactInstantiate(this.componentElement, null)
        let node = this.componentInstance.mount(parentNode)
        return node
    }

    if (!this.nativeNode) {
        this.nativeNode = reactDom.create(this.currentElement)
        // 事件绑定
        if (this.nodeType === Constant.NATIVE_NODE && this.currentElement.props) {
            reactEvents.register(this.nativeNode, this.currentElement.props)
        }
        if (parentNode) {
            this.parentNode = parentNode
            parentNode.appendChild(this.nativeNode)
        }
    }

    this.nativeNode['__reactInstance'] = {
        _currentElement: this.currentElement
    }

    this.mountChildren(this.nativeNode)
    return this.nativeNode
}

// 挂载子节点
mountChildren(parentNode) {
    if (!this.childrenInstance || this.childrenInstance.length === 0) {
        return
    }
    this.childrenInstance.forEach((v) => {
        if (Util.isArray(v)) {
            v.forEach((child) => {
                child.mount(parentNode)
            })
        } else {
            console.log(parentNode)
            v.mount(parentNode)
        }
    })
}

这里注意一个点:

实际上在实例化过程中,我们并不会将 React 节点转化成原生节点进行实例化。React 节点转化成原生节点这一步是在节点挂载的时候完成的。
React 节点挂载的过程中,我们会根据实例化好的节点信息reactDom.create(this.currentElement) 创建出一个真实的 原生节点(浏览器节点),然后插入 dom 结构中去。
如果碰到 React 节点,则是先通过 render() 方法返回的 dom 模版,实例化出一份 节点实例,再进行节点挂载。
依次递归完成子节点的挂载操作。

合成事件绑定、属性绑定(可以先忽略)

React 的节点属性中,通常以驼峰形式命名,像style属性接收的也是 JSON 对象形式,与原生节点的属性格式不同。

prefixList = [
    'transform',
    'transition'
];
cssNumber = [
    "column-count",
    "fill-opacity",
    "font-weight",
    "line-height",
    "opacity",
    "order",
    "orphans",
    "widows",
    "z-index",
    "zoom"
];

//将样式对象转化为可使用的样式对象
parseStyleObj(data) {
    let _data = {};
    Object.keys(data).forEach((v) => {
        Util.upperToLine(v)
        let name = Util.upperToLine(v);
        let val = data[v];
        if (typeof(val) == 'number' && this.cssNumber.indexOf(name) < 0) {
            val = val + 'px';
        }
        _data[name] = val;
        if (this.prefixList.indexOf(name) >= 0) {
            _data["-webkit-" + name] = val;
        }
    })
    return _data;
}
styleObjStringify(styleObj) {
    if (!styleObj || Object.keys(styleObj).length == 0) {
        return '';
    }
    return Object.keys(styleObj).map((v) => {
        return v + ":" + styleObj[v];
    }).join(";")
}

//dom信息绑定
parse(node, props) {
    if (!props) {
        return;
    }
    let propKeys = Object.keys(props);

    //className
    if (Util.has(props, 'className')) {
        if (props.className) {
            node.className = props.className;
        }
        Util.arrayDelete(propKeys, "className")
    }

    //style
    if (Util.has(props, 'style')) {
        let style = this.parseStyleObj(props.style);
        node.setAttribute("style", this.styleObjStringify(style));
        Util.arrayDelete(propKeys, "style")
    }

    //defaultValue
    if (Util.has(props, 'defaultValue')) {
        node.setAttribute("value", props.defaultValue);
        Util.arrayDelete(propKeys, "defaultValue")
    }

    //dangerouslySetInnerHTML
    if (Util.has(props, 'dangerouslySetInnerHTML')) {
        if (!props.dangerouslySetInnerHTML || !props.dangerouslySetInnerHTML.__html) {
            return;
        }
        node.innerHTML = props.dangerouslySetInnerHTML.__html;
        Util.arrayDelete(propKeys, "dangerouslySetInnerHTML")
    }

    propKeys.forEach((v) => {
        let val = props[v];
        if (Util.isUndefined(val)) {
            return;
            //val = true;
        }

        if (DOMPropertyConfig.isProperty(v)) {
            let attr = Util.upperToLine(v)
            if (v == 'htmlFor') {
                attr = 'for';
            }
            if (val === false) {
                node.removeAttribute(attr);
                return;
            }
            if (val === true) {
                node.setAttribute(attr, 'true');
            } else {
                node.setAttribute(attr, val);
            }
            return;
        }

        //data-*
        if (/^data-.+/.test(v) || /^aria-.+/.test(v)) {
            node.setAttribute(v, val);
            return;
        }

        //事件绑定
        let e = DOMPropertyConfig.getEventName(v);
        if (e) {
            if (Util.isFun(val)) {
                node.addEventListener(e, (ev) => {
                    val();
                }, false)
            }
        }
    })
}

setState 介绍

在理解 React 之前我们都会把 setState 当成是一个异步过程,下面见一个例子:

class App extends Component {
	state = { val: 0 }
	increment = () => {
		this.setState({ val: this.state.val + 1 })
		console.log(this.state.val) // 输出的是更新前的val --> 0
	}
	render () {
		return (
			<div onClick={this.increment}>test</div>
		)
	}
}

从结果上看,我们很容易把 setState 当成是一个异步方法。
其实 setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是 increment(钩子函数)的调用顺序在更新之前,导致在钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
那么如何让 setState 以同步形式使用呢?

  • setTimeout 的形式强制改变调用栈
  • 用原生事件绑定形式,不使用 React 的合成事件
componentDidMount() {
    document.body.addEventListener('click', this.changeValue, false)
 }

建议跑一下代码试试:v1.1

Promise解析(三)

一、概述

  1. Promise 简单来说就是一个对象,他拥有三种状态pendingfulfilledrejected(过程中、成功和失败)。我们可以简单理解 Promise 为一个过程,它不受外界因素干扰,任何其他操作都不能强行改变 Promise 的状态。
    2.Promise 的状态只会从 pendingfulfilled 或者 pendingrejected,简单的说就是一个处理成功的过程和一个处理失败的过程,而且一旦得到结果,状态就不能改变。

二、举例说明

注:下面会有很多模拟异步数据返回的形式,这里先封一个公共函数供下面的demo使用:

var ajaxDataFn = function(val, delay) {
    return new Promise(function(resolve) {
        setTimeout(function() {
            resolve({ val: val, delay: delay })
        }, delay)
    })
}

(1)Promise 实例化后会立马执行,状态变化后会触发then提前注册好的回调函数(包括成功回调和失败回调)

var promise = new Promise((resolve, reject) => {
    console.log('step1');
    resolve();
});
promise.then(() => {
    console.log('Resolved.');
})
console.log('Hi!')
// step1
// Hi!
// Resolved

(2).then()的链式调用写法

(层级较深)

ajaxDataFn('test1', 500).then(function(res) {
    console.log(res.val);
    ajaxDataFn(`${res.val} test2`, 1000).then(function(res) {
        console.log(res.val);
        ajaxDataFn(`${res.val} test3`, 1500).then(function(res) {
            console.log(res.val);
        })
    })
})
// test1
// test1 test2
// test1 test2 test3

(建议的写法)

ajaxDataFn('test1', 500).then(function(res) {
    console.log(res.val);
    return ajaxDataFn(`${res.val} test2`, 1000)
}).then(function(res) {
    console.log(res.val);
    return ajaxDataFn(`${res.val} test3`, 1500)
}).then(function(res) {
    console.log(res.val);
})
// test1
// test1 test2
// test1 test2 test3

两种写法都能达到逻辑上的目的,但是用这种链式的写法就能很好的避免掉层级过深的问题,逻辑也能更加清晰,偶合度也低。

(3).catch()的使用

demo1:

var promise = new Promise((resolve, reject) => {
    throw new Error('error');
});
promise.catch(err => console.log("error: ", err));
// error:  Error: error

demo2:

var  promise = new Promise((resolve, reject) => {
    reject(new Error('error'));
});
promise.catch(err => console.log("error: ", err));
// error:  Error: error

demo3:

var promise = new Promise((resolve, reject) => {
    throw new Error('error');
});
promise.catch(err => {
    console.log("error: ", err)
    return new Promise((resolve, reject) => {
        resolve('catchVal')
    })
}).then(res => {
    console.log(res)
});
// error:  Error: error
// catchVal

tip: .catch() 仍然可以返回一个 Promise 对象,后面还可以继续 .then() 调用

(4)all()race()的使用
可以参考Promise解析(一)中的demo

(5)reject()resolve()的使用

Promise.resolve('test').then(function(res) {
    console.log(res);
})
// test
Promise.reject('test').then(null, function(res) {
    console.log(res);
})
// test
var p = new Promise(function(resolve) {
    resolve('test');
})

Promise.resolve(p).then(function(res) {
    console.log(res);
})
// test
console.log(p === Promise.resolve(p))
// true

tip: Promise.resolve()会返回一个 Promise 示例,它的参数可以是一个值也可以是一个 Promise 实例,且当参数是 Promis 实例时,Promise.resolve(p) === p。因此在真实场景中,当使用第三方库时,别人提供的 Promise 实例返回,建议都用 Promise.resolve() 包装后再使用,这样是最安全的。哪怕别人的库写的有问题也不会影响我们逻辑的正常运行。

三、实战应用

先介绍.then()的实现简化版原理(可以前往Promise解析(二)查看)

 this.then = function(onFulfilled) {
    return new Promise(function(resolve) {
        function handle() {

            var returnVal = isFn(onFulfilled) && onFulfilled(promise.value) || promise.value;
            // var returnVal = onFulfilled(promise.value);
            if (isFn(returnVal.then)) {
                returnVal.then(function(res) {
                    resolve(res)
                })
            } else {
                resolve(returnVal);
            }
        }
        if ('PENDING' === promise.status) {
            promise._resolveList.push(handle);
        } else if ('FULFILLED' === promise.status) {
            handle(promise.value)
        }
    })
}

================part1================

下面来试一把。。

function test1() {
    console.log('test1');
    return ajaxDataFn('test1', 1000);
}

function test2() {
    console.log('test2');
    return ajaxDataFn('test2', 1000);
}

function test3() {
    console.log('test3');
    return console.log;
}

// 题1
test1().then(function() {
    return test2();
}).then(test3);

// 题2
test1().then(function() {
    test2();
}).then(test3);

// 题3
test1().then(test2())
    .then(test3);

// 题4
test1().then(test2)
    .then(test3);

test1()test2() 均返回 promises。。。这四个题有什么区别呢,执行顺序又是怎样的呢????

=====================================

================part2================

ajaxDataFn([1, 2, 3], 500).then(function(res) {
    res.val.forEach(function(item) {
        ajaxDataFn(item, 500).then(function(res) {
            console.log(res)
        });
    })
}).then(function() {
    console.log('done')
})
// done
// {val: 1, delay: 500}
// {val: 2, delay: 500}
// {val: 3, delay: 500}

这段代码表达了通过第一个 Promise 返回一个数组 [1,2,3],再通过第二个 Promise 来将数组的每一个 item 输出,最后想输出结束标识。
然而第一个 Promise 返回的实际上是 undefined ,也就是说第二个.then()根本不会等到 list 中所有的item项执行完再去执行。

那么forEach(或者for)这类的循环又怎么和Promise一起来使用呢?我们可以看下上面介绍中提到的将所有的 list操作包装成一个新的 Promise 去执行。

ajaxDataFn([1, 2, 3], 500).then(function(res) {
    return Promise.all(res.val.map(function(item) {
        return ajaxDataFn(item, 500).then(function(res) {
            console.log(res)
        });
    }))
}).then(function() {
    console.log('done')
})
// {val: 1, delay: 500}
// {val: 2, delay: 500}
// {val: 3, delay: 500}
// done

=====================================

================part3================

Promise.all().then() 聊了那么多点之后,咱进阶的聊一下 .then() 的好用的地方

看完上述的那么多例子,可以总结出,在then()函数内部可以作3件事。

  1. return 一个Promise
  2. return 一个同步数据(当然可能是undefined
  3. throw一个异常

先来看下第一点:

ajaxDataFn('test1', 500).then(function(res) {
    console.log(res.val);
    return ajaxDataFn('test2', 500)
}).then(function(res) {
    console.log(res.val)
})
// test1
// test2

这是最常用的一种用法,但是千万别忘了return这个关键字哦!如果没写,那么下一个函数接收的就是undefined了,而且也不会等第一个请求结束了才去执行第二个。

看第二点:

var cache = null;
ajaxDataFn('test1', 300).then(function(res) {
    if (cache) {
        console.log('缓存中拉取数据')
        return cache;
    }
    return ajaxDataFn('test2', 300).then(function(res) {
    	console.log('从test2 Promise中拉取数据')
        cache = res;
        return cache;
    });
}).then(function(res) {
    console.log(res.val)
})

ajaxDataFn('test1', 1000).then(function(res) {
    if (cache) {
        console.log('缓存中拉取数据')
        return cache;
    }
    return ajaxDataFn('test2', 500).then(function(res) {
        console.log('从test2 Promise中拉取数据')
        cache = res;
        return cache;
    });
}).then(function(res) {
    console.log(res.val)
})
// 从test2 Promise中拉取数据
// test2
// 缓存中拉取数据
// test2

我们可以用将一些不变的请求数据存在本地变量中,然后下次请求来的时候就直接返回变量中存储的值,这样我们可以节省一些请求消耗。

然而这样写存在一个隐患,不知道大家发现没?同步数据就会涉及undefined的类型,你很有可能会把undefined传到下一个函数,因此在后面加上catch去捕获异常。

=====================================

================part4================

前面讲到then()的第二个参数rejectcatch()并非完全等价,下面就举例来说明

ajaxDataFn('test1', 300).then(function(res) {
    throw new Error('error');
}, function(res) {
	console.log('reject中捕获到了')
    console.log(res)
}).catch(function(error) {
	console.log('catch中捕获到了')
    console.log(error)
})
// catch中捕获到了
// Error: error
ajaxDataFn('test1', 300).then(function(res) {
    throw new Error('error');
}, function(res) {
    console.log('reject中捕获到了')
    console.log(res)
}).then(null, function(res) {
	console.log('下一级reject中捕获到了')
    console.log(res)
})
// 下一级reject中捕获到了
// Error: error

在同级别 resolve 中的处理错误,reject 是获取不到错误信息的,但是 catch 能获取到。然而下一级的 reject 也是能捕获到错误的。

=====================================

================part5================

前面抛出的问题,现在让我们一起来探讨下,是否都能解决了

test1().then(function() {
    return test2();
}).then(test3);
test1
|-----------------|
                  test2(undefined)
                  |------------------|
                                     test3(resultOfTest2)
                                     |------------------|

这是典型的 then()return 另一个 Promise 的场景,首先 test2test1 后执行肯定没问题。
而且下一个函数test3()会等前一个test2()请求完之后才执行。

test1().then(function() {
    test2();
}).then(test3);
test1
|-----------------|
                  test2(undefined)
                  |------------------|
                  test3(undefined)
                  |------------------|

前面提到在 then() 中想要返回另一个 Promise 的时候千万别忘了return关键字,这也是 then() 的副作用用法。
test1().then()中实际返回的是默认值undefined,因此 test3 并不会等到 test2 执行结束后才开始执行。
test2test1 执行完再执行肯定没问题。

test1().then(test2())
    .then(test3);
test1
|-----------------|
test2(undefined)
|---------------------------------|
                  test3(resultOfTest1)
                  |------------------|

下面的例子详细一些:

function test2() {
    return Promise.resolve('bar');
}
Promise.resolve('foo').then(test2).then(function(result) {
    console.log(result);
});
//bar
Promise.resolve('foo').then(test2()).then(function(result) {
    console.log(result);
});
//fool

这个知识点我前面没有介绍,在这里我讲一下(可以参考上面给出的.then()原理代码来看)。当then()中的参数不是一个函数的时候,内部逻辑会立即执行,也就是说在.then注册添加回调函数的时候已经执行了,更细致的说在 Promise 的状态还是 pending 的时候已经执行了 ,而且不会影响整个链路,很多书上称之为 “Promise穿透”,在这个场景中,test2 实际返回的是一个 Promise 实例不属于函数,所有在 test1 执行完之后立马执行 test3,而且 test2 不会影响整个进度,所以和test1同步进行。

test1().then(test2)
    .then(test3);
test1
|-----------------|
                 test2(resultOfTest1)
                 |------------------|
                                    test3(resultOfTest2)
                                    |------------------|

这里 test2 就像是把 then() 中的回调函数抽离到一个命名函数中。
这样实际上就和第一种情况一样,then()中的回调实际return了另一个Promise

function test2Copy() {
    return test2()
}
test1().then(test2Copy).then(test3);

作用域是干什么用的

在理解作用之前,我先介绍一下另外两个家伙---引擎和编译器。

  • 引擎:负责javascript整个执行的过程
  • 编译器:负责分词-解析词法-生成抽象语法树-生成可执行代码。javascript的代码片段编译发生在执行之前,大部分场景发生在代码执行前的几微秒。
  • 作用域:总的来讲就是统一收集维护标识符变量,并管理访问权限

下面针对一个示例代码来讲述三者之间微妙的关系:

var demo = 1;

实际上这段代码可以分解为两步,第一步是:var demo,第二步是demo = 1
当引擎执行到这段代码时,编译器首先会对这段代码进行如下处理:

  • 编译器会询问作用域是否已经存在一个叫demo的标识符(即变量)在同一个作用域中,若是,直接忽略这个声明,否则编译器会要求在当前作用域中声明一个叫demo的标识符,并分配存储内存。
  • 在编译器生成完可执行的代码之后,引擎开始处理demo = 1这个命令。
    1. 首先引擎会询问当前作用域是否已经存在一个叫做demo的标识符(变量)。
    2. 如果存在,引擎开始使用这个变量。
    3. 如果不存在,引擎继续向上访问上级作用域,直到找到这个标识符为止。如果最终询问到全局作用域仍然未找到,则抛出一个异常(ReferenceError)。

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

接下来讲一下作用域嵌套:

var b = 1;
function test() {
    console.log(b)
}
test();

当引擎执行到这一段代码时,会有以下几个步骤:

  • 引擎询问test的作用域是否存在b标识符
  • 结果在test的作用域中并不存在
  • 引擎接着询问test的上一级作用域(这里是全局作用域)
  • 结果找到了,开始使用b

总结下来,作用域就是一个集中管理变量,并且控制变量访问权限的枢纽。

axios 和 fetch 实战分析

前言

趁着搭建公司新版 react 移动端脚手架的机会,对 http-client 库细致了解了一番。axiosfetch是目前比较火热的两款产品,这里就我了解到的一些信息记录一下。

fetch api 使用总结

之前我在 PC 端项目中选择的是 fetch api,使用了将近两年左右,这里我将个人的使用体验分享一下。

  • fetch 默认不会携带 cookie, 我们需要手动设置 credentials 来支持 cookie 携带给服务端。
  • fetchresponse 返回状态码比较特殊,它把400500的返回码都当成成功的返回,我们在使用的时候可能需要特地封装一下。
  • fetch 没办法取消本次请求,只要你发送了本次请求,后续的情况都是不可控的。
  • 另外,我们知道 fetch 作为 AJAX 的一个替代品,它并不是基于 XHR 实现的,所以它并不能检测到请求的进度。

可能或许大概你会拿它和 $.ajaxaxios 这类封装的比较完善的库做对比,而且开始吐槽,这东西怎么哪哪都不如 axios
这里我们先要确定一下 fetch api 的定位,它是一个更加偏底层的库,提供了丰富的 API 给开发者调用,可以支持很多的底层功能,但是可能找不到基于场景封装好的功能。
因此,我们在选择 fetch 的时候需要考虑我们是不是有这么复杂的需求需要通过 fetch api 来支持,用 axios 是不是会更省事?

上述介绍了一大堆的 fetch api 的缺点,可能大家会认为我对 fetch api 有什么偏见... 哈哈😄。实际上我近两年的 http-client 库选择的都是 fetch api
还是那句话,没有最好的产品,只有最适合的产品。如果某个产品你用的习惯了,你就咋看咋顺眼。

axios 使用总结

从去年下半年开始,我开始在线上移动端使用 axios 。接下来讲一下我个人的使用情况。

刚开始用的时候我也是看了很多的文档资料,毕竟网上把这东西夸的天花乱坠的,到底这东西魅力在哪里,得自己试试才知道呀。

这里先介绍一下我之前对 axios 做的分析,具体是不是真的有那么大魅力,我们后面用场景验证。

  • axios 支持设置全局默认配置,比如项目中基本不会变的根域名、headers配置这些,我们都可以初始化设置一发。
  • axios 拦截器也是一个比较好用的功能,它允许在发 request 之前和接收 response 之前进行自己的一些逻辑处理。
  • 还有一点比较重要的是,这家伙支持请求取消呀,这能一定程度的减少网络请求资源浪费。

以上三点是我比较 care 的特性,其他的我就不罗列介绍了。
下面我们来看一下怎么封一个 axios 类(临时写的一个思路,未测试),让它帮我们的业务更好的赋能。

import axios from 'axios'
import Evnets from 'mona-events'

const CancelToken = axios.CancelToken

class Ajax extends Evnets {
    constructor(props) {
        super(props)
        axios.default.baseURL = 'https://api.monajs.cn'
        this.filter
    }

    get(url, params = {}, canCancel = false, cancelOption) {
        if (canCancel) {
            params.cancelToken = new CancelToken(function executor(c) {
                // executor 函数接收一个 cancel 函数作为参数
                cancelOption = c;
            })
        }
        return axios.get(url, params);
    }

    post(url, params = {}, canCancel = false, cancelOption) {
        if (canCancel) {
            params.cancelToken = new CancelToken(function executor(c) {
                // executor 函数接收一个 cancel 函数作为参数
                cancelOption = c;
            })
        }
        return axios.post(url, params)
    }

    filter() {
        axios.interceptors.request.use(config => {
            // 在发送请求之前做些什么
            return config;
        }, error => {
            // 对请求错误做些什么
            return Promise.reject(error);
        });
        axios.interceptors.response.use(response => {
            // 对响应数据做点什么
            return response;
        }, error => {
            // 对响应错误做点什么
            return Promise.reject(error);
        })
    }
}

上面基本上阐述了两个事情:

  • axios 配合我们的业务场景使用,例如结合服务端的业务返回码,接口请求异常,做统一逻辑处理,规避掉很多冗余代码。
  • axios 类初始化一些配置信息,在真实使用的时候其他的接口服务层类可以继承这个类,可以改写覆盖这些配置信息,也可以使用默认值。
  • axios 类配合 Events 类完成服务接口监听者模式,更高效的实现接口数据分发,减少掉代码中的回调函数传递。(这个点后续专门开一篇文章详细阐述其使用魅力)

总结下来我的观点是选择哪个 http-client 库进行业务开发取决于实际场景,在移动端开发,我个人比较偏向于 axios 。它能够帮我们较少成本的上手使用。

对前端路由选择的思考

前言

随着 SPA 广泛应用在前端项目中,前端路由这个概念也伴随着火了起来。
这家伙目前主要是通过 hashhitory api 这两个玩意实现,具体怎么实现的,后面稍微介绍一下。

本文不对这两个实现方案的好坏做过多评价,我依然觉得技术没有好坏,只有在具体的时间阶段和场景下,哪个更加合适一些。

接下来我来介绍一下这两个玩意的应用背景。

hashhistory 的变迁

SPA 单页面应用兴起的时候,大家都在寻找一个可以切换页面,且不会触发页面刷新的方案。
据我了解,hash 在被应用到前端路由之前,主要是被应用在页面锚点定位。
因为 hashurl 信息携带主要依赖 #下面的字符,前端系统可以通过 js 解析页面路径信息来处理后续的逻辑,所以应该是当时的最优解决方案。
例如:http://monajs.cn/docs/#home
后来,HTML5 中新添加了 History api ,它支持切换和修改历史状态。主要通过 backforwardgo
三个方法,对应浏览器的前进,后退,跳转操。
修改历史状态包括了 pushStatereplaceState 两个方法,这两个方法接收三个参数:stateObjtitleurl

history.pushState({name: 'yangxi'},  'title',  'url')

window.addEventListener('popstate',  (e) => {
     if (e.state && e.state.name === 'yangxi') {
         return
     }
     //TODO
},  false)

从代码中可以看到,我们通过 history 做页面跳转的时候,可以携带信息,而且不像 hash 跳转那样只能携带有限的字符信息。stateObj 可以携带各种数据对象。

这是 history api 对前端路由切换的一次全新赋能。

另一方面,使用 history api 来设计前端路由,改善了之前 #*** 的这种 hash 形式。一定程度上来讲美观了不少😄😄😄😄

以上特性,是我比较喜欢 history api 设计前端路由的重要理由,我自己也实现了一套基于 history apireact-router,有兴趣的朋友可以看一下。

前端路由实现思路

接下来我简单讲一下 router 的实现思路。
其实自己实现一个简单的 router 是比较简单的,我们只需要能监听到 url 的变化,然后对 url 进行一堆格式化分析,然后去匹配对应的页面实例。

  • hash 实现
    hash 实现路由是通过监听 hashchange 事件实现的,然后页面之间跳转则可以通过 location.hash 来做。
  • history 事件
    history 实现路由是通过监听 popstate 事件实现的,然后通过 history api 提供的 pushStatereplaceState 来做页面的跳转和回退。

介绍的比较粗浅,仅代表个人的一些使用总结

Promise解析(二)

在看这篇文章之前建议先看Promise解析(一),对 promise 有一定的了解能更高效的理解其原理。

我们通过demo来看一下 Promise 的一些特性。

new Promise(function(resolve) {
    resolve('B')
}).then(function(res) {
    console.log(res)
})

console.log('A')
// A、B

通过前面两篇文章的介绍,我们了解到:

  1. Promise的参数会立即执行,不受执行状态的影响。
  2. then 负责添加后续要执行的回调函数。
  3. 从程序实现上可以理解为把then方法的函数参数添加到一个执行队列,然后在Promise的状态从 pending 改变为 fulfilled 或者 rejected时,遍历执行这个操作队列。

接下来我们来实现一下:

雏形实现

function Promise(fn) {
    var promise = this;
    promise.callBackList = [];

    this.then = function(fn) {
        this.callBackList.push(fn)
    }

    function resolve(value) {
        promise.callBackList.forEach(function(cb) {
            cb(value)
        })
    }
    fn(resolve)
}

接下来我们用我们刚实现的 Promise 来验证一把:

new Promise(function(resolve) {
    resolve('B')
}).then(function(res) {
    console.log(res)
})

console.log('A')
// A

我们发现这个结果与我们预期的输出结果不一样,我们预期会先输出一个A然后输出B,然而结果却只输出一个A。分析一下上面的 Promise 雏形实现,不难发现 resolve 会先于 then 执行,也就是说 callBackList 是一个空数组。

支持异步

为了解决这个问题,我们可以为 resolve 的执行逻辑添加setTImeout,让这段逻辑在Js任务队列的末尾执行。

function resolve(value) {
    setTimeout(function() {
        promise.callBackList.forEach(function(cb) {
            cb(value)
        })
    })
}

支持链式调用

经常用 Promise 的同学都知道支持链式调用是 Promise 的一个优势,它支持将异步的代码以同步的方式来书写,从而使得代码逻辑清晰已读。实际上我们就是让 .then() 函数执行后返回this

this.then = function(fn) {
    this.callBackList.push(fn)
    return this
}

添加执行状态

Promise解析(一)中我们介绍过 Promisependingfulfilledrejected 三个互斥状态,存在 pendingfulfilledpendingrejected 两种场景,而且都是不可逆的。

下面结合状态来升级一把我们前面的雏形 Promise,这里先介绍resolve 执行时的状态变化,后续补充reject

function Promise(fn) {
    var promise = this;
    promise._resolveList = [];
    promise.status = 'PENDING';
    promise.value = null;
    this.then = function(onFulfilled) {
        if ('PENDING' === this.status) {
            this._resolveList.push(onFulfilled);
            return this;
        }
        onFulfilled(this.value);
        return this;
    }

    function resolve(value) {
        setTimeout(function() {
            promise.status = 'FULFILLED';
            promise.value = value;
            promise._resolveList.forEach(function(cb) {
                cb(promise.value);
            });
        });
    }
    fn(resolve)
}

下面来通过个demo来验证一发:

new Promise(function(resolve) {
    resolve('B');
}).then(function(res) {
    console.log(res);
})

console.log('A');
// A 、 B

写到这里,简单场景下的 Promise 已经实现完毕了。但是在实际应用场景中,Promise 大多是串行使用的,会通过很多的 then链式调用来拆分一个层级比较深的逻辑。

串行实现

先举个简单的例子来说明:

new Promise(function(resolve) {
    resolve('B');
}).then(function(res) {
    console.log(res);
    return 'C'
}).then(function(res) {
    console.log(res);
})

console.log('A');
// A 、 B 、 B

通过这个例子,我们发现这输出结果又和我们预期的结果有出入了。我们期望输出 ABC,可实际上却输出了 ABB。这主要是因为我们的整个 then 执行队列中的回调函数都共用了 resolve 传进来的同一个 value
Promise解析(一)中我们介绍过在then中重新返回一个新的 Promise 示例,提供给下一个 then 的回调函数来使用。

来看一下代码实现:

function Promise(fn) {
    var promise = this;
    promise._resolveList = [];
    promise.status = 'PENDING';
    promise.value = null;

    this.then = function(onFulfilled) {
        return new Promise(function(resolve) {
            function handle() {
                var value = onFulfilled(promise.value);
                resolve(value);
            }
            if ('PENDING' === promise.status) {
                promise._resolveList.push(handle);
            } else if ('FULFILLED' === promise.status) {
                handle(promise.value)
            }
        })
    }

    function resolve(value) {
        setTimeout(function() {
            promise.status = 'FULFILLED';
            promise.value = value;
            promise._resolveList.forEach(function(cb) {
                cb(promise.value);
            });
        });
    }
    fn(resolve)
}

我们再用刚次的例子来验证一下:

new Promise(function(resolve) {
    resolve('B');
}).then(function(res) {
    console.log(res);
    return 'C'
}).then(function(res) {
    console.log(res);
})

console.log('A');
// A 、 B 、 C

这个例子验证OK了,接下来我们再验证另一种场景,then 里面如果返回的是一个 Promise 示例又会是什么样的结果呢?

new Promise(function(resolve) {
    resolve('B');
}).then(function(res) {
    console.log(res);
    return new Promise(function(resolve) {
        resolve('C')
    });
}).then(function(res) {
    console.log(res);
})

console.log('A');
// A、B、Promise对象

观察输出结果,又和我们的预期有出入了,我们希望输出的是 C ,然而输出了一个Promise对象,接下来我们来对 then 完善这种场景。

this.then = function(onFulfilled) {
    return new Promise(function(resolve) {
        function handle() {

            var returnVal = isFn(onFulfilled) && onFulfilled(promise.value) || promise.value;
            // var returnVal = onFulfilled(promise.value);
            if (isFn(returnVal.then)) {
                returnVal.then(function(res) {
                    resolve(res)
                })
            } else {
                resolve(returnVal);
            }
        }
        if ('PENDING' === promise.status) {
            promise._resolveList.push(handle);
        } else if ('FULFILLED' === promise.status) {
            handle(promise.value)
        }
    })
}

再来验证一下上面的demo

new Promise(function(resolve) {
    resolve('B');
}).then(function(res) {
    console.log(res);
    return new Promise(function(resolve) {
        resolve('C')
    });
}).then(function(res) {
    console.log(res);
})

console.log('A');
// A、B、C

处理失败

上面介绍了成功时会将 pending 状态置为 fulfilled ,失败时会将 pending 置为 rejected,下面来把失败场景的代码补上:

function Promise(fn) {
    var promise = this;
    promise._resolveList = [];
    promise._rejectList = [];
    promise.status = 'PENDING';
    promise.value = null;
    promise.reason = null;

    this.then = function(onFulfilled, onRejected) {
        return new Promise(function(resolve, reject) {
            function handle() {

                var returnVal = isFn(onFulfilled) && onFulfilled(promise.value) || promise.value;
                // var returnVal = onFulfilled(promise.value);
                if (isFn(returnVal.then)) {
                    returnVal.then(function(res) {
                        resolve(res)
                    })
                } else {
                    resolve(returnVal);
                }
            }

            function errback(reason) {
                reason = isFn(onRejected) && onRejected(promise.reason) || promise.reason;
                reject(reason);
            }
            if ('PENDING' === promise.status) {
                promise._resolveList.push(handle);
                promise._rejectList.push(errback);
            } else if ('FULFILLED' === promise.status) {
                handle(promise.value)
            } else if ('FULFILLED' === promise.status) {
                errback(promise.reason)
            }
        })
    }

    function isFn(fn) {
        return typeof fn === 'function'
    }

    function resolve(value) {
        setTimeout(function() {
            promise.status = 'FULFILLED';
            promise.value = value;
            promise._resolveList.forEach(function(cb) {
                cb(promise.value);
            });
        });
    }

    function reject(reason) {
        setTimeout(function() {
            promise.status = 'REJECTED';
            promise.reason = reason;
            promise._rejectList.forEach(function(cb) {
                cb(promise.reason);
            });
        });
    }
    fn(resolve, reject)
}

到这里Promise的实现原理就over了,下面来讲一下Promise.resolvePromise.rejectcatchapi

Promise.resolve

Promise.resolve = function(value){
    return new Promise(function(resolve, reject){
        resolve(value)
    })
}

Promise.reject

Promise.reject = function(value){
    return Promise(function(resolve, reject){
        reject(value)
    })
}

catch

Promise解析(一)中讲到过,rejectcatch 并非完全等价,catch不仅能捕获到 Promise 的处理错误,而且还能捕获到 resolve 执行过程中的报错信息。实际上 catch 是通过在Promise 中加了一层then而间接的达到这种效果。

this.catch = function(onRejected){
    return this.then(undefined, onRejected)
}

从无到有搭建一套组件库

原文链接:#31

简介

在正式开始之前,我们先简单的回顾一下前端领域的发展历程。在最早期的阶段,我们只能裸写 js/css,慢慢的项目中就能看到非常多的重复代码。在这样的背景下,jQuery 和 Bootstrap 应运而生了,它所展示的组件设计**在之后许多的组件库中都能看到,也给项目开发带来了升级。进而进入 MVVM 主宰阶段,组件化的**开始像病毒一样在前端领域蔓延开来。

组件化更像是一种**,是前端工程师在代码复用和功能抽象上的一次实践。最终目的都是提效和项目优化。

会“偷懒”又何尝不是工程师的美德。

组件规划阶段

  • 定制化
  • 扩展性
  • 国际化

定制化

这里讲的定制化,可理解成组件换肤,但绝不止于颜色。像业界很多成熟的组件库都支持这样的定制功能。举两个例子:

像 Ant Design 组件库,它对外暴露了许多的样式变量,接入方可以通过覆盖样式变量的方式来定制符合自己要求的组件。因为 Ant Design 采用的是 Less 预处理器,所以接入的项目也必须支持 Less 编译。

@primary-color: #1890ff; // 全局主色
@link-color: #1890ff; // 链接色
@success-color: #52c41a; // 成功色
@warning-color: #faad14; // 警告色
@error-color: #f5222d; // 错误色
@font-size-base: 14px; // 主字号
@heading-color: rgba(0, 0, 0, 0.85); // 标题色
@text-color: rgba(0, 0, 0, 0.65); // 主文本色
@text-color-secondary : rgba(0, 0, 0, .45); // 次文本色
@disabled-color : rgba(0, 0, 0, .25); // 失效色
@border-radius-base: 4px; // 组件/浮层圆角
@border-color-base: #d9d9d9; // 边框色
@box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // 浮层阴影

这也是我之前一直采用的方式。

Fusion Design 中台组件库,它可以说是为定制化量身打造的。它梳理出了过千种配置规则,使用者可以通过可视化界面搭配出一套符合自己要求的规则。这些规则和变量最终会转换成 Sass/Less 变量,然后编译后动态的渲染到组件中。这种方式的优点很明显,你不用再去看一大堆规则和变量背后的作用打底是什么。但是耦合性太高也是一个比较严重的问题。

随着浏览器原生能力的加强,CSS Variable 的兼容性已经达到生产环境应用标准。配合上calc(),同样可以达到样式变量配置化的能力。

我们可以借鉴 Fusion Design 的设计思路,提供接入方一个可视化的配置平台,生成一套符合接入方要求的样式变量,不过不再是 Less 的配置变量,而是原生支持的 css 变量,最终打成一个单独的主题包。同时解决了预处理器的环境依赖和样式变量在组件中的侵入。

扩展性

这里讲的扩展性和定制化有一定的重复性,样式变量角度不作重复介绍。扩展性是组件库核心能力非常重要的一个考量因素。为了应对业务方多变的功能需求,接入方在选择组件库的时候都会对扩展性有很高的要求。一般可以从以下几点来衡量一套组件库的扩展性:

  • 组件的 api 是否足够全面
  • 是否有清晰的组件组合关系,组件粒度是否够细
  • 组件是否提供了完整的生命周期钩子
- 组件的 api 是否足够全面

对于开发者而言,选择接入的组件库就像是一家综合超市,最好是我想要的你都有,我不想要的你也有。使用者可以在组件的基础上很低成本的扩展出个性化的业务组件。

- 是否有清晰的组件组合关系,组件粒度是否够细

先举个例子,比如我现在想要一个时间选择器组件(DatePicker),但是组件库中的 DatePicker 并不能满足我的业务需求。这时候我需要重新实现一个 DatePicker,如果自己实现的话成本就会很高。如果用组件库现有的组件进行组合,就能大大降低开发成本。

因此梳理组件间的组合关系就显得尤为重要,我一般把组件划分为基础元件、能力组件、复合组件和业务组件四种类型,然后通过组件组合来形成一整套组件库,这块下面会再介绍。

- 组件是否提供了完整的生命周期钩子

组件的生命周期在实际场景中很少使用,但并非是一个多余的设计。使用者可以对组件的挂载和卸载时机了如指掌,可以在组件更加细分的节点执行业务逻辑。

国际化

这是组件推广开源一个比较重要的因素,像 Ant Design Vue 更是提供了36种语言包。

组件设计阶段

  • 确认组件间组合关系
  • 定制全局 css 变量
  • 定制一致性的 api
  • 国际化基于组件 or 基于配置

确认组件间组合关系

为了达到高扩展性的目的,原子设计理论是一个很恰当的指导方案。原子设计理论认为页面应该是可以组合和分解的,同理组件也是可以组合和分解的。比如可以把复杂组件(复合组件)拆分成能力组件的组合,进而拆分成基础元件,然后复杂组件又可以结合业务场景成为业务组件。

我们拿 DateSelect (时间选择器组件)举个例子例。DateSelect 可以拆分为 DatePicker (时间滚动选择器) 和 Popup (弹出层组件),而 DatePicker 组件可以由多个 Picker (弹出层组件)组成,进而 Picker 又可以在 Hammer (手势库)的基础上开发,而 Popup 也可以在 Mask (蒙层组件)的基础上扩展得到。
未命名

定制全局 css 变量

不仅组件间有组合复用关系,样式变量同样需要。要保证所有的组件都有一致的风格样式,我们需要维护一份全局的样式变量,例如字体颜色,字体大小等等。然后所有组件都在这份全局共有变量的基础上定义私有的样式变量。

:root{
    --namespace-common-color: '#333';
    --namespace-common-font-size-sm: 12px;
    --namespace-common-font-size-md: 14px;
    --namespace-common-font-size-lg: 16px;
    ...
}
.namespace-date-select {
    .date-select-confirm, .date-select-cancel {
        color: var(--namespace-common-color);
        font-size: var(--namespace-common-font-size-sm);
    }
    ...
}
...

定制一致性的 api

整套组件库的开发一般会由多人协同完成,在这种情况下,很容易就会导致同种功能的 api 命名不一致的情况。比如 Dialog 中的取消事件用了 onCancel ,而DateSelect 中的取消事件却用了 cancel。虽然这并不会影响组件库的运行,但是会显得很不规范,没有组件的设计和评审阶段。并且在接入方正式使用的时候也会非常的懵逼,使用成本随之增高。

onMount:        组件挂载                 
onWillMount:    组件卸载
onShow:         显示事件
onHide:         隐藏事件 
onCancel:       取消事件
onConfirm:      确认事件
onDelete:       删除事件
...

国际化基于组件 or 基于配置

在社区里众多的组件库中,对国际化的支持程度不尽相同。主要看组件库设计之初针对的用户群体,如果你只是想打造一个内部使用的组件库,国际化就可以忽略。

基于组件配置的国际化和基于全局配置的国际化两者有一定的差异。如果你想在组件库中支持多语言(比如A组件用中文,B组件用英文),那么选择基于组件进行配置会简单很多。如果不需要支持到这种程度,那么全局配置就省事的多了

组件实现阶段

  • 确认组件的私有 api 和私有样式变量
  • 方法 or 事件
  • 组件质量保证
  • ...

确认组件的私有 api 和私有样式变量

为了让使用者可以定制组件风格,我们需要进行变量抽取,为每一个组件抽取出尽量丰富的样式变量,使用者可以灵活的修改组件的风格。

:root{
    --date-select-font-size: var(--namespace-common-font-size-sm);
    --date-select-color: var(--namespace-common-color);
    ...
}
.namespace-date-select {
    .date-select-confirm, .date-select-cancel {
        color: var(--date-select-color);
        font-size: var(--date-select-font-size);
    }
    ...
}
...

方法 or 事件

不管在日常开发中还是组件开发中,这都是一个比较头疼的问题。用方法回调还是事件通常都能达到我们想要的效果,在什么场景下该用方法什么场景下该用事件就比较犯难了。

我们来分析一下两者都有什么优缺点。

先来看一下回调方法。举个例子,你提交了查询表单,点击查询按钮之后需要刷新一下列表。我们需要把刷新列表的回调方法传递到查询表单,提供给接口返回后进行调用。在简单的父子级组件上应用肯定是非常方便的。然而当组件的嵌套关系复杂之后,你可能需要在兄弟级组件间,甚至是毫不相干的两个组件间进行通信,这就变得很痛苦了,因为回调方法的传递链路会变得很长。总结下来它最大的特点就是链路非常的清晰很容易定位方法来源,而当组件间关系变得复杂之后就变得不好维护了。

再来看一下事件。与方法不同,事件不需要组件间传递方法,它只需要定义一个监听的 key ,然后在正确的时间点触发即可。可以看出它能比较好的解决上面提出的组件间关系复杂的场景,然而它的问题在于,你定义了过多的事件之后项目就会变得很难维护,因为它不像回调方法有清晰的传递关系,出问题了你甚至都不能定位,滥用事件是非常恐怖的一件事情。

在开发组件的过程中,如果是比较简单的父子级关系组件,用方法传递绝对优于事件。而像一些组件嵌套关系非常复杂的复合组件则推荐用事件来处理比较好。

组件质量保证

致力于打造一个优秀的开源组件库,对组件的质量需要有一定的追求和要求。可以从以下几个点来尽量保证组件的质量

  1. 组件开发前的设计评审
  2. 单元测试覆盖率
  3. 交叉 review

组件维护阶段

  • 发版规范
  • ...

发版规范

每一次的发版需要有一条 issure 和分支关联,在 issure 上记录下版本的改造点和优化点,以及对应的开发负责人。

请求跨域分析

概述

平时开发中,你发现这东西特别安静,安静到你大半年碰不到一次,但是到面试的时候,这家伙就时不时的出来难一难你。
今天我们一起来看看“跨域”这个东西!

为什么会出现跨域这个问题

我们这种搬砖的,碰到“跨域”的时候,心里可能会想是哪个人设计的“跨域”限制?这东西那么鸡肋,只会加重我的开发成本,为什么还要设计这个玩意呢?
这么追溯下去,其实“跨域”是一种浏览器行为,是浏览器的同源策略导致了跨域。
浏览器给出的官网解释是“同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。”
这么听下来,我们好像可以理解为浏览器为了保护访问者的信息安全而牺牲了一些东西。

如果没有浏览器的同源策略又会怎么样呢?

接下来我们就要搞明白,浏览器的安全策略到底是保护哪种用户行为?
实际上,浏览器是对两种行为添加了同源限制。

  • 接口请求
  • dom查询

接下来我们来看看没有同源限制会怎样的结果

  • 接口请求
    我们拿用户登陆(www.func-star.com/#/login)来举个🌰,我们都知道用户登陆成功之后会将用户信息种到 cookie (或者其他)中。后续再发送请求的时候,就会自动在请求头携带 cookie 信息用来标识用户。
    在你登陆成功之后你开始访问 www.func-star.com 站点下的其他页面,突然你的一个同事给你发了一个链接 www.wohuiluanlai.com 这个网站内部逻辑就负责向 www.func-star.com 中干各种坏事。
    因为没有同源限制,所以 www.wohuiluanlai.comwww.func-star.com 的用户信息就是通的,干的所有坏事都是以登陆者身份干的,合理合法。
    说到这,我们就会想 cookie 这东西我复制一下粘过去不是一样很简单,同源限制好像并不是很有必要。那么我们就要想是不是有办法限制 cookie 的操作呢?实际上,服务端可以设置 httpOnly,使得前端无法操作cookie。
  • dom查询
    现在假设有两个网站 A: www.zhifubao.com | B:www.zhifuba.com ,这两域名就差了一个字符,也就是我们常说的钓鱼网站。
// www.zhifuba.com
// HTML
<iframe name="zhifubao" src="www.zhifubao.com"></iframe>
// JS
// 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['zhifubao']
const username = iframe.document.getElementById('username')
const password = iframe.document.getElementById('password')

现在假设你没有注意点进去了 www.zhifuba.com,如果没有浏览器的同源策略,钓鱼网站就非常轻而易举的拿到你的用户名和密码。

另外,网络没有绝对的安全,哪怕同源策略的存在,也会有办法绕过这道限制。很多场景下考虑的是有没有必要,以及绕过这道限制所花费的成本能不能获取到对应的结果。

既然同源策略不可避免,那么我们又有什么解决办法呢?

为了让请求更加安全,浏览器有同源策略来限制跨域请求,那总不能不让开发者来进行跨域请求呀!
下面我们来介绍一下几种常用的跨域请求方式。

1.jsonp

前面我们讨论了那么久像 dom 查询和接口请求都被浏览器的同源策略限制了,但是像 scripts 这样的脚本资源,浏览器是没有进行限制的。
我们可以讨巧的用服务端自执行回调函数的形式,通过前后端约定函数名来达成数据交互的目的。

// jsonp
class Ajax {
    param(data){
        let _t = [];
	Object.keys(data).forEach(function(vi){
		if(data[vi] !== undefined){
			_t.push(vi+"="+data[vi]);
		}
	});
	return _t.join("&");
    }
    jsonp(url, data, cbkName, time_out = 10) {
        return new Promise((resolve, reject) => {
            if (!cbkName) {
                cbkName = 'jsonp' + (Date.now())
            }
            data = data ? data : {};
            Object.assign(data, {
                callback: cbkName
            })
            let script = document.createElement("script");
            script.async = 'async';
            script.src = url + '?' + this.param(data);
            window[cbkName] = function(res) {
                resolve(res)
                delete(window[cbkName])
            }
            let timeout = setTimeout(() => {
                window[cbkName] = function() {};
                reject("请求超时")
            }, time_out * 1000)
            script.onload = function() {
                clearTimeout(timeout);
                setTimeout(() => {
                    script.remove()
                }, 500)
            }
            script.onerror = function() {
                reject("请求失败")
                setTimeout(() => {
                    script.remove()
                }, 500)
            }
            document.body.appendChild(script)
        })
    }
}
export default new ajax;

2.ifram的应用

不难发现,上面的🌰支持了 get 形式的跨域请求,因为资源加载也是 get 形式的。
那么如果我们要支持 post 形式的请求又需要怎么做呢?
回想一下以前前后端不分离的年代,ajax 还没有流行起来的时候,<form> 标签大家一定印象还很深,它可以设置 method 属性,并且支持 post
接下来我们就来创建一个虚拟的 form 和一个 虚拟的 ifram 来模拟一番。

class Ajax {
    jsonpPost(url, data) {
        new Promise((resolve, reject) => {
            const iframe = document.createElement('iframe')
            iframe.name = 'iframePost'
            iframe.style.display = 'none'
            document.body.appendChild(iframe)
            const form = document.createElement('form')
            const node = document.createElement('input')
            iframe.onload = function(data) {
                console.log('请求成功')
                resolve(data || { success: true })
            }
            script.onerror = function(data) {
                console.log('请求失败')
                reject(data || { success: false })
            }
            form.action = url
            // 在指定的iframe中执行form
            form.target = iframe.name
            form.method = 'post'
            for (let name in data) {
                node.name = name
                node.value = data[name].toString()
                form.appendChild(node.cloneNode())
            }
            form.style.display = 'none'
            document.body.appendChild(form)
            form.submit()
            document.body.removeChild(form)
        })

    }
}
export default new ajax;

3.CORS

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。看起来这是个比较官方的解决方案,都进W3C标准了。
对于前端开发者来讲,这是一个非常棒的东西,因为不需要绕来绕去只为了发一个请求。
我们还是按照原来不跨域的方式去发送请求,只需要后端的接口支持一下跨域,问题就解决了,貌似是把问题抛给了后端。。。
详细说明可以阅读 CORS——跨域请求那些事儿

4.nginx代理

一起研究了那么多方法,都是在讨论怎么解决跨域。那有没有办法把跨域的请求直接转发到正确的后端服务器上呢?这样不是更省事吗。
大家肯定都想到了 nginx 这个神器了。下面来看一下具体怎么配置:

// nginx 配置
server{
    listen 8888;
    server_name otherserver;
    location ^~ /api {
        proxy_pass http://currentserver:8080;
    }
}

添加这个配置之后,前端同学就不需要再考虑跨域的问题了。所有匹配 otherserver:8888/api 格式的接口都会被代理转发到 http://currentserver:8080 服务器上。
这样前端在前后端数据交互的时候就不需要考虑跨域的问题。

参考文章:
不要再问我跨域的问题了
CORS——跨域请求那些事儿

koa-compose 分析

原文链接

介绍

在处理一些复杂逻辑的场景下,特别是一些长流程的任务,我们通常喜欢把流程拆分成单步小任务来执行,然后保证这些小任务按照既定的逻辑顺序执行。

或者我们希望对一个实体赋能,让它拥有错误检测、数据过滤、日志打印等功能。把每一项功能都抽成独立的中间件,分别负责独立的任务,就是一个比较好的选择。

koa-compose 模块可以将多个中间件函数合并成一个组合中间件函数,然后通过调用这个中间件函数就可以依次来执行这一系列中间件。

这个模块的源码比较简单,我们直接通过源码来分析一下它的处理过程:

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  // 只接收数组
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    // 只接收 function
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  // 返回一个匿名执行函数
  return function (context, next) {
    // last called middleware #
    let index = -1
    // 递归调用开始入口
    return dispatch(0)
    function dispatch (i) {
      // 避免重复调用
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      // 调用指针
      index = i
      let fn = middleware[i]
      // 当遍历完所有的中间件,执行 next 回调函数
      if (i === middleware.length) fn = next
      // 如果没有定义 next,直接 返回 undefined
      if (!fn) return Promise.resolve()
      try {
        // 每一个中间件都会有两个形参
        // 1.外部透传进来的 context 对象,2.next 回调方法
        // 下一个中间件的执行体,会作为上一个中间的 next 参数传递进去
        // 通过 next() 方法的调用,实现递归所有中间件
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

通过代码我们可以看出 compose 内部的中间件依次执行是通过 next() 来实现的。可以理解成接着往下执行,也可以表示执行下一个中间件。

next

const compose = require('koa-compose')

var compMiddleware = compose([
	middleware1,
	middleware2
])

var compMiddlewareNext = function () {
	console.log('中间件全部执行完毕')
}

compMiddleware('我是 context', compMiddlewareNext)

function middleware1(ctx, next) {
	console.log(ctx)
	console.log('第一个中间件')
	next()
	console.log('第一个中间件的next执行后')
}

function middleware2(ctx, next) {
	console.log(ctx)
	console.log('第二个中间件')
	next()
	console.log('第二个中间件的next执行后')
}
  • 输出
我是 context
第一个中间件
我是 context
第二个中间件
中间件全部执行完毕
第二个中间件的next执行后
第一个中间件的next执行后

上述代码的执行过程如下:

  1. 执行 middleware1,输出“第一个中间件”,调用 next()
  2. 执行 middleware2,输出“第二个中间件”,调用 next()
  3. 执行 compMiddlewareNext,输出“中间件全部执行完毕”
  4. 全部中间件执行完毕,执行步骤回到 middleware2 的执行体,输出“第二个中间件的next执行后”
  5. middleware2 执行完毕后,middleware1 继续执行,输出“第一个中间件的next执行后”

通过下面一段伪代码可能会让执行过程更加清晰一些:

compMiddleware {
    middleware1 {
        console.log('第一个中间件')
        // next() 调用的时候执行 middleware2
        middleware2 {
            console.log('第二个中间件')
            // next()调用的时候执行 compMiddlewareNext()
            console.log('中间件全部执行完毕')
            console.log('第二个中间件的next执行后')
        }
        console.log('第一个中间件的next执行后')
    }
}

广度优先和深度优先

最近在项目中遇到的场景,在这分享一下。

通常来讲,对一个树形数组做处理,特别是层级未知的数据结构,第一想法肯定是遍历。在不考虑性能的情况下,基本上都能解决。

下面来个例子:
数据结构:

let data = [{
        id: 1,
        name: 'a',
        children: [
            { id: 11, name: 'aa' },
            {
                id: 12,
                name: 'ab',
                children: [
                    { id: 121, name: 'aba' },
                    { id: 122, name: 'abb' }
                ]
            },
            { id: 13, name: 'ac' }
        ]
    },
    {
        id: 2,
        name: 'b',
        children: [
            { id: 21, name: 'ba' },
            { id: 22, name: 'bb' }
        ]
    },
    { id: 3, name: 'c' }
];

现在我们先用大家熟悉的递归来实现一遍。

let test = (() => {
    let res = '';
    return inner = (data, id) => {
        if (!data || data.length === 0) {
            return
        }

        data.forEach(v => {
            if (v.id === id) {
                res = v.name
            }

            if (v.children && v.children.length > 0) {
                inner(v.children, id)
            }
        })
        return res;
    }
})()

let val = test(data, 123);
console.log(val);

实现完成之后我们会思考怎么才能把它的性能提高,特别是在树层级特别深的场景下,递归的性能还是比较难以接受的。

优化方案:

针对二叉树遍历,有深度优先和广度优先两种通用解决方案,我们可以根据数据结构的广度和深度选择最优的解决方案。虽然在前端日常开发中,很少会用到算法,但是这种性能优化的方案还是可以借鉴的。

广度优先

根据广度优先的思路来看一下代码实现。

let test2 = (() => {
    let res = '';
    return inner = (data, id) => {
        if (!data || data.length === 0) {
            return
        }
        let stark = []; // 初始化一个数据栈
        // 将第一层数据压入栈
        data.forEach(v => {
            stark.push(v);
        })

        while (stark.length > 0) {
            let item = stark.shift();
            if (item.id === id) {
                res = item.name;
            }
            if (item.children && item.children.length > 0) {
                stark = stark.concat(item.children)
            }
        }
        return res;
    }
})()

let val = test2(data, 123);
console.log(val);

深度优先

根据深度优先的思路来看一下代码实现。

let test3 = (() => {
    let res = '';
    return inner = (data, id) => {
        if (!data || data.length === 0) {
            return
        }

        let stark = []; // 初始化一个数据栈
        // 将第一层数据压入栈
        data.forEach(v => {
            stark.push(v);
        })

        while (stark.length > 0) {
            let item = stark.shift();
            if (item.id === id) {
                res = item.name;
            }
            if (item.children && item.children.length > 0) {
                stark = item.children.concat(stark);
            }
        }
        return res;
    }
})()

let val = test3(data, 123);
console.log(val);

提升

变量提升比较偏理论,可能会比较枯燥。不过了解原理能帮我们分析问题,解决问题。

在讲变量提升之前,我先简单的讲述一下函数作用域。

var a = 1;
function test() {
    var b = 3;
    console.log( b ); // 3
}
test();
console.log( a ); // 1
console.log(b); // 报错

在这段代码中,在全局作用域中能访问到atest却访问不到b,这是因为test在执行的时候生成了自己的函数作用域泡,只能在自身作用域内部能访问的到。

接下来直接从demo入手讲两个点:

  • 声明提升,赋值不会提升
  • 函数声明和变量声明都会被提升,但是函数声明优先与变量声明

demo1:

console.log(a); // undefined
var a = 1;

很多人都理解这里能打印出undefined,而且都知道是因为变量声明被提升。但是为什么呢??

我们在作用域是干什么用的中讲到,代码的执行分为两个阶段,第一步是由编译器解析生成一个可执行的代码段,再由引擎负责来执行代码。简单的讲var a =1是由var aa = 1两步组成的。
知道这个点之后我们来分析一下上面demo1的代码的逻辑。在引擎接手来执行这段代码之前,编译器会先解析这段代码,在解析过程中,var a会由编译器负责去询问当前作用域,让作用域创建这个a标识符并进行管理维护。
因此在引擎开始执行这段代码的时候,a变量已经存在与当前作用域中,只是未进行赋值操作。

demo2:

test();
function test() {
    console.log( a ); // undefined
    var a = 1;
}

在demo2中,有两个点:

  1. test函数声明被提升
  2. test函数作用域中a变量声明也被提升

demo3:

test(); // 1
var test;
function test() {
    console.log(1);
}
foo = function() {
    console.log(2);
};

观察demo3中的代码,很多人可能以为var test在编译时已经被执行,所以在执行test()的时候应该是报TypeError,因为test当前是undefined,并未进行赋值操作。但是实际上却输出了结果:1
因为函数声明会优先于变量声明,在编译器执行到var test时,function test(){...}已经被当前作用域所创建,因此var test这句代码被忽略,从而在访问test()的时候输出了1

demo4:

test(); // 2
var test;
function test() {
    console.log(1);
}
function test() {
    console.log(2);
}

上述代码说明函数声明是可以被覆盖的。

自定义页面系统设计(一)

前言

在电商大环境里,活动页面的需求在前端领域是源源不断的,特别是在大促期间,就格外的多。
面对这些枯燥乏味且又长的差不多的活动页面,前端🐶们往往是非常厌恶的。
作为一个有梦想的前端🐶,我们当然是想办法解放我们的劳动力啦!

我们想要实现的究竟是个什么样的系统呢?

在回答这个问题之前,我们先来分析一下活动页面有哪些特征。

  • 活动页面的组成比较简单,通常由多个页面模块堆积组成,页面的布局不会很复杂
  • 模块的重用率很高,多个活动页面很大可能会用到同一个功能模块,只是数据会有不同
  • 绝大多数页面是一次性的,不需要后期维护
  • 活动页面的需求大,时间紧,运营说要就要
  • ...

逼逼了一大堆活动页面的特征,前端🐶心里想,这些简单的页面还让我来开发,简直就是浪费人才,要是运营自己能组装模块搭建页面就好了。
说了那么久终于进入正题了,对的!没错!我们就是要设计一个系统让运营来搭建活动页面,开发只需要负责开发功能模块。

接下来,我们需要给我们将要设计开发的系统起一个响亮的名字,因为需要符合程序员气质,那就取名叫「摩羯」(capricorn)吧!

开发过程中可能会遇到哪些问题

1.需要将页面拆分为页面模板和功能模块

页面由一个页面模板和多个功能模块组成,运营同学首先需要挑选一个页面的基础模板类型,然后再根据活动页面的功能组成,挑选功能模块来搭建页面。

举个例子:现在我们需要在微信容器内实现一个活动页面,这个页面涉及轮播图、页面导航和商品图墙三个功能,并且需要支持微信的一些功能。
这个场景下我们就需要一个微信基础页面模板,这个模板里面需要引入微信的一些sdk来支持微信独有的功能。并且我们需要实现三个功能模块,分别是轮播图模块、页面导航模块和商品图墙模块。

2.公共依赖抽离

一个页面由多个功能模块组成,而模块之间必然会存在公用的模块,比如reactreact-dom等等。
那么如果每一个模块打包的时候都打把公用的三方包打进去,那么最后组装出来的页面就会包含很多重复的依赖包,体积过大就会影响页面性能。

3.模块通信

我们将页面拆分为多个模块之后,模块之间就是相互独立的了。我们需要通过另一种手段来实现模块之间的交互以及数据的通信。

4.模块信息配置

上面在分析特征的时候讲到模块重用的概率会很高,多个活动页面中可能存在同一个功能模块,只是数据会有些出入。
那么我们在实现公用功能模块的时候就需要考虑到如何通过配置信息生成一个模块?模块的配置信息存储在哪里?模块的后端数据请求怎么获取?

5.最终页面的拼装

解决完上述问题之后,我们就万事具备了哈,那么我们怎么将这些抽象的模块组装成一个可用的页面呢?

分析完背景和问题点之后,我们可以一步步来实现我们的「摩羯」系统了哈~

闭包

闭包一直以一种神秘的姿态出现在前端领域中。大家都知道这东西在代码中无处不在,可又很难表达出这东西是什么干嘛用的。接下来我来简单聊一下我对闭包的理解,篇幅不会太长,意在让大家理解。

对闭包的使用释义,网上一搜一大堆,主要可以概括为以下几种:

  • 内部作用域访问到了上级作用域的变量
  • 内部函数被传递到所在词法作用域以外来调用
  • LIFE模式(立即执行函数)
  • 模块模式

这些都是闭包的使用场景,由此可见闭包的功能真的很强,而且细微实用。

在正式开始讲闭包之前,我先来回顾以下引擎这个东西,在作用域是干什么用的中介绍过引擎负责执行经由编译器解析后生成的可执行的代码段。
这里再补充一点,引擎还拥有一垃圾回收机制,当作用域为标识符分配的内存空间不再被使用的时候,就会由垃圾回收器负责将其回收并释放该内存。

下面来看一段代码来给出我对闭包的理解定义:

function test() {
    var a = 1;
    function inner() {
        console.log(a);
    }
    return inner;
}
var fn = test();
fn(); // 1

test()执行结束之后,我们可能会以为整个test()的内部作用域都被销毁掉,因为引擎的垃圾回收器会负责释放不再被使用的内存。然后实际上inner被正常执行了,并且访问到了a

这个现象的背后就是因为闭包在阻止着这次垃圾回收。我们观察代码可以看出inner函数在使用着a标识符(变量),而a变量却被定义在了其上级作用域中,因此在inner函数执行之前,引擎识别到a留着还有用,所以不能清理a所在的作用域,这就是闭包的一种现象。

现在我们来归纳以下,内部作用域保持着对外部作用域的引用,这个引用其实就是闭包。我个人理解闭包其实就是一个作用域的集合,它包含自身的词法作用域和对执行过程中用得到的作用域。在上面的代码中,inner()一直保持有对test()整个作用域的引用。

希望上面的解释可以为你解惑,下面来看一个用烂了的例子:

for (var i=1; i<=5; i++) {
    setTimeout( function test() {
        console.log(i);
    }, i*1000 );
}

这个demo很多人肯定已经看很多遍了, 知道会输出5个6。这里我结合上面的理论来阐述一遍。

这里先重温一下两个点:

  1. 词法作用域,引擎会先向当前作用域询问变量是否存在,如果找不到就往上级找。
  2. setTimeout是一个异步方法,他的回掉函数并不会立马被添加到主进程中来执行。

下面我来翻译一下上面的代码:

var i = 1;
setTimeout( function test() {
    console.log(i);
}, 1000 );

i = 2;
setTimeout( function test() {
    console.log(i);
}, 2000 );

i = 3;
setTimeout( function test() {
    console.log(i);
}, 3000 );

i = 4;
setTimeout( function test() {
    console.log(i);
}, 4000 );

i = 5;
setTimeout( function test() {
    console.log(i);
}, 5000 );

i = 6

实际上,setTimeout的回调函数test访问的都是同一个共享的作用域(这里其实就是全局作用域)。而且setTimeout是一个异步的执行过程,因此等test执行到的时候i早已经被赋值为6。所以会输出5个6

其实想要让它输出12345,大家肯定也都会,我们只需要用LIFE(立即执行函数)包一下这个for循环中的执行体,并将即时的状态i记录在内部作用域中,那当test执行的时候就能访问到不同的i了,如下代码:

for (var i=1; i<=5; i++) {
    (function(j){
        setTimeout( function test() {
            console.log(j);
        }, j*1000 );
    }(i))
}

下面我们也用代码来解释这个过程:

var i = 1;
(function(){
    var j = i
    setTimeout( function test() {
        console.log(j);
    }, 1000 );
}())

i = 2;
(function(){
    var j = i
    setTimeout( function test() {
        console.log(j);
    }, 2000 );
}())

i = 3;
(function(){
    var j = i
    setTimeout( function test() {
        console.log(j);
    }, 3000 );
}())

i = 4;
(function(){
    var j = i
    setTimeout( function test() {
        console.log(j);
    }, 4000 );
}())

i = 5;
(function(){
    var j = i
    setTimeout( function test() {
        console.log(j);
    }, 5000 );
}())

i = 6

从上述代码可以看到实际上test()在执行的时候访问的标识符j已经是当前作用域的上级作用域私有的,不再是共享同一个作用域。

另一种方法当然是用let来生成块级作用域,道理都差不多,不再介绍。

一款设计到极致的 React 表单组件

原文链接

1 前言

经常开发中后台项目的同学肯定都经历过大型表单的折磨,几十个甚至上百个表单元素足以让我们欲仙欲死,这可真是个体力活。特别当你选择 React 作为技术框架的情况下,这满屏幕的 onChange 简直是一个噩梦。

当然我们还是有追求的,肯定不会屈服于此。社区内有很多的解决方案,比如双向绑定就是一个接受度很高的策略。这种方式也是我两年前惯用的手法,来看一段代码:

<Input 
    value={this.state.title}
    maxLength={25}
    onChange={Bind.valueChange.bind(this, 'title')}
    placeholder='请输入标题' />

通过这种方式我们确实减少了大量的手写回调函数来绑定数据源,但是 onChang 这东西就像狗皮膏药一样依附在那里。

那有没有一种方式,能够完全解放这种重复代码,我们只需要通过配置数据源就可以达到数据收集的目的呢?

接下来我们开始来讨论一下极致的 React 表单解决方案:@monajs/react-form

2 使用方式

import React from 'react'
import { Button, Input, Select } from 'antd'
import Form from '@monajs/react-form'
import FormItem from '@/component/form-item'

const { TextArea } = Input
const { Option } = Select

const Home = () => {
  const formRef = React.createRef()

  const getForm = () => {
    const formData = formRef.current.getFormData()
    const verifyInfo = formRef.current.getVerifyInfo()
    console.log(formData)
    console.log(verifyInfo)
  }

  return (
    <Form ref={formRef}>
      <FormItem bn='name' label='输入框' required>
        <Form.Proxy 
            to={TextArea} 
            bn='name' 
            getValue={(val) => val.target.value} defaultValue='ss' />
      </FormItem>
    
      <FormItem bn='other' label='下拉框' desc='请选择' required>
        <Form.Proxy
          to={Select}
          bn='other'
          style={{ width: 300 }}
          placeholder='请输入other'>
          <Option key={'3'} value='3'>3</Option>
          <Option key={'4'} value='4'>4</Option>
        </Form.Proxy>
      </FormItem>
      <Button onClick={getForm}>提交</Button>
    </Form>
  )
}

export default Home
// 打印结果
{
    name: 'fangke',
    other: '3'
}

我们来分析一下上面的代码。

  1. 首先我插入了一个容器节点 Form
  2. 然后我们把 antd 的组件通过 Form.Proxy 架了一层代理,通过 to 属性来声明代理路径,通过 bn 属性来声明要绑定的数据源。
  3. 最后通过 Form 实例上的 getFormData 来获取最终的表单数据对象。

这里我先阐述主链路,像FormForm.ProxyFormItemgetValue等到底是干什么的会在后面详细介绍。

3 API 介绍

3.1 表单容器(Form)

顾名思义它是个容器,我们所有的表单元素都必须是它的子节点,然后我们可以通过节点实例来全局性的做一些操作,比如数据收集、错误收集和重置表单。

3.1.1 表单数据收集(getFormData)

实际上我们在第2节中已经了解了如何来获取表单的所有绑定数据,使用姿势比较简单,这里就不再重复阐述。

 const formData = formRef.current.getFormData()
 console.log(formData)
//打印结果
{name: "sss", id: "11", scholl: "2", other: "4"}

一般返回的结果就是我们最终想要的数据结构,但是我们日常的需求中也难免会碰到很多层级很深的数据格式,这块我们会在 3.2.1 章节进行介绍。

3.1.2 未通过校验信息收集(getVerifyInfo)

只有当我们的表单元素中绑定了 verify 属性,我们才会对其进行数据校验,并进行最终校验未通过信息收集。具体 verify 是如何执行的,我们将在 3.2.2 章节进行介绍。

 const verifyInfo = formRef.current.getVerifyInfo()
 console.log(verifyInfo)
//打印结果
 [
     {id: 1, val: "1", vm: FormItemComponent, isEmptyVerify: true, verifyMsg: ƒ},
     {id: 2, val: "s", vm: FormItemComponent, isRegVerify: true, verifyMsg: ƒ},
     {id: 3, val: "4", vm: FormItemComponent, isFunctionVerify: true, verifyMsg: ƒ}
 ]

返回结果中包含了以下信息:

字段 说明
id 表单元素的唯一id
val 表单元素的返回值
vm 表单元素的实例对象
isEmptyVerify 校验类型是否为空校验
isRegVerify 校验类型是否为正则校验
isFunctionVerify 校验类型是否为函数校验
verifyMsg 当校验未通过时,会通过该方法返回校验报错信息
  • 注:通过校验返回的错误信息,我们可以进行一些自定义操作,比如通过表单实例(vm)返回到指定位置。

3.1.3 重置表单(reset)

重置是表单操作中比较常见的功能,我们的组件设计当然也考虑到了这个场景。

formRef.current.reset()

3.2 组件赋能

通过上面的使用介绍,我们应该大致知道了我们是通过 bn 属性来进行数据绑定的,表单元素组件最终的返回值会被绑定到 bn 声明的字段上。

3.2.1 数据绑定(bn)

一级结构

在多数情况下,我们的表单是一级结构,是扁平的,我们只需要给 bn 属性传递一个 key 值就可以实现,例如:

<Form.Proxy bn='name' to={Input} getValue={(val) => val.target.value} />
// 返回结果
{
    name: "fangke"
}
json 格式

针对一些层级比较深的 json 数据结构,我们支持 . 点运算符,我们来看一个例子:

<Form.Proxy bn='people.name' to={Input} getValue={(val) => val.target.value} />
<Form.Proxy bn='people.age' to={Input} getValue={(val) => val.target.value} />
<Form.Proxy bn='type' to={Input} getValue={(val) => val.target.value} />
// 返回
{
    people: {
        name: 'fangke',
        age: 18
    },
    type: '贫民'
}
array 格式

针对数组类型的数据结构,我们支持 [] 运算符,我们来看一个例子:

<Form.Proxy bn='people[0]' to={Input} getValue={(val) => val.target.value} />
// 返回
{
    people: ['fangke']
}
混合格式

接下来我们看一下混合模式下的应用。

<Form.Proxy bn='CH.people[0].name' to={Input} getValue={(val) => val.target.value} />
<Form.Proxy bn='CH.type[0]' to={Input} getValue={(val) => val.target.value} />
<Form.Proxy bn='CH.father.name' to={Input} getValue={(val) => val.target.value} />
<Form.Proxy bn='CH.father.age' to={Input} getValue={(val) => val.target.value} />
// 返回
{
    CH: {
        people: [{
            name: 'fangke'
        }]
        type: ['贫民'],
        father: {
            name: 'fangke',
            age: 18
        }
    }
}

3.2.2 数据校验(verify)

在 3.1.2 章节中我们提到过当表单元素组件传递了 verify 属性,我们就会对其开启校验,接下来我们来详细介绍一下。

我们支持三种形式的形式:

  1. 非空校验
<Form.Proxy bn='name' to={Input} verify verifyMsg='name不允许为空' getValue={(val) => val.target.value} />

当输入值为空时,则校验不通过,并且提示信息为 verifyMsg 属性绑定的"name不允许为空"。

  1. 正则校验
<Form.Proxy bn='mobile' to={Input} verify={/^1[3456789]\d{9}$/} verifyMsg='手机号格式不符合要求' getValue={(val) => val.target.value} />

当输入值不匹配正则表达式时,则校验不通过,并且提示信息为 verifyMsg 属性绑定的"手机号格式不符合要求"。

  1. 函数校验
<Form.Proxy bn='name' to={Input} verify={(val) => val === 'fangke'} verifyMsg='请输入fangke' getValue={(val) => val.target.value} />

当输入值通过 verify 方法返回 false 时,则校验不通过,并且提示信息为 verifyMsg 属性绑定的"请输入fangke"。

3.2.3 数据校验(verifyMsg)

介绍完 3.2.1 大家肯定会有一个疑问,如果 verifyMsg 只支持传递字符串那我们如何进行个性化提示。

实际上我们的 verifyMsg 是支持函数形式的,我们可以根据输入值进行多形式提示。

<Form.Proxy to={Input} bn='name' getValue={(val) => val.target.value} verify={(val) => val === 'fangke'} verifyMsg={(verify) => verify.val} />

这个 demo 只有当你输入 “fangke” 时才不会提示,否则你输入什么就提示什么。

3.3 如何给组件赋能

3.3.1 方案一:Proxy

讲到这里,我们应该会有以下几个疑问:

问题一:Form.Proxy 到底是干什么的

我们先来设想一下,如果我们不用 Form.Proxy 来架设代理层,那么我们怎么让 Form 表单容器和表单元素组件建立联系,那么我们是不是就无法通过 Form 实例的 getFormData 方法来全局收集到所有的表单元素的输入值。

那我们就可以这么理解,通过 Form.Proxy 代理过后的组件就跟 Form 建立了通信,从而实现数据双向输送。

传递到 Form.Proxy 中的所有属性,都会透传到目标组件中(即 to 属性传递的组件),除了toverifyverifyMsg这些私有属性。

问题二:是不是所有的组件都可以用在这种模式下成为表单元素

只要组件支持 onChange 属性回调返回,那就可以通过 Form.Proxy 成为 Form 的表单元素。

问题三:为什么需要添加 getValue 属性

getValue 实际上是一种钩子形态,它让接入的组件可以更加灵活。
举个例子:

onChange = (e) => {
    console.log('val:' + e.target.value)
}
...
<Input onChange={this.onChange}>

Input 组件的形参实际上是一个合成事件对象,并不是我们最终想要的数据结果,getValue 就提供了这么一种能力来帮我们返回最终想要的数据。

如果 onChange 的形参已经是我们最终想要的数据结果,那么 getValue 就可以省略,因为我们会默认处理。

3.3.2 方案二:withFormContext

通过 Form.Proxy 我们确实达到了目的,代码中再也不需要写一大堆的 onChange 来绑定数据,我们只需要简单的一个 bn 进行绑定就可以实现数据全量收集。

但是 InputTextArea 上一大堆的 getValue 钩子,看着还是很难受,都是些重复代码。实际上, Form.Proxy 是针对一些自定义的组件而设计的,它适合于使用频率不高的组件。

InputTextAreaSelect 这些高频组件,我们推荐使用 withFormContext 进行一次封装,然后统一使用封装后的组件,看下面例子:

// input.jsx

import Form from '@monajs/react-form'
import { Input } from 'antd'

const { withFormContext } = Form

const TextArea = Input.TextArea

const I = withFormContext(Input, (val) => val.target.value)

I.TextArea = withFormContext(TextArea, (val) => val.target.value)

export default I

投入使用:

import React from 'react'
import { Button } from 'antd'
import Form from '@monajs/react-form'
import Input from './input.jsx'

const { TextArea } = Input

const Test = () => {
  const formRef = React.createRef()

  const getForm = () => {
    const formData = formRef.current.getFormData()
    console.log(formData)
  }

  return (
    <Form ref={formRef}>
      <TextArea bn='name' />
      <Input bn='age' />
      <Button onClick={getForm} >提交</Button>
    </Form>
  )
}

export default Test
// 打印结果
{
    name: 'fangke',
    age: 18
}

3.4 错误展示(withVerifyContext)

在 3.1.2 章节中我们介绍,通过 getVerifyInfo 方法我们可以获取到全量的校验未通过信息。那么我们能否实现一个实时报错的功能呢?

当然是可以,我们先来看一个封装好的实例,也就是我们 2 章节中使用的 FormItem 组件。

import React from 'react'
import PropTypes from 'prop-types'
import Form from '@monajs/react-form'
import { Row, Col } from 'antd'
import './index.less'

const DefaultFormWrap = (props) => {
  const {
    children = null,
    verifyMsg = '',
    required = false,
    label = '',
    desc = '',
    className = '',
    span = 6
  } = props

  return (
    <Row className={['page-form-item', className]}>
      <Col className={['label', { 'required': required }]} span={span}>{label}</Col>
      <Col className='content' span={24 - span}>
        {children}
        <If condition={verifyMsg}>
          <div className='error'>{verifyMsg}</div>
        </If>
        <If condition={!error && !verifyMsg && desc}>
          <div className='desc' dangerouslySetInnerHTML={{ __html: desc }} />
        </If>
      </Col>
    </Row>
  )

}

DefaultFormWrap.propTypes = {
  required: PropTypes.bool,
  span: PropTypes.number,
  label: PropTypes.string,
  desc: PropTypes.string,
  verifyMsg: PropTypes.string, // 附加属性
  className: PropTypes.string,
  children: PropTypes.node
}

export default Form.withVerifyContext(DefaultFormWrap)

实际上 FormItem 就是一个纯UI展示组件,通过 Form.withVerifyContext 高阶组件返回的组件会附加一个 verifyMsg 属性。如果校验未通过(实时进行:每次的 onChange 触发都会进行校验),就会收到校验未通过的提示信息,并做UI展示。

问题:我们如何让 FormItem 知道要提示哪一个表单元素的校验未通过信息

<FormItem bn='name' label='姓名' desc='请填写' required>
    <Input bn='name' />
</FormItem>

我们通过 bn 属性来跟表单元素进行绑定。FormItem 会提示跟自身 bn 绑定值一致的表单元素的校验信息。

3.5 错误校验上下文(FormVerifyContext)

除了通过 Form.withVerifyContext 高阶组件来获取单个校验信息,我们还可以通过上下文实时获取批量校验未通过信息。

import Form from '@monajs/react-form'
const { FormVerifyContext } = Form

...

<FormVerifyContext.Consumer>
    {(verifyInfo = {}) => (
       ...
    )}
</FormVerifyContext.Consumer>

4 使用场景

  1. 各种表单,特别是大型表单,能大幅减少重复代码量,并且能够快速搞定。
  2. 自定义表单系统,我们可以在这个组件的基础上,通过一份配置动态搭建出一个表单页面。

5 后续规划

后续会推出 antd 的一套配套组件,因为是透传,所以跟 antd 的使用无异。

React简析之实例化

概述

这是一个 React 原理解读的系列,将带你一步步实现一个简化版的 React。不多说废话,现在开始!!!
代码地址:v1.0

React的主入口

import { render } from 'react-dom'
...
render(<a>123</a>, document.getElementById('appWrapper'))

这行代码大家肯定都非常熟悉,render() 函数是 React 最基本的方法,是整个 React 执行的开端。
我们可以看到 render 接收两个参数,一个 dom 模版,另一个是模版插入的位置,那么 render() 具体做了哪些事情呢?

// ReactMount.js

import ReactInstantiate from './ReactInstantiate'

class ReactMount {
    render(nextElement, container, callback) {
        // 节点实例化
        let instance = new ReactInstantiate(nextElement, null, true)
        console.log(instance)
       // 节点挂载
        let node = instance.mount(null, true)
        container.appendChild(node)
    }
}

export default new ReactMount

从代码中我们可以看到 render() 先会将接收到的 dom 结构进行一个实例化的过程,并生成一个实例对象数据结构instance
接着是对实例对象进行挂载,这一节主要是将 React 节点解析成浏览器原生节点,添加合成事件、绑定可识别属性等。这一块我们在下一节会进行讲解。
再接着就是节点插入。

下面我们来介绍一下节点实例化到底干了哪些事情

在讲实例化之前,首先我们先了解一下 React 节点分为4种节点类型,分别是空节点、文本节点、原生节点(浏览器节点)以及 React 节点。
根据这个我们先来定义一个数据字典,如下:

// constant.js

export default {
    VERSION: '0.0.1',
    REACT_NODE: 'reactNode',
    EMPTY_NODE: 'emptyNode',
    TEXT_NODE: 'textNode',
    NATIVE_NODE: 'nativeNode'
}

实际上,每一个节点实例都会存储节点的一些信息,包含节点类型、节点的唯一key、节点的dom数据、子节点实例数组。

// ReactInstantiate.js

import reactDom from './ReactDom'
import reactEvents from './ReactEvents'
import ReactUpdater from './ReactUpdater'
import Constant from '../constant'
import Util from '../util'
import ValueData from '../data'

export default class ReactInstantiate {
    constructor(element, key) {
        // react节点
        this.currentElement = element
        // 节点唯一key
        this.key = key
        // 节点类型
        this.nodeType = reactDom.getElementType(element)
        // 节点实例化数据结构
        this.childrenInstance = this.instanceChildren()
    }

    // 递归实例化所有节点,忽略react组件节点
    instanceChildren() {
        if (this.nodeType === Constant.EMPTY_NODE || this.nodeType === Constant.TEXT_NODE || !this.currentElement.props || !this.currentElement.props.children) {
            return
        }

        let child = Util.toArray(this.currentElement.props.children)
        let childrenInstance = []
        // 为每一个节点添加唯一key
        child.forEach((v, i) => {
            let key = null
            if (null !== v && typeof v === 'object' && v.key) {
                key = '__@_' + v.key
            }
            if (Util.isArray(v)) {
                let c = []
                v.forEach((item, index) => {
                    let itemKey = '__$_' + i + '_' + index
                    if (null !== v && typeof v === 'object') {
                        if (!item.key) {
                            console.error(ValueData.keyNeedMsg)
                        } else {
                            itemKey = '__@_' + i + '_' + item.key
                        }
                    }
                    c.push(new ReactInstantiate(item, itemKey))
                })
                childrenInstance.push(c)
            } else {
                childrenInstance.push(new ReactInstantiate(v, key))
            }
        })
        return childrenInstance
    }
}

至此,节点的实例化过程已经完成,我们得到一个数据装载完整的react实例数据。
这边介绍的代码并不完整,可以前往v1.0,clone下来本地跑一遍。

自定义一个遍历器

在 es5 之前,遍历一个数组我们肯定会用标准的for循环。

var myArray = ['a', 'b', 'c'];
for (var i = 0, l = myArray.length; i < l; i++) {
    console.log(myArray[i]);
}
// a, b, c

实际上这里遍历的并不是数组的每一项值,而是数组的下标,通过下标指针来访问数组的值,如myArray[i]

同样 for..in 遍历对象也是遍历对象的属性,无法直接获取属性值,需要通过手动去获取。

那么如何直接去遍历属性值呢?ES6 增加了一种用来遍历数组的 for..of 循环语法(如果对象本身定义了迭代器的话也可以遍历对象):

var myArray = ['a', 'b', 'c'];
for (var v of myArray) {
    console.log( v );
}
// a, b, c

for..of首先会向myArray请求一个迭代器对象,然后通过迭代器对象的next()方法来遍历访问所有值。
因为数组有内置的@@itrator,所以数组可以使用for..of,来看一下内部具体是怎么运行的:

var myArray = ['a', 'b', 'c'];
var item = myArray[Symbol.iterator]();
item.next(); // {value: "a", done: false}
item.next(); // {value: "b", done: false}
item.next(); // {value: "c", done: false}
item.next(); // {value: undefined, done: true}

我们可以理解为遍历器对象本质上,就是一个指针对象。
在执行遍历之前,我们先创建一个指针对象并指向数组的初始位置,在第一次调用next()的时候,指针往下移动一位,指向数组的第一个成员,并返回对应的value和是否已经遍历完毕。一直调用next()知道遍历完数组所有的成员,即done: false

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。

接下来用进入主题,用几个例子来自定义一个遍历器。

var myObject = { a: 2,b: 3 };

for (var v of myObject) {
    console.log( v );
}
// TypeError: myObject is not iterable

当对一个对象进行遍历的时候,会抛出一个错误,因为对象内部没有定义Symbol.iterator返回函数。
当然我们可以自定义一个@@iterator来支持对象遍历,@@iterator 本身并不是一个迭代器对象,而是一个返回迭代器对象的函数。

var myObject = { a: 2,b: 3 };

Object.defineProperty(myObject, Symbol.iterator, {
    value: function() {
        var ins = this,
            index = 0,
            keys = Object.keys(ins);
        return {
            next: function () {
                return {
                    value: ins[keys[index++]],
                    done: index>keys.length
                }
            }
        }
    }
})
for (var v of myObject) {
    console.log( v );
}
// 2, 3

自定义遍历器,我们相当于控制了next()的返回值(例如改变返回的值),通过这一个特性,可以实现很多个性化的逻辑。

var myObject = { a: 2,b: 3 };

Object.defineProperty(myObject, Symbol.iterator, {
    value: function() {
        var ins = this,
            index = 0,
            keys = Object.keys(ins);
        return {
            next: function () {
                return {
                    value: ins[keys[index++]]*2,
                    done: index>keys.length
                }
            }
        }
    }
})
for (var v of myObject) {
    console.log( v );
}
// 4, 6

如何在 React 里面方便的写 CSS Modules

背景

在多人跨团队开发的项目中,CSS 全局污染经常是我们一个非常头疼的问题,通常我们会有以下两种做法:

  • 一种是依靠人为约定的形式,通过给 class 携带命名空间,来避免 class 冲突,当然我们也可以写插件来检测命名是否符合规范。
  • 另一种就是越来越被大家接受的 CSS Modules,其局部作用域和模块依赖的概念确实一定程度上解决了我们这个头疼的问题。

然而,CSS Modules 在解决这个问题的同时,也给我们带来了额外的代码成本。

import React from 'react';
import style from './App.css';

export default () => {
  return (
    <h1 className={style.title}>
      Hello World
    </h1>
  );
};

从上述代码可以看出 CSS Modules 要求我们采用className={style.title}这种方式,而不是往常的className='title'这种形式来书写 class。
当项目复杂起来之后,你会觉得维护一坨一坨这样的代码会觉得比较恶心,至少视觉上的效果就没有以前那么简洁明了

那么有没有办法让我们维持原来的写法,却能拥有 CSS Modules 的特性,甚至赋于一些条件判断、变量识别、循环读取这些能力呢?

解决方式

接下来我们介绍一下 @monajs/babel-plugin-react-css-modules 这款 babel 插件,它就是为了解决上述问题而专门设计的,它允许我们以多种姿势来书写 CSS Modules。

git项目地址

字符串形式

const a2 = (
	<div className='ssss fds ss'>
		<div>{events.on}</div>
	</div>
)

数组形式

const b1 = (
	<div className={['aaa', 'bbb']}>
		<div>{events.on}</div>
	</div>
)

const b2 = (
	<div className={['aaa', bbb, { 'bbb': this.isShow, ccc: true, 'ddd': false }]}>
		<div>{events.on}</div>
	</div>
)

const b3 = (
	<div className={[this.classname(), { 'fd-ssss-fdfdfd': true }, 'sss-dd-dd']}>
		<div>{events.on}</div>
	</div>
)

json形式

const c1 = (
	<div className={{ 'aaa': true }}>
		<div>{events.on}</div>
	</div>
)

const c2 = (
	<div className={{ 'aaa': true, bbb: events }}>
		<div>{events.on}</div>
	</div>
)

const c3 = (
	<div className={{ 'aaa': true, bbb: this.ctrl.isShow, 'ccc': this.showClassname() }}>
		<div>{events.on}</div>
	</div>
)

实战技巧之开发适配多框架的公用组件

在业务开发中,特别是当技术框架变动比较大的时候,如果我们想要开发一个公共业务组件。并且这个组件需要提供给各种技术框架的系统使用,这时候我们就需要在开发前进行谨慎的技术选型,将业务组件的适配性、可维护性达到最佳状态。

举一个具体的例子:
在一个商城应用在重构的时候,我们需要顺带开发一个收货地址管理的弹层组件,后续其他项目也会接入这个组件。这时候我们需要考虑的有以下几个点:

  1. 接入方的项目框架多样性,可能有 React、Vue、Angular等等
    2.组件的代码体积不能太大,不能影响系统的性能
    3.组件的开发成本和后期维护成本
    ...

出于这些因素的考虑,用原生 JS 来实现仿佛是最好的一个选择。如果是一个逻辑简单的组件,这应该是比较完美的。但是如果这个组件的逻辑复杂,那这一切就变得像噩梦了,我们又重新回到了以前频繁操作 dom 的时代。

于是我们就会考虑,那我们能不能在组件内单独引一个体积比较小的 UI 框架来提交我们的开发效率呢?当然是OK的,也是一个非常好的选择。

这时候我们又会有一个新的问题,当别人来维护你的组件的时候,因为不熟悉你引入的框架的使用姿势,就会觉得比较蛋疼,维护成本会比较高。

mona-react这是一个按照 React 语法编写的超轻量的 UI 框架,可以解决上面的所有困扰,非常适合应用在开发独立的业务组件。

对象复制之深复制和浅复制

首先我们先来了解一下深复制和浅复制之间的区别。

从字面上来看,我们会觉得浅复制就是非彻底的复制对象,而深复制即彻底的复制对象,新对象将跟原来的对象毫无关联。
实际上也差不多,浅复制只复制对象第一层的属性,而深复制则递归复制了所有层级的属性。

下面通过demo来分析一下

var origin = {a:1, b: {c:2}};
var origin1 = {a:1, b: [1,2,3]};
var obj = cloneObj(origin);
var obj1 = cloneObj(origin1);

function cloneObj(obj) {
    if(typeof obj !== 'object'){
        return;
    }
    var res = {};
    for(var i in obj) {
        if(obj.hasOwnProperty(i)){
            res[i] = obj[i]
        }
    }
    return res;
}

console.log(obj); // {a:1, b: {c:2}};
console.log(obj1); // {a:1, b:[1,2,3]};

上面是一段简单的浅复制的代码,代码中cloneObj函数只对原对象的第一层做了赋值操作。这样一来,如果第一层属性中如果有对象,就会出现问题,如下代码所示:

obj.b.c = 5;
console.log(origin.b.c); // 5
obj1.b[0] = 5;
console.log(origin1.b[0]); // 5

可以看出,对于字符串类型,浅复制是对值的复制,但是对于对象来说,浅复制是对对象地址的复制,objorigin指向的是同一块内存地址。

而对于深复制来讲,实际上就是用递归算法遍历原对象所有层级的属性,并复制到一个新生成的对象中,来看一下下面的demo

var origin = {a:1, b: {c:2}};
var origin1 = {a:1, b: [1,2]};
var obj = deepCopy(origin);
var obj1 = deepCopy(origin1);

function deepCopy(obj) {
    if(typeof obj !== 'object') {
        return ;
    }
    var res;
    if(Object.prototype.toString.call(obj) === '[object Array]') {
        res = [];
    }else {
        res = {};
    }
    
    for(var i in obj) {
        if(typeof obj[i] === 'object') {
            res[i] = deepCopy(obj[i]);
        }else {
            res[i] = obj[i];
        }
    }
    return res;
}

console.log(obj);
console.log(obj1);

上述深度复制的代码考虑了数组和对象两种引用类型数据结构,接下来我们来测试一下:

obj.b.c = 5;
console.log(origin.b.c); // 2
obj1.b[0] = 5;
console.log(origin1.b[0]); // 1

然而在实际应用场景中,浅复制的场景要比深复制更为普遍,所以在 ES6 中定义了Object.assign(..)方法来实现浅复制。

var origin = {a:1, b: {c:2}};
var obj = Object.assign({}, origin);
console.log(obj); // {a:1, b: {c:2}};
obj.b.c = 5;
console.log(origin.b.c); // 5

Object.assign 会遍历一个或多个源对象的所有可枚举(enumerable)的自有键,并把它们复制(使用 = 操作符赋值)到目标对象,最后返回目标对象。

发布订阅模式的巧妙应用

原文链接:#23

简介

作为一种优秀的设计模式,发布订阅模式被广泛的应用在前端领域。举个例子,在 vue 的源码中,为了让数据劫持和视图驱动解耦就是通过架设一层消息管理层实现的,而这一层消息管理层实现的原理就是发布订阅模式。再比如 Redux、Vuex 这些当下比较流行的库基本上都离不开发布订阅模式。

现在我们举个例子来感受下发布订阅模式,比如有这么一个场景:到饭点了,你想找几个同事一起出去搓一顿

  • 方案A:你在组里挨个问一遍,问了一大圈之后,找到了几个也正好想要出去吃的人。
  • 方案B:你在同事都在的群里发了条消息,问了一下谁想出去搓一顿的,然后同事A/B/C回复了你说要一起出去吃。

显而易见,方案B才是一个比较省事的方案。在这个简单的场景方案中,你就扮演着一个发布者(Publisher)的角色,而你的所有同事,都以订阅者(Subscriber)的身份接收着你发布的消息。
msg

在介绍发布订阅模式的巧妙应用场景之前,先来分析一下该模式的优劣势。

代码实现可以前往 events 查看。

发布订阅模式的优势

  • 松耦合
  • 易维护
  • 解决负载问题
  • ...

松耦合

发布者不需要知道有多少订阅者,以及订阅者接收到消息之后会干什么,而订阅者也不需要关心发布者会在什么时候发布消息,两者相互独立运行。

易维护

得益于松耦合的特性,发布者和订阅者之间没有直接的逻辑往来,也使得逻辑变得清晰可维护,只需要关心内部的逻辑即可。

解决负载问题

在后端开发中消息中间件是用来解决写库高并发的常用手段,在前端同样可行。在并发执行量较高的场景下,可以考虑使用消息机制分流,避开执行高峰期,异步执行。

发布订阅模式的劣势

耦合度低是发布订阅模式最大优点,但同时也是它最大的缺点。

  • 消息无状态
  • 订阅者的数量不可控
  • 发布者和订阅者的关系陌生
  • ...

消息无状态

订阅者只会在接收到消息的时候作出响应,但是如果发布者的消息发布失败了,订阅者是不会知道的。

订阅者的数量不可控

因为发布者跟订阅者是一对多的关系,所以不会限制订阅者的数量。在发布者发布消息的时候,所有的订阅者都会收到对应的消息。如果订阅者过多,就很容易阻塞住进程,甚至造成 cpu 占用过大的情况。

发布者和订阅者的关系陌生

在发布订阅模式下,订阅者只认识消息,不认识发布者,所以任何发布者都可以发布指定的消息来通知订阅者,哪怕它是恶意伪装的。

巧妙的应用

介绍了一大堆,现在我们来看看如何应用。

  • 前端曝光埋点
  • 图片/模块懒加载
  • 多场景触发
  • 层级关系复杂的组件间通信
  • ...

前端曝光埋点

前端曝光埋点应该可以说是家常便饭了,每一次新需求开发你都省不了曝光埋点。不同的开发进行曝光埋点的方式也不尽相同。下面来介绍几种方案:(为了更加形象,以下说的都是无限滚动列表曝光)

  • 在接口返回的时候,一次性遍历列表,并发送曝光请求。如大家所想,这种方式严重丧失了曝光的意义,曝光数据没有参考价值。不过我确实碰到过很多同学依然用着这种“曝光方式”。
  • 监听浏览器的滚动事件,滚动触发的时候获取列表的所有的节点,然后判断每个节点是否满足曝光条件进行曝光。这种方案的问题也很明显,如果列表变得很大,那么每次遍历的成本就变得很高,而且执行也会很频繁。
  • 在上条方案的基础上我们来优化一版,我们可以给滚动事件添加一个节流器,然后再给已经曝光过的节点添加一个标识,这样每次遍历判断的列表长度就是可控的了

经过上面一番优化之后,曝光埋点的方案看上去应该比较合理了。但是我是一个比较懒的人,一看到每次滚动触发都需要主动去获取节点列表,还要去遍历整个列表就比较烦了。

现在我们用发布订阅模式的**来设计一下这个问题。我们不妨把滚动触发的主体看成是发布者,然后把列表的每一项都当成是订阅者。当每次滚动触发的时候我们只是发布一则消息(event_explode)去通知列表中的每一项,由列表项(订阅者)自行来判断是否满足了曝光的条件并上报埋点,然后在曝光之后取消对这则消息的监听。

图片/模块懒加载

图片/模块按需加载是前端优化中比较常见的一种方案。其设计思路跟前端曝光埋点基本一致,这里不作重复介绍。

多场景触发

中后台开发有这么一个非常常见的场景:一个列表上同时有“修改”、“添加”和“删除”,在每次操作成功之后需要刷新列表。

比较常见的一种做法是把刷新列表的回调方法分别传递给“修改”、“添加”和“删除”模块,然后在操作成功之后执行回调函数(刷新列表)。还好只有三个操作,如果有七八个操作都会影响列表的数据,那就得把这个回调函数往五湖四海传了。
发布订阅模式松耦合的特性就比较完美的解决了这个问题,在每次操作成功之后只需要发布一则(event_refresh)消息,然后列表(订阅者)接收到这则消息之后自行进行刷新数据的操作。

前端工程化通常会设计一层 server 层用作接口请求封装。如果让 server 层继承我们的发布订阅模式,在操作接口请求返回成功的第一时间派发消息,就更好的解耦了操作模块和列表模块,操作模块都不需要关心操作成功之后需要干什么,因为 server 层已经帮它干了。

层级关系复杂的组件间通信

在日常开发中,特别是组件库开发过程中,用回调方法 or 事件经常是一件犯难的事情。组件间传递回调函数可以清晰的分析执行体来源,调用链路一目了然。然而在组件层级关系复杂的场景下,如果跨多层级传递回调函数,就会使得项目的耦合性很高,变得很难维护。另外如果组件间没有明显的关系,你就连传递回调函数的机会都没有了。

所以,如果组件间层级关系复杂,或者毫无关系的情况下,推荐使用发布订阅模式进行组件间通信。

发布订阅模式给我们带来很多便利的同时,也给我们带来了很多维护消息的成本,切忌不要滥用。

koa 源码分析

原文链接

目录结构

  • application.js : koa 框架的核心实现逻辑
  • context.js : 返回创建的网络请求的上下文
  • request.js : 返回 request 请求信息
  • response.js : 返回 response 响应信息

应用实例

这里我们通过 koa 的官方脚手架初始化出来的项目代码,来看一下如何使用 koa 模块。

只提取关键路径代码进行分析

// 项目目录下
// bin/www 

var app = require('../app');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

// 定义端口
var port = normalizePort(process.env.PORT || '3000');
// app.set('port', port);

/**
 * Create HTTP server.
 */

// 创建一个 http 服务
var server = http.createServer(app.callback());

/**
 * Listen on provided port, on all network interfaces.
 */

// 监听在指定端口
server.listen(port);

代码非常的简单,整个过程只是通过 http.createServer 创建了一个 http 服务。

这里有一个 app.callback 可能是我们的疑问点,因为通常创建 http 服务的方式是这样的:

http.createServer(function (req, res) {
    // TODO
}).listen(3000);

那现在我们可以先猜测,app.callback 可能也会返回一个回调函数,并且在原先的基础上赋能,做了更多的事情。

接下来我们来看一下 app.callback 是在哪里定义的。

// 项目目录下
// app.js

const Koa = require('koa')
const app = new Koa()
const json = require('koa-json')
const onerror = require('koa-onerror')
const logger = require('koa-logger')

// error handler
onerror(app)

// middlewares
app.use(json())
app.use(logger())

// error-handling
app.on('error', (err, ctx) => {
	console.error('server error', err, ctx)
})

module.exports = app

这里做了四件事情:

  1. new 了一个 koa 的实例
  2. 定义了一个错误处理函数
  3. 通过 app.use 注册了一些中间件
  4. 在 koa 实例上订阅了一个叫 error 的事件消息
  5. 把整个koa 实例 exports 出来

代码中并没有定义 app.callback 的方法逻辑,接着往上找 koa 类。

中间件注册和消息订阅的具体实现会在后面具体分析

application.js

  • 注: 主入口的代码有点多,如果不理解可以先跳过,后面会逐个方法进行讲解
// 源码目录下
// application.js
// 核心入口

'use strict';

/**
 * Module dependencies.
 */

const isGeneratorFunction = require('is-generator-function');
const debug = require('debug')('koa:application');
const onFinished = require('on-finished');
const response = require('./response');
const compose = require('koa-compose');
const isJSON = require('koa-is-json');
const context = require('./context');
const request = require('./request');
const statuses = require('statuses');
const Emitter = require('events');
const util = require('util');
const Stream = require('stream');
const http = require('http');
const only = require('only');
const convert = require('koa-convert');
const deprecate = require('depd')('koa');

/**
 * Expose `Application` class.
 * Inherits from `Emitter.prototype`.
 */

module.exports = class Application extends Emitter {

  constructor(options) {
    super();
    // 中间件执行队列
    this.middleware = [];
    // http 请求的上下文信息
    response和request.js一样,也是对http模块中的response对象进行封装,通过对response对象的某些属性和方法通过重写 getter/setter 函数进行代理
    this.context = Object.create(context);
    // 存储 request 相关的信息
    // request.js 主要是对原生的 http 模块的 request 对象进行封装
    this.request = Object.create(request);
    // 存储 response 相关的信息
    // response.js 主要是对原生的 http 模块的 response 对象进行封装
    this.response = Object.create(response);
  }

  /**
   * Use the given middleware `fn`.
   *
   * Old-style middleware will be converted.
   *
   * @param {Function} fn
   * @return {Application} self
   * @api public
   */

  use(fn) {
    // 中间件必须是个函数
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    // 判断是否是 GeneratorFunction 
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      // 若是 GeneratorFunction,则调用 koa-convert 模块的
      convert(fn) 方法将 generator 函数包装成 Promise
      // 为了兼容 1.x 的 koa 版本
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    // 添加中间件
    this.middleware.push(fn);
    return this;
  }

  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */

  // 返回给 http.createServer(function) 用的回调函数(function)
  callback() {
    // 使用 koa-compose 组合生成一个大的中间件
    const fn = compose(this.middleware);

    // Application 类继承了 events 模块,this.listenerCount 和 this.on 都是 events 上的方法
    // 消息的订阅和分发机制
    // 如果没有开始订阅 error 事件,则订阅,当接收到 error 消息的时候执行 this.onerror 方法
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    
    // handleRequest 相当于 http.createServer 中的回调函数,req => 原生 request,res => 原生 response
    const handleRequest = (req, res) => {
      // 创建一个全新的 context(ctx) ,包含 response 、request 等信息
      const ctx = this.createContext(req, res);
      // 并将 context 透传到中间件中执行
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

  /**
   * Handle request in callback.
   *
   * @api private
   */

  // fnMiddleware 是组合出来的中间件
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    // 错误处理函数
    const onerror = err => ctx.onerror(err);
    // 响应处理函数
    const handleResponse = () => respond(ctx);
    // 在 response 响应结束之后执行 context 上的 onerror 方法
    onFinished(res, onerror);
    // 执行所有中间件
    // 等中间件执行结束之后开始响应 response,这里表明了中间的执行时间节点
    // 中间件执行过程出现异常,也会执行 context 上的 onerror 方法
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

  /**
   * Initialize a new context.
   *
   * @api private
   */

  // 创建一个全新的 context 对象,建立 context 对象的 request 属性和 response 属性和原生 request 和 request 的联系
  createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    return context;
  }

  /**
   * Default error handler.
   *
   * @param {Error} err
   * @api private
   */

  onerror(err) {
    // 如果 err 不是 Error 的实例
    if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));

    if (404 == err.status || err.expose) return;
    if (this.silent) return;

    const msg = err.stack || err.toString();
    // 打印错误信息
    console.error();
    console.error(msg.replace(/^/gm, '  '));
    console.error();
  }
};

/**
 * Response helper.
 */

function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

  // 判断原生 response 是否是可写流
  if (!ctx.writable) return;

  const res = ctx.res;
  let body = ctx.body;
  // 获取响应的状态码
  const code = ctx.status;

  // ignore body
  // statuses 模块定义了一些状态码
  // 此处用来判断当前 statusCode 是否是 body 为空的类型
  // 当 statusCode 为 204、205和304时,body 返回 null
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  // HEAD 请求方式
  if ('HEAD' == ctx.method) {
    // headersSent 是 原生 response上的一个属性,返回响应头是否已经被发送。
    // 如果响应头还未发送 并且 body 是 JSON 对象
    if (!res.headersSent && isJSON(body)) {
       context 里写入 length
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }

  // status body
  // 当 body 为 null 时
  if (null == body) {
    // httpVersionMajor 是原生 request 上的一个属性,返回当前 http 版本。
    // 这里是为了给http1 和 http2 做一个 body 的兼容处理
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      body = ctx.message || String(code);
    }
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // responses
  // 处理 body 为 Buffer 类型
  if (Buffer.isBuffer(body)) return res.end(body);
  // 处理 body 为 string 类型
  if ('string' == typeof body) return res.end(body);
  // 处理 body 为 流类型
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  // body 为 json 类型
  body = JSON.stringify(body);
  // 如果响应头还未发送,将 body 的字节长度写入到 context 中 
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

首先,在构造函数里初始化了一个存储中间件的数组、一个存储 request 信息的对象、一个存储 response 信息的对象和一个存储请求上下文的对象。初始化出来的请求上下文的 context 对象会在后续介绍,这里先大致理解它是一个包含 request 对象和 respond 对象 的请求相关信息的集合,它会将 request 对象和 respond 对象委托给自己代理。

找到这里终于找到了核心的 app.callback 逻辑,接下来分析一下 callback 具体做了什么事情。

callback

  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */
  // 返回给 http.createServer(function) 用的回调函数(function)
  callback() {
    // 使用 koa-compose 组合生成一个大的中间件
    const fn = compose(this.middleware);

    // Application 类继承了 events 模块,this.listenerCount 和 this.on 都是 events 上的方法
    // 消息的发布订阅机制
    // 如果没有开始订阅 error 事件,则订阅,当接收到 error 消息的时候执行 this.onerror 方法
    // 默认错误处理方式
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    
    // handleRequest 相当于 http.createServer 中的回调函数,req === 原生 request,res === 原生 response
    const handleRequest = (req, res) => {
      // 创建一个全新的 context(ctx) ,包含 response 、request 等信息
      const ctx = this.createContext(req, res);
      // 并将 context 透传到中间件中执行
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

我们可以看到 callback 干了3件事情。

1.中间件组合

const fn = compose(this.middleware);

koa-compose 模块可以将多个中间组合成一个大的中间件,然后通过调用组合出来的中间件依次调用添加进来的中间件函数。不理解 koa-compose 工作原理的可以看一下 koa-compose 分析

这里我们可能会有一个疑问,那么中间件是怎么来的呢?

在 koa 项目的 app.js 文件中上面提到过 app.use 这个方法,现在我们先大致了解一下,koa 是通过 use 方法往实例中添加中间件的,后面会具体分析 use 到底做了什么。

const Koa = require('koa')
const app = new Koa()
const json = require('koa-json')
const logger = require('koa-logger')

// middlewares
app.use(json())
app.use(logger())

2.默认错误处理

if (!this.listenerCount('error')) this.on('error', this.onerror);

这行代码可能不是很好理解,因为在 application.js 的整个源码中并没有定义 listenerCounton 方法。

回头看一下我们在定义 Application 这个类的时候实际上继承了 event 模块的 Emitter 这个类,这是一个 消息分发订阅模式 的实现。

当 koa 实例上没有订阅 error 事件的时候,会默认订阅一个 error 事件,当接收到错误消息的时候执行 this.onerror 方法。

在 koa 项目的 app.js 文件中提到过,通过 app.on 我们可以订阅错误消息,如果在调用 callback 之前已经订阅过 error 事件,这里就不会再订阅执行默认操作。

const Koa = require('koa')
const app = new Koa()

// error-handling
app.on('error', (err, ctx) => {
	console.error('server error', err, ctx)
})

介绍到这里肯定会有一个疑问,既然订阅了消息,那么在哪里进行消息分发?我们接着往下看。

3.返回原生 http 请求的请求回调函数

return this.handleRequest(ctx, fn);

返回的新执行函数以 context 和组合后的中间件作为形参。通过 this.createContext 方法创建一个全新的请求上下文对象,包含 response 、request 等信息,并且创建相互之间的联系。

  /**
   * Handle request in callback.
   *
   * @api private
   */

  // fnMiddleware 是组合出来的中间件
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    // 错误处理函数
    const onerror = err => ctx.onerror(err);
    // 响应处理函数
    const handleResponse = () => respond(ctx);
    // 在 response 响应结束之后执行 context 上的 onerror 方法
    onFinished(res, onerror);
    // 执行所有中间件
    // 等中间件执行结束之后开始响应 response,这里表明了中间的执行时间节点
    // 中间件执行过程出现异常,也会执行 context 上的 onerror 方法
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

在新返回的请求回调函数里做了以下几件事情:

  1. 在执行体里会定义一个错误处理函数,注意这里要区分开上面说的实例上订阅的错误处理。
  2. 紧接着定义了一个响应处理函数,这里先大概了解一下 respond 的主要职责就是对请求上下文做一下判断处理,然后结束响应。
  3. 在 response 响应结束之后执行 context 上的 onerror 方法。onFinished 是 on-finished 模块中定义的方法,当 response 响应结束之后会进行一些操作
  4. 依次执行全部中间件
  5. 中间件执行完毕之后进行响应处理(handleResponse
  6. 若在执行中间件期间报错或是在响应处理期间报错,都会调用 context.onerror 方法。

接下来我们来看一下 context 对象上的 onerror 方法具体干了什么(这里我们看一下关键节点)。

// context.js 文件
//

  // 默认错误处理函数
  onerror(err) {
  
    // delegate
    // 派发 error 事件消息
    this.app.emit('error', err, this);

    const { res } = this;
    res.end(msg);
  },

看到这里我们终于知道错误消息是从哪里派发出来的了,我们再来分析一遍整个错误订阅和分发模式。

  1. 首先 koa 实例会在 callback 方法执行时检查用户是否自己订阅了错误处理方法,如果没有则默认订阅一个错误处理函数 - this.onerror 方法。
  2. 在response响应结束的时候,或是当中间件执行期间产生报错,都会执行 context 对象上的 onerror 方法,这个方法会分发错误消息,通知所有订阅者进行错误处理。

看到这里我们可能还会有一个疑问,为什么 context 对象中定义的 onerror 方法是默认的处理函数,那我们可以在哪里修改错误处理的默认行为呢?我们回过头来看一下最初在实例化 koa 的时候干了什么。

const onerror = require('koa-onerror')

// error handler
onerror(app)

这里的 onerror(app) 会覆盖 context 对象上定义的默认 onerror 方法。koa-onerror 这个模块具体做了什么可以自行前往查看,实现比较简单。

use

 /**
   * Use the given middleware `fn`.
   *
   * Old-style middleware will be converted.
   *
   * @param {Function} fn
   * @return {Application} self
   * @api public
   */

  use(fn) {
    // 中间件必须是个函数
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    // 判断是否是 GeneratorFunction 
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      // 若是 GeneratorFunction,则调用 koa-convert 模块的
      convert(fn) 方法将 generator 函数包装成 Promise
      // 为了兼容 1.x 的 koa 版本
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    // 添加中间件
    this.middleware.push(fn);
    return this;
  }

当调用 app.use 注册中间件的时候,会先判断注册注册体是否为 function 类型,并且当中间件是 GeneratorFunction 类型的时候,会先调用 koa-convert 模块的 convert(fn) 方法将 generator 函数包装成 Promise,用于兼容 koa1.x。

最后将处理后的中间件添加到中间件数组中,并且返回当前实例对象,可以支持链式调用。

讲到这里,application.js 的源码基本上分析结束了,接下来我们来看一下 context.js 是如何返回一个上下文对象的。

context.js

// context.js
const delegate = require('delegates');

/**
 * Context prototype.
 */

const proto = module.exports = {
    // 隐藏了自身定义的一些方法
};

/**
 * Response delegation.
 */
 
 // 将 response 对象委托给 context

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

/**
 * Request delegation.
 */
 
// 将 request 对象委托给 context

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .access('accept')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

在不知道 delegate 具体是干什么用的情况下,好像是将 context 、request 和 response 建立某种联系。接下来我们来分析一下 delegates 这个模块。

delegate 委托

module.exports = Delegator;

function Delegator(proto, target) {
  // 如果不是通过 new 标识符来创建实例,则帮你自动 new 一下,并返回一个实例
  // 可以支持链式调用
  // 这也是一种良好的兼容写法
  if (!(this instanceof Delegator)) return new Delegator(proto, target);
  // 被委托者
  this.proto = proto;
  // 委托者
  this.target = target;
  // 存储所有的方法
  this.methods = [];
  this.getters = [];
  this.setters = [];
  this.fluents = [];
}

/**
 * Delegate method `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */

Delegator.prototype.method = function(name){
  // 这里指代 context 对象
  var proto = this.proto;
  // 这里指代 response 对象,或是 request 对象
  var target = this.target;
  this.methods.push(name);

  // 将 request 和 response 对象上的方法复制给 context 对象
  proto[name] = function(){
    return this[target][name].apply(this[target], arguments);
  };

  return this;
};

/**
 * Delegator accessor `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */
// 相当于组合调用了 getter 和 setter 方法
Delegator.prototype.access = function(name){
  return this.getter(name).setter(name);
};

/**
 * Delegator getter `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */

Delegator.prototype.getter = function(name){
  // 这里指代 context 对象
  var proto = this.proto;
  // 这里指代 request 对象和 response 对象
  var target = this.target;
  this.getters.push(name);
  // 通过原生的 __defineGetter__ 方法开启一层代理
  // 访问 proto[name] 就相当于访问 proto[target][name]
  // 这里访问 context[name] 就代理到访问 context.request[name]
  proto.__defineGetter__(name, function(){
    return this[target][name];
  });

  return this;
};

/**
 * Delegator setter `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */

Delegator.prototype.setter = function(name){
  // 这里指代 context 对象
  var proto = this.proto;
  // 这里指代 request 对象和 response 对象
  var target = this.target;
  this.setters.push(name);

  // 通过原生的 __defineSetter__ 方法开启一层代理
  // 修改 proto[name] 就相当于修改 proto[target][name]
  // 这里修改 context[name] 就代理到修改 context.request[name]
  proto.__defineSetter__(name, function(val){
    return this[target][name] = val;
  });

  return this;
};

委托代理的逻辑比较简单,主要就是通过 defineGetterdefineSetter 将 request 对象和 response 对象委托到 context 对象上,从而实现代理功能。

源码分析中并没有对 request.js 和 response.js 做过多介绍,这两个 js 主要职责就是对原生 http 模块的 request 和 response 对象进行二次封装。

自定义页面系统设计(二)

接着自定义页面系统设计(一)讲,这个章节主要会讲解如何解决下面几个点:

  • 需要将页面拆分为页面模板和功能模块
  • 公共依赖抽离
  • 模块通信

建议先阅读自定义页面系统设计(一)

将页面拆分为页面模板和功能模块

自定义页面系统设计(一)讲到我们想要实现的是一个功能模块可以重复使用的系统,运营可以选择一个页面模板,然后自由的选择多个功能模块来搭建页面(可视化界面的设计后续会作介绍,这里先不涉及)。

基础页面模板简单来说就是页面的骨架,通常也就是一个index.html。而功能模块可以把它想象成页面的一个组成部分,一个页面由多个模块组成。

为了方便的创建并使用基础页面模板和功能模块,我们需要先做以下几件准备工作:

1. 统一管理一个页面模板仓库

页面模板主要有以下两个用途:

  • 开发者的本地调试
  • 页面的创建

我们需要维护一个仓库来统一管理模板,另外,我建议通过分支来管理模板类型,这样模板项目地址不会分散。
capricorn-html-template项目就是我们的模板管理项目,我们将默认模板维护在default分支下,开发人员可以根据实际需要来新增一个模板。

默认模板目录结构

  .
  ├── index.html                    // 主入口
  └── package.json                  // 依赖管理文件

2. 统一管理一个模块初始化模板仓库

模块初始化模板主要是在开发人员新创建一个功能模块的时候会用到。我们同样需要维护一个仓库来统一管理模块初始化模板,同样还是通过分支来管理。

capricorn-module-template仓库就是用来管理模块初始化模板的。我们将默认模板维护在default分支下,开发人员可以根据实际需要来新增一个模板。

默认模板目录结构

  .
  ├── assets                  // 模块打包地址
  ├── template           
  │    ├── index.html         // 模板页面
  │    └── package.json       // 模板依赖
  └── src
      └── app.jsx             // 模块主入口

3. 开发一个命令行工具

准备好基础页面模板和功能模块模板之后,我们需要一个命令行工具给他们俩更好的赋能。比如快速创建模块、快速搭建页面和发布模块。

在实际应用场景中,我们需要通过页面可视化的搭建页面。本地我们将通过命令行工具来实现页面搭建的任务。后续章节会对可视化作单独介绍。

  • 安装
npm i capricorn-cli -g
  • 开始使用
capricorn model 

模块初始化演示

我们通过capricorn init出来的项目,只需要npm i安装完依赖之后,npm start就可以启动新模块项目了。
接下来我们就可以随心所欲的进行 coding 了。

当功能模块实现完之后,我们可以通过npm run build来进行模块打包,打包后的模块压缩包会生成在assets文件夹下。
在模块打包完之后我们需要执行capricorn release将打包后的脚本推送到npm仓库并且将模块的信息同步到数据库。

因为我们这里是本地开发,我们就直接将包推送到npm仓库即可。

一波操作之后,一个带版本的模块就在根目录下生成完毕了。

公共依赖抽离

上面我们介绍,我们只需要capricorn model就可以初始化一个模块项目,那么如果每一个项目都独立打包自己的三方依赖包,那么最终生成的页面体积就会非常的庞大。
出于这个考虑,我们肯定需要做三方依赖抽离,将这些公共的依赖统一在全局管理,提供给所有的功能模块使用。那么将公共依赖放在基础页面模板里就是一个比较好的选择了。

<html>
<head>
    <meta charset="UTF-8" />
    <title>capricorn</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
    <meta name="keywords" content="">
    <meta name="description" content="">
    <link rel="stylesheet" href="https://capricorn.static.monajs.cn/assets/base.css">
</head>
<body>
<script crossorigin src="https://capricorn.static.monajs.cn/assets/base.js"></script>
<script crossorigin src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
</body>
</html>

这是默认模板里的代码,我们可以看到react.production.min.jsreact-dom.production.min.js这两个基础依赖都采用了cdn引用方式,不再通过依赖包的方式注入到项目中,这样可以让所有的功能模块共同使用同一个资源包。

模块通信

自定义页面系统设计(一)中介绍过,我们将模块从页面中抽离出来之后,它们都是独立的个体。我们需要提供一个桥梁去支持模块之间的通信。

import Events from 'mona-events'

!window.Capricorn && (window.Capricorn = {})

// 摩羯系统全局消息监听机制,负责模块间通信
window.Capricorn.Events = Events
window.Capricorn.events = new Events

mona-events`是一个事件管理的三方依赖包,这里我们通过事件监听与消息派发的机制来促成模块间的通信。

下面来看一个🌰:

// module-a

...
window.Capricorn.events.on('module_a_test', (res) => {
	console.log('接收到消息')
	console.log(res)
	// do something
})
...
// module-b

...
window.Capricorn.events.emit('module_a_test', {
	moduleName: 'module-b',
	message: 'send message!'
})
...

这样module-a就能响应module-b的交互请求了。

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.