koa-router源码学习小结
程序员文章站
2022-04-29 17:32:37
koa 框架一直都保持着简洁性, 它只对 node 的 http 模块进行了封装, 而在真正实际使用, 我们还需要更多地像路由这样的模块来构建我们的应用, 而 koa-ro...
koa 框架一直都保持着简洁性, 它只对 node 的 http 模块进行了封装, 而在真正实际使用, 我们还需要更多地像路由这样的模块来构建我们的应用, 而 koa-router 是常用的 koa 的路由库. 这里通过解析 koa-router 的源码来达到深入学习的目的.
源码架构图
调用链路-routes()
http请求调用流程
usage
const koa = require('koa'); const router = require('koa-router'); 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
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
function layer(path, methods, middleware, opts) { this.opts = opts || {}; this.name = this.opts.name || null; this.methods = []; // 存放path路径参数的一些属性,eg: /test/:str => { 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方法
/** * create `router.verb()` methods, where *verb* is one of the http verbs such * as `router.get()` or `router.post()`. * * match url patterns to callback functions or controller actions using `router.verb()`, * where **verb** is one of the http verbs such as `router.get()` or `router.post()`. * * additionaly, `router.all()` can be used to match against all methods. * * ```javascript * router * .get('/', (ctx, next) => { * ctx.body = 'hello world!'; * }) * .post('/users', (ctx, next) => { * // ... * }) * .put('/users/:id', (ctx, next) => { * // ... * }) * .del('/users/:id', (ctx, next) => { * // ... * }) * .all('/users/:id', (ctx, 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', (ctx, 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('/', (ctx, next) => {...}); * posts.get('/:pid', (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()); * ``` * * #### 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', (ctx, 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属性 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; }; });
router.prototype.register
/** * create and register a route. * * @param {string} path path string. * @param {array.<string>} methods array of http verbs. * @param {function} middleware multiple middleware also accepted. * @returns {layer} * @private */ router.prototype.register = function (path, methods, middleware, 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, 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) { // 将router中this.params维护的参数中间件挂载到layer实例中 route.param(param, this.params[param]); }, this); // 所有layer实例存放在router的stack属性中 stack.push(route); return route; };
router.prototype.match
/** * match given `path` and return corresponding routes. * * @param {string} path * @param {string} method * @returns {object.<path, pathandmethod>} 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请求方法 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
/** * 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属性是三次匹配的结果,表示最终是否匹配成功 if (!matched.route) return next(); // 同时满足路由匹配和http请求方法的layer数组 var matchedlayers = matched.pathandmethod // 匹配多个路由时认为最后一个是匹配有效的路由 var mostspecificlayer = matchedlayers[matchedlayers.length - 1] ctx._matchedroute = mostspecificlayer.path; if (mostspecificlayer.name) { ctx._matchedroutename = mostspecificlayer.name; } // 将匹配的路由reduce为一个数组 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
/** * returns separate middleware for responding to `options` requests with * an `allow` header containing the allowed methods, as well as responding * with `405 method not allowed` and `501 not implemented` as appropriate. * * @example * * ```javascript * var koa = require('koa'); * var router = require('koa-router'); * * var app = new koa(); * var router = new router(); * * app.use(router.routes()); * app.use(router.allowedmethods()); * ``` * * **example with [boom](https://github.com/hapijs/boom)** * * ```javascript * var koa = require('koa'); * var router = require('koa-router'); * var boom = require('boom'); * * var app = new koa(); * var router = new router(); * * app.use(router.routes()); * app.use(router.allowedmethods({ * throw: true, * notimplemented: () => 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); // 如果该方法在router实例的methods中不存在 if (!~implemented.indexof(ctx.method)) { // 如果在初始化router时配置了throw属性为true if (options.throw) { var notimplementedthrowable; if (typeof options.notimplemented === 'function') { // 指定了报错函数 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方法 ctx.set('allow', allowedarr.join(', ')); } } else if (allowedarr.length) { if (ctx.method === 'options') { // 如果是options请求,则认为是请求成功,响应200,并根据options请求约定返回允许的http方法 ctx.status = 200; ctx.body = ''; ctx.set('allow', allowedarr.join(', ')); } else if (!allowed[ctx.method]) { // 如果请求方法在router实例的methods中存在,但是在匹配的路由中该http方法不存在 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请求方法错误 ctx.status = 405; ctx.set('allow', allowedarr.join(', ')); } } } } }); }; };
router.prototype.use
/** * 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; };
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。