Express 源码解析
首先看下 express 源码结构:

回想一下 express 的使用:
var express = require('express')
var app = express()
app.use ...
app.get ...
app.listen ...可以看出 express 做的其实相当于对原生 nodejs http.createserver 的一系列封装,封装成 app,并提供一系列拓展api(中间件、请求调用)来处理用户请求。
express 入口
再看看express 的入口 express.js ,看下它做了什么:
/**
* Expose `createApplication()`.
*/
var proto = require('./application');
exports = module.exports = createApplication;
/**
* Create an express application.
*
* @return {Function}
* @api public
*/
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;
}export 出一个 createApplication 方法,对应着我们使用 express 的使用方式: var app = express()
可以看出这个createApplication 返回了一个 app 函数,这个app函数接收 req、res、next 用来处理用户的请求,怎么处理呢,交给 app.handle 处理。
app 函数会经过两次 mixin 处理,可以看出 app 继承了 eventemitter 和 proto (也就是 application)
并且增加了 request、response 两个分别基于 req和 res拓展的对象,并给他们增加到 app 的引用
application
我们首先来看 application 是怎样一个对象。
最常用的 listen 方法,即对 http.createServer 的封装:
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};app.use
另一个核心方法 app.use , 是我们注册中间件的入口。
app.use = function use(fn) {
var offset = 0;
var path = '/';
// default path to '/'
// disambiguate app.use([fn])
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;
}
}
var fns = flatten(slice.call(arguments, offset));
if (fns.length === 0) {
throw new TypeError('app.use() requires a middleware function')
}
// setup router
this.lazyrouter();
var router = this._router;
fns.forEach(function (fn) {
// non-express app
if (!fn || !fn.handle || !fn.set) {
return router.use(path, fn);
}
debug('.use app under %s', path);
fn.mountpath = path;
fn.parent = this;
// restore .app property on req and res
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);
});
});
// mounted an app
fn.emit('mount', this);
}, this);
return this;
};我们通常有两种形式注册中间件:app.use(fn),app.use(‘path’, fn), 不配置 path 则默认在到跟路径 ’/‘,所以在 7-21行先做了一轮入参处理
随后调用了一次 this.lazyrouter(); 实例化一个 router 对象,并在接下来,42-49行可以看到实际上是调用了 router 对象的 use 方法。
所以来看一下 this.lazyrouter(); 做了啥:
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));
}
};可以看到 router 对象是通过 Router 构造的, 他是如何构造 router 对象的,我们待会再看。
而第8-9行也是调用了 router 对象的 use方法注册了两个中间件
- query 中间件,基于 parseurl 和 qs 处理请求的 query
- middleware.init 返回的中间件,做的事是重新设置了 req和 res 的原型,分别指向 app.request 和 app.response。
app.route
除了 app.use , 我们很快还能发现另一个核心方法 app.route 。以及对 apphttp.VERB 这一类方法的代理。
app.route = function route(path) {
this.lazyrouter();
return this._router.route(path);
};
/**
* Delegate `.VERB(...)` calls to `router.VERB(...)`.
*/
methods.forEach(function(method){
app[method] = function(path){
if (method === 'get' && arguments.length === 1) {
// app.get(setting)
return this.set(path);
}
this.lazyrouter();
var route = this._router.route(path);
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});可以看到他们的实质都是是调用 this.lazyrouter(); 实例化一个 router 对象,随后调用了 router 对象的 route 方法。所以当我们使用 app.route app.get app.post 这一类路由中间件的时候,都是在调用 router 对象的 route 方法。
Router
所以接下来我们看看 Router 构造函数,看看 router对象是如何构造的,以及router 对象的 use 和 route 方法是怎样的。
Router 构造函数的入口在 router/index.js
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;
};我们知道中间件就是匹配到路径就会执行的回调函数,可以看到 router 会把这件事直接交给 router.handle 做,再进一步看看 handle 方法。handle 方法很长,但是可以很快找到重点:
// 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;
}
function matchLayer(layer, path) {
try {
return layer.match(path);
} catch (err) {
return err;
}
}可以看到是在遍历一个 stack 数组,为了找到命中路径的 layer对象(第8行),拿到命中路径的 layer 对象之后,会调用 layer.handle_request(req, res, next); 来处理中间件。
再往下还有一段:
// 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 相关变量,具体是什么先不管,直接看注释我们大概可以明白这是把代理了 apphttp.VERB 的 router.route 方法又交给了 route[method] 处理。
Layer
上面出现了一个新概念 layer 对象,留意一下 Router 目录,其中有一个 layer.js 文件。

