资讯专栏INFORMATION COLUMN

koa-router 源码浅析

SillyMonkey / 3104人阅读

摘要:代码结构执行流程上面两张图主要将的整体代码结构和大概的执行流程画了出来,画的不够具体。那下面主要讲中的几处的关键代码解读一下。全局的路由参数处理的中间件组成的对象。

代码结构

执行流程

上面两张图主要将koa-router的整体代码结构和大概的执行流程画了出来,画的不够具体。那下面主要讲koa-router中的几处的关键代码解读一下。

读代码首先要找到入口文件,那几乎所有的node模块的入口文件都会在package.json文件中的main属性指明了。koa-router的入口文件就是lib/router.js

第三方模块

首先先讲几个第三方的node模块了解一下,因为后面的代码讲解中会用到,不去看具体实现,只要知道其功能就行:
koa-compose:
提供给它一个中间件数组, 返回一个顺序执行所有中间件的执行函数。
methods:
node中支持的http动词,就是http.METHODS,可以在终端输出看看。
path-to-regexp:
将路径字符串转换成强大的正则表达式,还可以输出路径参数。

Router & Layer

RouterLayer 分别是两个构造函数,分别在router.jslayer.js中,koa-router的所有代码也就在这两个文件中,可以知道它的代码量并不是很多。

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"
  ];

  this.params = {};
  this.stack = [];
};

首先是

if (!(this instanceof Router)) {
  return new Router(opts);
}

这是常用的去new的方式,所以我们可以在引入koa-router时:

const router = require("koa-router")()

而不用:

const router = new require("koa-router")() // 这样也是没问题的

this.methods:
在后面要讲的allowedMethods方法中要用到的,目的是响应options请求和请求出错的处理。

this.params:
全局的路由参数处理的中间件组成的对象。

this.stack:
其实就是各个路由(Layer)实例组成的数组。每次处理请求时都需要循环这个数组找到匹配的路由。

Layer: 创建各个路由实例

function Layer(path, methods, middleware, opts) {
  ...

  this.stack = Array.isArray(middleware) ? middleware : [middleware];

  // 为给后面的allowedMthods处理
  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    if (this.methods[l-1] === "GET") {
      // 如果是get请求,则支持head请求
      this.methods.unshift("HEAD");
    }
  }, this);

  // 确保路由的每个中间件都是函数
  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;
  // 利用path-to-rege模块生产的路径的正则表达式
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);

  ...
};

这里的this.stackRouter中的不同,这里的是路由所有的中间件的数组。(一个路由可以有多个中间件)

router.register()

作用:注册路由

从上一篇的代码结构图中可以看出,Router的几个实例方法都直接或简介地调用了register方法,可见,它应该是比较核心的函数, 代码不长,我们一行行看一下:

Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {};
  var router = this;

  // 全部路由
  var stack = this.stack;

  // 说明路由的path是支持数组的
  // 如果是数组的话,需要递归调用register来注册路由
  // 因为一个path对应一个路由
  if (Array.isArray(path)) {
    path.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts);
    });

    return this;
  }

  // 创建路由,路由就是Layer的实例
  // mthods 是路由处理的http方法
  // 最后一个参数对象最终是传给Layer模块中的path-to-regexp模块接口调用的
  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);
  }

  // 将全局的路由参数添加到每个路由中
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param]);
  }, this);

  // 往路由数组中添加新创建的路由
  stack.push(route);

  return route;
};
router.verb()

verb => get|put|post|patch|delete
作用:注册路由

这是koa-router提供的直接注册相应http方法的路由,但最终还是会调用register方法如:

router.get("/user", function(ctx, next){...})

和下面利用register方法等价:

router.register("/user", ["get"], [function(ctx, next){...}])

可以看到直接使用router.verb注册路由会方便很多。来看看代码:
你会发现router.js的代码里并没有Router.prototype.get的代码出现,原因是它还依赖了上面提到的methods模块来实现。

// 这里的methods就是上面的methods模块提供的数组
methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    var middleware;

    // 这段代码做了两件事:
    // 1.name 参数是可选的,所以要做一些参数置换的处理
    // 2.将所有路由中间件合并成一个数组
    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;
    }

    // 调用register方法
    this.register(path, [method], middleware, {
      name: name
    });

    return this;
  };
});
router.routes()

作用:启动路由

这是在koa中配置路由的重要一步:

var router = require("koa-router")();
...
app.use(router.routes())

就这样,koa-router就启动了,所以我们也一定会很好奇这个routes函数到底做了什么,但可以肯定router.routes()返回了一个中间件函数。
函数体长了一点,简化一下看下整体轮廓:

Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this;
  var dispatch = function dispatch(ctx, next) {
    ...
  }
  dispatch.router = this;
  return dispatch;
};

这里形成了一个闭包,在routes函数内部返回了一个dispatch函数作为中间件。
接下来看下dispatch函数的实现:

