koa-router源码学习小结


Posted in Javascript onSeptember 07, 2018

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

源码架构图

koa-router源码学习小结

调用链路-routes()

koa-router源码学习小结

HTTP请求调用流程

koa-router源码学习小结

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
List Installed Software Features
Jun 11 Javascript
基于jquery的无限级联下拉框js插件
Oct 29 Javascript
jquery插件制作简单示例说明
Feb 03 Javascript
JavaScript实现快速排序(自已编写)
Dec 19 Javascript
Jquery插件实现点击获取验证码后60秒内禁止重新获取
Mar 13 Javascript
JavaScript实现数字数组正序排列的方法
Apr 06 Javascript
javascript中return,return true,return false三者的用法及区别
Nov 17 Javascript
详解Javascript事件驱动编程
Jan 03 Javascript
JS中mouseup事件丢失的原因与解决办法
Jun 14 Javascript
jQuery实现的鼠标拖动浮层功能示例【拖动div等任何标签】
Dec 29 jQuery
JS实现的碰撞检测与周期移动完整示例
Sep 02 Javascript
如何实现vue的tree组件
Dec 03 Vue.js
Vue.js实现表格渲染的方法
Sep 07 #Javascript
vue基于element的区间选择组件
Sep 07 #Javascript
vue-cli监听组件加载完成的方法
Sep 07 #Javascript
原生JS实现DOM加载完成马上执行JS代码的方法
Sep 07 #Javascript
vue加载完成后的回调函数方法
Sep 07 #Javascript
使用vue-router与v-if实现tab切换遇到的问题及解决方法
Sep 07 #Javascript
VUE DOM加载后执行自定义事件的方法
Sep 07 #Javascript
You might like
PHP新手上路(二)
2006/10/09 PHP
php验证手机号码(支持归属地查询及编码为UTF8)
2013/02/01 PHP
PHP动态规划解决0-1背包问题实例分析
2015/03/23 PHP
thinkphp实现附件上传功能
2017/05/26 PHP
PHP笛卡尔积实现原理及代码实例
2020/12/09 PHP
从零开始学习jQuery (六) jquery中的AJAX使用
2011/02/23 Javascript
javascript圆盘抽奖程序实现原理和完整代码例子
2014/06/03 Javascript
一步一步封装自己的HtmlHelper组件BootstrapHelper(二)
2016/09/14 Javascript
JavaScript 轮播图和自定义滚动条配合鼠标滚轮分享代码贴
2016/10/28 Javascript
使用jQuery 操作table 完成单元格合并的实例
2017/12/27 jQuery
jQuery实现导航样式布局操作示例【可自定义样式布局】
2018/07/24 jQuery
详解es6超好用的语法糖Decorator
2018/08/01 Javascript
Vue项目总结之webpack常规打包优化方案
2019/06/06 Javascript
vue+vant实现商品列表批量倒计时功能
2020/01/13 Javascript
CKEditor扩展插件:自动排版功能autoformat插件实现方法详解
2020/02/06 Javascript
jQuery实现穿梭框效果
2021/01/19 jQuery
[04:42]2015国际邀请赛CDEC战队晋级之路
2015/08/13 DOTA
[40:53]完美世界DOTA2联赛PWL S3 Magma vs DLG 第二场 12.18
2020/12/20 DOTA
Python入门篇之函数
2014/10/20 Python
Python入门篇之正则表达式
2014/10/20 Python
python DataFrame 修改列的顺序实例
2018/04/10 Python
Python BS4库的安装与使用详解
2018/08/08 Python
对python 命令的-u参数详解
2018/12/03 Python
Python中的集合介绍
2019/01/28 Python
对Python3使运行暂停的方法详解
2019/02/18 Python
使用python对多个txt文件中的数据进行筛选的方法
2019/07/10 Python
Django后台管理系统的图文使用教学
2020/01/20 Python
深入理解Python 多线程
2020/06/16 Python
如何在Win10系统使用Python3连接Hive
2020/10/15 Python
python实现三壶谜题的示例详解
2020/11/02 Python
利用CSS3的定位页面元素
2009/08/29 HTML / CSS
HTML+CSS+JavaScript实现图片3D展览的示例代码
2020/10/12 HTML / CSS
简洁自适应404页面HTML好看的404源码
2020/12/16 HTML / CSS
乡镇2014法制宣传日活动总结
2014/11/01 职场文书
2015年家长学校工作总结
2015/04/22 职场文书
Spring Boot 的创建和运行示例代码详解
2022/07/23 Java/Android