Express源码阅读

本文基于Express 4.15.2

我之前的Express学习笔记还在这里

Express常被用来进行Node.js后台的快速搭建。它仅仅对Node.js进行了简单的封装,结合中间件等很自然和好用的概念,很容易上手和学习。Express的API主要包括Application,Request,Response,Router几个部分,这是Express代码主要实现的部分。在我看来,Express贵在它的中间件,它提供了足够自由的空间但也做出规范,提供req, res, next, err给中间件操作。它的生态系统也是围绕这个展开的。

创建服务器

让我们从头回忆,用Express创建一个简单的服务器。像下面这样。

1
2
3
4
5
6
7
8
9
10
var express = require('express')
var app = express()
app.get('/', function (req, res) {
res.send('Hello World!')
})
app.listen(3000, function () {
console.log('Hello world!')
})

先通过构造函数创建一个Express应用(Application)。接着为它指定路由规则。最后通过app.listen()的方式启动服务器。对比下Node.js原生的写法:

1
2
3
4
5
6
var http = require('http');
var server = http.createServer(function(request, response) {
res.write('Hello world');
res.end();
}).listen(3000);

那么Express的app究竟是什么,路由中间件又是如何绑定上去的。这些问题需要通过源码来解答。

代码结构

Express代码整体设计并不复杂(相对于Vue这样的复杂设计),比较容易看懂,一些常见的功能已经事先写成依赖包抽取出来,如debug(打印debug信息)和deprecate(显示API已废弃)等。Express的源码部分位于lib/路径下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
- lib/
- middleware/
- init.js
- query.js
- router/
- index.js
- layer.js
- route.js
- application.js
- express.js
- request.js
- response.js
- utils.js
- view.js
...
- index.js
...
1
2
...
module.exports = require('./lib/express');

根目录下的index.js是整个项目的入口,所做的工作只是引入lib/中的express.jslib/目录下middleware目录下放置了内置的中间件,router中放置中间件的功能实现。下面的几个文件中

  • application.js 应用的定义,app对象的API
  • express.js,对app,router等功能的封装
  • request.js和response.js是对http中res以及req的封装和增强
  • utils.js 常用工具函数的封装
  • view.js 建立拓展名和渲染引擎的联系

Application

express.jsApplication.js大致告诉我们了expressapp究竟为何物。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//express.js
exports = module.exports = createApplication;
function createApplication() {
// 创建一个app
var app = function(req, res, next) {
app.handle(req, res, next);
};
//继承EventEmitter和Application.js中定义的app对象
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
...
// 初始化app
app.init();
return app;
}
/*
* 一些暴露公用接口的操作
*/
/*
* 告知一些方法已废弃
*/

上面的mixin引入自merge-description,功能非常简单——通过描述符融合两个对象并返回,源码也很简单,主要由getOwnPropertyDescriptordefineProperty方法实现,感兴趣的可以一看。

app通过mixin继承了两个预定义对象,其中EventEmitter来自Node.js的API,继承后app将获得事件发布订阅功能。protoApplication.js导出定义。其中定义了app.listen()方法。

1
2
3
4
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};

综上就很明白了

  • 首先,express()返回的app其实是一个函数,可以接受req, res以及定义在Express中的next
  • 之后,app.listen()方法实际上执行了http.createServer(app).listen()

总结,express()是个工厂函数,负责生产作为http.createServer(app)的参数。之后,让我们继续深入看看Application.js中的代码。

app的工作主要是在Application.js中进行的。下面介绍一些它的属性和相关方法

  • cacheObject,缓存视图信息
  • engineObject,视图渲染引擎
    • engine(ext, fn),绑定拓展名和渲染方法,返回app自身用于链式调用
  • settings,app的设置信息,可以通过setget方法设置和获取。
    • get(setting)(或set(setting))获取配置值,这里是完整的配置表
    • set(setting, value)设置配置值
    • enabled(setting)disabled(setting)语法糖,获取Boolean类型配置值
    • enable(setting)disable(setting)语法糖,设置Boolean类型配置值
  • locals在app生命周期内存储用户信息
  • mountpath 顶级app挂载路径
    • path()返回app的挂载路径
    • use(fn)挂载中间件时可以指定第一个参数为挂载路径
  • router 该路由中间件属性在4.x中已废弃,由route(path)方法替代
  • _routerlazyrouter内部方法中加载,Router对象,中间件的容器。详细介绍在下文的Router一节中。