var dispatch = function dispatch(ctx, next) {
    var path = router.opts.routerPath || ctx.routerPath || ctx.path;

    // router.match函数内部遍历所有路由(this.stach),
    // 根据路径和请求方法找到对应的路由
    // 返回的matched对象为: 
    /* 
      var matched = {
        path: [], // 保存了path匹配的路由数组
        pathAndMethod: [], // 保存了path和methods都匹配的路由数组
        route: false // 是否有对应的路由
      };
    */
    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;
    }

    // 如果没有对应的路由,则直接进入下一个中间件
    if (!matched.route) return next();

    // 找到正确的路由的path
    var mostSpecificPath = matched.pathAndMethod[matched.pathAndMethod.length - 1].path;
    ctx._matchedRoute = mostSpecificPath;

    // 使用reduce方法将路由的所有中间件形成一条链
    layerChain = matched.pathAndMethod.reduce(function(memo, layer) {

      // 在每个路由的中间件执行之前,根据参数不同,设置 ctx.captures 和 ctx.params
      // 这就是为什么我们可以直接在中间件函数中直接使用 ctx.params 来读取路由参数信息了
      memo.push(function(ctx, next) {

        // 返回路由的参数的key 
        ctx.captures = layer.captures(path, ctx.captures);

        // 返回参数的key和对应的value组成的对象
        ctx.params = layer.params(path, ctx.captures, ctx.params);

        // 执行下一个中间件
        return next();
      });

      // 将上面另外加的中间件和已有的路由中间件合并到一起
      // 所以最终 layerChain 将会是一个中间件的数组
      return memo.concat(layer.stack);
    }, []);

    // 最后调用上面提到的 compose 模块提供的方法,返回将 layerChain (中间件的数组) 
    // 顺序执行所有中间件的执行函数, 并立即执行。
    return compose(layerChain)(ctx, next);
  };
router.allowMethods()

作用: 当请求出错时的处理逻辑

同样也是koa中配置路由的中一步:

var router = require("koa-router")();
...
app.use(router.routes())
app.use(router.allowMethods())

可以看出,该方法也是闭包内返回了中间件函数。我们将代码简化一下:

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) {
        ...

        if (!~implemented.indexOf(ctx.method)) {
          if (options.throw) {
            ...
          } else {
            ctx.status = 501;
            ctx.set("Allow", allowedArr);
          }
        } else if (allowedArr.length) {
          if (ctx.method === "OPTIONS") {
            ctx.status = 204;
            ctx.set("Allow", allowedArr);
          } else if (!allowed[ctx.method]) {
            if (options.throw) {
              ...
            } else {
              ctx.status = 405;
              ctx.set("Allow", allowedArr);
            }
          }
        }
      }
    });
  };
};

眼尖的同学可能会看到一些http code404, 501, 204, 405
那这个函数其实就是当所有中间件函数执行完了,并且请求出错了进行相应的处理:

如果请求的方法koa-router不支持并且没有设置throw选项,则返回 501(未实现)

如果是options请求,则返回 204(无内容)

如果请求的方法支持但没有设置throw选项,则返回 405(不允许此方法 )

总结

粗略浅析了这么些,能大概知道了koa-router的工作原理。笔者能力有限,有错误还请指出。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80893.html

相关文章

  • generator探幽(1)--koa中间件机制浅析

    摘要:当运行到时,不会暂停,而是直接跳进函数执行函数内的代码。由于函数中没有,因此会一直执行完函数中的代码,并返回至函数中执行后面的代码。 本系列旨在通过对co,koa等库源码的研究,进而理解generator在异步编程中的重大作用(ps:所有代码请在node --harmony或者iojs环境中运行) koa中间件的形式 相信用过koa的小伙伴一定很熟悉下面这段代码 var app ...

    Jensen 评论0 收藏0
  • 教你从写一个迷你koa-router到阅读koa-router源码

    摘要:本打算教一步步实现,因为要解释的太多了,所以先简化成版本,从实现部分功能到阅读源码,希望能让你好理解一些。 本打算教一步步实现koa-router,因为要解释的太多了,所以先简化成mini版本,从实现部分功能到阅读源码,希望能让你好理解一些。希望你之前有读过koa源码,没有的话,给你链接 最核心需求-路由匹配 router最重要的就是路由匹配,我们就从最核心的入手 router.get...

    yzzz 评论0 收藏0
  • Koa-router 优先级问题

    摘要:问题描述在使用作为路由遇到了一个优先级问题如下代码在访问时路由会优先匹配到路由返回这个问题就很尴尬了项目空闲下来去翻看源码终于找到了原因问题原因的源码并不长和两个文件加起来共一千多行代码建议可以结合这篇文章阅读其中造成这个问题的原因 问题描述 在使用Koa-router作为路由遇到了一个优先级问题.如下代码 // routerPage.js file const router = re...

    Paul_King 评论0 收藏0
  • koa源码阅读[2]-koa-router

    摘要:第三篇,有关生态中比较重要的一个中间件第一篇源码阅读第二篇源码阅读与是什么首先,因为是一个管理中间件的平台,而注册一个中间件使用来执行。这里写入的多个中间件都是针对该生效的。 第三篇,有关koa生态中比较重要的一个中间件:koa-router 第一篇:koa源码阅读-0 第二篇:koa源码阅读-1-koa与koa-compose koa-router是什么 首先,因为koa是一个管...

    oneasp 评论0 收藏0
  • 玩转Koa -- koa-router原理解析

    摘要:四路由注册构造函数首先看了解一下构造函数限制必须采用关键字服务器支持的请求方法,后续方法会用到保存前置处理函数存储在构造函数中初始化的和属性最为重要,前者用来保存前置处理函数,后者用来保存实例化的对象。 一、前言   Koa为了保持自身的简洁,并没有捆绑中间件。但是在实际的开发中,我们需要和形形色色的中间件打交道,本文将要分析的是经常用到的路由中间件 -- koa-router。   ...

    wthee 评论0 收藏0

发表评论

0条评论

SillyMonkey

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<