express源码分析

前言

express是node.js的一个轻量级后端框架,本文将对express的源码实现逻辑及主要服务的实现方式做一些分析

基本结构

直接npm install express,可以看到express的包结构如下:

├── 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
├── History.md
├── index.js
├── LICENSE
├── package.json
└── Readme.md

express模块的入口是 index.js,该文件中又引入了./lib/express.js,并将其用module.exports导出。

express.js

lib/express.js是对express所有定义及功能模块的整合,该文件的开头如下:

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
var EventEmitter = require('events').EventEmitter;
var mixin = require('merge-descriptors');
var proto = require('./application');
var Route = require('./router/route');
var Router = require('./router');
var req = require('./request');
var res = require('./response');
exports = module.exports = createApplication;
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
// 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 }
})
app.init();
return app;
}

文件一开始便引入了一些基础模块,基本来说根据这些模块的引入基本对express结构基本有一些理解了,之后的分析思路也会沿着这些引入的文件来进行。
先大概介绍下这些模块做了哪些事情:

  • EventEmitter: node.js的events模块
  • mixin: 用来合并对象的工具
  • proto: express应用的原型对象,在application.js里详细定义
  • Route: 定义最基本Route对象,包括app.post,app.all等以及Router对象的http方法都是从这里继承的
  • Router: 完整的Router对象,继承了Route的http方法,也集合了./router/layer.js下的路由初始化方法及路由处理方法,相当于是路由功能的整合
  • req: request对象
  • res: response对象

在引入了这些模块之后,就有了整个express的应用创建方法createApplication,也相当于是express的一个main函数。
createApplication函数中一开始就把需要返回的值app定义为一个函数,该函数有req, res, next三个参数,这不得不使人联想起app.get(‘/‘, function(req, res, next) {})里的req,res,next。在这个函数的内部则直接调用app的handle方法,并将req,res,next作为参数传入其中。
app.handle这个方法是在./application中定义的并通过mixin将该方法合并到app对象上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
});
// no routes
if (!router) {
debug('no routes defined on app');
done();
return;
}
router.handle(req, res, done);
};

该方法的作用是将res, res逐级分发到express应用每个路由中,以便执行各个路由相匹配的操作。
其实这个函数最关键的部分是在router.handle(req, res, done)这个方法的执行上,这是真正的路由分发执行操作,在./router/index.js里面定义,之后会对这部分进行分析讲解。

接着createApplication里的操作。在定义了app之后,执行了两个mixin方法,分别将 EventEmitter./application.js 中的属性和方法合并到app之中。

在 mixin 之后,createApplicaion 又使用了对象字面量的定义方法,定义了app.requestapp.response对象,分别以 ./request.js./response.js为原型对象,并赋值app属性且指向app本身(这里是为了在之后的response对象或request对象中,能够使用this.app访问已经创建的express实例)。

然后就是执行app.init(),这个init()方法也是在./application.js中定义的,用来初始化express应用的设置

1
2
3
4
5
6
7
app.init = function init() {
this.cache = {};
this.engines = {};
this.settings = {};
this.defaultConfiguration();
};

在createApplicaion的最后返回组建好的app对象。

在createApplicaion之后,便是一些公共API的导出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
exports.application = proto;
exports.request = req;
exports.response = res;
/**
* Expose constructors.
*/
exports.Route = Route;
exports.Router = Router;
/**
* Expose middleware
*/
exports.query = require('./middleware/query');
exports.static = require('serve-static');

对于这些公共api模块的功能和作用也不多说了,express的文档有相应的说明。

./express.js的最后,遍历了一个数组:

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
[
'json',
'urlencoded',
'bodyParser',
'compress',
'cookieSession',
'session',
'logger',
'cookieParser',
'favicon',
'responseTime',
'errorHandler',
'timeout',
'methodOverride',
'vhost',
'csrf',
'directory',
'limit',
'multipart',
'staticCache',
].forEach(function (name) {
Object.defineProperty(exports, name, {
get: function () {
throw new Error('Most middleware (like ' + name + ') is no longer bundled with Express and must be installed separately. Please see https://github.com/senchalabs/connect#middleware.');
},
configurable: true
});
});

这是因为express 4.x之后,很多中间件依赖没有在express内部导入了,但是express有时会用到这些中间件,这里是一个中间件检测,告诉开发者数组内的中间件需要从外部install进来。

router