除了这些属性外,还有一些常见或内部使用的方法

  • defaultConfiguration初始化默认配置
  • lazyrouter自身路由的懒加载,原因写在了源码注释
  • handle(req, res, cb)最关键的app方法,用于分发请求完成路由功能。但它实际上只是对router.handle做了简单的封装。并在最后通过finalhandler来做兜底。finalhandler是个很轻量级的依赖包,用于生成404页面和记录错误。详细文档见github.
  • use(fn)最关键的app方法,用于添加中间件,主要逻辑是将arguments中的pathfn列表拆开来,再调用router的use方法注册
  • route(path)调用router的route方法注册一个路由中间件
  • param(name, fn)在自身的router对象中注册参数匹配回调
  • VERB(path, fn)为HTTP动词绑定路径和处理函数,主要功能也是由router代劳。有意思的是,HTTP动词是通过methods这个依赖包来返回的,这个包原理也非常简单——http.METHODS
  • all(path)用来匹配任意一个HTTP动词,其余功能和上面相同,实现上即将该路由中间件的所有动词都绑定上处理函数。
  • del 已废弃
  • render(name, options, cb)调用渲染引擎渲染模板
  • listen()即上文提到的方法,对http.listen的封装

中间件

中间件是Express设计的最精妙的地方,也是工作量最大的地方。之前Express利用了Connect来做这项工作。在当前版本中放在了router目录下去实现。整体来看,一个Express应用就是一系列的中间件首尾相接组成的。那么中间件是什么呢?用官网的话说就是“Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle.”从code的角度来看,就是下面这样

1
2
3
4
var myMiddleware = function(req, res, next){
// do something
next();
};

如果是错误处理的中间件,需要将err传入为第一个参数

1
2
3
4
var myErrorMiddleware = function(req, res, next){
// do something
next();
};

所以,一个请求进入Express后,处理的流程大致是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
---------------
| middleware1 |
---------------
---------------
| ... ... ... |
---------------
---------------
| middlewareN |
---------------

其中每个中间件能拿到reqres抑或err,在完成自身工作后调用next()执行下一个中间件。那么这些middleware是怎么放置在这条链上,请求又是如何通过next()一步步向下传递的呢?

中间件分两种:普通中间件和路由中间件。它们都通过handle方法处理;前者通过use方法注册,后者通过VERB(或all)方法注册。其中

  • 前者匹配所有以path开始的路径,而后者会精确匹配path路径;
  • 前者对于请求的方法不做要求,而后者只能处理指定的请求。
1
2
3
4
5
6
7
app.use('/user', function(req, res, next) {
// do something
});
app.get('/user', function(req, res, next) {
// do something
});

app层面

在app层面,即Application.js中,是由app.use(fn), app.VERB(或app.all)和app.handle(req, res, cb)完成的。而它们只是router的同名方法的简单封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
app.use = function use(fn) {
var offset = 0;
var path = '/';
// 由于这个API支持多参数,需要先判断第一个参数是否为路径
// 并通过offset存储结果
if (typeof fn !== 'function') {
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
// first arg is the path
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
// 根据offset获取中间件函数
var fns = flatten(slice.call(arguments, offset));
...
// 调用lazyrouter
...
fns.forEach(function (fn) {
// 原生函数时,调用router.use注册即可
if (!fn || !fn.handle || !fn.set) {
return router.use(path, fn);
}
...
// 中间件为router或app时,handle方法才是我们需要的
// 储存req和res的app属性到处理它的app
router.use(path, function mounted_app(req, res, next) {
var orig = req.app;
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
});
});
...
}, this);
return this;
};

其中flatten用于将多层嵌套数组扁平化为1层。可见到,app的use方法在做了预处理工作后,调用router的use完成注册工作。

