koa-router源碼分析

koa-router是什麼

koa-router的github主頁給出的定義是:html

Router middleware for koa.

定義很是簡潔,含義也一目瞭然,它就是koa的路由中間件。koa爲了自身的輕量,不在內核方法中綁定任何中間件,而路由功能對於一個web框架來講,又是必不可少的基礎功能。所以koa-router爲koa提供了強大的路由功能。node

提到路由中間件,這裏補充兩點,對於後面理解koa-router很是有幫助。git

  • 什麼是中間件
中間件(Middleware) 是一個函數,它能夠訪問請求對象(request object (req)), 響應對象(response object (res)), 和 web 應用中處於請求-響應循環流程中的中間件,通常被命名爲 next 的變量。
中間件的功能包括:
執行任何代碼。
修改請求和響應對象。
終結請求-響應循環。
調用堆棧中的下一個中間件。
若是當前中間件沒有終結請求-響應循環,則必須調用 next() 方法將控制權交給下一個中間件,不然請求就會掛起。
  • router和route的區別

route就是一條路由,它將一個URL路徑和一個函數進行映射,如/users => getAllUsers()
router能夠理解爲一個容器,管理全部route,當接收到一個http請求,根據請求url和請求方法去匹配對應的中間件,這個過程由router管理。github

koa-router源碼結構

圖片描述

如上圖,koa-router的源碼結構很是簡單,核心文件只有router.js和layer.js兩個文件,代碼量也很是少,包括註釋在內總共不超過1000行。經過package.json文件中的main字段,找到入口文件是「lib/router.js」。router.js文件定義了一個構造函數Router,並在Router上定義了一個靜態方法和一些原型方法,以下圖所示。
圖片描述web

layer.js文件定義了一個構造函數Layer,並在Layer上定義了幾個原型方法,以下圖所示。
圖片描述正則表達式

基本用法

var Koa = require('koa');
var Router = require('koa-router');
 
var app = new Koa();
var router = new Router();
 
router.get('/', (ctx, next) => {
  // ctx.router available
});
 
app
  .use(router.routes())
  .listen(3000);

實例化一個koa-router,註冊相應路由中間件,而後經過routes方法生成一個koa中間件並掛載到koa上,這樣就給koa添加了最基本的路由功能。json

router.js源碼分析

...
var methods = require('methods');
...

module.exports = Router;

// Router構造函數
function Router(opts) {
  // 寬容性處理,若是沒經過new調用,仍然返回new調用後的實例
  if (!(this instanceof Router)) {
    return new Router(opts);
  }

  this.opts = opts || {}; // 實例化時傳入的參數
  // 這裏的methods和上面require的methods有什麼關係?
  this.methods = this.opts.methods || [
    'HEAD',
    'OPTIONS',
    'GET',
    'PUT',
    'PATCH',
    'POST',
    'DELETE'
  ];

  this.params = {};
  this.stack = []; // 存儲Layer實例,那麼Layer是什麼?
};

// 定義get|post|put|...方法
methods.forEach(function (method) {
  ...
});

首先定義一個了構造函數Router,實例化時支持傳入一個對象參數,構造函數裏初始化了一些實例屬性。數組

構造函數Router裏面的this.methods和外部require的methods有什麼關係?
this.methods由上面給出的7種http請求方法組成;
外部引入的methods等價於Node.js中的http.METHODS轉化爲小寫後的形式,即:app

methods = [
  'acl',
  'bind',
  'checkout',
  'connect',
  'copy',
  'delete',
  'get',
  'head',
  'link',
  'lock',
  'm-search',
  'merge',
  'mkactivity',
  'mkcalendar',
  'mkcol',
  'move',
  'notify',
  'options',
  'patch',
  'post',
  'propfind',
  'proppatch',
  'purge',
  'put',
  'rebind',
  'report',
  'search',
  'subscribe',
  'trace',
  'unbind',
  'unlink',
  'unlock',
  'unsubscribe' ]

this.methods是methods的一個子集,搞清楚它們的值是什麼後,咱們來看一下它們的含義,this.methods是koa-router默認支持的http請求方法,若是沒有在構造函數定義其餘方法,那麼採用默認支持方法以外的http方法請求時,會拋出異常。(在allowedMethods方法裏實現)框架

Router實例的get/post等方法在哪定義的呢?看下面這段代碼

methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    var middleware; // 是一個數組,支持傳入多箇中間件

    // 支持傳2個參數或3個參數,若是傳2個參數,則name值爲null
    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;
  };
});

