下篇: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”不会被输出。