Context 上下文对象 ctx -> Koa Context ctx.req -> req -> IncommingMessage ctx.res -> res -> ServerResponse
ctx.req 是 node 原生的,ctx.request 是 koa 封装的请求对象,提供了更高级的 API 和更友好的方法(内部实现是 ctx.req
)
在 lib/context.js 中:
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 delegate (proto, 'request' ) .method ('acceptsLanguages' ) .method ('acceptsEncodings' ) .method ('acceptsCharsets' ) .method ('accepts' ) .method ('get' ) .method ('is' ) .access ('querystring' ) .access ('idempotent' ) .access ('socket' ) .access ('search' ) .access ('method' ) .access ('query' ) .access ('path' ) .access ('url' ) .access ('accept' ) .getter ('origin' ) .getter ('href' ) .getter ('subdomains' ) .getter ('protocol' ) .getter ('host' ) .getter ('hostname' ) .getter ('URL' ) .getter ('header' ) .getter ('headers' ) .getter ('secure' ) .getter ('stale' ) .getter ('fresh' ) .getter ('ips' ) .getter ('ip' )
koa 将 request 对象上的方法、访问器、getter 代理到 context 对象上,这使得开发者可以直接使用 ctx.xx 来访问 request.xx
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 .method ('accepts' ) .method ('get' ) .method ('is' ) ctx.request .accepts ('json' ); ctx.accepts ('json' ); .access ('method' ) .access ('query' ) .access ('path' ) .access ('url' ) ctx.request .path ctx.path ctx.request .method = 'POST' ctx.method = 'POST' .getter ('protocol' ) .getter ('host' ) .getter ('hostname' ) .getter ('header' ) .getter ('ip' ) ctx.request .ip ctx.ip
洋葱圈的实现 我们这样使用中间件:
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);
Koa 和 Express 中间件的区别
Express 中间件是基于回调函数的,使用 next() 控制流程;中间件按顺序同步执行,直到调用 next() 才会进入下一个中间件;通过 next(err) 传递错误,需要在专门的错误处理中间件中捕获。
Koa 中间件是基于 async/await 的,使用 await next() 控制流程;中间件按顺序执行,但支持“洋葱模型”,即先进入的中间件最后退出(请求在经过中间件的时候会执行 2 次,可以很方便进行前置和后置的处理);通过 try/catch 捕获错误,或使用 Koa 的全局错误处理机制。
Express 使用 req(请求对象)和 res(响应对象)分别处理请求和响应, 需要在 req 或 res 上挂载数据,手动传递;Koa 使用 ctx(上下文对象)统一处理请求和响应,可以直接在 ctx 上挂载数据(例如:traceId 这种需贯穿整个请求(之后在任何地方进行其他调用都需使用)的属性便可挂载上去),无需手动传递。