下篇:express、koa1、koa2的中间件原理

时间:2022-10-04 14:49:25

本作品采用知识共享署名 4.0 国际许可协议进行许可。转载联系作者并保留声明头部与原文链接https://luzeshu.com/blog/express-koa
本博客同步在http://www.cnblogs.com/papertree/p/7156402.html


上篇博客《Es5、Es6、Es7中的异步写法》总结了es不同标准下的异步写法。
这篇博客总结分别依赖于es5、es6、es7的express、koa@1koa@2的中间件机制

2.1 express@4.15.3

2.1.1 例子

  1 'use strict';
  2
  3 var express = require('express');
  4 var app = express();
  5
  6 app.use((req, res, next) => {
  7   console.log('middleware 1 before');
  8   next();
  9   console.log('middleware 1 after');
 10 });
 11
 12 app.use((req, res, next) => {
 13   console.log('middleware 2 before');
 14   next();
 15   console.log('middleware 2 after');
 16 });
 17
 18 app.use((req, res, next) => {
 19   console.log('middleware 3 before');
 20   next();
 21   console.log('middleware 3 after');
 22 });
 23
 24 app.listen(8888);

启动后执行“wget localhost:8888”以触发请求。
输出:

[Sherlock@Holmes Moriarty]$ node app.js
middleware 1 before
middleware 2 before
middleware 3 before
middleware 3 after
middleware 2 after
middleware 1 after

通过调用next(),去进入后续的中间件。
如果少了第14行代码,那么middleware 3不会进入。

2.1.2 源码

1. node原生创建一个http server

  1 'use strict';
  2
  3 var http = require('http');
  4
  5 var app = http.createServer(function(req, res) {
  6   res.writeHead(200, {'Content-Type': 'text/plain'});
  7   res.end('Hello world');
  8 });
  9
 10 app.listen(8889)

2. 通过express()创建的app

express/lib/express.js
 16 var mixin = require('merge-descriptors');
 17 var proto = require('./application');

 23 /**
 24 * Expose `createApplication()`.
 25 */
 26
 27 exports = module.exports = createApplication;
 28
 29 /**
 30 * Create an express application.
 31 *
 32 * @return {Function}
 33 * @api public
 34 */
 35
 36 function createApplication() {
 37   var app = function(req, res, next) {
 38     app.handle(req, res, next);
 39   };

 42   mixin(app, proto, false);

 55   return app;
 56 }

express/lib/application.js
 38 var app = exports = module.exports = {};

616 app.listen = function listen() {
617   var server = http.createServer(this);
618   return server.listen.apply(server, arguments);
619 };

可以看到 app=require('express')()返回的是createApplication()里的app,即一个function(req, res, next) {} 函数。
当调用app.listen()时,把该app作为原生的http.createServer()的回调函数。因此,接收请求时实际上是进入了37~39行代码的回调函数。
进而进入到app.handle(req, res, next)。

3. 中间件的添加与触发

在中间件的处理过程中,实际上经过几个对象阶段。
app(express/lib/application.js) -> Router(express/lib/router/index.js) -> Layer(express/lib/router/layer.js)

一个app中通过this._router维护一个Router对象。
一个Router通过this.stack 维护很多个Layer对象,每个Layer对象封装一个中间件。

在2.1.1的例子中,添加一个中间件,通过app.use(fn) -> app._router.use(path, fn) -> app.stack.push(new Layer(paht, {}, fn))

当一个请求到来时触发中间件执行,通过
app.handle(req, res, undefined) //原生的http.createServer()的回调函数参数只接收req、res两个参数,next参数为undefined)
-> app._router.handle(req, res, done)
-> layer.handle_requeset(req, res, next)

express/lib/application.js
137 app.lazyrouter = function lazyrouter() {
138   if (!this._router) {
139     this._router = new Router({
140       caseSensitive: this.enabled('case sensitive routing'),
141       strict: this.enabled('strict routing')
142     });
143
144     this._router.use(query(this.get('query parser fn')));
145     this._router.use(middleware.init(this));
146   }
147 };

158 app.handle = function handle(req, res, callback) {
159   var router = this._router;
160
161   // final handler
162   var done = callback || finalhandler(req, res, {
163     env: this.get('env'),
164     onerror: logerror.bind(this)
165   });
166
167   // no routes
168   if (!router) {
169     debug('no routes defined on app');
170     done();
171     return;
172   }
173
174   router.handle(req, res, done);
175 };

