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 相关文章推荐
jquery 框架使用教程 AJAX篇
Oct 11 Javascript
浅析webapp框架AngularUI的demo
Dec 21 Javascript
js关于命名空间的函数实例
Feb 05 Javascript
JavaScript在Android的WebView中parseInt函数转换不正确问题解决方法
Apr 25 Javascript
js实时获取窗口大小变化的实例代码
Nov 18 Javascript
JS中位置与大小的获取方法
Nov 22 Javascript
JS使用正则截取两个字符串之间的字符串实现方法详解
Jan 06 Javascript
assert()函数用法总结(推荐)
Jan 25 Javascript
javascript中call,apply,callee,caller用法实例分析
Jul 24 Javascript
浏览器JavaScript调试功能无法使用解决方案
Sep 18 Javascript
解决vue props传Array/Object类型值,子组件报错的情况
Nov 07 Javascript
Vue向后台传数组数据,springboot接收vue传的数组数据实例
Nov 12 Javascript
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.ini安全配置检测工具pcc简单介绍
2015/07/02 PHP
php判断邮箱地址是否存在的方法
2016/02/13 PHP
微信公众平台开发教程⑤ 微信扫码支付模式介绍
2019/04/10 PHP
JavaScript页面刷新与弹出窗口问题的解决方法
2010/03/02 Javascript
jQeury淡入淡出需要注意的问题
2010/09/08 Javascript
jquery attr 设定src中含有&amp;(宏)符号问题的解决方法
2011/07/26 Javascript
jquery数据验证插件(自制,简单,练手)实例代码
2013/10/24 Javascript
三种方式获取XMLHttpRequest对象
2014/04/21 Javascript
深入理解JavaScript中的对象
2015/06/04 Javascript
jquery实现的蓝色二级导航条效果代码
2015/08/24 Javascript
全面了解JavaScript的数据类型转换
2016/07/01 Javascript
原生js实现图片放大缩小计时器效果
2017/01/20 Javascript
解决vue router组件状态刷新消失的问题
2018/08/01 Javascript
小程序实现可拖动的悬浮按钮
2020/09/07 Javascript
[02:07]2018DOTA2亚洲邀请赛主赛事第三日五佳镜头 fy极限反杀
2018/04/06 DOTA
python dict remove数组删除(del,pop)
2013/03/24 Python
启动targetcli时遇到错误解决办法
2017/10/26 Python
基于Python开发chrome插件的方法分析
2018/07/07 Python
django之跨表查询及添加记录的示例代码
2018/10/16 Python
python 多线程对post请求服务器测试并发的方法
2019/06/13 Python
微信公众号token验证失败解决方案
2019/07/22 Python
Python如何实现强制数据类型转换
2019/11/22 Python
css 如何让背景图片拉伸填充避免重复显示
2013/07/11 HTML / CSS
使用canvas绘制贝塞尔曲线
2014/12/17 HTML / CSS
简历自我评价怎么写好呢?
2014/01/04 职场文书
会计专业毕业自荐书范文
2014/02/08 职场文书
户外活动策划方案
2014/03/12 职场文书
小学优秀班干部事迹材料
2014/05/25 职场文书
个人查摆问题及整改措施
2014/10/16 职场文书
免职证明样本
2014/10/23 职场文书
2014年技术员工作总结
2014/11/18 职场文书
文明班级申报材料
2014/12/24 职场文书
评先进个人材料
2014/12/29 职场文书
2016年国庆节67周年活动总结
2016/04/01 职场文书
2019年“红色之旅”心得体会1000字(3篇)
2019/09/27 职场文书
MySQL的prepare使用以及遇到的bug
2022/05/11 MySQL