./router文件夹下包括三个文件:

  • layer.js:定义中间件的基本数据结构
  • route.js:定义express的路由中间件Route;
  • index.js:定义一个中间件容器,也就是Router对象,用来存放路由中间件(Route)以及其他功能中间件

RouterRoute 的区别:Router可以看作是一个中间件容器,不仅可以存放路由中间件(Route),还可以存放其他中间件;而Route仅仅是路由中间件,封装了路由信息。
Router和Route都各自维护了一个stack数组,该数组就是用来存放中间件和路由的。

layer.js

首先来看layer.js中对于中间件的初始定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var pathRegexp = require('path-to-regxp');
function Layer(path, options, fn) {
if (!(this instanceof Layer)) {
return new Layer(path, options, fn)
}
debug('new %s', 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);
if (path === '/' && opts.end === false) {
this.regexp.fast_slash = true;
}
}

path参数不用多说,就是传入的url字符串,这里使用了path-to-regexp这个库,用来匹配url字符串,optionspath-to-regexp需要的配置参数,即为 {sensitive: Boolean, stric: Boolean, end: Boolean}。npm上有该库的详细使用说明,这里就不再讲解了。
fn也就是中间件里的回调处理函数,在Layer初始化的时候将它赋值给了自己的handle属性。

之后Layer还定义了三个操作方法:handle_error, handle_request, match

handle_error就是定义的express应用中的错误处理部分,例如app.use(fuction(err, req, res, next){})最后就会执行到这里。
handle_request定义的就是express应用中的路由中间件请求处理函数,也就是例如app.get(‘/test’, function(req, res, next){})的操作最后的执行位置。
match定义的是匹配path参数的操作,使用path-to-regexp的操作方法,例如在请求过程中/foo/23与就会和之前定义的/foo/:id相匹配,并最终将对应的23赋值req.params.id,这一部分的操作需要结合path-to-regexp的操作方法去了解。

整个Layer的定义其实并不复杂,它定义了中间件的基本数据结构,是后面Router和Route对象实现的基础。

route.js

同样,先从Route对象的初始化入手:

1
2
3
4
5
6
7
8
function Route(path) {
this.path = path;
this.stack = [];
debug('new %s', path);
this.methods = {};
}

path参数不用多说,stack是一个存放layer组件的数组,methods是存放HTTP方法的Object,例如{‘get’: true, ‘post’: true},即表示该Route中间件只能接受get和post方法。

紧接着Route通过原型链的方式定义了两个与methods紧密相关的方法:

  • _handles_method: 判断Route对象中是否存在method(传入参数)方法,并且如果method值为head,当作get方法处理;
  • _options:返回Route对象的methods值,并且如果存在get,则再添加一个head值。

然后就是比较重要的部分,中间件的派发操作:

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
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) {
if (err && err === 'route') {
return done();
}
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);
}
}
};

在知道了layerstack这两个事物的基础上,这个函数的操作流程就很好理解了,其实就是通过函数递归的方法,对Route对象的stack按插入顺序进行遍历,然后依次执行stack里的layer的过程。
当然,首先req.method也就是请求的http方法必须在Route对象中的methods之中。

最后就是定义如何调用Route对应的HTTP方法,也就是router.get,router.post等最终执行的地方

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
Route.prototype.all = function all() {
var handles = flatten(slice.call(arguments));
for (var i = 0; i < handles.length; i++) {
var handle = handles[i];
if (typeof handle !== 'function') {
var type = toString.call(handle);
var msg = 'Route.all() requires callback functions but got a ' + type;
throw new TypeError(msg);
}
var layer = Layer('/', {}, handle);
layer.method = undefined;
this.methods._all = true;
this.stack.push(layer);
}
return this;
};
methods.forEach(function(method){
Route.prototype[method] = function(){
var handles = flatten(slice.call(arguments));
for (var i = 0; i < handles.length; i++) {
var handle = handles[i];
if (typeof handle !== 'function') {
var type = toString.call(handle);
var msg = 'Route.' + method + '() requires callback functions but got a ' + type;
throw new Error(msg);
}
debug('%s %s', method, this.path);
var layer = Layer('/', {}, handle);
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);
}
return this;
};
});

两个代码块一个是定义了Route.all,一个是通过遍历methods(require(‘methods’),存储了各种HTTP请求方法)将其中的元素赋值到成Route对象的属性,也就有了Route.get,Route.post等方法。
其实这两个代码块其中的执行流程都大同小异。
在这里需要注意的是定义Route.method(这里method指代all,get,post等)时,其中第一行代码

1
var handles = flatten(slice.call(arguments));

