主题
Express源码解读04 - 路由
1. 路由基本原理
Express
里面,我们通常会定义一堆路由,然后,当请求进来的时候,会去我们定义好的路由里面通过method
和path
匹配,匹配到后就执行里面的请求处理函数。
Express
的路由里面有三个构造函数,分别是Router
、Route
、Layer
,它们的实例分别是router
、route
、layer
其中:
Router
Router
里面维护了一个路由数组stack
,存放着一个个的layer
。Express
里面定义的每个路由,在router
的stack
里面都会有对应的一个layer
- 在每个
layer
里面,存放着path
(请求路径)和一个route
处理函数dispatch
,当路径匹配成功后,就会调用dispatch
进入到自己的route
里面。 - 每个
layer
上面,都有一个自己的route
Route
Route
里面也维护了一个stack
,也是存放着一个个route
的layer
Express
一个路由的处理函数可以有很多个,每一个处理函数就会对应一个layer
route
里面的layer
里面,存放的有method
(请求方法),和一个个事件处理函数handler
,调用dispatch
方法可以跳出route
直接进入router
里面的下一个layer
.
Layer
Router
和Route
里面都有Layer
,他们是同一个类,只是作用不同,源码里面只是复用了代码而已。
2. 路由懒加载
为了性能考虑,Express的路由不会在创建应用的时候就加载,而是在需要使用的时候才会加载.比如,在自己开发的项目中定义了app.get()
,app.use()
,或者,直接使用路由模块express.Router()
时候, 才会进行路由的加载.
js
app.lazyrouter = function lazyrouter() {
if (!this._router) { // 若app下还没有路由示例,就实例化一个路由并挂载到app上
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));
}
};
new Router
的过程先放一边,后面分析.
可以看到_router
是Router
的实例.并挂载到了app
上面.
3. 路由原理解析
Express
里面内置了路由模块,当请求进来时,此时内部已经定义好了各种路由,源码lib/application.js
文件里面定义了app.handle
方法,请求会首先走进这个方法,方法如下:
js
app.handle = function handle(req, res, callback) {
var router = this._router;
// 没有匹配到路由,会走done方法,返回 Connot GET /xxx
var done =
callback ||
finalhandler(req, res, {
env: this.get("env"),
onerror: logerror.bind(this),
});
if (!router) {
debug("no routes defined on app");
done();
return;
}
// 进入路由匹配和业务逻辑处理
router.handle(req, res, done);
};
源码里面有两个概念:应用、路由,请求进来会先将请求交给路由处理,若是有对应的路由则由路由处理,但是若某个路由匹配不上,会返回Connot <method> /xxxx
,这个处理逻辑是应用处理的。
那么现在先来看一下路由是如何进行匹配的。
1. 路由匹配原理
Express
里面,有路由,中间件,其中路由的使用方式可以直接通过app.get
、app.post
等方式来使用也可以通过const Router = express.Router
来通过router
的方式单独使用路由。中间件app.use
的使用方式和路由很相似,可以带上路径,当带路径时候,不区分请求方式,只要能匹配上路径的路由就能进入这个中间件。app.get
等路由方法,处理路径外,可以有很多个参数,每个参数的回调可以有第三个参数next
,那么这些API的使用在Express
里面核心原理都是Router
的原理。
例如:
js
// 1. 正常请求
app.get('/login',function(req,res) {
// do something
})
// 1.1 多个参数的路由
app.get('/login',function(req,res,next) {
// do something
next()
},function(req,res) {
// do something
})
// 2. 中间件
app.use(function(req,res,next) {
// do something
})
// 3. 路由
const router = express.Router()
router.get('/user/login',function(req,res) {
// do something
})
源码lib/application.js
里面,通过methods
模块,在app
上循环定义了各种请求方法对应的匹配逻辑。
js
methods.forEach(function (method) {
app[method] = function (path) {
// 这里是用来获取app上面的一些属性值的逻辑,与路由匹配无关
if (method === "get" && arguments.length === 1) {
// app.get(setting)
return this.set(path);
}
this.lazyrouter();
var route = this._router.route(path); // 用path在内部实例化外层layer和route
route[method].apply(route, slice.call(arguments, 1)); // 进入route匹配method
return this;
};
});
可以看到,首先会根据请求路径得到route
,从上面可以知道,route
是Route
的实例,因此可以知道。我们定义的每一个路由,其实都有于之对应的route
,并且是根据path
来进行区分的,所以路由定义时候,若多个路由相同的情况下,定义到上面的路由会覆盖下面的路由。
下面来看一下_router
里面的逻辑
源码lib/router/index.js
里面,是定义路由router
入口的文件:
js
// proto是Router.prototype 项目内定义的每一个路由都会走一次这个方法
proto.route = function route(path) {
var route = new Route(path); // 每一个真是的路由都会有一个自己的 route
var layer = new Layer(path, { // 每一个路由都会有一个layer
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
layer.route = route; // layer上面有route
// this 代表 router
this.stack.push(layer); // Express路由模块维护一个stack,也就是路由数组,数组内存放的是一个个的layer
return route; // 最终返回得到route
};
上面的代码是var route = this._router.route(path)
这行代码的内部逻辑。可以看到,面在Router
原型上面有一个route
方法,这个方法会得到一个Route
的实例route
,并将其返回,另外,还实例化了一个Layer
得到layer
,并且每个路由都会对应一个layer
,Express
路由是放到一个数组stack
内进行维护的,数组内的每一项是一个个的layer
。
既然数组内的每一项是一个个的layer
,那么我们来看一下router
数组里面的layer
里面到底有什么,还有route.dispatch
这个方法具体逻辑。
1. layer
里面有什么?
看一下layer
实例化时候传入的参数,有path
、{sensitive: this.caseSensitive, strict: this.strict, end: true}
、 route.dispatch.bind(route)
这是哪个参数。
看一下layer
内部,位置lib/router/layer.js
:
js
function Layer(path, options, fn) {
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
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
内部的逻辑其实就是在layer上有path
属性,layer
原型上还有一个match
方法,用来匹配路径的,里面处理了路径上带参数的情况,但是你最终逻辑最后都是返回true
或false
,也就是路径是否匹配成功;
js
Layer.prototype.match = function match(path) {
var match
if (path != null) {
// fast path non-ending match for / (any path matches)
if (this.regexp.fast_slash) {
this.params = {}
this.path = ''
return true
}
// fast path for * (everything matched in a param)
if (this.regexp.fast_star) {
this.params = {'0': decode_param(path)}
this.path = path
return true
}
// match the path
match = this.regexp.exec(path)
}
if (!match) {
this.params = undefined;
this.path = undefined;
return false;
}
// store values
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])
if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
params[prop] = val;
}
}
return true;
};
当然,只有外层的layer
,也就是router
数组里面的layer
才会匹配路径。内层的layer
是不会匹配路径的,因此,match
方法只有外层会用到。
2. route.dispatch
具体逻辑
在layer
里面,只有当请求处理函数的参数不等于4个或者小于3个的时候,才会调用route.dispatch
方法,并且传入的参数会根据当前的处理函数类型而定。那么可以知道,layer
里面的这两个方法的逻辑定是为内层layer
做准备的,因为,外层layer
是没有存请求处理函数的,存的只是path
和route
。
js
// 错误处理函数
Layer.prototype.handle_error = function handle_error(error, req, res, next) {
var fn = this.handle; // route.dispatch
if (fn.length !== 4) {
// not a standard error handler
return next(error);
}
try {
fn(error, req, res, next);
} catch (err) {
next(err);
}
};
// 正常请求处理函数
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle; // route.dispatch
if (fn.length > 3) {
// not a standard request handler
return next();
}
try {
fn(req, res, next); // route.dispatch
} catch (err) {
next(err);
}
};
route.dispatch
逻辑在源码lib/router/route.js
里面:
js
Route.prototype.dispatch = function dispatch(req, res, done) {
var idx = 0; // 用于标识当前是第几个layer
var stack = this.stack;
var sync = 0
if (stack.length === 0) { // 没有定义路由,直接跳出router,由应用返回 Cannot GET /
return done();
}
var method = req.method.toLowerCase(); // http请求方式默认是大写的,先转为小写,方便后续比较
if (method === 'head' && !this.methods['head']) {
method = 'get';
}
req.route = this;
next();
function next(err) {
// signal to exit route
if (err && err === 'route') return done();
// signal to exit router
if (err && err === 'router') return done(err)
// max sync stack
if (++sync > 100) return setImmediate(next, err) // 递归的数量大于100此都没匹配上
var layer = stack[idx++] // 当前的layer
// end of layers
if (!layer) return done(err) // 匹配完都没匹配到,就跳出路由
if (layer.method && layer.method !== method) { // 根据每个路由的请求方法匹配,
next(err) // 方法没匹配到,就继续递归匹配下一个layer
} else if (err) {
layer.handle_error(err, req, res, next);// 匹配到了但属于错误处理的
} else {
layer.handle_request(req, res, next); // 匹配成功,且属于正常请求
}
sync = 0 // 匹配结束后,将sync置为初始值
}
};
3. 问:什么时候调用的route.dispatch
?
我们来捋一捋,请求进来时候,先走的lib/application.js
里面的app.handle
方法:
js
app.handle = function handle(req, res, callback) {
var router = this._router;
// 用于返回 Connot GET /xxx
var done = callback || finalhandler(req, res, {
env: this.get("env"),
onerror: logerror.bind(this),
});
if (!router) return done(); // 没有定义路由,返回找不到路径 Connot GET /xxx
router.handle(req, res, done); // Router.prototype.handler
};
然后调用router.handle
方法,router.handle
方法内核心逻辑,就是匹配router
的stack
内的每一个layer
的path
,匹配上后,就会调用这个layer
对应route
的dispatch
方法进入到route
内部,router.handle
源码如下:
js
proto.handle = function handle(req, res, out) { // 这里的ourt其实就是application里面的done
var self = this;
var idx = 0; // 用于记录当前是第几个layer(外层)
var protohost = getProtohost(req.url) || ''
var removed = '';
var slashAdded = false;
var sync = 0
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;
var parentUrl = req.baseUrl || '';
var done = restore(out, req, 'baseUrl', 'next', 'params');
// setup next layer
req.next = next;
// for options requests, respond with a default if nothing else responds
if (req.method === 'OPTIONS') { // 处理 options请求
done = wrap(done, function(old, err) {
if (err || options.length === 0) return old(err);
sendOptionsResponse(res, options, old);
});
}
// setup basic req values
req.baseUrl = parentUrl;
req.originalUrl = req.originalUrl || req.url;
next();
function next(err) {
var layerError = err === 'route'
? null
: err;
// remove added slash
if (slashAdded) {
req.url = req.url.slice(1)
slashAdded = false;
}
// restore altered req.url
if (removed.length !== 0) {
req.baseUrl = parentUrl;
req.url = protohost + removed + req.url.slice(protohost.length)
removed = '';
}
// signal to exit router
if (layerError === 'router') return setImmediate(done, null);
// no more matching layers
if (idx >= stack.length) return setImmediate(done, layerError);
// max sync stack
if (++sync > 100) return setImmediate(next, err);
// 拿到请求路径
var path = getPathname(req);
if (path == null) return done(layerError);
// 找到下一个匹配的layer
var layer, match, route;
// 当没匹配到路径并且还没到最后一个layer,则继续匹配
while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path); // 用路径匹配layer 要么得到boolean,要么得到error
route = layer.route;
if (typeof match !== 'boolean') { // 得到的路径匹配结果不是true false,就是用路径匹配layer时候抛错了,此时match是错误
layerError = layerError || match;
}
// 用path匹配layer出错时,开始下一轮循环匹配
if (match !== true) continue;
// layer上没有route,开始下一轮循环(正常处理非路由处理程序)
if (!route) 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); // 判断route里面是否有对应请求方式,返回boolean
// 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;
}
}
// no match
if (match !== true) return done(layerError);
// store route for dispatch on change
if (route) req.route = route;
// Capture one-time layer values
req.params = self.mergeParams
? mergeParams(layer.params, parentParams)
: layer.params;
var layerPath = layer.path;
// this should be done for the layer
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
next(layerError || err)
} else if (route) {
layer.handle_request(req, res, next)
} else {
trim_prefix(layer, layerError, layerPath, path)
}
sync = 0
});
}
function trim_prefix(layer, layerError, layerPath, path) {
if (layerPath.length !== 0) {
// Validate path is a prefix match
if (layerPath !== path.slice(0, layerPath.length)) {
next(layerError)
return
}
// Validate path breaks on a path separator
var c = path[layerPath.length]
if (c && c !== '/' && c !== '.') return next(layerError)
// Trim off the part of the url that matches the route
// middleware (.use stuff) needs to have the path stripped
debug('trim prefix (%s) from url %s', layerPath, req.url);
removed = layerPath;
req.url = protohost + req.url.slice(protohost.length + removed.length)
// Ensure leading slash
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
// Setup base URL (no trailing slash)
req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
? removed.substring(0, removed.length - 1)
: removed);
}
debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
};