下一步走到register函數,register函數的做用是根據路由、http請求方法、處理函數、自定義配置實例化一個Layer,Layer實例包含一些屬性(如regexp)用來進行路由匹配。這裏可能對Layer仍是不太理解,首先Layer是一個構造函數,它定義了一些重要的屬性,這些屬性的做用是爲了在routes方法裏進行路由匹配和執行對應的中間件,Layer也就至關於咱們在第一部分提到的route,它定義了一個URL和對應中間件的映射關係,有了這個對應關係,咱們就能夠對到來的請求進行路由匹配和執行對應的中間件。而後把Layer實例push到Router實例的stack中。

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

  var router = this;
  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, // When false the path will match at the beginning. (default: 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) {
    route.param(param, this.params[param]);
  }, this);

  stack.push(route);

  return route;
};

接下來咱們看一個很是重要的函數,routes函數返回了一個dispatch函數,名字是否是很是眼熟,koa-compose裏也有一個dispatch函數。返回的dispatch函數做爲一個koa中間件。它從koa中間件的第一個參數context對象中拿到url,而後進行路由匹配,匹配失敗則執行一下koa中間件;匹配成功,則執行相應的中間件。

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;
    // 調用Router原型上的match方法,返回一個matched對象
    // match方法在下面再分析
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;

    // 當koa上掛載多個Router實例時,if可能會被執行到
    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      ctx.matched = matched.path;
    }

    ctx.router = router;

    // 沒有匹配到路由,執行下一個中間件
    // 若是匹配到路由,匹配的路由方法中沒有調用next()方法時,下一個中間件就不會執行了
    if (!matched.route) return next();

    var matchedLayers = matched.pathAndMethod
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    ctx._matchedRoute = mostSpecificLayer.path;
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }

    // 對匹配到的路由中間件數組作一個處理
    // 在每個路由中間件前面添加一箇中間件,
    // 給ctx添加3個屬性(路由path、路由參數對象、路由命名)
    // 這樣後面的路由中間件就能方便地獲取到ctx上的這3個屬性
    layerChain = matchedLayers.reduce(function(memo, layer) {
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerName = layer.name;
        return next();
      });
      return memo.concat(layer.stack);
    }, []);

    return compose(layerChain)(ctx, next);
  };

  dispatch.router = this;

  return dispatch;
};

下面咱們看一下Router原型上的match方法的代碼

Router.prototype.match = function (path, method) {
  var layers = this.stack;
  var layer;
  var matched = {
    path: [],
    pathAndMethod: [],
    route: false
  };

  // 遍歷全部路由(layer實例,即處理後的route)
  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];

    debug('test %s %s', layer.path, layer.regexp);

    // 經過正則匹配path
    if (layer.match(path)) {
      matched.path.push(layer);
        
      // 匹配http請求方法
      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer); // 這裏是真正要執行的路由方法
        if (layer.methods.length) matched.route = true;
      }
    }
  }

  return matched;
};

Router原型上的match方法主要是調用Layer原型上的的match方法,即經過正則匹配path,匹配成功以後再匹配http請求方法,當都匹配成功,將matched.route置爲true,表示路由匹配成功。

至此,Router原型上的幾個核心函數就講解完了,其餘一些函數就不展開分析了。

layer.js源碼分析

layer.js定義了一個構造函數Layer,它對path|methods|middleware|opts進行一些處理,好比將路由path統一轉換爲正則表達式,將中間件統一轉換爲數組形式等,至關定義咱們的route。Layer原型對象上定義了一些方法,如match用於路由路徑匹配,params方法用於提取路由參數對象,captures用於提取路由path等。

// 構造函數
function Layer(path, methods, middleware, opts) {
  this.opts = opts || {};
  this.name = this.opts.name || null;
  this.methods = [];
  this.paramNames = []; // 保存路由參數名
  this.stack = Array.isArray(middleware) ? middleware : [middleware];

  // GET方法前添加一個HEAD方法
  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    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;
  // 將路由、路由參數轉化成正則表達式形式(match方法即經過這個正則表達式去匹配path)
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);

  debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
};

本文對koa-router總體流程和核心函數進行了一些簡單的分析,有表述不正確的地方,但願你們批評指出。

參考列表:
https://cnodejs.org/topic/578...
https://cnodejs.org/topic/579...
https://www.cnblogs.com/chris...

相關文章
相關標籤/搜索