关于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不支持函数重载)
来源:CSDN
作者:IT 哈
链接:https://blog.csdn.net/qq_31967569/article/details/103783939