function Layer(path, options, fn) {
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);
// set fast path flags
this.regexp.fast_star = path === '*'
this.regexp.fast_slash = path === '/' && opts.end === false
}
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
...
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};可以看到 layer 做的就是维护一个路径 path 和注册的回调函数的关系,同时会把 path 转成正则在请求阶段匹配路径调用注册的回调函数。layer.handle_request所最终执行的函数即使 layer 对象实例化时传入的函数 fn,注意这里还回把 next 传给 fn
回头再看看 Router 构造函数的入口 router/index.js 在哪些地方 实例化了 layer 对象,两个地方:
- Router.prototype.use
- Router.prototype.route
这样绕了一圈,我们又回到了最初来看 Router 构造函数的目的:来看 use 和 route 这两个方法。
在 Router.prototype.use 中,是这样构造 layer 对象的:
proto.use = function use(fn) {
...
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
this.stack.push(layer);
return this;
};在 Router.prototype.route 中,则是这样构造 layer 对象的:
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;
};比较一下两种方式可以发现:
- Router.prototype.use 创建的 layer,注册的回调函数是传入的 fn,layer.route 是 undefined
- Router.prototype.route 创建的 layer,注册的回调函数是一个 route 实例的 route.dispatch,layer.route 是这个 route实例
Route
上面这个 route 实例是通过 Route 创建的,我们再留意一下 Router 目录,其中还有一个 route.js 文件。

