上一篇是在应用层上介绍了Koa2的中间件用法,下面我们深入源码理解Koa2的中间件机制。
xyzcoding:Koa2第二篇:中间件Koa2中koa-compose的使用
/**
* 添加中间件的方法
* Use the given middleware `fn`.
*
* Old-style middleware will be converted.
*
* @param {Function} fn
* @return {Application} self
* @api public
*/
use(fn) {
// fn必须是函数
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
// 兼容生成器函数的中间件写法
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');
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
*/
callback() {
// 传入的是一个数组
const fn = compose(this.middleware);
// http://nodejs.cn/api/events.html#events_emitter_listenercount_eventname
// 添加默认的错误异常处理
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
// 创建每一次请求的下文文对象
const ctx = this.createContext(req, res);
// this.handleRequest处理每一次的请求流程
// 熟悉的朋友可能就想到我们添加一个中间件的写法 app.use(async(ctx, next){ ... })
return this.handleRequest(ctx, fn);
};
// 返回http.createServer()的参数requestListener
return handleRequest;
}
/**
* Handle request in callback.
* 处理HTTP的请求回调
* @api private
*/
handleRequest(ctx, fnMiddleware) {
// 每个请求会经过这个方法,后面调试可以看到
const res = ctx.res;
// http://nodejs.cn/api/http.html#http_response_statuscode
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
接下来我们通过断点调试的方式来熟悉中间件的来龙去脉。
const Koa = require('koa')
const app = new Koa()
const convert = require('koa-convert');
// 日志中间件
app.use(async(ctx, next) => {
console.log('middleware before await');
const start = new Date()
await next();
console.log('middleware after await');
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
app.use(async(ctx, next) => {
console.log('response');
ctx.body = ""
})
module.exports = app
通过debug模式启动
// bin/www
var server = http.createServer(app.callback());
进入koa-compose源码断点的位置是从
// application.js
callback() {
// 进入koa-compose
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;
}

执行完一遍koa-compose后,会返回一个包装函数。
然后在浏览器端访问http://localhost:3000/1212
此时一个请求进来会经过application.js中的handleRequest方法。

handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// fnMiddleware(ctx)执行后,koa-compose中闭包正式开始了。
// 于是第一个中间件开始执行
// 此处传入的ctx其实会挂载到后续中间件的context对象。
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
从调试进入fnMiddleware(ctx)方法,你就会发现进入koa-compose的返回函数中,i=0,从第一个中间件函数开始执行。

可以看的从第一个中间件开始执行,这里需要注意dispatch中的注释地方。
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
// 执行返回的都是Promise
if (!fn) return Promise.resolve()
try {
// 这里需要特别注意
// fn(context, dispatch.bind(null, i + 1))
// 其实就是对应我们async中间件 async(ctx, next) context就是上下文对象, dispatch.bind(null, i + 1)就是下一个中间件函数
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
第一个中间件执行返回Promise化的fn(context, dispatch.bind(null, i + 1))。熟悉的朋友就马上意识到我们async函数中间件写法。
// context 就是 ctx
// dispatch.bind(null, i + 1) 就是next,下一个中间件
app.use(async (ctx, next) => {
...
})
于是开始执行我们第一个中间件。

当接着执行到await next()时候,断点调试发现。

你会发现index变为1了。继续单步调试,执行return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

执行第二个中间件。执行完后回溯(后续看看async_hook.js源码)流程。

于是正常的流程我们这里就走完了。
但是如果我们修改下开始的demo
// 日志中间件
app.use(async (ctx, next) => {
console.log('middleware before await');
const start = new Date()
await next();
console.log('middleware after await');
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
app.use(async (ctx, next) => {
console.log('response');
await next() // 也就是说这里一个调用next()。其实它并不存在
ctx.body = "koa2"
})


i等于2,next其实并不存在。因此会走下面代码流程
if (i === middleware.length) fn = next
// 执行返回的都是Promise
if (!fn) return Promise.resolve()
相当于什么没有做。

于是接下来流程跟最开始一样了。
上述文章内容大致可以说明一个HTTP请求在Koa2的流转过程。
总结
简单来说的话, koa-compose的功能就是把 compose([f1, f2, f3, ..., fn])转化为f1(ctx, (ctx, (...fn))))。
Koa2的中间件可以对请求作出处理,也可以对响应作出处理。当一个请求进来,会依次被各个中间件处理,中间件跳转是通过async next(),当请求到达一个中间件并且被处理后不执行next(),那么此时程序会逆序执行前面的中间件剩下的业务逻辑,这个就是回溯,也就是我们常说的洋葱模型。