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


    本作品采用知识共享署名 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@1、koa@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”不会被输出。

  • 相关阅读:
    《人月神话》阅读笔记2
    【个人作业】单词链
    【个人作业】找水王
    【团队】 冲刺一(10/10)
    【团队】 冲刺一(9/10)
    【个人作业】单词统计续
    【团队】 冲刺一(8/10)
    【团队】 冲刺一(7/10)
    【团队】 冲刺一(6/10)
    【团队】 冲刺一(5/10)
  • 原文地址:https://www.cnblogs.com/papertree/p/7156402.html
Copyright © 2020-2023  润新知