需要路由中间件时,我们需要使用动词对应的方法(或all)去注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
methods.forEach(function(method){
// 根据形参个数避免app.get带来的歧义
app[method] = function(path){
if (method === 'get' && arguments.length === 1) {
// app.get(setting)
return this.set(path);
}
this.lazyrouter();
// 调用router的route方法构造一个路由中间件
var route = this._router.route(path);
// 为路由中间件特定方法指定处理函数
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});

app的handle方法就比较简单了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
app.handle = function handle(req, res, callback) {
var router = this._router;
// 创建兜底的final handler
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
});
// 没有路由对象时,就可以结束了
if (!router) {
debug('no routes defined on app');
done();
return;
}
// 调用router的handle方法
router.handle(req, res, done);
};

Router层面


Express里中间件的具体实现在Router对象中。Router包含了Express中最为核心的概念。app中的许多API都是对Router API的简单封装。可以通过app._router来访问app的Router对象。Router的源码位于lib/router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var proto = module.exports = function(options) {
var opts = options || {};
//Router本身也是一个函数
function router(req, res, next) {
router.handle(req, res, next);
}
// 将下文中所写的方法指定为router的prototype
setPrototypeOf(router, proto)
// Router的属性初始化
...
// 存储中间件的stack属性
router.stack = [];
return router;
};

Router对象有一个关键属性stack,为一个数组,存放着所有的中间件。每一个中间件都是一个Layer对象,如果该中间件是一个路由中间件,则相应的Layer对象的route属性会指向一个Route对象,表示一条路由。

注册

每次调用app.use()时,会执行router.use()stack属性添加一个新的中间件,这个中间件是由Layer对象包装的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
proto.use = function use(fn) {
// 处理输入参数,抽出path和fn
// 过程同app.use
...
if (callbacks.length === 0) {
throw new TypeError('Router.use() requires middleware functions');
}
// 循环fn数组,添加中间件
for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];
// 错误检测和打印信息
...
// 创建Layer对象
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
// 指定route属性为undefined,表示是普通中间件
layer.route = undefined;
// 入栈
this.stack.push(layer);
}
return this;
};

对于路由中间件要复杂些,路由中间件是通过router.route()方法注册的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
proto.route = function route(path) {
// 创建新的Route对象
var route = new Route(path);
// 创建Layer对象
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route)); // 绑定this为route对象
// 指定layer的route属性为当前的路由组件,表示是路由中间件
layer.route = route;
// 入栈
this.stack.push(layer);
return route;
};

看来,还需要去route.js中看看这个对象的定义。

1
2
3
4
5
6
7
8
9
10
function Route(path) {
this.path = path;
// 存放路由处理函数的stack
this.stack = [];
debug('new %o', path)
// 方法名和对应handler的键值对
this.methods = {};
}

可以看到,Route对象也有一个stack属性,为一个数组,其中的每一项也是一个Layer对象,是对路由处理函数的包装。我们可以把它理解成一个路由中间件对象。每次调用router.route()的时候,实际上是新建了一个layer放在router.stack中;并设置layer.route为新建的Route对象。

之后,通过route[method].apply(route, slice.call(arguments, 1))为特定方法绑定handler,route[method]定义在route.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
methods.forEach(function(method){
Route.prototype[method] = function(){
var handles = flatten(slice.call(arguments));
for (var i = 0; i < handles.length; i++) {
// handle即用户指定的处理函数数组
var handle = handles[i];
// 检测handle合法性
...
// 新建layer对象
var layer = Layer('/', {}, handle);
layer.method = method;
// 更新this.methods数组,并将layer入栈
this.methods[method] = true;
this.stack.push(layer);
}
return this;
};
});

即,当调用route.VERB()的时候,新建一个layer放在route.stack中。

通过上面的分析可以发现,Router其实是一个二维的结构。一个可能的router.stack结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
----------------
| layer1 |
----------------
---------------- layer2.route.stack ------------ ------------ ------------
| layer2 | ------------------> | layer2-1 |-->| layer2-2 |-->| layer2-3 |
---------------- ------------ ------------ ------------
---------------- layer3.route.stack ------------ ------------
| layer3 | ------------------> | layer3-1 |-->| layer3-2 |
---------------- ------------ ------------
----------------
| ...... |
----------------
----------------
| layerN |
----------------

