洋葱圈的实现 我们这样使用中间件:
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 const Koa = require ('koa' );const app = new Koa ();app.use (async (ctx, next) => { await next (); const rt = ctx.response .get ('X-Response-Time' ); console .log (`${ctx.method} ${ctx.url} - ${rt} ` ); }); app.use (async (ctx, next) => { const start = Date .now (); await next (); const ms = Date .now () - start; ctx.set ('X-Response-Time' , `${ms} ms` ); }); app.use (async ctx => { ctx.body = 'Hello World' ; }); app.listen (3000 );
中间件都是async函数,koa能够保证这些中间件是按照你加入中间件的顺序执行,这到底是怎么做到的?app.use的方法将所有的函数放入了自身维护的middleware数组。app.listen创建了一个httpServer并将onRequest回调指定为app.callback,app.callback主要做了2件事:添加onerror,处理用户请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 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,这个函数的实现如下(在koa-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 30 31 32 33 34 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!' ) } return function (context, next ) { 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函数,一直迭代所有的中间件。本文开篇的那个例子中1处的await next
将代码的执行权交给了2处记录下开始时间,接着执行4处的await,将代码的执行权交给了6接下来4处的await执行完了执行5,最后1处的await执行完了,至此中间完成了。
原理如下:
首先,我们在写中间时会有await next()
的用法(注意,await
会等到后面的Promise resolve
或reject
后才厚向下继续执行),那么执行await next()
就会转而执行dispatch(i + 1)
,直到最后一个中间件;当执行到最后一个再执行dispatch
时,会触发if(!fn) return Promise.resolve()
, 最后一个中间件开始执行await next()
后面的逻辑,完成后,执行倒数第二个,依次执行到第一个中间件。
注意,当中间件中有两处await next()
时,会触发if(i <= index) return Promise.reject(new Error('next() called multiple times'))
, 抛出错误。
这个过程有点像深度优先遍历,递归到树的最深处,然后逐层返回,最后的结果是初始调用第一层的结果(对应在koa中是我们use的第一个中间件)。其实和下面的代码是一个意思:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function fn1 (next ) { console .log ('fn1 start' ); next (); console .log ('fn1 end' ); return 1 ; } function fn2 (next ) { console .log ('fn2 start' ); next (); console .log ('fn2 end' ); return 2 ; } function fn3 ( ) { console .log ('fn3' ); return 3 ; } const ret = fn1 (() => fn2 (() => fn3 ()));console .log (ret);
输出:
1 2 3 4 5 6 fn1 start fn2 start fn3 fn2 end fn1 end 1
整个函数的返回值是最外层函数的值。改写成递归就是如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const fns = [fn1, fn2, fn3];function compose (fns ) { function dfs (i ) { let fn = fns[i]; if (!fn) return ; return fn (function next ( ) { return dfs (i + 1 ); }); } return dfs (0 ); } compose (fns);