koa中间件执行顺序
最近在学习koa中间件执行的原理,看下面代码:
import Koa from 'koa';
import fetch from 'node-fetch';
const app = new Koa();
app.use((ctx, next) => {
console.log(1);
next();
console.log(1);
})
app.use((ctx, next) => {
console.log(2);
next();
console.log(2);
})
app.use((ctx, next) => {
console.log(3);
next();
console.log(3);
})
app.listen('3000', () => fetch('http:127.0.0.1:3000'))
猜猜输出结果? 是1, 1, 2, 2, 3, 3
吗?,结果是
1
2
3
3
2
1
要理解这个行为,先理解koa的洋葱模型,app.use
注册的中间件的回调就是按照这个模型来迭代的。
所以上面中间件回调是这样一个执行顺序:
在执行回调的时候,如果调用了next()
则进入下一个中间件回调,在如果某个中间件抛出了错误,则停止迭代。
koa中中间件的迭代实现是依赖了koa-compose
这个包,源码很少,十分精简。
koa中间件执行原理
koa-compose.js
/**
* 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) {
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) {
// 是否多次调用next();
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
// 从第一个中间件回调开始
index = i
// 取出回调
let fn = middleware[i]
// 已经迭代到最后一个注册的中间件,fn赋值为next
if (i === middleware.length) fn = next
// 如果next没有定义,结束,返回一个Promise
// 如果定义了next回调,则执行next一次,因为compose函数可以单独使用
// comopose([middleware])(context, () => console.log(6666))
if (!fn) return Promise.resolve()
try {
// 执行中间件回调,并把dispatch作为第二个参数传next
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
// 任何一个中间件跑出错误则停止迭代,返回一个失败的Promise
return Promise.reject(err)
}
}
}
}
其中compose
入参middleware
就是上面app.use
注册的函数,
middleware: Function[] = [function, function, function];
上面主要的核心就是这行,把dispath作为async(ctx, next) {}
的第二个参数传入,并通过bind
绑定了参数i+1
Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
为什么要这样写dispatch.bind(null, i + 1)
, 主要是简洁,实际上你这样写也是ok
的,最终都是一个闭包。
Promise.resolve(fn(context, () => dispatch.call(null, i + 1)));
调试koa-compose
只看代码,阅读还是比较难的。这里最好通过断点的方式来梳理执行顺序。 增加调试命令
{
"scripts": {
"debug": "node ./test/koa.js"
}
}
然后就可以轻松断点调试了