koa-router 源碼淺析

代碼結構

執行流程

上面兩張圖主要將koa-router的總體代碼結構和大概的執行流程畫了出來,畫的不夠具體。那下面主要講koa-router中的幾處的關鍵代碼解讀一下。javascript

讀代碼首先要找到入口文件,那幾乎全部的node模塊的入口文件都會在package.json文件中的main屬性指明瞭。koa-router的入口文件就是lib/router.jsjava

第三方模塊

首先先講幾個第三方的node模塊瞭解一下,由於後面的代碼講解中會用到,不去看具體實現,只要知道其功能就行:
koa-compose:
提供給它一箇中間件數組, 返回一個順序執行全部中間件的執行函數。
methods
node中支持的http動詞,就是http.METHODS,能夠在終端輸出看看。
path-to-regexp
將路徑字符串轉換成強大的正則表達式,還能夠輸出路徑參數。node

Router & Layer

RouterLayer 分別是兩個構造函數,分別在router.jslayer.js中,koa-router的全部代碼也就在這兩個文件中,能夠知道它的代碼量並非不少。 git

Router: 建立管理整個路由模塊的實例github

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時:json

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

而不用:數組

const router = new require('koa-router')() // 這樣也是沒問題的

this.methods:
在後面要講的allowedMethods方法中要用到的,目的是響應options請求和請求出錯的處理。閉包

this.params:
全局的路由參數處理的中間件組成的對象。app

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
那這個函數其實就是當全部中間件函數執行完了,而且請求出錯了進行相應的處理:

  1. 若是請求的方法koa-router不支持而且沒有設置throw選項,則返回 501(未實現)

  2. 若是是options請求,則返回 204(無內容)

  3. 若是請求的方法支持但沒有設置throw選項,則返回 405(不容許此方法 )

總結

粗略淺析了這麼些,能大概知道了koa-router的工做原理。筆者能力有限,有錯誤還請指出。

相關文章
相關標籤/搜索