koa-router源码学习小结

koa 框架一直都保持着简洁性,它只对 node 的 HTTP 模块进行了封装,而在真正实际使用,我们还需要更多地像路由这样的模块来构建我们的应用,而 koa-router 是常用的 koa 的路由库. 这里通过解析 koa-router 的源码来达到深入学习的目的.

源码架构图

调用链路-routes()

HTTP请求调用流程

Usage

const app = new Koa();
const router = new Router();

router.get('/',async (ctx,next) => {
console.log('index');
ctx.body = 'index';
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000);

Router

rush:js;"> 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'
];

// 存放router.param方法指定的参数的中间件
this.params = {};
// 存放layer实例
this.stack = [];
};

Layer

{ name: str,prefix: '/' ....} this.paramNames = []; // 存放该路由的中间件 this.stack = Array.isArray(middleware) ? middleware : [middleware];

methods.forEach(function(method) {
var l = this.methods.push(method.toupperCase());
// 如果支持get请求,一并支持head请求
if (this.methods[l-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;
// 将路由转为正则表达式
this.regexp = pathToRegExp(path,this.paramNames,this.opts);

debug('defined route %s %s',this.methods,this.opts.prefix + this.path);
};

给Router实例挂载HTTP方法

{ * ctx.body = 'Hello World!'; * }) * .post('/users',next) => { * // ... * }) * .put('/users/:id',next) => { * // ... * }) * .del('/users/:id',next) => { * // ... * }) * .all('/users/:id',next) => { * // ... * }); * ``` * * When a route is matched,its path is available at `ctx._matchedRoute` and if named,* the name is available at `ctx._matchedRouteName` * * Route paths will be translated to regular expressions using * [path-to-regexp](https://github.com/pillarjs/path-to-regexp). * * Query strings will not be considered when matching requests. * * #### Named routes * * Routes can optionally have names. This allows generation of URLs and easy * renaming of URLs during development. * * ```javascript * router.get('user','/users/:id',next) => { * // ... * }); * * router.url('user',3); * // => "/users/3" * ``` * * #### Multiple middleware * * Multiple middleware may be given: * * ```javascript * router.get( * '/users/:id',* (ctx,next) => { * return User.findOne(ctx.params.id).then(function(user) { * ctx.user = user; * next(); * }); * },* ctx => { * console.log(ctx.user); * // => { id: 17,name: "Alex" } * } * ); * ``` * * ### nested routers * * nesting routers is supported: * * ```javascript * var forums = new Router(); * var posts = new Router(); * * posts.get('/',next) => {...}); * posts.get('/:pid',next) => {...}); * forums.use('/forums/:fid/posts',posts.routes(),posts.allowedMethods()); * * // responds to "/forums/123/posts" and "/forums/123/posts/123" * app.use(forums.routes()); * ``` * * #### Router prefixes * * Route paths can be prefixed at the router level: * * ```javascript * var router = new Router({ * prefix: '/users' * }); * * router.get('/',...); // responds to "/users" * router.get('/:id',...); // responds to "/users/:id" * ``` * * #### URL parameters * * Named route parameters are captured and added to `ctx.params`. * * ```javascript * router.get('/:category/:title',next) => { * console.log(ctx.params); * // => { category: 'programming',title: 'how-to-node' } * }); * ``` * * The [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module is * used to convert paths to regular expressions. * * @name get|put|post|patch|delete|del * @memberof module:koa-router.prototype * @param {String} path * @param {Function=} middleware route middleware(s) * @param {Function} callback route callback * @returns {Router} */ var methods = require('methods');

methods.forEach(function (method) {
Router.prototype[method] = function (name,path,middleware) {
var middleware;

// 如果指定了路由name<a href="https://www.jb51.cc/tag/shuxing/" target="_blank" class="keywords">属性</a>

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;
}

// 路由<a href="https://www.jb51.cc/tag/zhuce/" target="_blank" class="keywords">注册</a>

this.register(path,[method],{
name: name
});

return this;
};
});

Router.prototype.register

rush:js;"> /** * Create and register a route. * * @param {String} path Path string. * @param {Array.} methods Array of HTTP verbs. * @param {Function} middleware Multiple middleware also accepted. * @returns {Layer} * @private */ Router.prototype.register = function (path,opts) { opts = opts || {};

var router = this;
// layer实例数组,初始为空数组
var stack = this.stack;

// support array of paths
if (Array.isArray(path)) {
// 如果是多路径,递归注册路由
path.forEach(function (p) {
router.register.call(router,p,opts);
});

return this;
}

// create route
var route = new Layer(path,{
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) {
// 将router中this.params维护的参数中间件挂载到layer实例中
route.param(param,this.params[param]);
},this);

// 所有layer实例存放在router的stack属性
stack.push(route);

return route;
};

Router.prototype.match

rush:js;"> /** * Match given `path` and return corresponding routes. * * @param {String} path * @param {String} method * @returns {Object.} returns layers that matched path and * path and method. * @private */ Router.prototype.match = function (path,method) { // layer实例组成的数组 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);

// 1.匹配路由

if (layer.match(path)) {
matched.path.push(layer);

  // 2.匹配http请求<a href="https://www.jb51.cc/tag/fangfa/" target="_blank" class="keywords">方法</a>

if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
matched.pathAndMethod.push(layer);
// 3.指定了http请求方法,判定为路由匹配成功
if (layer.methods.length) matched.route = true;
}
}
}