187 app.use = function use(fn) {
...
213   // setup router
214   this.lazyrouter();
215   var router = this._router;
216
217   fns.forEach(function (fn) {
218     // non-express app
219     if (!fn || !fn.handle || !fn.set) {
220       return router.use(path, fn);
221     }
...
241   return this;
242 };

express/lib/router/index.js
136 proto.handle = function handle(req, res, out) {
137   var self = this;
...
151   // middleware and routes
152   var stack = self.stack;
...
174   next();
175
176   function next(err) {
...
317       layer.handle_request(req, res, next);
...
319   }
320 };

428 proto.use = function use(fn) {
...
464     var layer = new Layer(path, {
465       sensitive: this.caseSensitive,
466       strict: false,
467       end: false
468     }, fn);
469
470     layer.route = undefined;
471
472     this.stack.push(layer);
473   }
474
475   return this;
476 };

express/lib/router/layer.js
 86 Layer.prototype.handle_request = function handle(req, res, next) {
 87   var fn = this.handle;
 88
 89   if (fn.length > 3) {
 90     // not a standard request handler
 91     return next();
 92   }
 93
 94   try {
 95     fn(req, res, next);
 96   } catch (err) {
 97     next(err);
 98   }
 99 };

在app._router.handle()里面,最关键的形式是:

174   next();
175
176   function next(err) {
317       layer.handle_request(req, res, next);
319   }

这段代码把next函数传回给中间件的第三个参数,得以由中间件代码来控制往下走的流程。而当中间件代码调用next()时,再次进入到这里的next函数,从router.stack取出下游中间件继续执行。



2.2 koa@1.4.0

2.2.1 例子

  1 'use strict';
  2
  3 var koa = require('koa');
  4 var app = koa();
  5
  6 app.use(function*(next) {
  7   console.log('middleware 1 before');
  8   yield next;
  9   console.log('middleware 1 after');
 10 });
 11
 12 app.use(function*(next) {
 13   console.log('middleware 2 before');
 14   yield next;
 15   console.log('middleware 2 after');
 16 });
 17
 18 app.use(function*(next) {
 19   console.log('middleware 3 before');
 20   yield next;
 21   console.log('middleware 3 after');
 22 });
 23
 24 app.listen(8888);

写法跟express很像,输出也一样。

[Sherlock@Holmes Moriarty]$ node app.js
middleware 1 before
middleware 2 before
middleware 3 before
middleware 3 after
middleware 2 after
middleware 1 after

2.2.2 源码

koa源码很精简,只有四个文件。

1. 创建一个app

koa/lib/application.js
 26 /**
 27 * Application prototype.
 28 */
 29
 30 var app = Application.prototype;
 31
 32 /**
 33 * Expose `Application`.
 34 */
 35
 36 module.exports = Application;
 37
 38 /**
 39 * Initialize a new `Application`.
 40 *
 41 * @api public
 42 */
 43
 44 function Application() {
 45   if (!(this instanceof Application)) return new Application;
 46   this.env = process.env.NODE_ENV || 'development';
 47   this.subdomainOffset = 2;
 48   this.middleware = [];
 49   this.proxy = false;
 50   this.context = Object.create(context);
 51   this.request = Object.create(request);
 52   this.response = Object.create(response);
 53 }
...
 61 /**
 62 * Shorthand for:
 63 *
 64 * http.createServer(app.callback()).listen(...)
 65 *
 66 * @param {Mixed} ...
 67 * @return {Server}
 68 * @api public
 69 */
 70
 71 app.listen = function(){
 72   debug('listen');
 73   var server = http.createServer(this.callback());
 74   return server.listen.apply(server, arguments);
 75 };
...
121 app.callback = function(){
122   if (this.experimental) {
123     console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')
124   }
125   var fn = this.experimental
126     ? compose_es7(this.middleware)
127     : co.wrap(compose(this.middleware));
128   var self = this;
129
130   if (!this.listeners('error').length) this.on('error', this.onerror);
131
132   return function handleRequest(req, res){
133     res.statusCode = 404;
134     var ctx = self.createContext(req, res);
135     onFinished(res, ctx.onerror);
136     fn.call(ctx).then(function handleResponse() {
137       respond.call(ctx);
138     }).catch(ctx.onerror);
139   }
140 };

通过var app = koa()返回的app就是一个new Application实例。
同express一样,也是在app.listen()里面调用原生的http.createServer(),并且传进统一处理请求的function(req, res){}

2. 中间件的添加与触发

koa的一样通过app.use()添加一个中间件,但是源码比express简单得多,仅仅只是this.middleware.push(fn)。

koa/lib/application.js
102 app.use = function(fn){
103   if (!this.experimental) {
104     // es7 async functions are not allowed,
105     // so we have to make sure that `fn` is a generator function
106     assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
107   }
108   debug('use %s', fn._name || fn.name || '-');
109   this.middleware.push(fn);
110   return this;
111 };

当一个请求到来时,触发上面app.callback()源码里面的handleRequest(req, res)函数。调用fn.call(ctx)执行中间件链条。
那么这里的关键就在于fn。

 13 var compose = require('koa-compose');
...
121 app.callback = function(){
122   if (this.experimental) {
123     console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')
124   }
125   var fn = this.experimental
126     ? compose_es7(this.middleware)
127     : co.wrap(compose(this.middleware));
128   var self = this;
129
130   if (!this.listeners('error').length) this.on('error', this.onerror);
131
132   return function handleRequest(req, res){
133     res.statusCode = 404;
134     var ctx = self.createContext(req, res);
135     onFinished(res, ctx.onerror);
136     fn.call(ctx).then(function handleResponse() {
137       respond.call(ctx);
138     }).catch(ctx.onerror);
139   }
140 }

这里的this.experimental不会为true的了。否则会console.error()。
着重看co.wrap(compose(this.middleware))

这里的co.wrap()实际上就是上篇博客《Es5、Es6、Es7中的异步写法》 讲的co库的内容。

co/index.js
 26 co.wrap = function (fn) {
 27   createPromise.__generatorFunction__ = fn;
 28   return createPromise;
 29   function createPromise() {
 30     return co.call(this, fn.apply(this, arguments));
 31   }
 32 };

这里的fn参数来自compose(this.middleware)返回的Generator函数,Generator函数通过co.call()调用后执行至结束并返回promise对象。
但是co.wrap()本身还不会调用co.call()进而触发执行中间件链条。co.wrap()只是返回了一个createPromise()函数,在该函数里面才会执行中间件链条。
因此,co.wrap()返回的fn,在请求到来触发handleRequest(req, res)之后,通过fn.call(ctx)时才会执行中间件。ctx是针对每次请求包装的上下文。
这个ctx即createPromise()的this,再通过co.call(this, ...),传给了compose(this.middleware)返回的Generator函数的this。
这个this在compose源码里面(在下面)再通过middleware[i].call(this, next),传给了用户的中间件代码的this。

再回来看compose(this.middleware)如何把中间件数组处理成一个Generator函数返回给co调用。
compose()函数来自koa-compose包,这个包只有一个文件,且很短。

// version 2.5.1
koa-compose/index.js
  1
  2 /**
 3 * Expose compositor.
 4 */
  5
  6 module.exports = compose;
  7
  8 /**
 9 * Compose `middleware` returning
 10 * a fully valid middleware comprised
 11 * of all those which are passed.
 12 *
 13 * @param {Array} middleware
 14 * @return {Function}
 15 * @api public
 16 */
 17
 18 function compose(middleware){
 19   return function *(next){
 20     if (!next) next = noop();
 21
 22     var i = middleware.length;
 23
 24     while (i--) {
 25       next = middleware[i].call(this, next);
 26     }
 27
 28     return yield *next;
 29   }
 30 }
 31
 32 /**
 33 * Noop.
 34 *
 35 * @api private
 36 */
 37
 38 function *noop(){}

这里的middleware[i]循环是从最后的中间件往前的遍历。
首先co.call()触发的是compose()返回的一个匿名的Generator函数。拿到的参数next实际上传给了最后一个中间件的next。
进入匿名函数的循环里面,最后一个中间件(比如第3个)调用之后返回一个Iterator(注意Generator调用后还不会执行内部代码),这个Iterator作为第2个中间件的next参数。第二个中间件调用之后同样返回Iterator对象作为第一个中间件的next参数。
而第一个中间件返回的Iterator对象被外层的匿名Generator函数yield回去。
触发之后便是执行第一个中间件,在第一个中间件里面yield next,便是执行第二个中间件。



2.3 koa@2.3.0

2.3.1 例子

  1 'use strict';
  2
  3 var Koa = require('koa');
  4 var app = new Koa();            // 不再直接通过koa()返回一个app
  5
  6 app.use(async (ctx, next) => {
  7   console.log('middleware 1 before');
  8   await next();
  9   console.log('middleware 1 after');
 10 });
 11
 12 app.use(async (ctx, next) => {
 13   console.log('middleware 2 before');
 14   await next();
 15   console.log('middleware 2 after');
 16 });
 17
 18 app.use(async (ctx, next) => {
 19   console.log('middleware 3 before');
 20   await next();
 21   console.log('middleware 3 after');
 22 });
 23
 24 app.listen(8888);

输出同上两个都一样。

2.3.2 源码

koa@2的app.listen()和app.use()同koa1差不多。区别在于app.callback()和koa-compose包。

koa/lib/application.js
 32 module.exports = class Application extends Emitter {
...
125   callback() {
126     console.log('here');
127     const fn = compose(this.middleware);
128     console.log('here2');
129
130     if (!this.listeners('error').length) this.on('error', this.onerror);
131
132     const handleRequest = (req, res) => {
133       res.statusCode = 404;
134       const ctx = this.createContext(req, res);
135       const onerror = err => ctx.onerror(err);
136       const handleResponse = () => respond(ctx);
137       onFinished(res, onerror);
138       return fn(ctx).then(handleResponse).catch(onerror);
139     };
140
141     return handleRequest;
142   }
...
189 };

koa2不依赖于Generator函数特性,也就不依赖co库来激发。
通过compose(this.middleware)把所有async函数中间件包装在一个匿名函数里头。
这个匿名函数在请求到来的时候通过fn(ctx)执行。
在该函数里面,再依次处理所有中间件。

看compose()源码:

koa-compose/index.js
// version 4.0.0
  1 'use strict'
  2
  3 /**
  4  * Expose compositor.
  5  */
  6
  7 module.exports = compose
  8
  9 /**
 10  * Compose `middleware` returning
 11  * a fully valid middleware comprised
 12  * of all those which are passed.
 13  *
 14  * @param {Array} middleware
 15  * @return {Function}
 16  * @api public
 17  */
 18
 19 function compose (middleware) {
 20   if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
 21   for (const fn of middleware) {
 22     if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
 23   }
 24
 25   /**
 26    * @param {Object} context
 27    * @return {Promise}
 28    * @api public
 29    */
 30
 31   return function (context, next) {
 32     // last called middleware #
 33     let index = -1
 34     return dispatch(0)
 35     function dispatch (i) {
 36       if (i <= index) return Promise.reject(new Error('next() called multiple times'))
 37       index = i
 38       let fn = middleware[i]
 39       if (i === middleware.length) fn = next
 40       if (!fn) return Promise.resolve()
 41       try {
 42         return Promise.resolve(fn(context, function next () {
 43           return dispatch(i + 1)
 44         }))
 45       } catch (err) {
 46         return Promise.reject(err)
 47       }
 48     }
 49   }
 50 }

31~49行的代码,在请求到来时执行,并执行中间件链条。
第42~44行代码就是执行第i个中间件。传给中间件的两个参数context、next函数。当中间件await next()时,调用dispatch(i+1),等待下一个中间执行完毕。

注意到42行把中间件函数的返回值使用Promise.resolve()包装成Promise值。我们可以在中间件里面返回一个Promise,并且等待该Promise被settle,才从当前中间件返回。
比如2.3.1的例子中的第二个中间件修改成:

 12 app.use(async (ctx, next) => {
 13   console.log('middleware 2 before');
 14   await next();
 15   console.log('middleware 2 after');
 16   return new Promise((resolve, reject) => {
 17     setTimeout(() => {
 18       console.log('timeout');
 19       return resolve();
 20     }, 3000);
 21   });
 22 });

那么输出会变成:

[Sherlock@Holmes Moriarty]$ node app.js
middleware 1 before
middleware 2 before
middleware 3 before
middleware 3 after
middleware 2 after
timeout
middleware 1 after

但注意如果漏写了第19行代码,即Promise不会被settle,那么最后的“middleware 1 after”不会被输出。