接着koa源码解读 一文中的末尾接着唠嗑 koa-router。
链式调用
在 koa 中,对中间件的使用是支持链接调用的。同样,
对于多个路径的请求,koa-router 也支持链式调用:
router .get('/', function *(next) { this.body = 'Hello World!'; }) .post('/users', function *(next) { // ... }) .put('/users/:id', function *(next) { // ... }) .del('/users/:id', function *(next) { // ... });
因为每个动词方法都会返回router本身:
methods.forEach(function (method) { Router.prototype[method] = function (name, path, middleware) { var middleware; if (typeof path === 'string' || path instanceof RegExp) { middleware = Array.prototype.slice.call(arguments, 2); } else { middleware = Array.prototype.slice.call(arguments, 1); path = name; name = null; } this.register(path, [method], middleware, { name: name }); return this; }; });
路由实现
Node 本身提供了数十个 HTTP 请求动词,koa-router 只是实现了部分常用的:
function Router(opts) { if (!(this instanceof Router)) { return new Router(opts); } this.opts = opts || {}; this.methods = this.opts.methods || [ 'HEAD', 'OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE' ]; //省略 };
这些请求动词的实现是通过第三方模块 methods
支持的,然后 koa-router 内部进行了注册处理:
methods.forEach(function (method) { Router.prototype[method] = function (name, path, middleware) { //见上述代码 this.register(path, [method], middleware, { name: name }); return this; }; });
this.register
接受请求路径,方法,中间件作为参数,返回已经注册的路由:
Router.prototype.register = function (path, methods, middleware, opts) { opts = opts || {}; var stack = this.stack; // create route var route = new Layer(path, methods, middleware, { //Layer是具体实现,包括匹配、中间件处理等 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 || "", }); //other code return route; };
由上述代码可知,koa-router 是支持中间件来处理路由的:
myRouter.use(function* (next) { console.log('aaaaaa'); yield next; }); myRouter.use(function* (next) { console.log('bbbbbb'); yield next; }); myRouter.get('/', function *(next) { console.log('ccccccc'); this.response.body = 'Hello World!'; }); myRouter.get('/test', function *(next) { console.log('dddddd'); this.response.body = 'test router middleware'; });
通过 router.use
来注册中间件,中间件会按照顺序执行,并会在匹配的路由的回调之前调用:
对于不匹配的路由则不会调用。同时,如果注册的路由少了 yield next
, 则之后的中间件以及被匹配的路由的回调就不会被调用;路由的中间件也是支持链接调用的:
Router.prototype.use = function () { var router = this; //other code return this; };
中间件也支持特定路由和数组路由:
// session middleware will run before authorize router .use(session()) .use(authorize()); // use middleware only with given path router.use('/users', userAuth()); // or with an array of paths router.use(['/users', '/admin'], userAuth());
从上述分析可知,对于同一个路由,能用多个中间件处理:
router.get( '/users/:id', function (ctx, next) { return User.findOne(ctx.params.id).then(function(user) { ctx.user = user; return next(); }); }, function (ctx) { console.log(ctx.user); // => { id: 17, name: "Alex" } } );
这样的写法看起来会更紧凑。
路由前缀
Koa-router允许为路径统一添加前缀:
var myRouter = new Router({ prefix: '/koa' }); // 等同于"/koa" myRouter.get('/', function* () { this.response.body = 'koa router'; }); // 等同于"/koa/:id" myRouter.get('/:id', function* () { this.response.body = 'koa router-1'; });
也可以在路由初始化后设置统一的前缀,koa-router 提供了 prefix
方法:
Router.prototype.prefix = function (prefix) { prefix = prefix.replace(///$/, ''); this.opts.prefix = prefix; this.stack.forEach(function (route) { route.setPrefix(prefix); }); return this; };
所以,以下代码是和上述等价的:
var myRouter = new Router(); myRouter.prefix('/koa'); // 等同于"/koa" myRouter.get('/', function* () { this.response.body = 'koa router'; }); // 等同于"/koa/:id" myRouter.get('/:id', function* () { this.response.body = 'koa router-1'; });
参数处理和重定向
路径的参数通过 this.params
属性获取,该属性返回一个对象,所有路径参数都是该对象的成员:
// 访问 /programming/how-to-node router.get('/:category/:title', function *(next) { console.log(this.params); // => { category: 'programming', title: 'how-to-node' } });
param
方法可以对参数设置条件,可用于常规验证和自动加载的验证:
router .get('/users/:user', function *(next) { this.body = this.user; }) .param('user', function *(id, next) { var users = [ '0号用户', '1号用户', '2号用户']; this.user = users[id]; if (!this.user) return this.status = 404; yield next; })
param
接受两个参数:路由的参数和处理参数的中间件:
Router.prototype.param = function (param, middleware) { this.params[param] = middleware; this.stack.forEach(function (route) { route.param(param, middleware); }); return this; };
如果 /users/:user 的参数 user 对应的不是有效用户(比如访问 /users/3),param 方法注册的中间件会查到,就会返回404错误。
也可以将参数验证不通过的路由通过 redirect
重定向到另一个路径,并返回301状态码:
router.redirect('/login', 'sign-in'); // 等同于 router.all('/login', function *() { this.redirect('/sign-in'); this.status = 301; });
all
是一个私有方法,会处理某路由的所有的动词请求,相当于一个中间件。如果在 all
之前或者之后出现了处理同一个路由的动词方法,则要调用 yield next
,否则另一个就不会执行:
myRouter.get('/login',function* (next) { this.body = 'login'; // 没有yield next,all不会执行 yield next; }).get('/sign',function* () { this.body = 'sign'; }).all('/login',function* () { console.log('login'); }); myRouter.get('/sign2',function* () { this.body = 'sign'; }).all('/login2',function* () { console.log('login2'); //没有yield next,get不会执行 yield next; }).get('/login2',function* (next) { this.body = 'login'; });
redirect
方法的第一个参数是请求来源,第二个参数是目的地,两者都可以用路径模式的别名代替,还有第三个参数是状态码,默认是 301:
Router.prototype.redirect = function (source, destination, code) { // lookup source route by name if (source[0] !== '/') { source = this.url(source); } // lookup destination route by name if (destination[0] !== '/') { destination = this.url(destination); } return this.all(source, function *() { this.redirect(destination); this.status = code || 301; }); };
命名路由和嵌套路由
对于非常复杂的路由,koa-router 支持给复杂的路径模式起别名。别名作为第一个参数传递给动词方法:
router.get('user', '/users/:id', function *(next) { // ... });
然后可以通过 url
实例方法来生成路由:
router.url('user', 3); // => "/users/3" //等价于 router.url('user', { id: 3 }); //=> 'users/3'
该方法接收两个参数:路由别名和参数对象:
Router.prototype.url = function (name, params) { var route = this.route(name); if (route) { var args = Array.prototype.slice.call(arguments, 1); return route.url.apply(route, args); } return new Error("No route found for name: " + name); };
第一个参数用于 route
方式查找匹配的别名,找到则返回 true
,否则返回 false
:
Router.prototype.route = function (name) { var routes = this.stack; //路由别名 for (var len = routes.length, i=0; i<len; i++) { if (routes[i].name && routes[i].name === name) { return routes[i]; } } return false; };
除了实例方法 url
外,koa-router 还提供一个静态的方法 url
生成路由:
var url = Router.url('/users/:id', {id: 1}); // => "/users/1"
第一个参数是路径模式,第二个参数是参数对象。
除了给路由命名,koa-router 还支持路由嵌套处理:
var forums = new Router(); var posts = new Router(); posts.get('/', function (ctx, next) {...}); posts.get('/:pid', function (ctx, next) {...}); forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods()); // responds to "/forums/123/posts" and "/forums/123/posts/123" app.use(forums.routes());