koa被认为是第二代node web framework,它最大的特色就是独特的中间件流程控制,是一个典型的洋葱模型。koa和koa2中间件的思路是同样的,可是实现方式有所区别,koa2在node7.6以后更是能够直接用async/await
来替代generator
使用中间件,本文以最后一种状况举例。javascript
下面两张图是网上找的,很清晰的代表了一个请求是如何通过中间件最后生成响应的,这种模式中开发和使用中间件都是很是方便的java
来看一个koa2的demo:node
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
,控制台打印:git
1 2 3 2 1
怎么样,是否是有一点点感受了。当程序运行到await next()
的时候就会暂停当前程序,进入下一个中间件,处理完以后才会仔回过头来继续处理。也就是说,当一个请求进入,#1
会被第一个和最后一个通过,#2
则是被第二和倒数第二个通过,依次类推。github
koa的实现有几个最重要的点web
翻看源码咱们发现app.listen
使用了this.callback()
来生成node的httpServer的回调函数segmentfault
listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); }
那就再来看this. callback
数组
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
。cookie
this.middleware看起来应该是中间件的集合,查了下代码,果不其然:app
this.middleware = [];
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; }
抛开兼容和判断,这段代码只作了一件事:
use(fn) { this.middleware.push(fn); return this; }
原来当咱们app.use
的时候,只是把方法存在了一个数组里。
那么compose
又是什么呢。跟踪源码能够看到compose来自koa-compose
模块,代码也很少:(去掉了一些不影响主逻辑的判断)
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
中的方法。
return Promise.resolve(fn(context, function next () { return dispatch(i + 1) }))
这段代码就很巧妙的实现了两点:
1. 将`context`一路传下去给中间件 2. 将`middleware`中的下一个中间件`fn`做为将来`next`的返回值
这两点也是洋葱模型实现的核心。
再往下看代码实际上就没有太多花样了。createContext
和handleRequest
作的事其实是把ctx和中间件进行绑定,也就是第一次调用compose
返回值的地方。
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); }