return matched;
};

Router.prototype.routes

rush:js;"> /** * Returns router middleware which dispatches a route matching the request. * * @returns {Function} */ 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);
var layerChain,layer,i;

if (ctx.matched) {
ctx.matched.push.apply(ctx.matched,matched.path);
} else {
ctx.matched = matched.path;
}

ctx.router = router;

// route<a href="https://www.jb51.cc/tag/shuxing/" target="_blank" class="keywords">属性</a>是三次匹配的结果,表示最终是否匹配成功

if (!matched.route) return next();

// 同时满足路由匹配和http请求<a href="https://www.jb51.cc/tag/fangfa/" target="_blank" class="keywords">方法</a>的layer数组

var matchedLayers = matched.pathAndMethod
// 匹配多个路由时认为最后一个是匹配有效的路由
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path;
if (mostSpecificLayer.name) {
ctx._matchedRouteName = mostSpecificLayer.name;
}

// 将匹配的路由reduce为<a href="https://www.jb51.cc/tag/yige/" target="_blank" class="keywords">一个</a>数组

layerChain = matchedLayers.reduce(function(memo,layer) {
// 执行注册路由中间件之前,对context中的一些参数进行设置
memo.push(function(ctx,next) {
// :path/XXX 捕获的路径
ctx.captures = layer.captures(path,ctx.captures);
// 捕获的路径上的参数,{ key: value }
ctx.params = layer.params(path,ctx.captures,ctx.params);
// 路由名称
ctx.routerName = layer.name;
return next();
});
// 返回路由中间件的数组
return memo.concat(layer.stack);
},[]);

// 处理为promise对象

return compose(layerChain)(ctx,next);
};

dispatch.router = this;

return dispatch;
};

Router.prototype.allowedMethod

