2.6 路由
2.6.1 router/index.js
2.6.1.1 全局变量
声明了一些全局变量,objectRegExp 是一个正则表达式,toString 之后用来判断是否是对象。其他的就是保存了一下函数引用。
var objectRegExp = /^\[object (\S+)\]$/;
var slice = Array.prototype.slice;
var toString = Object.prototype.toString;
2.6.1.2 设置导出
默认导出的是一个函数,并且调用这个函数可以得到 router,且用 proto 保存一下这个函数。router 函数代理的 router.handle 方法,并且设置 router 的原型链为 proto,然后初始化 router 上面的一些变量。
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;
};
2.6.1.3 param 参数方法
在阅读此方法之前,大家可以先看一下 param 的 API 文档,看一下它是如何工作的,这样便于我们理解函数的功能,进而理解函数的逻辑。
查看一个函数,我们以理解他的功能为优先。
proto.param = function param(name, fn) {
// 假如第一个参数是一个函数,说明是自定义函数,直接放到 this._params 数组里面。
if (typeof name === 'function') {
deprecate('router.param(fn): Refactor to use path params');
this._params.push(name);
return;
}
// 函数的全局变量,用 params 保存一下 this._params
var params = this._params;
var len = params.length;
var ret;
if (name[0] === ':') { // 假如 name 以 :冒号开头,则去掉
deprecate('router.param(' + JSON.stringify(name) + ', fn): Use router.param(' + JSON.stringify(name.substr(1)) + ', fn) instead');
name = name.substr(1);
}
for (var i = 0; i < len; ++i) { // 执行 params(this._params) 里面的每一个函数,以 name 与 fn 传递进去,假如有返回值,则返回值做为新的 fn 作为参数传递。
if (ret = params[i](name, fn)) {
fn = ret;
}
}
// ensure we end up with a
// middleware function
if ('function' !== typeof fn) { // 确保 fn 是一个 middleware 函数
throw new Error('invalid param() call for ' + name + ', got ' + fn);
}
(this.params[name] = this.params[name] || []).push(fn); // 当 this.params[name] 不为空的时候,直接push fn 否则就等于 [fn]
return this;
};
2.6.1.4 handle 方法
handle 方法是 router 里面的入口函数。下面我将解释注释在代码中。
proto.handle = function handle(req, res, out) {
var self = this; // 保存 this
debug('dispatching %s %s', req.method, req.url);
var idx = 0;
var protohost = getProtohost(req.url) || '' // 从 url 中拿到协议
var removed = '';
var slashAdded = false;
var paramcalled = {};
// store options for OPTIONS request
// only used if OPTIONS request
var options = []; // 配置项目
// middleware and routes
var stack = self.stack;
// manage inter-router variables
var parentParams = req.params; // 保存一些 req 上面的参数
var parentUrl = req.baseUrl || '';
var done = restore(out, req, 'baseUrl', 'next', 'params'); // 保存一下变量,方法如下
function restore(fn, obj) {
var props = new Array(arguments.length - 2); // 长度为3 的空数组
var vals = new Array(arguments.length - 2); // 长度为3 的空数组
for (var i = 0; i < props.length; i++) {
props[i] = arguments[i + 2]; // 此时 props = ['baseUrl', 'next', 'params'];
vals[i] = obj[props[i]]; // 此时 vals = [req['baseUrl'], req['next'], req['params']];
}
return function () { // 返回一个函数,调用即可恢复
// restore vals
for (var i = 0; i < props.length; i++) {
obj[props[i]] = vals[i]; // 恢复之前保存的引用
}
return fn.apply(this, arguments); // 恢复之后再调用一下 fn (out),并且参数照原样传递。
};
}
// setup next layer
req.next = next; // 保存一下 next 方法, next 方法在下面
// for options requests, respond with a default if nothing else responds
if (req.method === 'OPTIONS') {
done = wrap(done, function(old, err) {
if (err || options.length === 0) return old(err); // 假如有错,抛出err. old -> done -> out (next)
sendOptionsResponse(res, options, old); // 返回一个 options 的响应,告诉前端,我支持 post 等等方法
});
function wrap(old, fn) { // 把 old 作为 fn 的第一个参数,调用 fn
return function proxy() { // 返回一个函数
var args = new Array(arguments.length + 1); // 新建一个数组,长度等于参数长度 + 1
args[0] = old; // 把 index 为 0 的位置放置 old
for (var i = 0, len = arguments.length; i < len; i++) {
args[i + 1] = arguments[i]; // 从 1 开始报错参数传递过来的值
}
fn.apply(this, args); // 调用 fn 并且把所有的参数传给 fn
};
}
}
// setup basic req values
req.baseUrl = parentUrl; // 保存一些引用到 req 上面去。
req.originalUrl = req.originalUrl || req.url;
next(); // 执行 next 方法
function next(){.....}
}
};
2.6.1.5 next 方法
我们可以忽略掉 process_params,(嵌套太深,不易阅读,了解其功能就行,处理一些参数的) 关于 setImmediate 更多请阅读这里
function next(err) {
var layerError = err === 'route'
? null
: err; // 保存一下错误
// 移除斜线 /
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
// 保存一些改变了的参数到 req
if (removed.length !== 0) {
req.baseUrl = parentUrl;
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
}
// 假如出错就在下一次事件循环之前调用 done(next) 方法
if (layerError === 'router') {
setImmediate(done, null)
return
}
// 没有匹配到响应的
if (idx >= stack.length) {
setImmediate(done, layerError); // 下一次事件循环之前调用
return;
}
// 从 req 上面拿到 path
var path = getPathname(req);
if (path == null) {
return done(layerError); // 拿不到,抛出错误
}
// 寻找路由匹配
var layer;
var match;
var route;
while (match !== true && idx < stack.length) {
layer = stack[idx++]; // stack 是 this.stack
match = matchLayer(layer, path); // 路径与该 layer 是否匹配
route = layer.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; // 没有 route 对象,同样继续下一次循环
}
if (layerError) {
// routes do not match with a pending error
// 出错了,继续下一个循序
match = false;
continue;
}
var method = req.method; // 从 req 上拿到该请求所使用的方法
var has_method = route._handles_method(method); // 判断 route 上面是否有该方法
// build up automatic options response
if (!has_method && method === 'OPTIONS') { // 假如不存在,且是 options 方法,则添加一个 options 方法
appendMethods(options, route._options());
}
// don't even bother matching route
if (!has_method && method !== 'HEAD') { // 假如没有匹配且方法不等于 head 继续下一次循环
match = false;
continue;
}
}
// no match
if (match !== true) {
return done(layerError); // 没有匹配到
}
// store route for dispatch on change
if (route) {
req.route = route; // 假如拿到了 layer 上上面的 route,则保持到 req 对象上面去。
}
// Capture one-time layer values
// 判断是否需要对 params 进行参数合并 self.mergeParams 是 router 构造器的一个选项参数 opts.mergeParams 传递进来的
req.params = self.mergeParams
? mergeParams(layer.params, parentParams)
: layer.params;
var layerPath = layer.path;
// 调用 process_params 函数,处理当前 layer 的所有参数
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
return next(layerError || err);
}
if (route) {
return layer.handle_request(req, res, next); // 假如有 route 直接调用该 layer 处理该路由的逻辑
}
trim_prefix(layer, layerError, layerPath, path); // 假如没有 route,说明还要通过 trim_prefix 做一些处理,处理完之后,再调用 layer.handle_request
});
}
2.6.1.6 为 router 代理 route 的请求(all、get、post、options等等)方法
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;
};
});
2.6.1.7 use 方法
我们再来看一下 router 暴露出来的公共方法 use,它返回 this,也就是 router 对象,而且它也是一个中间件。
proto.use = function use(fn) {
var offset = 0;
var path = '/'; // 默认值
// default path to '/'
// disambiguate router.use([fn])
if (typeof fn !== 'function') { // 假如 fn 不是一个函数,说明 fn 传递的是 path 参数
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) { // 是一个非空数组
arg = arg[0]; // arg 保存数组第一项
}
// first arg is the path
if (typeof arg !== 'function') { // 假如 arg 还不是函数, 说明是路径
offset = 1; // use(['get','post'], function cal(){});
path = fn; // 取到 path = ['get','post']
}
}
var callbacks = flatten(slice.call(arguments, offset)); // [cal()]
if (callbacks.length === 0) {
throw new TypeError('Router.use() requires middleware functions'); // 没有回调
}
for (var i = 0; i < callbacks.length; i++) { // 循序处理 回调
var fn = callbacks[i];
if (typeof fn !== 'function') {
throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn));
}
// add the middleware
debug('use %o %s', path, fn.name || '<anonymous>')
var layer = new Layer(path, { // 为该 path 新建一个 layer 层,并且传入 fn
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined; // 设置一个 route 默认值
this.stack.push(layer); // 放到 this.stack 里面去
}
return this;
};
2.6.1.8 route 方法
同样也是新建 layer 的一种方式,不过它返回的是 route 对象。
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;
};
2.6.1.9 从 API 来看一看两种路由区别。
app.route 代理的 router.route ,以这样的形式返回 route,这样可以以链式调用的方式指定相同 path 处理不同的方法,这样我们就非常容易实现 RESTful API 的路由模式。
app.route('/book')
.get(function(req, res) {
res.send('Get a random book');
})
.post(function(req, res) {
res.send('Add a book');
})
.put(function(req, res) {
res.send('Update the book');
});
express.Router() 的返回值是 this,也是一个中间件,所以,我们可以通过 app.use 指定路由中间件,并且指定在何种 path 下才执行这些中间件。这样我们就可以把路由文件给分开。
var express = require('express');
var router = express.Router();
// 该路由使用的中间件
router.use(function timeLog(req, res, next) {
console.log('Time: ', Date.now());
next();
});
// 定义网站主页的路由
router.get('/', function(req, res) {
res.send('Birds home page');
});
// 定义 about 页面的路由
router.get('/about', function(req, res) {
res.send('About birds');
});
module.exports = router;
var birds = require('./birds');
...
app.use('/birds', birds);
2.6.2 router/layer.js
解析 path之后,调用 fn
2.6.2.2 path-to-regexp 库
这个文件依赖了一个外置库 path-to-regexp
,它的简单使用如下所示。通过这个模块可以非常方便的实现路由参数匹配的功能。
var re = pathToRegexp('/:foo/:bar', keys)
// keys = [{ name: 'foo', prefix: '/', ... }, { name: 'bar', prefix: '/', ... }]
re.exec('/test/route')
//=> ['/test/route', 'test', 'route']
2.6.2.2 构造器
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
}
把传入的路由处理函数 fn 保存到 handle 上面,顺便保存一下 fn 的名字。声明一个 params 和一个 path,通过 pathRegexp 解析 path, 存储 keys 到 this 上面去。
2.6.2.3 handle_request
handle_error 就是处理错误的方法,这个忽略掉,先看 handle_request,非常简单调用 fn 即可
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
if (fn.length > 3) {
// not a standard request handler
return next();
}
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};
2.6.2.4 match
解析 path,将得到的 params 保持到 this.params 上面去。
Layer.prototype.match = function match(path) {
var match
match = this.regexp.exec(path) // 匹配路径
this.params = {};
this.path = match[0]
var keys = this.keys;
var params = this.params;
for (var i = 1; i < match.length; i++) {
var key = keys[i - 1];
var prop = key.name;
var val = decode_param(match[i]) // 对参数进行解码 (decodeURIComponent)
if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
params[prop] = val; // this.params 上保存 match 匹配到的变量。
}
}
return true;
};
2.6.3 router/route.js
2.6.3.1 导出了什么
导出了 Route,Route 上面有3个实例属性,path 路径,stack 数组(用来保存 layer),methods 方法对象。
module.exports = Route;
function Route(path) {
this.path = path;
this.stack = [];
debug('new %o', path)
// route handlers for various http methods
this.methods = {};
}
2.6.3.2 dispatch 方法
var idx = 0;
var stack = this.stack;
if (stack.length === 0) { // 没有回调函数
return done(); // 直接跳出该路由
}
var method = req.method.toLowerCase();
if (method === 'head' && !this.methods['head']) { // 假如 method 为 head 请求,但系统不支持 head,则转换为 get 请求
method = 'get';
}
req.route = this;
next();
在 req.route 上面挂载了 this, 紧接着调用 next 方法。next 方法里面除去一些判断的逻辑,其主要逻辑是从 stack 拿出 layer 然后调用了 layer.handle_request 方法。
var layer = stack[idx++];
if (err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);
}
2.6.3.3 all 方法
把一些无关紧要的代码删除掉之后,看起来也非常简单,handles 保存了所有回调,把这些回调包装成 layer 推进 stack。
Route.prototype.all = function all() {
var handles = flatten(slice.call(arguments));
for (var i = 0; i < handles.length; i++) {
var handle = handles[i];
var layer = Layer('/', {}, handle);
this.stack.push(layer);
}
return this;
};
2.6.3.4 其他方法(get、post、delete 等等)
功能与上面的 all 方法一样,只不过是给 Route 的实例上添加的 get、 post、delete 等等方法而已。
methods.forEach(function(method){
Route.prototype[method] = function(){
var handles = flatten(slice.call(arguments));
for (var i = 0; i < handles.length; i++) {
var handle = handles[i];
var layer = Layer('/', {}, handle);
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);
}
return this;
};
});
2.6.4 总结
我们来总结一下路由的逻辑。
2.6.4.1 功能汇总
layer.js 用于解析 path 对应的 params 参数,与保存处理请求的回调 fn。
route.js 用于绑定 Http 方法与对应的 path 路径,route 里面有一个 stack 数组,保存了许多 layer 实例。通过 dispatch 可以调用 layer 的处理请求 handle_request 方法。
2.6.4.2 路由添加的区别
Router 添加路由有2种方式
- use
- route
当使用 use 的时候,会创建一个 layer,解析 path 参数与 保存 fn,并放到 router.stack 数组里面,返回一个中间件其实就是 router 本身。
当使用 route 的时候,会新建一个 route,与 layer 。这个 route 不是一个中间件。但是可以通过它的 get/post/options 等等方法,给 route.stack (这里的 stack 是 route 里面的)添加 layer。
也就是说 router.stack 是保存 layer 的,route.stack 也是保存 layer 的,那么有什么区别呢?
我们拿以下这个例子来说, app.route('/book')
会放到 router 里面去,后面的 .get
/ .post
会放到 route 里面去。
app.route('/book')
.get(function(req, res) {
res.send('Get a random book');
})
.post(function(req, res) {
res.send('Add a book');
})
.put(function(req, res) {
res.send('Update the book');
});
而在代码上的区别就是,router.js 里面的 layer 有一个 route
属性。
// router.js - route 方法
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
layer.route = route;
route.js 里面的 layer 则有一个 method 属性。
// route.js
var layer = Layer('/', {}, handle);
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);
2.6.4.4 Router 以中间件的形式是如何被用起来的呢?
可以回顾一下 2.6.1.4 小节的内容。
其实是在构造器里面调用了 router.handle,而又在 handle 方法中通过 path 去匹配 router 的 stack 里面所有的 layer(一个请求是可以匹配多个layer),命中之后再调用 process_params 处理参数,把参数添加到 req.params 上面去。只有当处理完参数之后,才在 process_params 回调里面调用 layer 的 handle_request 方法继而执行用户自定义的逻辑。
┌────────────────┐ ┌──────────────────────────────┐
│ Request │ │ Layer │
└────────────────┘ │ │
│ │ │
│ │ │
▼ │ ┌───────────────────┐ │
┌──────────────────┐ │ │fn(req, res, next) │ │
│ Router handle │ │ └───────────────────┘ │
└──────────────────┘ │ ▲ │
│ │ │ │
│ │ │ │
▼ │ │ │
┌──────────────────┐ callback │ ┌──────────────────┐ │
│ process_params │──────────────┼────▶│ handle_request │ │
└──────────────────┘ │ └──────────────────┘ │
└──────────────────────────────┘
图由 monodraw 软件所画。