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

下篇:express、koa1、koa2的中间件原理的更多相关文章

  1. express框架安装及中间件原理

    本文主要介绍express中间件的原理,来应对面试. 1.安装express及初始化: npm install express-generator -g   =>   express expre ...

  2. express文件上传中间件Multer详解

    express文件上传中间件Multer详解 转载自:https://www.cnblogs.com/chengdabelief/p/6580874.html   Express默认并不处理HTTP请 ...

  3. 学习laravel源码之中间件原理

    刨析laravel源码之中间件原理 在看了laravel关于中间件的源码和参考了相关的书籍之后,写了一个比较简陋的管道和闭包实现,代码比较简单,但是却不好理解所以还是需要多写多思考才能想明白其中的意义 ...

  4. nodejs 实践:express 最佳实践(二) 中间件

    express 最佳实践(二):中间件 第一篇 express 最佳实践(一):项目结构 express 中最重要的就是中间件了,可以说中间件组成了express,中间件就是 express 的核心. ...

  5. Express中间件原理详解

    前言 Express和Koa是目前最主流的基于node的web开发框架,他们的开发者是同一班人马.貌似现在Koa更加流行,但是仍然有大量的项目在使用Express,所以我想通过这篇文章说说Expres ...

  6. express中间件原理 && 实现

    一.什么是express中间件? 什么是express中间件呢? 我们肯定都听说过这个词,并且,如果你用过express,那么你就一定用过express中间件,如下: var express = re ...

  7. Koa框架实践与中间件原理剖析

     最近尝试用了一下Koa,并在此记录一下使用心得. 注意:本文是以读者已经了解Generator和Promise为前提在写的,因为单单Generator和Promise都能够写一篇博文来讲解介绍了,所 ...

  8. Node.js原生及Express方法实现注册登录原理

    由于本文只是实现其原理,所以没有使用数据库,只是在js里面模拟数据库,当然是种中还是需要用数据库的. 1.node.js原生方法 ①html页面,非常简单,没有一丝美化~我们叫它user.html & ...

  9. express koa koa2 优缺点分析

    发布日期 2009年6月26日,TJ 提交 Express 第一次 commit.目前拥有 5000 多次 commit. 2013年8月17日, TJ 只身一人提交 Koa 第一次 commit.目 ...

随机推荐

  1. react 组件架构

    容器型组件(container component) 含有抽象数据而没有业务逻辑的组件 负责管理数据和业务逻辑,不负责 UI 的呈现 带有内部状态 展示型组件(presentational compo ...

  2. Python9-面对对象2-day23

    #计算正方形的周长和面积 class Square: def __init__(self,side_len): self.side_len = side_len def perimeter(self) ...

  3. windows下的host工作原理

    在Window系统中有个Hosts文件(没有后缀名),在Windows98系统下该文件在Windows目录,在Windows2000/XP系统中位于C:\Winnt\System32\Drivers\ ...

  4. MySQL5.7 服务 crash 后无法启动

    事发背景 测试环境更换数据盘,直接采取在线将数据目录暴力拷贝到新盘,然后将原服务关闭,启用新盘. 服务是可以正常启动的,但是没多会开发就反应服务down了,错误日志输出 -- :: InnoDB: F ...

  5. BZOJ 3527 [Zjoi2014]力 ——FFT

    [题目分析] FFT,构造数列进行卷积,挺裸的一道题目诶. 还是写起来并不顺手,再练. [代码] #include <cmath> #include <cstdio> #inc ...

  6. [无趣]bit reverse

    真不想承认啊,因为年轻而犯下的错误! inline void _BR(int* a,int r){ for(int i=0,j=1;i<r;++i,j<<=1){ for(int k ...

  7. POJ1635 树的最小表示法(判断同构)

    Some major cities have subway systems in the form of a tree, i.e. between any pair of stations, ther ...

  8. 在 Linux 实例上自动安装并运行 VNC Server

    原文网址:https://help.aliyun.com/knowledge_detail/41181.html?spm=5176.8208715.110.11.4c184ae8mlC7Yy 您可以使 ...

  9. SHoj 420 购买装备

    购买装备 发布时间: 2017年7月9日 18:17   最后更新: 2017年7月9日 21:05   时间限制: 1000ms   内存限制: 128M 描述 最近盛大的一款游戏传奇世界极其火爆. ...

  10. Codeforces Round #269 (Div. 2) D - MUH and Cube Walls kmp

    D - MUH and Cube Walls Time Limit:2000MS     Memory Limit:262144KB     64bit IO Format:%I64d & % ...