这里的handles就是app.get(‘/path’, fn1, fn2, fn3)中的fn1,fn2,fn3等,也就是中间件的回调函数。
但是如果这样使用,有人会问不应该是slice.call(arguments, 1),也就是从第二个参数开始截取吗?(slice 是 Array.prototype.slice,在route.js开头定义的)
刚开始看到这里的时候,笔者也有这个疑问,后来在index.js里面,也就是定义Router对象的地方,找到这么一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
proto.route = function route(path) {
var route = new Route(path);
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
};
// create Router#VERB functions
methods.concat('all').forEach(function(method){
proto[method] = function(path){
var route = this.route(path)
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});

这里可以看出Route是被放在了Router的stack里的layer.route,然后在调用类似Router[method](path, fn1, fn2)的时候,已经将其中path提取出来,并且直接通过调用this.route(path)赋值到Route中的path属性,之后将fn1,fn2通过slice.call(arguments,1)的方式截取出来,使用route[method].apply调用到Route.method方法。
所以在Route.method调用的时候,其arguments已经是回调函数fn1fn2等的数组了。

OK,解决了这个疑问,继续下一个重点,可以看见在Route.method的定义中,最终都返回了this,加上之前对于arguments的处理,就形成了路由中间件的灵活调用方法:

1
2
3
4
5
router.get('/path', fn1, fn2, fn3);
router.get('/path', [fn1, [fn2, [fn3]]]);
router.get('/path', fn1).get('/path', fn2).get('/path', fn3);

这三个最终实现的结果是一样的,第一个和第二个没有什么区别,第三个有些许不一样,第一个和第二个在Router中’fn1,fn2,fn3’都是在同一个layer.route之中,而第三个则是在不同的layer.route之中。
第一个和第二个是通过遍历Route的stack来找到fn进行执行,而第三个是遍历Router的stack来找到fn进行执行。简单来说就是一个放在外层的stack,一个放在内层的stack。

其实在route.js这部分必须要结合index.js来看,不然对于一些实现方法不是很好理解。

index.js

老规矩,还是先从导出对象的基本定义开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var proto = module.exports = function(options) {
var opts = options || {};
function router(req, res, next) {
router.handle(req, res, next);
}
// mixin Router class functions
setPrototypeOf(router, proto);
router.params = {};
router._params = [];
router.caseSensitive = opts.caseSensitive;
router.mergeParams = opts.mergeParams;
router.strict = opts.strict;
router.stack = [];
return router;
};

这里的初始化定义应该不难看懂,options参数就是pathRegexp要求的三个配置参数caseSensitive,mergeParams,strict
router.stack前面也解释得比较多了,这里也不再赘述。router.paramsrouter._params是定义app.params(param, fn)中会使用到存储对象。
需要注意的是Router最终返回的是 router.handle(req,res,next)的执行函数,router.handle是定义的Router中的路由派发操作,类似Route.dispatch,之后这里会详细解释。

在这之后在Router对象上定义了param(name, fn)方法,这其实就是app.param(name, fn)定义的部分,这部分会在appliction.js绑定到app.param上,也就成了express文档里的app.param。这部分没有什么难以理解的地方,结合文档看基本可以理解,所以这里就跳过proto.param(name, fn)。

之后就是在Router的初始化里提到过的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
44
45
46
47
48
49
50
51
proto.handle = function handle(req, res, out) {
var self = this;
debug('dispatching %s %s', req.method, req.url);
var search = 1 + req.url.indexOf('?');
var pathlength = search ? search - 1 : req.url.length;
var fqdn = req.url[0] !== '/' && 1 + req.url.substr(0, pathlength).indexOf('://');
var protohost = fqdn ? req.url.substr(0, req.url.indexOf('/', 2 + fqdn)) : '';
var idx = 0;
var removed = '';
var slashAdded = false;
var paramcalled = {};
// store options for OPTIONS request
// only used if OPTIONS request
var options = [];
// middleware and routes
var stack = self.stack;
// manage inter-router variables
var parentParams = req.params;
var parentUrl = req.baseUrl || '';
var done = restore(out, req, 'baseUrl', 'next', 'params');
// setup next layer
req.next = next;
// for options requests, respond with a default if nothing else responds
if (req.method === 'OPTIONS') {
done = wrap(done, function(old, err) {
if (err || options.length === 0) return old(err);
sendOptionsResponse(res, options, old);
});
}
// setup basic req values
req.baseUrl = parentUrl;
req.originalUrl = req.originalUrl || req.url;
next();
function next(err) {
/**next 定义,暂时省略 **/
}
function trim_prefix(layer, layerError, layerPath, path) {
/**暂时省略 **/
}
}

因为这部分加上内部定义的函数的话,代码量比较多,一下贴出来不太容易理清思路,所以先将nexttrim_prefix函数省略(trim_prefix是在next内部调用的),先看看handle大概做了一件什么事情。
之前也说过router.handle是派发req和res到每个路由中间件的操作,联系之前提到的Route中的dispatch方法是通过函数调用的方法来遍历stack的中间件实现的req和res的派发,所以这里也是用了同样的操作:“通过递归调用next来实现路由派发”。
但是在调用next()之前,router.handlereq.url也就是原始的url进行了处理,把请求协议和search参数以及host都提取了出来,所以可以理解为在派发路由前,先对url做了一个预处理操作。

接下来就是仔细看一下看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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
function next(err) {
var layerError = err === 'route'
? null
: err;
// remove added slash
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
// restore altered req.url
if (removed.length !== 0) {
req.baseUrl = parentUrl;
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
}
// no more matching layers
if (idx >= stack.length) {
setImmediate(done, layerError);
return;
}
// get pathname of request
var path = getPathname(req);
if (path == null) {
return done(layerError);
}
// find next matching layer
var layer;
var match;
var route;
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;
}
}
// no match
if (match !== true) {
return done(layerError);
}
// store route for dispatch on change
if (route) {
req.route = route;
}
// Capture one-time layer values
req.params = self.mergeParams
? mergeParams(layer.params, parentParams)
: layer.params;
var layerPath = layer.path;
// this should be done for the layer
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);
});
}

