koa2短小精悍,女人不愛男人愛。git
以前一隻有用koa寫一點小程序,自認爲還吼吼哈,知道有一天某人問我,你說一下 koa或者express中間件的實現原理。而後我就支支吾吾,很久吃飯都不香。github
那麼瞭解next的最好辦法是什麼, 百度,谷歌,知乎? 沒錯,確定有用,我以爲最有用的是看源碼和debug去理解。express
先看下面的一段代碼 ,會輸出什麼,只會輸出 X-Response-Time小程序
const Koa = require('koa'); const app = new Koa(); // x-response-time app.use(async (ctx) => { const start = Date.now(); //await next(); const ms = Date.now() - start; ctx.set('X-Response-Time', `${ms}ms`); console.log('X-Response-Time', `${ms}ms`) }); // logger app.use(async (ctx) => { const start = Date.now(); //await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}`); }); // response app.use(async ctx => { console.log('Hello World') ctx.body = 'Hello World'; }); app.listen(3000);
而後修改爲以下代碼,會依次輸出api
const Koa = require('koa'); const app = new Koa(); // x-response-time app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; ctx.set('X-Response-Time', `${ms}ms`); console.log('X-Response-Time', `${ms}ms`) }); // logger app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}`); }); // response app.use(async ctx => { console.log('Hello World') ctx.body = 'Hello World'; }); app.listen(3000);
從上面的結果看來,發現什麼沒有,沒有next 就沒有下面的執行,可就簡單的一個 await next(), 爲嘛會有這種效果,這裏,我首先簡單說一下koa2中間件的實現原理。promise
這裏先從 koa的使用提及app
const Koa = require('koa'); const app = new Koa();
app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}`); });
app.listen(3000);
咱們順藤摸瓜,打開 koa裏面的application.js (或者直接debug進入),koa
1.首先看 use ,就是push一個函數到 this.middlewareasync
2. 再看listen, 方法裏面 http.createServer(this.callBack), this.callBack返回的是 function(req,res){......}的函數,連起來就是 http.createServer(function(req,res){....}),標準的http建立服務的方法
函數
3. 最後看callback,裏面的核心方法, compose(this.middleware) 返回一個promise,處理完畢後再執行 handleResponse
這三個連起來,就是每次請求的時候,先進入callback, compose中間件,執行完畢後,接着處理請求。那剩下的重點變爲 compose
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; }
listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); }
callback() { const fn = compose(this.middleware); if (!this.listeners('error').length) this.on('error', this.onerror); const handleRequest = (req, res) => { res.statusCode = 404; const ctx = this.createContext(req, res); const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fn(ctx).then(handleResponse).catch(onerror); }; return handleRequest; }
咱們繼續深刻研究 compose,看源碼,核心依舊是標粗的部分,核心的核心就是dispatch, dispatch會根據 middleware 的長度,依次執行。
'use strict' /** * Expose compositor. */ module.exports = compose /** * 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) { 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) } } } }
注意下面,若是 next爲空,直接返回,也就出現了咱們第一段代碼的狀況,後面的中間件就game over了。
if (i === middleware.length) fn = next if (!fn) return Promise.resolve()
在往下分析,假定如今執行第一個fn,這個時候第一個fn是什麼
return Promise.resolve(fn(context, function next () { return dispatch(i + 1) }))
這時候fn爲以下,
fn = async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; ctx.set('X-Response-Time', `${ms}ms`); console.log('X-Response-Time', `${ms}ms`) }
與上面的參數對應關係以下
context :ctx,
next : function next(){ return dispatch(i+1)}
因此 await next() 就等於 await function next(){ return dispatch(i+1)} , 而 dispatch(i+1)就進入了下一個中間件了。
核心就是 dispatch(i+1),也就是dispatch(1) , dispatch自己返回promise, 因此你就在這裏 await 。
依此類推 disptach(1) 會執行 this.middleware[1], 那個時候 fn就爲 logger執行的函數,就這麼推下去。
關於結束,仍是 next 不存在的時候。 結果完畢後,再依次往上走。
因此執行的順序是越先註冊越後執行, 固然還得看你 await next() 放在什麼位置。 由於這裏個人 console.log都放在了 await的後面,都放到前面,結果如何,親自測試一下嘍。