熟悉Koa的同学都知道use是用来注册中间件的方法,相比较Koa中的全局中间件,koa-router的中间件则是路由级别的。
Router.prototype.use = function () {
var router = this; var middleware = Array.prototype.slice.call(arguments); var path; // 支持多路径在于中间件可能作用于多条路由路径 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) { // 嵌套路由 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 { // 路由级别中间件 创建一个没有method的Layer实例 router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath }); } }); return this; };
koa-router中间件注册方法主要完成两项功能:
将路由嵌套结构扁平化,其中涉及到路由路径的更新和param前置处理函数的插入;
路由级别中间件通过注册一个没有method的Layer实例进行管理。
五、路由匹配
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]; if (layer.match(path)) { // 路由路径满足要求 matched.path.push(layer); if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) { // layer.methods.length === 0 该layer为路由级别中间件 // ~layer.methods.indexOf(method) 路由请求方法也被匹配 matched.pathAndMethod.push(layer); // 仅当路由路径和路由请求方法都被满足才算是路由被匹配 if (layer.methods.length) matched.route = true; } } } return matched; };
match方法主要通过layer.match方法以及methods属性对layer进行筛选,返回的matched对象包含以下几个部分:
path: 保存所有路由路径被匹配的layer;
pathAndMethod: 在路由路径被匹配的前提下,保存路由级别中间件和路由请求方法被匹配的layer;
route: 仅当存在路由路径和路由请求方法都被匹配的layer,才能算是本次路由被匹配上。
另外,在ES7之前,对于判断数组是否包含一个元素,都需要通过indexOf方法来实现, 而该方法返回元素的下标,这样就不得不通过与-1的比较得到布尔值:
if (layer.methods.indexOf(method) > -1) { ... }
而作者巧妙地利用位运算省去了“讨厌的-1”,当然在ES7中可以愉快地使用includes方法:
if (layer.methods.includes(method)) { ... }
六、路由执行流程
理解koa-router中路由的概念以及路由注册的方式,接下来就是如何作为一个中间件在koa中执行。
koa中注册koa-router中间件的方式如下:
const Koa = require('koa'); const Router = require('koa-router'); const app = new Koa(); const router = new Router(); router.get('https://www.jb51.net/', (ctx, next) => { // ctx.router available }); app .use(router.routes()) .use(router.allowedMethods());
从代码中可以看出koa-router提供了两个中间件方法:routes和allowedMethods。
1、allowedMethods()
Router.prototype.allowedMethods = function (options) { options = options || {}; var implemented = this.methods; return function allowedMethods(ctx, next) { return next().then(function() { var allowed = {}; if (!ctx.status || ctx.status === 404) { ctx.matched.forEach(function (route) { route.methods.forEach(function (method) { allowed[method] = method; }); }); var allowedArr = Object.keys(allowed); if (!~implemented.indexOf(ctx.method)) { // 服务器不支持该方法的情况 if (options.throw) { var notImplementedThrowable; if (typeof options.notImplemented === 'function') { notImplementedThrowable = options.notImplemented(); } else { notImplementedThrowable = new HttpError.NotImplemented(); } throw notImplementedThrowable; } else { // 响应 501 Not Implemented ctx.status = 501; ctx.set('Allow', allowedArr.join(', ')); } } else if (allowedArr.length) { if (ctx.method === 'OPTIONS') { // 获取服务器对该路由路径支持的方法集合 ctx.status = 200; ctx.body = ''; ctx.set('Allow', allowedArr.join(', ')); } else if (!allowed[ctx.method]) { if (options.throw) { var notAllowedThrowable; if (typeof options.methodNotAllowed === 'function') { notAllowedThrowable = options.methodNotAllowed(); } else { notAllowedThrowable = new HttpError.MethodNotAllowed(); } throw notAllowedThrowable; } else { // 响应 405 Method Not Allowed ctx.status = 405; ctx.set('Allow', allowedArr.join(', ')); } } } } }); }; };