Layer

Layer对象的构造函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Layer(path, options, fn) {
// 实现函数名前有无new返回相同的效果
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
debug('new %o', path)
var opts = options || {};
this.handle = fn;
this.name = fn.name || '<anonymous>';
this.params = undefined;
this.path = undefined;
this.regexp = pathRegexp(path, this.keys = [], opts);
// 设置特殊情况的快速匹配
this.regexp.fast_star = path === '*'
this.regexp.fast_slash = path === '/' && opts.end === false
}

Layer的属性和一些方法介绍如下

  • handle 用户指定的中间件函数
  • name 函数名
  • params 参数名,在执行match时赋值
  • path 路径名,在执行match时赋值
  • regexp 路径的正则表达形式,由pathRegexp转换完成
  • keys 路径匹配结果信息
  • route 路由中间件的Route对象,或undefined

上面看到,普通中间件和路由中间件都通过Layer的形式插入在stack中。尽管它们都有构造函数中声明的哪些属性,这两种Layer还是有所区别:

  • Router中的Layer对象具有route属性,如果该属性不为undefined,则表明为一个路由中间件;而Route中的Layer对象没有route属性
  • Route中的Layer对象具有method属性,表明该路由函数的HTTP方法;而Router中的Layer对象没有method属性
  • Route中的Layer对象的keys属性值均为[]regexp属性值均为/^\/?$/i,因为在Route模块中创建Layer对象时使用的是Layer('/', {}, fn)

请求处理

在中间件注册完成后,剩下的工作都是由app.handle()或者说router.handle()完成的。这部分代码比较复杂。大致结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
proto.handle = function handle(req, res, out) {
...
// middleware and routes
var stack = self.stack;
...
next();
function next(err) {
...
while (match !== true && idx < stack.length) {
...
}
// no match
if (match !== true) {
return done(layerError);
}
...
}
function trim_prefix(layer, layerError, layerPath, path) {
...
}
};

在初始化和预处理后,调用了next()函数。在next()函数中,主要的部分是while语句判断是否遍历完成整个stack,完成后执行done()。这部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;
if (typeof match !== 'boolean') {
// hold on to layerError
layerError = layerError || match;
}
if (match !== true) {
continue;
}
if (!route) {
// process non-route handlers normally
continue;
}
if (layerError) {
// routes do not match with a pending error
match = false;
continue;
}
var method = req.method;
var has_method = route._handles_method(method);
// build up automatic options response
if (!has_method && method === 'OPTIONS') {
appendMethods(options, route._options());
}
// don't even bother matching route
if (!has_method && method !== 'HEAD') {
match = false;
continue;
}
}

其中layer表示当前中间件,调用matchLayer方法即layer.match(path)判断是否和当前路径匹配(这个过程会更新layer中的pathparams)。之后的逻辑如下:

  1. 如果match不为true,即中间件和路径不匹配,则处理下一个中间件
  2. 如果matchtrueroute不存在,表示不是一个路由中间件,执行continue,之后跳出循环
  3. 如果matchtrue,且route存在。说明是需要的中间件。
    3.1 如果has_methodfalse且HTTP方法为OPTIONS,则执行appendMethods(options, route._options())添加支持方法
    3.2 如果has_methodfalse且HTTP方法不为HEAD,则设置match为false,即该路由无法处理该请求,此时由于match依然满足循环条件,因此会对下一个中间件进行判断
    3.3 如果has_methodtrue,则由于match不再满足循环条件,因此会跳出循环

整体来看,循环的主要作用就是从当前下标开始找出第一个能够处理该HTTP请求的中间件。如果是非路由中间件,则只要匹配路径即可;如果是路由中间件,则需要同时匹配路径和HTTP请求方法。

while语句后,如果matchtrue,说明遍历完成,直接执行done()。否则将匹配中得到的pathparams交给process_params方法作参数预处理。