在这里主要关注一下stack的遍历操作。在next之中stack的遍历先是通过while循环判断match值来确定是否找到匹配的layer, match是通过调用matchLayer返回结果,该方法定义为:

1
2
3
4
5
6
7
function matchLayer(layer, path) {
try {
return layer.match(path);
} catch(err) {
return err;
}
}

其实就是调用layer.match,只是加了个try catch而已。

如果while循环如果找到了匹配的layer,那么会就会调用proto.process_params的方法,也就是self.process_params()这部操作;

proto.process_params的源码这里就不贴出来了,了解一下操作流程就好。这个方法需要对应proto.param来理解,其实就是查询并调用proto.param里面定义且匹配的req.param绑定的function,这些回调函数都是存储在proto.params里面的,所以这里就又会有一个通过函数递归的遍历操作。
在遍历完了之后就是调用

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

如果layer.route是存在的,结合之前分析的LayerRoute以及在Route的分析中提前提到这部分后面定义的Router.route,也就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
proto.route = function route(path) {
var route = new Route(path);
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
};

layer.handle_request会执行layer.routedispatch操作,也就是在Route中派发路由最终执行到在express中定义的对应的路由操作函数,之后又执行next()就又到了这里的proto.stack中的下一个遍历操作。
但是如果layer.route不存在,说明这只是一个中间件而不是路由中间件,也就是用route.use定义的中间件,所以就调用trim_prefix方法来执行路由中间件:

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
function trim_prefix(layer, layerError, layerPath, path) {
var c = path[layerPath.length];
if (c && '/' !== c && '.' !== c) return next(layerError);
// Trim off the part of the url that matches the route
// middleware (.use stuff) needs to have the path stripped
if (layerPath.length !== 0) {
debug('trim prefix (%s) from url %s', layerPath, req.url);
removed = layerPath;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// Ensure leading slash
if (!fqdn && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
// Setup base URL (no trailing slash)
req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
? removed.substring(0, removed.length - 1)
: removed);
}
debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}

这个方法是在proto.handle里定义,和next同级。

OK,这里再回到while,如果while循环没有找到匹配的layer就进行收尾工作,要么就根据layerError也就是next(err)中的err参数判断是否已经匹配到结果正常首尾,或者就是404没找到匹配。

分析到这里,这里使用两张图对上面的分析进行一个整理。

