版本7.4.0
比较长,建议一边看一边打断点,光看一遍估计是不知道在说什么的,因为它的思路不是面向过程的那种一条线,而是互相穿插,绕来绕去的。
koa-router的核心其实是path-to-regexp这个库,不过koa-router做了非常多的参数处理和封装,主要提供的功能有:基础的rest方法的注册、嵌套路由、路由中间层、参数处理等,我们一个一个来看。
先来看看koa-router的基本使用
var Koa = require('koa');
var Router = require('koa-router');
var app = new Koa();
var router = new Router();
router.get('/a', (ctx, next) => {
console.log(ctx.router)
next();
ctx.body = 'aaa';
}, (ctx, next) => {
console.log('a2')
});
// 命名路由
router.get('b', '/b', (ctx, next) => {
console.log(ctx.router)
ctx.body = 'bbb';
});
app
.use(router.routes());
app.listen(3000);
这里比较关键的步骤如下:
1.实例化了Router
2.调用实例的get方法,定义了两个路由a和b
3.调用实例的routes方法,将返回注册为应用的中间件
我们要说的,除了上述几个点以外,还有register、use、param函数,嵌套的路由如何实现,路由的前缀形式等
我们按顺序来看,首先是构造函数,构造函数没干啥,初始化了opts、methods、params、stack几个变量,紧跟着通过methods.forEach在prototype上定义了get、post等方法,methods的列表可以自己看一下,在这里https://github.com/jshttp/methods/blob/master/index.js。这些方法进行了参数处理后直接调用了register函数
function Router(opts) {
if (!(this instanceof Router)) {
return new Router(opts);
}
this.opts = opts || {};
// this.methods只在allowedMethods里面用到了,如果不提供methods参数,下面这些就是默认实现的方法
this.methods = this.opts.methods || [
'HEAD',
'OPTIONS',
'GET',
'PUT',
'PATCH',
'POST',
'DELETE'
];
// params保存参数处理相关内容,stack保存Layer实例
this.params = {};
this.stack = [];
};
// 这个methods和上面的this.methods要区分一下
methods.forEach(function (method) {
Router.prototype[method] = function (name, path, middleware) {
var middleware;
if (typeof path === 'string' || path instanceof RegExp) {
// 注意这两个slice将middleware变成了数组,注册的时候可以传多个方法
// 可以看看上面第一个例子里的/a路径注册的处理函数
middleware = Array.prototype.slice.call(arguments, 2);
} else {
middleware = Array.prototype.slice.call(arguments, 1);
path = name;
name = null;
}
// 调用了register方法
this.register(path, [method], middleware, {
name: name
});
// 这一句保证了链式调用
return this;
};
});
在看register方法的代码前我们来看一看register方法的简单使用,注意register支持传入数组形式的path,而第二个参数methods只能是数组形式,
router.register(['/c', '/cc'], ['GET', 'POST'], (ctx, next) => {
console.log(ctx.router)
ctx.body = 'register';
});
register方法代码如下,比较重要的几个地方,一是通过path.forEach将传入的数组形式的路径,分别单独调用register,二是通过Layer构造了一个route对象,并且push到stack里,三是对param的处理
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是数组,每个路径单独调用register方法
path.forEach(function (p) {
router.register.call(router, p, methods, middleware, opts);
});
return this;
}
// 调用Layer的构造函数
// 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) {
// 处理实例化的时候传入的prefix选项,参见下文的嵌套路由部分,setPrefix是Layer的函数
route.setPrefix(this.opts.prefix);
}
// add parameter middleware
// 对param的处理
Object.keys(this.params).forEach(function (param) {
route.param(param, this.params[param]);
}, this);
// 所有注册的路由全都存在stack里面,每个path作为一个元素,一个path可能有多个methods和多个处理函数
stack.push(route);
return route;
};
因为register里面有用到,所以我们来看一看Layer的构造函数,注意这里的this.methods和this.stack和前面不一样了,是Layer自己定义的
function Layer(path, methods, middleware, opts) {
this.opts = opts || {};
this.name = this.opts.name || null;
this.methods = [];
this.paramNames = [];
this.stack = Array.isArray(middleware) ? middleware : [middleware];
// register的methods参数必须是数组是因为这里直接拿来就forEach
methods.forEach(function(method) {
var m = this.methods.push(method.toUpperCase());
// 如果注册了一个get请求,那么添加一个对应的head请求的处理
if (this.methods[m-1] === 'GET') {
this.methods.unshift('HEAD');
}
}, this);
// ensure middleware is a function
this.stack.forEach(function(fn) {
var type = (typeof fn);
if (type !== 'function') {
throw new Error(
methods.toString() + " `" + (this.opts.name || path) +"`: `middleware` "
+ "must be a function, not `" + type + "`"
);
}
}, this);
this.path = path;
// 生成路径匹配用的正则表达式
// paramNames是pathToRegExp来赋值的,如果路径是/foo/:bar这样的,paramNames里面就会有值
// 如果是/foo/bar,paramNames就是空数组
this.regexp = pathToRegExp(path, this.paramNames, this.opts);
debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
};
实际上Layer返回的route是这样么一个对象(下面是以本文第一个例子里的b的那个路由为例得到的值),其中opts是一些pathToRegExp接受的选项,regex是pathToRegExp的返回值,stack是我们注册路由的处理函数,建议自己打断点看一下
{
methods: ['HEAD', 'GET'],
name: null,
opts: Object
paramNames: [],
path: '/b',
regex: Object,
stack: [middleware]
}
到这里为止关于register的函数我们看完了,可能贴的代码有点多不方便看,但是最核心的部分其实只有一个pathToRegExp,另外都是一些参数的处理。
回看一眼本文开头的例子,所有路由注册完毕以后,我们会调用routes方法,除去allowedMethods,实际上看完routes方法我们就算完成了一个简单的流程。
在看routes方法之前,我们可以确定的是,routes方法执行后返回的一定是koa中间件的格式,也就是一个函数,并且有ctx和next两个参数。
routes方法的代码如下,核心就是dispatch函数,我们的所有路由都会经过这个函数,里面调用了Router.prototype.match,用我们stack里面的全部middleware的regexp去匹配请求路径,匹配成功的middleware的处理函数先转成数组,再通过compose转换成单个函数执行。这里还是比较绕的,建议自己打断点看一下每一步的结果。
// middleware和routes是一样的
Router.prototype.routes = Router.prototype.middleware = function () {
var router = this;
// 最终的处理路由的地方就是这个函数
var dispatch = function dispatch(ctx, next) {
debug('%s %s', ctx.method, ctx.path);
// ctx.path是koa赋值的,ctx.routerPath不知道是哪里来的
var path = router.opts.routerPath || ctx.routerPath || ctx.path;
// 调用了match方法,根据this.stack里面注册的路由生成的正则,一个一个匹配,
// 返回是否匹配成功和匹配到的Layer实例
var matched = router.match(path, ctx.method);
var layerChain, layer, i;
if (ctx.matched) {
ctx.matched.push.apply(ctx.matched, matched.path);
} else {
ctx.matched = matched.path;
}
// 在处理函数里可以获取ctx.router
ctx.router = router;
// route标志不为true就执行下一个中间件,没有匹配成功或者是use方法注册的路由都会导致route标志为false
if (!matched.route) return next();
// pathAndMethod是我们注册的middleware,
// 往ctx里放两个值_matchedRoute和_matchedRouteName
var matchedLayers = matched.pathAndMethod
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path;
if (mostSpecificLayer.name) {
ctx._matchedRouteName = mostSpecificLayer.name;
}
// 匹配成功的layer,拼成一个数组
layerChain = matchedLayers.reduce(function(memo, layer) {
memo.push(function(ctx, next) {
ctx.captures = layer.captures(path, ctx.captures);
// 对param的处理
ctx.params = layer.params(path, ctx.captures, ctx.params);
ctx.routerName = layer.name;
return next();
});
return memo.concat(layer.stack);
}, []);
// 通过compose将数组形式的layers变成一个嵌套的函数并执行,这里return的是compose后执行的结果
// 是一个Promise.resolve或者Promise.reject
return compose(layerChain)(ctx, next);
};
dispatch.router = this;
return dispatch;
};
这里还是要贴一下match的代码,因为下面讲 match的时候发现有挺多细节和match相关
Router.prototype.match = function (path, method) {
var layers = this.stack;
var layer;
var matched = {
path: [],
pathAndMethod: [],
route: false
};
for (var len = layers.length, i = 0; i < len; i++) {
layer = layers[i];
debug('test %s %s', layer.path, layer.regexp);
if (layer.match(path)) {
matched.path.push(layer);
if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
matched.pathAndMethod.push(layer);
// 只有当注册的methods不为空,才会将route标志位设为true,这个标志位在上面的routes方法里有用到,
// 如果只使用use方法来注册某个路由,这个判断就不会通过,导致注册的中间件不会执行
// 但是如果使用verb注册过某个路由,use也注册了这个路由或者use注册了一个不带路由的中间件,use注册的中间件就会执行
if (layer.methods.length) matched.route = true;
}
}
}
return matched;
};
这样基本的流程我们就说完了,基本流程用一个图表示就是:verb注册 => register => 调用routes()
接下来我们说些进阶的使用和实现
嵌套路由应该还是比较常用的,我们先来看一个例子
var test = new Router({
// 注意这里
prefix: '/test'
});
var forums = new Router();
test.get('/test', (ctx, next) => {
console.log('test');
ctx.body = 'test';
});
// 注意这里
test.prefix('/prefix');
// 注意这里
forums.use('/forums/:fid?', test.routes(), test.allowedMethods());
app.use(forums.routes());
app.listen(3001);
这个例子里,要注意三个地方,第一个是test在实例化的时候,传入了prefix前缀,第二个是test调用了prefix方法,为自己注册的router添加了prefix这个前缀,第三个是forums,它调用了use方法,传入了test定义的router,对/forums/123/prefix/test/test
和/forums/prefix/test/test
这样的路径会有响应。
实例化时传入的参数,是在register函数里处理的,调用了Layer的setPrefix方法,可以回看上文的register那一节;prefix方法也是调用了Layer的setPrefix方法,其实还是pathToRegExp帮忙处理的,代码如下
Layer.prototype.setPrefix = function (prefix) {
if (this.path) {
this.path = prefix + this.path;
this.paramNames = [];
this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
}
return this;
};
然后我们来看看use方法
除了在嵌套路由的时候用到,use还可以为路由添加类似中间层的处理,是比较重要的一个函数,但是也有一些坑,use的用法我所知道的有这么几种,
// 直接传处理函数,只有当前这个router实例中的某个路径匹配成功,才会执行处理函数
router.use((ctx, next)=>{
...
})
// 传路径加处理函数,路径可以是字符串或者是数组
router.use(['/users', '/id'], (ctx, next)=>{
...
})
// 传一个其他router实例的中间件,注意这个例子最终的效果不是分别添加了/users和/id前缀,而是添加了/id/users前缀,还将中间件注册了两次
// 原因是use操作的是routes函数返回的结果,也就是dispatch函数,而dispatch和router实例是绑定的
router.use(['/users', '/id'], otherRouter.routes())
// 上面提到的问题也会导致下面这个例子和想象的结果不太一样,会发现无法响应other路径,查看middleware会发现/prefix/other路径的中间件注册了两次
otherRouter.get('/other', (ctx, next)=>{
...
})
app.use(otherRouter.routes());
router.use('/prefix', otherRouter.routes());
app.use(router.routes());
use的代码如下,上面例子里关于dispatch和router绑定的问题有点难讲清楚,大概和复杂类型的变量,赋值给了另一个变量后进行操作,发现原来的变量也变了差不多的意思
Router.prototype.use = function () {
var router = this;
var middleware = Array.prototype.slice.call(arguments);
var path;
// 处理第一个参数是路径数组的情况,和register一样如果是数组的话每个元素单独调用一回
// support array of paths
if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
middleware[0].forEach(function (p) {
router.use.apply(router, [p].concat(middleware.slice(1)));
});
return this;
}
// 处理第一个参数是路径的情况
var hasPath = typeof middleware[0] === 'string';
if (hasPath) {
path = middleware.shift();
}
middleware.forEach(function (m) {
// 如果有router,其实就是已经调用过routes方法,如果没有,就调用register方法
if (m.router) {
// 处理前缀
m.router.stack.forEach(function (nestedLayer) {
if (path) nestedLayer.setPrefix(path);
if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix);
router.stack.push(nestedLayer);
});
// 对param的处理
if (router.params) {
Object.keys(router.params).forEach(function (key) {
m.router.param(key, router.params[key]);
});
}
} else {
// register的定义function (path, methods, middleware, opts),方便对比,methods传的是空数组
router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
}
});
return this;
};
在前面的代码中我们已经看到过很多次param和params这样的变量或者函数,这几个变量和函数是为了处理路径里的变量,比如下面例子里的user,这种方式提供了可以抽离的参数校验功能
router
.param('user', (id, ctx, next) => {
ctx.user = users[id];
if (!ctx.user) return ctx.status = 404;
return next();
})
.get('/users/:user', ctx => {
ctx.body = ctx.user;
})
.get('/users/:user/friends', ctx => {
return ctx.user.getFriends().then(function(friends) {
ctx.body = friends;
});
})
param函数往params变量中添加处理函数,register和use函数中都循环调用了Layer的param函数,routes函数中调用了Layer实例的params方法,这一块说实话还没怎么看明白,也比较细节,看到这里基本上使用过程中发现什么问题可以找得到是哪里,所以就不深入了(),比较重要的应该只有Layer.prototype.param。
这篇文章没想到会写这么长,也没想到koa-router有那么多细节,感觉80%的代码都贴上来了,这里还想讲一个函数,最后一个,allowedMethods,先看例子
router
.get('a', '/a', (ctx, next) => {
console.log('a1');
next();
}, (ctx, next) => {
console.log('a2');
next();
});
app.use(router.routes());
app.use(router.allowedMethods());
要注意的是,如果中间件的next链断掉了,上面例子中的某一个next没有写,都会造成allowedMethods不会执行,因为allowedMethods的实现里有一句next().then()
,另外的部分都还好理解,就不贴了
终于写完了,下一篇应该会写body吧,敬请期待