在实际使用中可以看出,koa中间件的执行是洋葱模型,请求进入时依次执行中间件,返回结果时再反过来依次执行中间件,如下图:
那么这种执行机制是如何实现的呢,接下来探讨一下该执行机制的实现原理:
首先看koa中间件执行源码:
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
分析源码可以看出,koa在执行中间时把中间件数组传递给了compose函数,compose源码如下:
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) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
从源码可以看出,中间件数组在经过compose处理后,返回了一个接收context和next参数的函数。
1,这个函数返回的又是一个promise对象。
2,而es7中新增的async await函数会结合promise使用,实现函数执行权的传递和等待。
koa中间件实例:
async function middleware1(ctx, next) {
console.log('action 001');
ctx.data.push(1);
await next();
console.log('action 006');
ctx.data.push(6);
}
中间件执行分析:
下面是application中和middleware相关的源码:
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
执行过程:
1,compose函数返回一个第一个中间件函数:(context, next) => Promise.resolve(fn1(context, dispatch.bind(null, i + 1)));
2,执行fnMiddleware (ctx).then()时会执行 fn1(context, dispatch.bind(null, i + 1))
3,从中间件的代码可以看到next参数当前是dispatch.bind(null, i + 1),而dispatch.bind(null,i+1)返回的是promise对象
await next()实际上是等待下一个中间件执行完毕
4,依次执行中间件知道最后一个,这就是洋葱模型的进入阶段;
退出阶段:
5,执行完最后一个中间件,此时会执行倒数第二个中间件的剩余逻辑,即await next()之下的代码
6,依次向上执行,出洋葱圈模型。
关于koa中间件源码的简单理解:
从compose可以看出,dispatch函数实现了promise.resolve的嵌套,koa中间件的生命周期就是 Promise.resolve
的嵌套;
每个生命周期是:Promise.resolve(middleware)
嵌套中执行中间件
Promise.resolve(middleware1)
middleware1前置操作 等待Promise.resolve(middleware2)
middleware2前置操作 等待Promise.resolve(middleware3)
...
middleware3中后置操作
middleware2中后置操作
middleware1中后置操作
用图表表示:
参考:https://github.com/koajs/koa
https://chenshenhai.github.io/koajs-design-note/note/chapter02/02.html