1
2
3
4
5
6
7
8
9
10
11
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
return next(layerError || err);
}
if (route) {
return layer.handle_request(req, res, next);
}
trim_prefix(layer, layerError, layerPath, path);
});

从回调函数中可以看到,如果是路由中间件可以直接调用layer.handle_request(req, res, next)执行真正的中间件函数。如果是普通中间件,还需要在trim_prefix中对路径处理后才会调用layer.handle_request(req, res, next)

1
2
3
4
5
6
7
8
9
function trim_prefix(layer, layerError, layerPath, path) {
...
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}

在路由中间件中,layer.handle_request(req, res, next)调用的中间函数实际上是route.dispatch方法,在Route对象内容分发请求,它的逻辑相对router.handle要简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Route.prototype.dispatch = function dispatch(req, res, done) {
var idx = 0;
var stack = this.stack;
if (stack.length === 0) {
return done();
}
var method = req.method.toLowerCase();
if (method === 'head' && !this.methods['head']) {
method = 'get';
}
req.route = this;
next();
function next(err) {
// signal to exit route
if (err && err === 'route') {
return done();
}
// signal to exit router
if (err && err === 'router') {
return done(err)
}
var layer = stack[idx++];
if (!layer) {
return done(err);
}
if (layer.method && layer.method !== method) {
return next(err);
}
if (err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
};

可以看到,next函数逻辑像下面这样

  1. 如果有错,直接调用done()传递错误
  2. 获取layer,如果不存在,调用done()
  3. layer和当前方法不匹配时,跳过当前layer,执行next(),继续下一个route函数
  4. layer和当前方法匹配时,根据有无错误执行layer.handle_error(err, req, res, next)或是layer.handle_request(req, res, next)

综上,一个请求到达时,流程顺序像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
----------------
| layer1 |
----------------
----------------
| layer2 |
----------------
---------------- layer3.route.stack ------------ ------------ ------------
| layer3 | ------------------> | layer3-1 |-->| layer3-2 |-->| layer3-3 | ---
---------------- ------------ ------------ ------------ |
|
---------------------------------------------------------------------------
---------------- layer4.route.stack ------------ ------------
| layer4 | ------------------> | layer4-1 |-->| layer4-2 | ---
---------------- ------------ ------------ |
|
------------------------------------------------------------
----------------
| ...... |
----------------
----------------
| layerN |
----------------

每个中间件的处理过程则像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
No --------------------
------| path match |
| --------------------
| ↓ Yes
| -------------------- Yes --------------------- No
| | has route |-------| http method match |------
| -------------------- --------------------- |
| ↓ No | Yes |
| -------------------- | |
| | process params |<----------------- |
| -------------------- |
| ↓ |
| -------------------- |
| | execute function | |
| -------------------- |
| ↓ |
| -------------------- |
----->| next layer |<---------------------------------
--------------------

在参数处理的过程中,每个参数的处理函数只会执行一次,并将结果保存在缓存中。在处理同一个请求的过程中,如果需要处理某个参数,会首先检查缓存,如果缓存中不存在,才会执行其处理函数。

内置中间件

middleware目录下,放置了两个Express内置中间件,在app.lazyRouter中自动加载。

1
2
3
4
5
6
7
8
9
10
11
app.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
});
this._router.use(query(this.get('query parser fn')));
this._router.use(middleware.init(this));
}
};

其中第一个的作用是解析URL query,query(this.get('query parser fn'))用于设置URL query解析器。第二个的作用是将req和res分别暴露给对方,并让它们分别继承自express定义的app.requestapp.response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
exports.init = function(app){
return function expressInit(req, res, next){
if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');
req.res = res;
res.req = req;
req.next = next;
setPrototypeOf(req, app.request)
setPrototypeOf(res, app.response)
res.locals = res.locals || Object.create(null);
next();
};
};

express.js中,reqres分别继承自了request.jsresponse.js的导出对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createApplication() {
...
// expose the prototype that will get set on requests
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
...
}

其他

关于参数处理和视图渲染,我看得不是很仔细,就不再赘述了。有兴趣的可以自行去参考中的链接学习。

参考