-
首先说下什么是中间件:
中间件函数能够访问请求对象 (req)、响应对象 (res) 以及应用程序的请求/响应循环中的下一个中间件函数。
换句话说,中间件类似过滤器,在请求和相应到来时,先进行处理掉一些相对简单的逻辑
-
中间件过程的主要逻辑大致是:
- 使用use进行路由和handle函数的绑定,通过一个全局的数组(假定routes)记录每个use的匹配(路由->处理函数)
- 在请求到来的时候,对请求的路由进行匹配,在routes中遍历,只要和请求的路由匹配,都纳入处理函数的数组中(handle)
- 通过参数个数的不同,区分异常处理函数和普通处理函数,然后分成两组
- 遍历普通处理函数数组,执行中间件
- 如果中间件的执行过程中出现错误,把err传递给next函数,进行异常处理函数的调用
-
express框架中的中间件和以上提到的逻辑是一样的,略去不表。
但是Koa框架中的中间件有些特殊,它使用了es6的新特性,把中间件的过程变得有些不一样:
中间件中通过yield next进行下一个中间件的调用,等到调用链最顶端的那个函数处理完之后,就逆序返回,继续执行之前的中间件逻辑
-
es6中引入了Generator函数,和python中的Generator差不多,类似一个状态机,封装了多个内部状态。通过yield语句进行『暂停』,输出当前的状态。(但是有个限制就是yield语句只能在Generator函数中使用,不能在不同函数中使用。)因此通过yield可以实现同步的操作。
异步编程中有『协程』的概念,A任务执行,中间把执行权交给B,自己挂起,B执行一段时间后,适时把执行权再返回给A继续执行,以此实现同步。
因此在Generator函数中实现同步,就可以在请求资源的时候,函数A把执行权交出,通过yield 申请异步的资源,然后异步请求完成后,通过调用A.next(),把执行权返回给A,A就可以继续执行,这时候A已经拿到了异步申请的资源
- 上述流程,如果有多次的yield,手动执行next肯定是很不对的。es6中有个co模块,通过递归调用next以实现函数的自动执行:
function co(gen){
var g = gen()
function run(ret){
if(ret.done) return
if(ret.value instanceof Promise) ret.value.then((d)=>{
run(g.next(d))
})
else run(g.next())
}
run(g.next())
}
通过传入一个generator的函数,然后在run函数中进行递归,如果yield的是一个promise对象,那么下次第调用就要从他的处理函数中调用next方法。(并且由于generator函数是失忆的,不会保存上一次调用的结果,所以需要从then方法中获取promise传给处理函数的数据data,然后返回回去)。
注意这里不能把递归写成while……刚开始的时候自己想着写个while就好了…然而这样由于while每次只是定义了继续next,如果ret的value是promise,那么还只是定义了promise的then方法。又由于js的event loop机制,异步的函数会放在另一个事件池中,等到该次的事件执行结束了,再进行调用…于是while久等事件不至,进入了死循环…然后就发生了死锁……囧…… (啊……上次把页面跑崩还是在面试官的电脑上(真是不堪回首))
- 有了co,再把中间件的概念加上。首先因为会有许多中间件,所以用全局变量gg数组保存中间件的每个处理函数。由于可能一个中间件在还没有处理完的时候调用了yield next,为了在next函数处理完之后可以返回到之前的状态,进行后续的处理,需要一个gs数组来保存那些执行到一半的函数的状态。 修改下代码:
var gg = []
function use(gen){
gg.push(gen)
}
function trigger(){
var ne = {},
gs = [],
index = 0
function next(){
var gen = gg[index](ne)
gs.push(gen)
co(gen)
}
function co(gen, data){
if(!gen) return
var result = gen.next(data) //记得加data,不然白传了,data在下一次调用的时候使用
if(result.done) {
index--
if(gs[index])
co(gs[index])
return
}
if(result.value instanceof Promise)
result.value.then((d)=>{
co(gen, d)
})
else if(result.value === ne) {
index++
next()
} else
co(gen)
}
next()
}
- 在koa中的源码是这样的:
app.use = function(fn){
if (!this.experimental) {
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
};
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
}
}
co.wrap(compose(this.middleware)); //调用的时候。。然而实际上就是调用了co
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
//保存执行状态
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
参考资料: