关于express.js的实现源码解读,版本为 4.14。主要为路由部分。
一个Web框架最重要的模块是路由功能,该模块的目标是:能够根据method、path匹配需要执行的方法,并在定义的方法中提供有关请求和回应的上下文。
模块声明
express
中的路由模块由Router完成,通过完成调用Router()
得到一个router
的实例,router
既是一个对象,也是一个函数,原因是实现了类似C++中的()
重载方法,实质指向了对象的handle
方法。router
的定义位于router/index.js中。
// router/index.js - line 42
var proto = module.exports = function(options) {
var opts = options || {};
// like operator() in C++
function router(req, res, next) {
router.handle(req, res, next);
}
//...
}
接口定义
router
对外(即开发者)提供了路由规则定义的接口:get
、put
等对应于HTTP method类别,函数签名都是$method(path, fn(req, res), ...)
,接口的方法通过元编程动态定义生成,可以这样做的根本原因是方法名可以使用变量的值定义和调用,Java中的反射特性也可间接实现这点,从而大量被应用于Spring框架中。
// router/index.js - line 507
// create Router#VERB functions
// --> ['get', 'post', 'put', ...].foreach
methods.concat('all').forEach(function(method){
// so that we can write like 'router.get(path, ...)'
proto[method] = function(path){
// create a route for the routing rule we defined
var route = this.route(path)
// map the corresponding handlers to the routing rule
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});
路由定义
在规则定义的接口中,路由规则的定义需要router
保存路由规则的信息,最重要的是方法、路径以及匹配时的调用方法(下称handler),还有其他一些细节信息,这些信息(也可以看做是配置)的保存由Route对象完成,一个Route对象包含一个路由规则。Route
对象通过router
对象的route()
方法进行实例化和初始化后返回。
// router/index.js - line 491
proto.route = function route(path) {
// create an instance of Route.
var route = new Route(path);
// create an instance of Layer.
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
// layer has a reference to route.
layer.route = route;
// router has a list of layers which is created by 'route()'
this.stack.push(layer);
return route;
};
Route
的成员变量包括路径path
,以及HTTP method的路由配置接口集,这里和router
中一样的技巧提供了method所有类别的注册函数,此处无关紧要,只要route
能够得到路由配置的method值即可,将method作为一个参数传入或者作为方法名调入都可以。
route()
方法除了实例化一个Route
外,还是实例化了一个Layer
,这个的Layer
相当于是对应Route
的总的调度器,封装了handlers的调用过程,先忽略。
真正将handlers传入到route
中发生在510行,也即上述route
提供的注册函数。由于一条路由设置中可以传入多个handler,因此需要保存有关handler的列表,每一个handler由一个Layer
对象进行封装,用以隐藏异常处理和handler调用链的细节。因此,route
保存了一个Layer
数组,按handler在参数中的声明顺序存放。这里体现Layer
的第一个作用:封装一条路由中的一个handler,并隐藏链式调用和异常处理等细节。
// router/route.js - line 190
for (var i = 0; i < handles.length; i++) {
var handle = handles[i];
/* ... */
// create a layer for each handler defined in a routing rule
var layer = Layer('/', {}, handle);
layer.method = method;
this.methods[method] = true;
// add the layer to the list.
this.stack.push(layer);
}
返回到router
中,最初实例化一个route
的方法route
中,还实例化了一个Layer
,并且router
保存了关于这些Layer的一个列表,由于我们可以在router
定义多个路由规则,因此这是Layer的第二个作用:封装一条路由中的一个总的handler,同样也封装了链式调用和异常处理等细节。这个总的handler即是遍历调用route下的所有的handler的过程,相当于一个总的Controller,每一个handler实际上是通过对应的小的Layer
来完成handler的调用。
由route()
方法可知,总的handler定义在route
的dispatch()
方法中,该方法中,的确在遍历route
对象下的Layer
数组(成员变量stack
以及方法中的idx++
)。
// router/index.js - line 491
proto.route = function route(path) {
var route = new Route(path);
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
// the 'big' layer's handler is the method 'dispatch()' defined in route
}, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
};
路由匹配
整理路由配置过程,思考每个路由配置信息的保存位置,有:
-
路由规则,一条对应于一个
Route
中,并包装一个Layer
。 -
所有路由规则保存在
Router
中的stack
数组中。 -
对于一个路由规则:
-
路径在
Route
和Layer
的成员变量path
。 -
HTTP method在
Route
下每个handler对应的Layer
中的method
成员变量,以及Route
下的成员变量methods
标记了各个method是否有对应的Layer
。 -
handler,每一个都包装成一个
Layer
,所有的Layer
保存在Route
中的stack
数组中。
-
有了如上信息,当一个请求进来需要寻找匹配的路由变得清晰。路由匹配过程定义在Router
的handle()
方法中(router/index.js 135行)(回顾:Router()
方法实际上调用了handle()
方法。)
handle()
方法中,不关注解析url字符串等细节。从214行可发现,不考虑异常情况,寻找匹配路由的过程其实是遍历所有Layer
的过程:
-
对于每个
Layer
,判断req
中的path
是否与layer
中的path
匹配,若不匹配,继续遍历(path匹配过程后述); -
若path匹配,则再取
req
中的method
,通过route
的methods
成员变量判断在该route
下是否存在匹配的method,若不匹配,继续遍历。 -
若都匹配,则提取路径参数(形如
/:userId
的通配符),调用关于路径参数的handler。(通过router.param()
设置的中间件) -
调用路由配置
route
的handlers,这又是遍历route
下的小的Layer
数组的过程。 -
决定是否返回1继续遍历。返回到
stack
的遍历是通过尾递归的方式实现的,注意到next
被传入layer.handle_request
的方法中,handle_request
中处理事情最后向handler
传入next
,从而是否继续遍历取决于handler的实现是否调用的next()
方法。express的实现大量使用尾递归尾调用的模式,如process_params()
方法。
简化版的路由匹配过程如下所示:
// router/index.js - line 214
proto.handle = function handle(req, res, out) {
// middleware and routes
var stack = self.stack;
next();
// for each layer in stack
function next(err) {
// idx is 'index' of the stack
if (idx >= stack.length) {
setImmediate(done, layerError);
return;
}
// get pathname of request
var path = getPathname(req);
// find next matching layer
var layer;
var match;
var route;
while (match !== true && idx < stack.length) {
layer = stack[idx++];
// match the path ?
match = matchLayer(layer, path);
route = layer.route;
if (match !== true) {
continue;
}
// match the method ?
var method = req.method;
var has_method = route._handles_method(method);
if (!has_method && /**/) {
match = false;
continue;
}
}
// no match
if (match !== true) {
return done(layerError);
}
// Capture one-time layer values
// get path parameters.
req.params = /*...*/;
// this should be done for the layer
// invoke relative path parameters middleware, or handlers
self.process_params(layer, paramcalled, req, res, function (err) {
if (route) {
// invoke all handlers in a route
// then invoke the 'next' recursively
return layer.handle_request(req, res, next);
}
});
}
}
特殊路由
在路由匹配的分析中,省略了大量细节。
-
通过
Router.use()
配置的普通中间件:默认情况下,相当于配置了一个path
为'/'
的路由,若参数提供了path
,则相当于配置了关于path
的全method的路由。不同的是,handlers不使用route
封装,每一个handler直接使用一个大的Layer
封装后加入到Router
的stack
列表中,Layer
中的route
为undefined
。原因是route
参杂了有关http method有关的判断,不适用于全局的中间件。 -
通过
Router.use()
配置的子路由,use()
方法可以传入另一个Router
,从而实现路由模块化的功能,处理实际上和普通中间件一样,但此时传入handler为Router
,故调用Router()时即调用Router
的handle()
方法,使用这样的技巧实现了子路由的功能。// router/index.js - line 276 // if it is a route, invoke the handlers in the route. if (route) { return layer.handle_request(req, res, next); } // if it is a middlewire (including router), invoke Router(). trim_prefix(layer, layerError, layerPath, path);
子路由功能还需要考虑父路径和子路径的提取。这在trim_prefix
方法(router/index.js 212行),当route
为undefined
时调用。直接将req
的路径减去父路由的path
即可。为了能够在子路由结束时返回到父路由,需要从子路径恢复到带有父路径的路径(信息在req
中),结束时调用done()
,done
指向restore()
方法,用于恢复req
的属性值。
// router/index.js - line 602
// restore obj props after function
function restore(fn, obj) {
var props = new Array(arguments.length - 2);
var vals = new Array(arguments.length - 2);
// save vals.
for (var i = 0; i < props.length; i++) {
props[i] = arguments[i + 2];
vals[i] = obj[props[i]];
}
return function(err){
// restore vals when invoke 'done()'
for (var i = 0; i < props.length; i++) {
obj[props[i]] = vals[i];
}
return fn.apply(this, arguments);
};
}
-
通过
app
配置的应用层路由和中间件,实际上由app
里的成员变量router
完成。默认会载入init
和query
中间件(位于middleware/下),分别用于初始化字段操作以及将query
解析放在req
下。 -
通过
Router.param()
配置的参数路由,router
下params
成员变量存放param
映射到array[: handler]
的map,调用路由前先调用匹配参数的中间件。
路径参数
现在考虑带有参数通配符的路径配置和匹配过程。细节在Layer
对象中。
路径的匹配实际上是通过正则表达式的匹配完成的。将形如
'/foo/:bar'
转为
/^\/foo\/(?:([^\/]+?))\/?$/i
正则的转换由第三方模块path-to-regex
完成。解析后放在req.params
中。
链式调用和异常处理
在handler的调用中都使用了尾调用尾递归模式设计(也可以理解为责任链模式、管道模式),包括:
-
Router
中的handle
方法调用匹配路由的总handler和中间件。 -
Router
中的路径参数路由(params
)的调用过程。 -
Route
中dispatch
方法处理所有的handlers和每一个Layer
中的handle配合。
链式调用示意图:
-
每一个节点都不了解自身的位置以及前后关系,调用链只能通过
next()
调用下一个,若不调用则跳过,并调用done()
结束调用链。 -
调用链的一个环节仍可以是一个调用链,形成层次结构(思考上述提到的大
Layer
和小Layer
的关系) -
子调用链中的
done()
方法即是父调用链中的next()
方法。 -
出现异常则:
-
若能够接受继续进行,不中断调用链,则可以继续调用
next
方法,带上err
参数,即next(err)
。最终通过done(err)
将异常返回给父调用链。 -
若不能接受,需要中断,则调用
done
方法,,带上err
参数,即done(err)
。
-
-- Fin --
进阶
-
视图渲染模块 render实现,在applications.js 和 view.js 中。
-
对
req
和res
的扩展,header处理。 -
express从0.1、1.0、2.0、3.0、4.0的变化与改进思路。
-
与koa框架的对比
感想
-
express的代码其实不多。
-
路由部分其实写得还是比较乱,大量关于细节的if、else判断,仍是过程式的风格,功能的实现并没有特别的算法技巧,尤其是路由,直接是一个一个试的。框架的实现并不都是所想的如此神奇或者高超。
-
一些不当的代码风格,如route.get等API中没有在函数签名中写明handler参数,直接通过argument数组取slice得到,而且为了实现同一函数名字的不同函数参数的重载,不得不在函数中判断参数的类型再 if、 else 。(js不支持函数重载)