首先是layer,Route和Router这三者的关系(原图片来自文章从express源码中探析其路由机制

layer,Route,Router

然后是proto.handle的一个执行流程:

proto.handle

有了这两张图,再配合之前的文字描述,应该对路由这部分的处理不会有什么问题了。

request 和 response

先说request.js,这里是定义app.method(path, function(req, res, next) {}) 中 req对象的地方,首先先看req对象初始化

var req = Object.create(http.IncommingMessage.prototype);

req是以http.IncommingMessage的原型创建的对象,也就是说http.IncommingMessage该有的事件和方法,req都是有的,并且req也是一个Readable Stream,如果要具体了解,可以去看看http.IncommingMessage的文档

在这个文件里面express文档中关于req的方法都在这里有对应的定义,并且也不难读,所以这里也就不再多说。

值得一提的是req.params, req.query和req.body都不是在这里初始化和定义的:

  • req.params: 之前在介绍layer.js的时候提到过,req.params是在中间件匹配的过程中生成的,这个params对象先是在layer中初始化并且通过path-to-regexp匹配生成键值对,然后再router/index.js定义Router的handle方法的里,将params值赋给req:
1
2
3
4
5
6
7
8
9
proto.handle = function handle(req, res, out) {
/* 省略 */
// Capture one-time layer values
req.params = self.mergeParams
? mergeParams(layer.params, parentParams)
: layer.params;
/* 省略 */
};
  • req.query: 这里的可以去看middleware/query.js 中,首先这里定义了query函数中间件,然后在application.js里面定义的lazyrouter方法里,以router.use方法调用这个query函数中间件:
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));
}
};

而这个lazyrouter方法呢只要是使用了app.use或者app.get, app.post等都会调用,所以req.query是在这个时候生成的。

  • req.body: 现在这个对象已经不再express处理了,主要是因为body-parser已经从express里依赖的包里面独立出去了,所以这部分的处理可以去看body-parser这个包,当然req.body也是在中间件中生成的;

然后我们再来看response.js,这里是定义app.method(path, function(req, res, next) {})中的res的地方,首先是res的初始化:

1
var res = Object.create(http.ServerResponse.prototype);

res是以http.ServerResponse的原型创建的对象,也就是说http.ServerResponse的事件和方法基本也适用于它,并且也是一个Writable Stream对象。res相对于req就要简单一些,在express文档里面提到的方法在这里都是在这里定义的,所以也不再多说了。

application

application.js, 这里就是定义初始化express应用的地方,也就是express.js中的express应用的proto定义的地方,里面也定义了express()返回的对象里面可使用的各个方法,阅读起来难度也不大。

总的来说application.js算是一个集大成所在的地方了,这里也是express应用对象定义的入口,在分析完各个部分之后,再在application这里看一遍整理一下,应该就比较清楚整个express是什么样的结构了。

本来这里有一个lazyrouter方法应该说一下,但是在分析req.query的时候已经提到过了,这里就不再赘述了。

从请求到响应

以创建一个express服务为例

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
const responseTime = require('response-time');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const expressSession = require('express-session');
const express = require('express');
const compression = require('compression');
const http = require('http');
const path = require('path');
const router = require('../routes');
const app = express();
const resolve = file => path.resolve(__dirname, file);
app.use(compression());
app.use(responseTime());
app.use(bodyParser.json({limit: '10mb'}));
app.use(bodyParser.urlencoded({limit: '10mb', parameterLimit: 100000, extended: false}));
app.use(cookieParser());
app.use(router);
app.use(function(req, res, next) {
next({status: 'pageNotFound', code: 404});
});
app.listen(8000, function() {
console.log('start up!');
});

app.listen方法在application.js里面有定义:

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

关于http.createServer在官方文档里有说明:

1
2
3
4
5
6
7
8
9
http.createServer([requestListener])
Added in: v0.1.13
requestListener <Function>
Returns: <http.Server>
Returns a new instance of http.Server.
The requestListener is a function which is automatically added to the 'request' event.

而app(req, res, next)的执行时直接调用的app.handle(req, res, next)方法,也就是在express.js中的createApplication中定义的:

1
2
3
4
5
6
7
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
/* 省略 */
}

app.handle()一执行就是直接按照之前app中定义好的中间件顺序进行匹配并且执行了,整个express服务也就这样开始一步一步处理请求,直到最后res对象发送请求到客户端。

总结

这篇文章由于笔者的拖延症,整整拖了半年左右才写完,不过最重要的部分也就是中间件那部分的内容是一开始就完成了的。

由于完成的事件跨度较大,所以express的代码有部分的更新变动可能会导致此文章贴出的源码前后会有一些差异,不过总的实现方法和思想倒是没啥变化。

express是一个非常轻量的框架,用来实现一些微服务特别便利,但是如果要用来做企业级的应用和服务的话,就需要制定许多约束或者说是规范。笔者也正在这方面努力踩坑=。=。