koa2短小精悍,女人不愛男人愛。
之前一只有用koa寫一點小程序,自認為還吼吼哈,知道有一天某人問我,你說一下 koa或者express中間件的實現原理。然后我就支支吾吾,好久吃飯都不香。
那么了解next的最好辦法是什么, 百度,谷歌,知乎? 沒錯,肯定有用,我覺得最有用的是看源碼和debug去理解。
先看下面的一段代碼 ,會輸出什么,只會輸出 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);
然后修改成如下代碼,會依次輸出
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中間件的實現原理。
這里先從 koa的使用說起
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進入),
1.首先看 use ,就是push一個函數到 this.middleware
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的后面,都放到前面,結果如何,親自測試一下嘍。
最后簡單的模擬一下 Promise.resolve(fn()),
1. fn為一個異步函數,所以里面可以await
2. fn最后返回的是一個Promise對象
3. 當Promise.then, Promise.resolve返回是一個Promise對象時,會執行該Promise對象,並進入下一個環節
4 . 所以p1, p2依次執行,最后結果為6
var p1 = function () { return new Promise((resolve, reject) => { setTimeout(function () { console.log('p1', new Date().toLocaleString()) resolve(1) }, 2000) }) } var p2 = function () { return new Promise((resolve, reject) => { setTimeout(function () { console.log('p2', new Date().toLocaleString()) resolve(6) }, 4000) }) } console.log('start', new Date().toLocaleString()) Promise.resolve(fn()).then(r => { console.log('end', new Date().toLocaleString()) console.log(r) }) async function fn() { let a = await p1() let b = 4 return p2() } // start 2018/3/15 下午8:16:37 // p1 2018/3/15 下午8:16:39 // p2 2018/3/15 下午8:16:43 // end 2018/3/15 下午8:16:43 // 6