new Boom.notImplemented(),* methodNotAllowed: () => new Boom.methodNotAllowed() * })); * ``` * * @param {Object=} options * @param {Boolean=} options.throw throw error instead of setting status and header * @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error * @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error * @returns {Function} */ Router.prototype.allowedMethods = function (options) { options = options || {}; var implemented = this.methods;

return function allowedMethods(ctx,next) {
// 所有中间件执行完之后执行allowedMethod方法
return next().then(function() {
var allowed = {};

  // 没有响应状态码或者响应了404

if (!ctx.status || ctx.status === 404) {
// 在match方法中,匹配的路由的layer实例对象组成的数组
ctx.matched.forEach(function (route) {
route.methods.forEach(function (method) {
// 把匹配的路由的http方法保存起来,认为是允许的http请求方法
allowed[method] = method;
});
});

var allowedArr = Object.keys(allowed);

    // 如果该<a href="https://www.jb51.cc/tag/fangfa/" target="_blank" class="keywords">方法</a>在router实例的methods中不存在
if (!~implemented.indexOf(ctx.method)) {
  // 如果在初始化router时配置了throw<a href="https://www.jb51.cc/tag/shuxing/" target="_blank" class="keywords">属性</a>为true
 if (options.throw) {
  var notImplementedThrowable;
  if (typeof options.notImplemented === 'function') {
    // 指定了报错<a href="https://www.jb51.cc/tag/hanshu/" target="_blank" class="keywords">函数</a>
   notImplementedThrowable = options.notImplemented(); // set whatever the user returns from their function
  } else {
    // 没有指定则抛出http异常
   notImplementedThrowable = new HttpError.NotImplemented();
  }
  throw notImplementedThrowable;
 } else {
   // 没有配置throw则响应501
  ctx.status = 501;
  // 设置响应头中的allow字段,返回允许的http<a href="https://www.jb51.cc/tag/fangfa/" target="_blank" class="keywords">方法</a>
  ctx.set('Allow',allowedArr.join(','));
 }
} else if (allowedArr.length) {
 if (ctx.method === 'OPTIONS') {
   // 如果是OPTIONS请求,则认为是请求成功,响应200,并根据OPTIONS请求约定返回允许的http<a href="https://www.jb51.cc/tag/fangfa/" target="_blank" class="keywords">方法</a>
  ctx.status = 200;
  ctx.body = '';
  ctx.set('Allow','));
 } else if (!allowed[ctx.method]) {
   // 如果请求<a href="https://www.jb51.cc/tag/fangfa/" target="_blank" class="keywords">方法</a>在router实例的methods中存在,但是在匹配的路由中该http<a href="https://www.jb51.cc/tag/fangfa/" target="_blank" class="keywords">方法</a>不存在
  if (options.throw) {
   var notAllowedThrowable;
   if (typeof options.methodNotAllowed === 'function') {
    notAllowedThrowable = options.methodNotAllowed(); // set whatever the user returns from their function
   } else {
    notAllowedThrowable = new HttpError.MethodNotAllowed();
   }
   throw notAllowedThrowable;
  } else {
    // 响应405 http请求<a href="https://www.jb51.cc/tag/fangfa/" target="_blank" class="keywords">方法</a><a href="https://www.jb51.cc/tag/cuowu/" target="_blank" class="keywords">错误</a>
   ctx.status = 405;
   ctx.set('Allow','));
  }
 }
}

}
});
};
};

Router.prototype.use

rush:js;"> /** * Use given middleware. * * Middleware run in the order they are defined by `.use()`. They are invoked * sequentially,requests start at the first middleware and work their way * "down" the middleware stack. * * @example * * ```javascript * // 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()); * * app.use(router.routes()); * ``` * * @param {String=} path * @param {Function} middleware * @param {Function=} ... * @returns {Router} */ Router.prototype.use = function () { var router = this; var middleware = Array.prototype.slice.call(arguments); var path;

// support array of paths
// 如果第一个参数是一个数组,且数组中元素为字符串
if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
// 递归调用use方法
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()方法返回的dispatch中间件,即这是一个嵌套的路由
if (m.router) {
// 遍历router.stack属性中所有的layer
m.router.stack.forEach(function (nestedLayer) {
// 被嵌套的路由需要以父路由path为前缀
if (path) nestedLayer.setPrefix(path);
// 如果父路由有指定前缀,被嵌套的路由需要把这个前缀再加上
if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix);
router.stack.push(nestedLayer);
});

if (router.params) {
Object.keys(router.params).forEach(function (key) {
m.router.param(key,router.params[key]);
});
}
} else {
router.register(path || '(.*)',[],m,{ end: false,ignoreCaptures: !hasPath });
}
});

return this;
};

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程之家。

相关文章

前言 做过web项目开发的人对layer弹层组件肯定不陌生,作为l...
前言 前端表单校验是过滤无效数据、假数据、有毒数据的第一步...
前言 图片上传是web项目常见的需求,我基于之前的博客的代码...
前言 导出Excel文件这个功能,通常都是在后端实现返回前端一...
前言 众所周知,js是单线程的,从上往下,从左往右依次执行,...
前言 项目开发中,我们可能会碰到这样的需求:select标签,禁...