前言 koa 被认为是第二代 node web framework,它最大的特点就是独特的中间件流程控制,是一个典型的洋葱模型。koa 和 koa2 中间件的思路是一样的,但是实现方式有所区别,koa2 在 node7.6 之后更是可以直接用 async/await 来替代 generator 使用中间件,本文以最后一种情况举例。
洋葱模型 下面两张图是网上找的,很清晰的表明了一个请求是如何经过中间件最后生成响应的,这种模式中开发和使用中间件都是非常方便的
来看一个 koa2 的 demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const Koa = require('koa'); const app = new Koa(); const PORT = 3000; // #1 app.use(async (ctx, next)=>{     console.log(1)     await next();     console.log(1) }); // #2 app.use(async (ctx, next) => {     console.log(2)     await next();     console.log(2) }) app.use(async (ctx, next) => {     console.log(3) }) app.listen(PORT); console.log(`http://localhost:${PORT}`); 
 
访问 http://localhost:3000,控制台打印: 
 
怎么样,是不是有一点点感觉了。当程序运行到 await next()的时候就会暂停当前程序,进入下一个中间件,处理完之后才会在回过头来继续处理。也就是说,当一个请求进入,#1 会被第一个和最后一个经过,#2 则是被第二和倒数第二个经过,依次类推。
实现 koa 的实现有几个最重要的点
context 的保存和传递 
中间件的管理和 next 的实现 
 
翻看源码我们发现 app.listen 使用了 this.callback()来生成 node 的 httpServer 的回调函数
1 2 3 4 5 listen(...args) {     debug('listen');     const server = http.createServer(this.callback());     return server.listen(...args); } 
 
那就再来看 this.callback
1 2 3 4 5 6 7 8 9 10 11 12 callback() {     const fn = compose(this.middleware);     if (!this.listeners('error').length) this.on('error', this.onerror);     const handleRequest = (req, res) => {       const ctx = this.createContext(req, res);       return this.handleRequest(ctx, fn);     };     return handleRequest; } 
 
这里用 compose 处理了一下 this.middleware,创建了 ctx 并赋值为 createContext 的返回值,最后返回了 handleRequest。
this.middleware 看起来应该是中间件的集合,查了下代码,果不其然:
 
1 2 3 4 5 6 7 8 9 10 11 12 use(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; } 
 
抛开兼容和判断,这段代码只做了一件事:
1 2 3 4 use(fn) {     this.middleware.push(fn);     return this; } 
 
原来当我们 app.use 的时候,只是把方法存在了一个数组里。 那么 compose 又是什么呢。跟踪源码可以看到 compose 来自 koa-compose 模块,代码也不多:(去掉了一些不影响主逻辑的判断)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function compose (middleware) {   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, function next () {           return dispatch(i + 1)         }))       } catch (err) {         return Promise.reject(err)       }     }   } } 
 
比较关键的就是这个 dispatch 函数了,它将遍历整个 middleware,然后将 context 和 dispatch(i + 1)传给 middleware 中的方法。
1 2 3 return Promise.resolve(fn(context, function next () {       return dispatch(i + 1) })) 
 
这段代码就很巧妙的实现了两点:
1 2 3 1. 将`context`一路传下去给中间件 2. 将`middleware`中的下一个中间件`fn`作为未来`next`的返回值 
 
这两点也是洋葱模型实现的核心。 再往下看代码实际上就没有太多花样了。 createContext 和 handleRequest 做的事实际上是把 ctx 和中间件进行绑定,也就是第一次调用 compose 返回值的地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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.cookies = new Cookies(req, res, {      keys: this.keys,      secure: request.secure    });    request.ip = request.ips[0] || req.socket.remoteAddress || '';    context.accept = request.accept = accepts(req);    context.state = {};    return context;  }  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);  }