【原创】express3.4.8源码解析之路由中间件
前言
注意
:旧文章转成markdown格式。
跟大家聊一个中间件,叫做路由中间件,它并非是connect中内置的中间件,而是在express中集成进去的。
显而易见,该中间件的用途就是 ------ 路由分发 ,表面上看它的路由机制有点像Backbone.Router,实际上它的实现要比Backbone.Router复杂的多,功能也相应地要强很多。
它可以做到:
- 根据http采用不同的请求方法来采用不同的路由(如:get,post等)
- 根据请求的url与path生成的正则表达式进行匹配的结果进行不同路由(如:/user/:id => //user/(?😦[/]+?))/?$/)
- 根据path生成的表达式获取请求参数,并传递给路由处理函数(如:/user/:id => /user/322 => req.params.id为322)
- 根据不同的情况,选择性决定是否执行下一个路由处理函数(通过调用next()),或者是下一个路由(通过调用next('route'))
例子
var express = require('express');
var app = express();
app.set('port', 3000);
// 加载路由中间件
app.use(app.router);
// 当作数据库
var users = [
{ id: 0, name: 'lovesueee'}
];
function loadUser(req, res, next) {
var user = users[req.params.id];
if (user) {
req.user = user;
next();
} else {
next(new Error('Failed to load user ' + req.params.id));
}
}
// 第一个get路由,两个回调
app.get('/user/:id', loadUser, function(req, res){
res.send('Viewing user ' + req.user.name);
});
// 第二个get路由,一个回调,path和第一个路由相同
app.get('/user/:id', function(req, res){
res.send('Looking user ' + req.user.name);
});
app.listen(app.get('port'), function () {
console.log('server listening...');
});
上面是一个简单的例子(文件名为test.js),我们在终端执行:node test.js
,并打开浏览器输入地址:http://127.0.0.1:3000/user/0
,此时我们看到会看到页面显示:'Viewing user lovesueee'。
ok,我们理一下思路,程序是这样执行的:
- 服务端通过
app.use(app.router);
添加路由中间件,通过app.get(..)
添加了两个path相同,回调函数不同的路由。 - 然后开始监听3000端口,客户端便输入http://127.0.0.1:3000/user/0发起请求
- 服务端接受请求,程序必然是先经过中间件的处理,app.router作为路由中间件也不例外(ps: 注意app.router是一个中间接接口函数)
- 当程序经过app.router中间件的时候,会进行路由匹配,显然这里是匹配上了,那么将会顺序执行loadUser以及其后面的匿名函数。
值得注意的几点:
app.get(..)
方法能够接受多个回调作为路由处理函数,就像第一个路由那样(有loadUser函数以及跟着后面的匿名函数)。- 在loadUser函数中的
req.params.id为0
,显然是'/user/:id'
生成的正则表达式/^\/user\/(?:([^\/]+?))\/?$/
匹配后得到的,然后传递给req.params.id的。 - 最后能在浏览器中输出结果:
'Viewing user lovesueee'
,显然是第一个路由的第二个回调函数得以执行才造成的,那么该函数的执行我们自然会联想到是loadUser函数中的next()方法的调用触发的(就像之前的中间件一样),而且req.user
也是loadUser函数通过req变量传递过来的。
几个思考?
- 如果去掉loadUser中的
next();
,会怎样? --- 显然是第二个回调函数不再被调用,客户端长久处于pending状态,最后超时。 - 如果访问url是
http://127.0.0.1:3000/user/1
,会怎样? --- 向客户端返回错误信息,根据环境的(即process.env.NODE_ENV
为development还是production)的不同显示不同的错误信息。 - 如果我们执行的不是
next()
而是next('route');
,会怎样? --- 这时浏览器显示的将是:'Looking user lovesueee'
,程序没有执行第一个路由的第二个回调,而是立马执行了第二个路由的回调。
源码分析
这里主要讲解两个函数:
- app.get(name) | app.get(path, fn1, [fn2, fn3]) --- 注册路由
- app.router --- 路由执行入口
app.get
这个函数有两个作用:
- 当参数只有一个时,用来获取配置信息,与
app.set(..)
相对应。 - 当参数大于一个是,用来为get请求注册路由,也就是该方法注册的路由匹配条件有两个:1. get请求 2. url匹配path生成的正则。
注意:这里我们以app.get()
方法为例,其他的路由方法同样原理,像app.post()
等。
ok,我们首先当然得找到app.get()
方法的源代码在哪里,如下(application.js中):
// 给app赋值所有的http method方法
methods.forEach(function(method){
app[method] = function(path){
// get时且参数唯一个时,表示到settings里取配置
if ('get' == method && 1 == arguments.length) return this.set(path);
// deprecated
if (Array.isArray(path)) {
console.trace('passing an array to app.VERB() is deprecated and will be removed in 4.0');
}
// if no router attached yet, attach the router
// 如果没有调用路由中间件,这里自动调用
if (!this._usedRouter) this.use(this.router);
// setup route
// 调用Router实例,创建一个路由route
// 这里的参数可以是这样(path, fn1, [fn2, fn3], fn4,...)
// fn代表callback
this._router[method].apply(this._router, arguments);
return this;
};
});
这里通过循环methods
数组,给app
添加http的一系列方法,如:get, post ,delete等。
当我们调用app.get()
时,
if (!this._usedRouter) this.use(this.router);
这样的一句,是为了防止我们在使用app.get()
之前,没有先注册路由中间件(即app.use(app.router);
),帮我们注册一下。- 程序最后,调用
this._router[method].apply(..);
,(method这里为'get')告诉我们真正的路由注册其实在于this._router所引用的对象。
ok,于是我很好奇地在application.js中找到了this._router的定义,如下:
app.defaultConfiguration = function(){
...
// 初始化Router实例
// 用于路由请求
this._router = new Router(this);
// 搜集的路由的映射
// 将map挂载到this对象上,那么可以通过app.routes访问
// 结构如下:
// {
// get : [
// Route1,
// Route2,
// ...
// ],
// post : [],
// ...
// }
this.routes = this._router.map;
// 定义route的getter
// 用户通过调用app.use(app.router),启动路由中间件
this.__defineGetter__('router', function(){
// 标识路由中间件已启用
// 并通过从app.setttings中的配置设置router的相应配置
// 如:路由大小写敏感和路由严格模式
// 最后返回路由中间件的接口函数
this._usedRouter = true;
this._router.caseSensitive = this.enabled('case sensitive routing');
this._router.strict = this.enabled('strict routing');
return this._router.middleware;
});
...
};
这里我们会有所发现:
this._router = new Router(this);
是初始化了一个Router实例,也就是说之前的app.get()
其实是调用Router实例的get方法,我们待会再看。- 这里定义了app.router的一个getter,在调用
app.router时
,this._usedRouter
被设置为true,表示调用过了;另外返回的this._router.middleware;
就是路由中间件的接口函数,即路由的入口处。 - 我上面的中文注释暴露了this._router.map的结构,这个我们接下来说。
到这里,我们的目光应该同一放到这个Router实例上,我找到了它的类,在express/lib/router/index.js,如下:
function Router(options) {
options = options || {};
var self = this;
this.map = {};
this.params = {};
this._params = [];
this.caseSensitive = options.caseSensitive;
this.strict = options.strict;
this.middleware = function router(req, res, next){
self._dispatch(req, res, next);
};
}
很醒目,这里其实就是定义了一些对象属性,我们更关注的其实应该是this.middleware = function router(req, res, next){..}
,果然不出所料时中间件接口函数,当请求走到这里时,路由中间件调用自己_dispatch
方法开始进行路由分发,具体的我们在讲app.router
时再说,赶紧回到我们要深追的app.get()
上。
刚才说到Router实例的get方法,我们在这个文件里找到:
methods.forEach(function(method){
Router.prototype[method] = function(path){
var args = [method].concat([].slice.call(arguments));
this.route.apply(this, args);
return this;
};
});
同样是遍历methods
赋予Router原型一系列http方法,归根结底还是调用this.route(..)
方法,并将传入method(这里是'get'),顺着代码往上找,找到:
Router.prototype.route = function(method, path, callbacks){
// flatten操作
// [fn1, [fn2, fn3], fn4] => [fn1, fn2, fn3, fn4]
var method = method.toLowerCase()
, callbacks = utils.flatten([].slice.call(arguments, 2));
// ensure path was given
// 确保路由route是有路径path可依据的
if (!path) throw new Error('Router#' + method + '() requires a path');
// ensure all callbacks are functions
// 保证所有callback都是函数
callbacks.forEach(function(fn){
if ('function' == typeof fn) return;
var type = {}.toString.call(fn);
var msg = '.' + method + '() requires callback functions but got a ' + type;
throw new Error(msg);
});
// create the route
debug('defined %s %s', method, path);
// 创建Route实例
var route = new Route(method, path, callbacks, {
sensitive: this.caseSensitive,
strict: this.strict
});
// add it
// 向当前Router实例的map映射里添加该route
(this.map[method] = this.map[method] || []).push(route);
return this;
};
哈哈,终于到了柳暗花明的地方了,和中间件的注册一样,注册暂时先存储,看看上面的代码和注释,我们可以看到:
- 其实可以像
app.get(path, fn1,[fn2, fn3]);
这样调用,因为有这一句utils.flatten([].slice.call(arguments, 2));
。 - 每次调用
app.get()
都会产生一个Route实例(即便它们的path是一样的,就像上面例子中的第一个和第二个路由的path一样,却是两个Route实例) - Router实例中最终维护了一个map映射,它的结构如下:
{
get : [
Route实例1,
Route实例2,
...
],
post : [],
...
}
,建议大家可以看看我之前画的结构图。
- 最终焦点就都到了Route实例中去了,我们可以想到Router实例存储着多个Route实例,那么Route实例自然会存储着method,path,callbacks这些信息。
最后,找到express/lib/router/route.js,如我所说:
function Route(method, path, callbacks, options) {
options = options || {};
this.path = path;
this.method = method;
this.callbacks = callbacks;
// 将path转换为正则
this.regexp = utils.pathRegexp(path
, this.keys = []
, options.sensitive
, options.strict);
}
// 通过正则进行路由匹配
Route.prototype.match = function(path){...};
总结下就是:
就是存在app._router
,它是一个Router实例,Router实例有一个map,可以理解为路由映射,里面存储着各种http方法的路由集合,
每个集合的元素其实是一个Route实例,每个Route实例包含一个正则,一个method用来匹配url,一些callbacks,用来回调处理逻辑。
我这里还是给一张图吧,假设路由是这样的:
// 第一个get路由,两个回调
app.get('/user/:id', loadUser, function(req, res){
res.send('Viewing user ' + req.user.name);
});
// 第二个get路由,一个回调,path和第一个路由相同
app.get('/user/:id', function(req, res){
res.send('Looking user ' + req.user.name);
});
// 第三个get路由,path和第一个路由不同
app.get('/post/:pid', function(req, res){
res.send('Viewing post');
});
// 第四个post路由
app.post('/post/:pid', function(req, res){
// ...
});
对应的存储的map为:
app.router
前面已经提到app.router
其实返回的是一个中间件处理函数function router(req, res, next) {self._dispatch(req, res, next);}
,那么我们就应该把焦点放到self._dispatch(req, res, next);
这个函数上。
我们可以试想一下,前面我们通过app.get()
注册了路由,也就是将相关的信息存储起来了,那么_dispatch(..)
函数所做的就应该是遍历map映射查找是否与当前url匹配的Route实例,如果有,那么执行Route实例存储的callbacks来执行业务逻辑,并且可以在任何callback里决定是否要执行接下来的callback或者是匹配接下来的路由,听到这里是不是感觉跟中间件的实现很像?没错,答案是肯定的。
我们同样找到代码的位置:
Router.prototype._dispatch = function(req, res, next){
var params = this.params
, self = this;
debug('dispatching %s %s (%s)', req.method, req.url, req.originalUrl);
// route dispatch
// 路由分发
// i是索引,表示从第i个route开始匹配
// err用于传递路由时的错误信息
// 第一次自调用pass
(function pass(i, err){
var paramCallbacks
, paramIndex = 0
, paramVal
, route
, keys
, key;
// match next route
// 匹配下一个合适的路由
function nextRoute(err) {
pass(req._route_index + 1, err);
}
// match route
// 匹配路由
req.route = route = self.matchRequest(req, i);
// implied OPTIONS
if (!route && 'OPTIONS' == req.method) return self._options(req, res, next);
// no route
// 如果匹配不到,那么直接执行下一个中间件
if (!route) return next(err);
debug('matched %s %s', route.method, route.path);
// we have a route
// start at param 0
// params匹配到的变量值,如{id : 3, ...}
// keys变量名数组,通过解析路由是获取而来,如(:id -> id)
// param函数中的将会用到i进行索引,所以这里i必须重置为0,
req.params = route.params;
keys = route.keys;
i = 0;
// param callbacks
function param(err) {
// 重置为0
paramIndex = 0;
// 依次获取params的key,如 id
// 获取params对应key的value
// 获取params对应key的回调
key = keys[i++];
paramVal = key && req.params[key.name];
paramCallbacks = key && params[key.name];
try {
// err为'route',进入下一个route匹配
// 我们正常会通过调用next('route')走这一步
if ('route' == err) {
nextRoute();
// 如果报错,那么
// 首先先重置i为0(因为这里都引用同一个i)
// 然后执行路由的回调函数(处理错误的)
} else if (err) {
i = 0;
callbacks(err);
// 如果存在paramCallbacks(即通过app.param('id', function() {...});)注册的
// 且paramVal有值
// 那么执行paramCallbacks
} else if (paramCallbacks && undefined !== paramVal) {
paramCallback();
// 如果存在key,那么迭代执行param()
} else if (key) {
param();
// 最后,再执行完paramCallbacks后,最后执行路由的callbacks
} else {
i = 0;
callbacks();
}
} catch (err) {
param(err);
}
};
// 第一次调用param
param(err);
// single param callbacks
// 调用每一个param 的回调函数列表
// 通过自调用,循环函数列表
// 如果出错,或者循环完毕,继续处理下一个param
function paramCallback(err) {
var fn = paramCallbacks[paramIndex++];
if (err || !fn) return param(err);
fn(req, res, paramCallback, paramVal, key.name);
}
// invoke route callbacks
// 调用路由回调函数
// 依然是将当前函数作为next传给回调fn,进行下一个函数的调用
// 这里又是迭代
function callbacks(err) {
var fn = route.callbacks[i++];
try {
// 结束路由函数的调用,进入到下一个路由匹配
if ('route' == err) {
nextRoute();
// 如果报错,且fn存在
// 参数个数 < 4,进行下一个路由函数的调用
// 否则,调用fn处理当前错误(错误处理函数)
} else if (err && fn) {
if (fn.length < 4) return callbacks(err);
fn(err, req, res, callbacks);
// 没有错误,且存在回调
// 参数个数 < 4,处理正常函数逻辑(逻辑处理函数)
// 否则,忽略回调,进行下一个路由回调调用
} else if (fn) {
if (fn.length < 4) return fn(req, res, callbacks);
callbacks();
// 最后如果不存在回调了,那么进行一个路由匹配
} else {
nextRoute(err);
}
} catch (err) {
callbacks(err);
}
}
})(0);
};
上面的代码稍微长了点,不过有了中文注释应该可以理解~~
好困好困,这里我就简单说一下:
- pass函数主要就是进行下一个路由(Route)的匹配,当我们在callback里面调用
next('route')
,最终就会迭代地执行pass函数。 - paramCallback函数与
app.param()
相关联,主要用于处理匹配到的请求参数(这里就不细说了),处理完后调用next()
,让callbacks能够访问到处理过的请求参数,从而进行一定的逻辑处理。 - callbacks函数,就是调用Route实例中存储的一系列回调函数,同样是通过
next()
,调用下一个回调,如果有错误,也可以next(new Error('xxx'))
,最终反馈到中间件处理那边。
最后
希望文章对读者有用,欢迎多多交流。