为什么要这么设计呢,我们首先来看一下 route.dispatch:
Route.prototype.dispatch = function dispatch(req, res, done) {
...
req.route = this;
next();
function next(err) {
...
var layer = stack[idx++];
if (err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
};可以看到他的做法跟 router.handle 的做法类似,遍历 stack 数组取出 layer对象(第9行),调用 layer.handle_request(req, res, next); 来处理中间件。
注意他遍历 stack 数组的做法不是直接循环,而是通过 idx++ 和 next 传递,可以回头看看layer.handle_request的实现里是把 next 传给了定义layer 对象实例时注册的回调函数,这里的回调函数就是 route.dispatch.bind(route),也就是 route.dispatch 自己,从而实现了遍历 stack 数组。
接下来我们就需要看看这个 layer 对象又是从哪来的了,可以很快找到这个 route 创建 layer 的地方:
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 a callback function but got a ' + type
throw new Error(msg);
}
debug('%s %o', method, this.path)
var layer = Layer('/', {}, handle);
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);
}
return this;
};可以看出他实际是维护了是一个统一 path (’/‘)对应多种 method handle 的关系,而 handle 也正是来自我们之前在 Router 构造函数入口 router/index.js 看到的把代理了 apphttp.VERB 的 router.route 方法又交给了 route[method] 的处理。
application 总结
到此我们终于把从 application 启动服务、注册和调用中间件的源码全部过完了,有关 router、route、layer的概念比较绕,接下来梳理一下。
一个 application 做的事包括三部分:
启动服务
- 调用
http.createServer
- 调用
注册中间件,application(express)通过 layer 对象维映射路由 path 和中间件回调函数 handle 的关系,并维护了一个 layer stack 数组:
- 自己调用
app.lazyrouter注册两个默认中间件,最终会回归到router.use处理:middleware.init(this)重设 res 和 req 的原型query处理请求参数
- 调用方通过
app.use,router.use注册的中间件,最终会回归到router.use处理:router.use会创建一个 layer,注册的回调函数是传入的 fn,layer.route 是 undefined
- 调用方通过
app.all,app[http.VERB],app.route,router.all,router[http.VERB],router.route注册中间件,最终回归到router.route处理:router.route会创建一个 layer,这个 layer.route 是这个 route 实例- 这个 route 实例会根据 http.VERB 再创建一系列 layer ,这些layer注册的回调函数是跟 http.VERB 对应的 fn handle
- 有两种处理的原因就在于,路由相关中间件不仅需要匹配路径还需要匹配 http.VERB
- 自己调用
响应请求
- 调用
server.listen - 中间件已经注册好,遍历 layer stack 数组,根据注册的区别执行中间件回调函数
- 回归到
router.use处理的中间件,router.handle路径匹配后拿到 layerlayer.handle_request执行这个 layer 的 fn
- 回归到
router.route处理的中间router.handle路径匹配后拿到 layer,这个 layer 中取到 route,这个route 也是一个 layer 实例- 用这个 route 的
route._handles_method判断 http.VERB 是否匹配 - 随后
route.dispatch遍历 route layer 实例的 layer stack,找到匹配 的 layer layer.handle_request执行这个 layer 的 fn
- 回归到
- 调用
express 处理请求和注册调用中间件的核心原理即是如此。源码里还有 request.js response.js 两个文件,内容相对简单,看起来比较快。
request
首先 req 这个对象是拓展自 http.IncomingMessage.prototype 的
var req = Object.create(http.IncomingMessage.prototype)先看一些通用方法:
req.get
request.get即req.header获取请求头的方法,这里会做一些入参大小写兼容处理(Content-Type、content-type),并且兼容了 referer (Referer的正确英语拼法是referrer。由于早期HTTP规范的拼写错误,为了向下兼容就保留了 https://zh.wikipedia.org/wiki/HTTP_referer)

req.accepts
req.accepts检查是否支持某种 MIME type,入参兼容数组,逗号连接字符串,不支持会返回 undefined(这时候应用中应该返回 406)
req.range
req.range- 两个参数 一个 size,一个 options(包含一个参数combine标识 重叠数据是否要合并)。
- 返回的结果
- -2 size 语法错误
- -1 size 无效,对应状态码 416
- undefined,请求头不带 range,没有size
- 一个数组,形式为
[{ start, end }, ...], 并包含一个属性type === 'bytes'
- 不过这些都是靠 range-parser 这个第三方模块实现的
关于range
http1.1中新增字段,如果server支持range,需要在响应头中添加 Accept-ranges: bytes(不支持返回 Accept-ranges: none),之后client才可以发起带range的请求。
server 通过判断请求头中的 Range: bytes=0-xxx,判断是不是要做 range请求
如果 xxx存在且有效,返回文件内容,状态码206(partial content),设置 content-range
如果 xxx 无效,状态码 416(request range not satisfiable),
如果请求头不带range,正常响应,不设置 content-range
req.param支持从 url params、body、query取值,排序即为优先级req.is判断请求是不是符合的 Content-Type ,基于 type-is 库实现
只读属性
同时还定义了一系列只读属性,记录一些值得关注的:
protocal- 通过
this.connection.encrypted判断是不是 https - 如果有 trust proxy,会用 trust proxy 获取 protocal,直接返回
- 如果设置了 X-Forwarded-Proto header,会根据 这个 header 取值,如果有多个(,分隔)只取第一个
- 通过
- ip 和 ips
- ip 根据 trust proxy 获取请求的 ip 地址
- ips 根据trust proxy 获取从最近的一个代理地址到请求 ip地址的列表(不包含 socket 地址)
addrs.reverse().pop():[client, proxy1, proxy2] - addrs 的获取通过 proxy-addr 库实现
- hostname
- 如果有 trust proxy,取 trust proxy 的 host
- 如果有 X-Forwarded-Host,取 X-Forwarded-Host 配置的第一个
- 支持 ipv6 地址,增加了对协议头[] 的判断
- fresh 和 stale
- 对于get请求,当 2xx或 304的时候,判断 Etag 和 Last modified 有没有变化(基于fresh库实现)
response
res 对象拓展自 http.ServerResponse.prototype
var res = Object.create(http.ServerResponse.prototype)几个比较重要的方法:
res.send
res.send- 老接口支持
res.send(body, status)的方式设置返回值,所以首先通过判断 arguments 做了一系列兼容性处理 - 随后根据入参数据类型,设置 content-type, 其中对于 object 类型的数据,如果是 buffer,content-type: bin,如果是 json,会交给
this.json方法特殊处理 - 随后计算 content-length
- 随后设置 etag,如果 204和 304,content-type content-length,transfer-encoding会被去除
- 设置
this.end
- 老接口支持
res.json
res.json
对json做特殊处理,类似 Json.stringify。增加了对是否需要 escape 的配置,避免某些字符会导致 html 异常
function stringify (value, replacer, spaces, escape) {
var json = replacer || spaces
? JSON.stringify(value, replacer, spaces)
: JSON.stringify(value);
if (escape) {
json = json.replace(/[<>&]/g, function (c) {
switch (c.charCodeAt(0)) {
case 0x3c:
return '\\u003c'
case 0x3e:
return '\\u003e'
case 0x26:
return '\\u0026'
default:
return c
}
})
}
return json
}把 0x3c (<)、0x3e (>)、0x26 (&) 转成 unicode 编码,避免被 html当做特殊标签字符解析
res.sendFile
res.sendFile
我们调用 sendFile的方式是 res.sendFile(path, options, callback) (options 和 callback 很多时候不配直接用默认值)
如果 opts.root 没有设置,那么path一定要是一个 绝对路径
if (!opts.root && !isAbsolute(path)) {
throw new TypeError('path must be absolute or specify root to res.sendFile');
}接下来是读取文件的关键,通过 send(经典的 send包 ) 创建一个文件流
var file = send(req, path, opts);随后监听文件流的各个事件:
file.on('directory', ondirectory);
file.on('end', onend);
file.on('error', onerror);
file.on('file', onfile);
file.on('stream', onstream);
onFinished(res, onfinish);其中 onFinished 会在下一个事件循环开始调用 我们的 callback
setImmediate(function () {
...
if (done) return;
done = true;
callback();
});最后通过pipe传给 res
file.pipe(res);res.download
res.download
download 的 实现和 sendFile 很接近 不同点在于会设置 ‘content-disposition’ 的响应头 (res.attachment 也会)
随后再调用 res.send
res.redirect
res.redirect入参是目标 url- 第一步设置 响应头 header 中的 location field
// Set location header
address = this.location(address).get('Location');
res.location = function location(url) {
var loc = url;
// "back" is an alias for the referrer
if (url === 'back') {
loc = this.req.get('Referrer') || '/';
}
// set location
return this.set('Location', encodeUrl(loc));
};
- 调用 res.format 格式化 res.body
- 设置 content-length 和状态码
CURRENTLY BUILDING
EveryCityMap
Style, share, and export custom maps for 500 cities. Choose a theme, edit the layers, and download a 1440px WebP without signing up.