首先看下 express 源码结构:
回想一下 express 的使用:
var express = require('express')
var app = express()
app.use ...
app.get ...
app.listen ...
可以看出 express 做的其实相当于对原生 nodejs http.createserver
的一系列封装,封装成 app,并提供一系列拓展api(中间件、请求调用)来处理用户请求。
再看看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 是怎样一个对象。
最常用的 listen 方法,即对 http.createServer 的封装:
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
另一个核心方法 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方法注册了两个中间件
除了 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 对象的 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 对象,留意一下 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 构造函数的目的:来看 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;
};
比较一下两种方式可以发现:
上面这个 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 启动服务、注册和调用中间件的源码全部过完了,有关 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 是 undefinedapp.all
, app[http.VERB]
, app.route
, router.all
, router[http.VERB]
, router.route
注册中间件,最终回归到 router.route
处理:router.route
会创建一个 layer,这个 layer.route 是这个 route 实例响应请求
server.listen
router.use
处理的中间件,router.handle
路径匹配后拿到 layerlayer.handle_request
执行这个 layer 的 fnrouter.route
处理的中间router.handle
路径匹配后拿到 layer,这个 layer 中取到 route,这个route 也是一个 layer 实例route._handles_method
判断 http.VERB 是否匹配route.dispatch
遍历 route layer 实例的 layer stack,找到匹配 的 layerlayer.handle_request
执行这个 layer 的 fnexpress 处理请求和注册调用中间件的核心原理即是如此。源码里还有 request.js
response.js
两个文件,内容相对简单,看起来比较快。
首先 req 这个对象是拓展自 http.IncomingMessage.prototype
的
var req = Object.create(http.IncomingMessage.prototype)
先看一些通用方法:
request.get
即 req.header
获取请求头的方法,这里会做一些入参大小写兼容处理(Content-Type、content-type),并且兼容了 referer (Referer的正确英语拼法是referrer。由于早期HTTP规范的拼写错误,为了向下兼容就保留了 https://zh.wikipedia.org/wiki/HTTP_referer)req.accepts
检查是否支持某种 MIME type,入参兼容数组,逗号连接字符串,不支持会返回 undefined(这时候应用中应该返回 406)req.range
[{ start, end }, ...]
, 并包含一个属性 type === 'bytes'
关于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
判断是不是 httpsaddrs.reverse().pop()
:[client, proxy1, proxy2]res 对象拓展自 http.ServerResponse.prototype
var res = Object.create(http.ServerResponse.prototype)
几个比较重要的方法:
res.send
res.send(body, status)
的方式设置返回值,所以首先通过判断 arguments 做了一系列兼容性处理this.json
方法特殊处理this.end
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
我们调用 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
download 的 实现和 sendFile 很接近 不同点在于会设置 ‘content-disposition’ 的响应头 (res.attachment 也会)
随后再调用 res.send
res.redirect
入参是目标 url// 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));
};