下篇:express、koa一、koa2的中間件原理

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」以觸發請求。
輸出:
node

[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不會進入。
express

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)。

api

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對象封裝一箇中間件。
promise

在2.1.1的例子中,添加一箇中間件,經過app.use(fn) -> app._router.use(path, fn) -> app.stack.push(new Layer(paht, {}, fn))app

當一個請求到來時觸發中間件執行,經過
app.handle(req, res, undefined) //原生的http.createServer()的回調函數參數只接收req、res兩個參數,next參數爲undefined)
-> app._router.handle(req, res, done)
-> layer.handle_requeset(req, res, next)


dom

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()裏面,最關鍵的形式是:koa

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很像,輸出也同樣。async

[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()實際上就是上篇博客《Es五、Es六、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」不會被輸出。

相關文章
相關標籤/搜索