koa-router源码解读
SunShinewyf opened this issue · 1 comments
继上一次的koa
源码解读,今天来聊一下koa-router
。
文件结构及内容关联
从源码文件中可以看到,koa-router
只有两个文件,layer.js
和router.js
,分别定义了Router
和Layer
两个对象。相对于express
的内置路由,koa-router
少了一个route
对象,使得逻辑更加简单,下面通过一张图来解释Router
和Layer
对象之间的关系。
正如上图显示,Router
对象中有一个stack
的成员属性,而stack
又是一个由Layer
组成的数组,这样就使两者关联起来了。两个对象之间的原型函数函数也列举出来了,比较直观和简单。
运行流程
在我们的项目路由文件中引入koa-router
的时候,如下:
const router = require('koa-router')()
然后执行定义如下路由的时候:
router.get('/', async (ctx, next) => {
await ctx.render('users/index',{
title:'用户中心'
})
})
其实首先调用的是router.js/routes
这个入口函数。下面为routes
函数的源码:
Router.prototype.routes = Router.prototype.middleware = function () {
var router = this;
var dispatch = function dispatch(ctx, next) {
debug('%s %s', ctx.method, ctx.path);
var path = router.opts.routerPath || ctx.routerPath || ctx.path;
var matched = router.match(path, ctx.method); //通过path=users/index和method=GET来判断匹配
var layerChain, layer, i;
if (ctx.matched) {
ctx.matched.push.apply(ctx.matched, matched.path);
} else {
ctx.matched = matched.path;
}
ctx.router = router;
if (!matched.route) return next();
var matchedLayers = matched.pathAndMethod
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path;
if (mostSpecificLayer.name) {
ctx._matchedRouteName = mostSpecificLayer.name;
}
layerChain = matchedLayers.reduce(function(memo, layer) {
memo.push(function(ctx, next) {
ctx.captures = layer.captures(path, ctx.captures);
ctx.params = layer.params(path, ctx.captures, ctx.params);
return next();
});
return memo.concat(layer.stack);
}, []);
return compose(layerChain)(ctx, next);
};
dispatch.router = this;
return dispatch;
};
其中定义了一个dispatch
函数用来处理路由。其中this
对象为当前文件定义的路由对象。例如w我的项目文件中定义了一个路由的user.js
文件,并且内容如下:
const router = require('koa-router')()
router.prefix('/users')
router.get('/', async (ctx, next) => {
await ctx.render('users/index',{
title:'用户中心'
})
})
router.get('/register/:id', async (ctx, next) => {
await ctx.render('users/register',{
title:'用户注册'
})
})
router.get('/login',async (ctx,next) => {
await ctx.render('users/login',{
title:'用户登录'
})
})
router.get('/setting',async (ctx,next) => {
await ctx.render('users/setting',{
title: '用户设置'
})
})
module.exports = router
那么这个router
对象结构如下:
其中method
为Router
对象构造函数中定义的:
this.methods = this.opts.methods || [
'HEAD',
'OPTIONS',
'GET',
'PUT',
'PATCH',
'POST',
'DELETE'
];
stack
数组就是user.js
中定义的四个Layer
,接下来就是进行遍历并且匹配。找到可以匹配当前path
和method
的layer
并且放到layerChain
中。最后将这个layerChain
交由compose
去处理,也就是遍历中间件。
值得一提的是,在调用compose
之前,会首先给matchedLayers
添加一个中间件,具体代码如下:
layerChain = matchedLayers.reduce(function(memo, layer) {
memo.push(function(ctx, next) {
ctx.captures = layer.captures(path, ctx.captures);
ctx.params = layer.params(path, ctx.captures, ctx.params);
return next();
});
return memo.concat(layer.stack);
}, []);
这一段代码主要是给先给中间件数组添加一个中间件函数,用于获取路由中的参数并且赋值给ctx
中的params
。此时传入compose
的middleware
数组结构如下:
其中function(ctx,next)
就是上面我们push
进去的函数,然后下面的async (ctx,next) =>{}
就是我们传入的中间件函数。
例如,对于下面这种情况:
router.get('/register/:id', async (ctx, next) => {
await ctx.render('users/register',{
title:'用户注册'
})
})
如我们访问127.0.0.1:3001/users/register/id=123
就可以得出ctx.params
为{id:123}的这个参数对象。
所以koa
中首先会给每个路由添加一个获取路由参数的中间件,然后依次处理后面自定义的中间件。
总结一下,正常的路由流程大概是如下图所示:
router.prototype.register()
代码如下:
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};
var router = this;
var stack = this.stack;
// support array of paths
if (Array.isArray(path)) {
path.forEach(function (p) {
router.register.call(router, p, methods, middleware, opts);
});
return this;
}
// create route
var route = new Layer(path, methods, middleware, {
end: opts.end === false ? opts.end : true,
name: opts.name,
sensitive: opts.sensitive || this.opts.sensitive || false,
strict: opts.strict || this.opts.strict || false,
prefix: opts.prefix || this.opts.prefix || "",
ignoreCaptures: opts.ignoreCaptures
});
if (this.opts.prefix) {
route.setPrefix(this.opts.prefix);
}
// add parameter middleware
Object.keys(this.params).forEach(function (param) {
route.param(param, this.params[param]);
}, this);
stack.push(route);
return route;
};
可以看到,这个函数还是比较简单的,首先是判断传进来的path
是否是数组,否则就递归执行自身,直到数组中的每一个值都完成注册为止。否则就创建一个Layer
对象push
进当前router
对象的
stack
数组中。从而完成注册功能。
对于router
中的其他原型函数,就不再具体分析了
上面只是个人的一些探索,若有不对的地方,欢迎私聊拍砖。