koa-router 源碼由淺入深的分析(7.4.0版本的)

首先簡單的介紹下什麼koa-router,爲何要使用它,能夠簡單看下上一篇文章. 瞭解koa-routerhtml

首先咱們來看下koa-router的源碼的基本結構以下,它是由兩部分組成的:node

------- koa-router
| |--- lib
| | |-- layer.js
| | |-- router.js

如上基本結構。正則表達式

一:router.js 代碼基本結構express

咱們先看 router.js 代碼結構以下:數組

module.exports = 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 = [];
};

Router.prototype.del = Router.prototype['delete'];
Router.prototype.use = function () {
  // ....
}
Router.prototype.prefix = function (prefix) {
  // ....
}
Router.prototype.routes = Router.prototype.middleware = function () {
  // ...
}
Router.prototype.allowedMethods = function (options) {
  // ...
}
Router.prototype.all = function (name, path, middleware) {
  // ...
}
Router.prototype.redirect = function (source, destination, code) {
  // ...
}
Router.prototype.register = function (path, methods, middleware, opts) {
  // ...
}
Router.prototype.route = function (name) {
  // ...
}
Router.prototype.url = function (name, params) {
  // ...
}
Router.prototype.match = function (path, method) {
  // ...
}
Router.prototype.param = function (param, middleware) {
  // ...
}
Router.url = function (path, params) {
  // ...
}

如上就是koa-router中的router.js 中的代碼結構,定義了一個 Router 函數,而後在該函數的原型上定義了不少方法。而後使用 module.exports = Router; 導出該函數。所以若是咱們要使用該router函數的話,須要首先導入該router.js 代碼,所以須要 var Router = require('koa-router'); 而後再實例化該router函數,如代碼:var router = new Router(); 或者咱們直接能夠以下編寫代碼:var router = require('koa-router')(); 好比以下koa-router代碼的demo列子:瀏覽器

const Koa = require('koa');
const app = new Koa();

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

// 添加路由
router.get('/', ctx => {
  ctx.body = '<h1>歡迎光臨index page 頁面</h1>';
});

router.get('/home', ctx => {
  ctx.body = '<h1>歡迎光臨home頁面</h1>';
});

// 加載路由中間件
app.use(router.routes());

app.use(router.allowedMethods());

app.listen(3001, () => {
  console.log('server is running at http://localhost:3001');
});

當咱們運行該js文件的時候,在瀏覽器訪問 http://localhost:3001/ 的時候,就會顯示 "歡迎光臨index page 頁面" 這些信息,當咱們在瀏覽器訪問 http://localhost:3001/home 的時候,在頁面上會顯示 "歡迎光臨home頁面" 等信息。它是如何調用的呢?app

首先咱們來分析下,Router這個構造函數代碼;基本源碼以下:koa

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 = [];
};

如上代碼,首先會判斷 是不是該Router實列,若是不是的話,就實例化該對象。所以咱們 Router() 或 new Router() 這樣調用效果是一致的。該Router函數會傳入一個對象參數 opts,該對象 opts會有methods這樣的key。會傳入一些http方法等。函數

而後 this.methods; 它是一個數組是存放容許使用的HTTP的經常使用的方法名,後面會使用到,先保存到 this.methods裏面。post

this.params = {}; 定義了一個對象,它具體幹什麼用的,我暫時也不知道,先放在這裏。
this.stack = []; 定義了一個數組,先放在這裏,我也不知道具體幹什麼用的。

二. 路由註冊

第二步咱們就是添加咱們的路由,好比app.js代碼以下:

// 添加路由
router.get('/', ctx => {
  ctx.body = '<h1>歡迎光臨index page 頁面</h1>';
});

在咱們的router.js源碼中會有這麼一段代碼,咱們來看下:

var methods = require('methods');

methods函數源碼以下:

/*!
 * methods
 * Copyright(c) 2013-2014 TJ Holowaychuk
 * Copyright(c) 2015-2016 Douglas Christopher Wilson
 * MIT Licensed
 */

'use strict';
/**
 * Module dependencies.
 * @private
 */

var http = require('http');
/**
 * Module exports.
 * @public
 */

module.exports = getCurrentNodeMethods() || getBasicNodeMethods();
/**
 * Get the current Node.js methods.
 * @private
 */

function getCurrentNodeMethods() {
  return http.METHODS && http.METHODS.map(function lowerCaseMethod(method) {
    return method.toLowerCase();
  });
}

/**
 * Get the "basic" Node.js methods, a snapshot from Node.js 0.10.
 * @private
 */

function getBasicNodeMethods() {
  return [
    'get',
    'post',
    'put',
    'head',
    'delete',
    'options',
    'trace',
    'copy',
    'lock',
    'mkcol',
    'move',
    'purge',
    'propfind',
    'proppatch',
    'unlock',
    'report',
    'mkactivity',
    'checkout',
    'merge',
    'm-search',
    'notify',
    'subscribe',
    'unsubscribe',
    'patch',
    'search',
    'connect'
  ];
}

所以在咱們的 router.js 中 這樣引入 var methods = require('methods');後,咱們的methods的值被保存爲以下:

var methods = [
  'get',
  'post',
  'put',
  'head',
  'delete',
  'options',
  'trace',
  'copy',
  'lock',
  'mkcol',
  'move',
  'purge',
  'propfind',
  'proppatch',
  'unlock',
  'report',
  'mkactivity',
  'checkout',
  'merge',
  'm-search',
  'notify',
  'subscribe',
  'unsubscribe',
  'patch',
  'search',
  'connect'
];

而後router.js 代碼中的源碼由以下代碼:

methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    var middleware;

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

也就是說遍歷 methods 上面的數組中保存的 get/post/... 等方法。最後就變成以下這樣的:

Router.property['get'] = function(name, path, middleware) {};
Router.property['post'] = function(name, path, middleware) {};
Router.property['put'] = function(name, path, middleware) {};
Router.property['head'] = function(name, path, middleware) {};

..... 等等這樣的函數。經過如上代碼,咱們再來看下咱們的app.js中的這句代碼就能夠理解了:

router.get('/', ctx => {
  ctx.body = '<h1>歡迎光臨index page 頁面</h1>';
});

router.get('/home', ctx => {
  ctx.body = '<h1>歡迎光臨home頁面</h1>';
});

如上代碼 router 是 Router的實列返回的對象,Router的原型對象上會有 get, post,put .... 等等這樣的方法,所以咱們可使用 router.get('/', ctx => {}). 這樣添加一個或多個路由了。

其中咱們的 Router的原型上(property)的get/post 等方法會有三個參數,第一個參數name,咱們能夠理解爲字符串,它能夠理解爲路由的路徑(好比上面的 '/', 或 '/home') 這樣的。第二個參數 path 就是咱們的函數了。該函數返回了 ctx對象。咱們能夠作個簡單的打印,以下方法內部:

Router.prototype[method] = function (name, path, middleware) {
  console.log(name);
  console.log(path);
  console.log('-----');
  console.log(middleware);
  console.log(1111111);
}

當咱們 node app.js 從新執行的時候,在node命令行中,能夠看到以下打印信息:

能夠看到,咱們 router.get('/', ctx => {}) 添加路由的時候,console.log(name); 打印的是 '/'; console.log(path); 打印的是 [Function], console.log(middleware); 打印的就是 undefined了。當咱們添加 home路由的時候 router.get('/home', ctx => {}), console.log(name) 打印的是 '/home'了,console.log(path); 打印的是 [Function], console.log(middleware); 打印的也是 undefined了。

如上分析咱們能夠看到 Router中的各個方法已經能夠理解添加路由了,下面咱們繼續看下該方法的內部代碼是如何判斷的?代碼以下:

Router.prototype[method] = function (name, path, middleware) {
  var middleware;

  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.get('user', '/users/:id', (ctx, next) => {
  ctx.body = 'hello world';
});
const r = router.url('user', 3);
console.log(r); // 生成路由 /users/3

按照官網的解釋是:路由也能夠有names(名字),router.url 方法方便咱們在代碼中根據路由名稱和參數(可選)去生成具體的 URL。可能在開發環境中會使用到。所以會有如上兩種狀況。兩個參數或三個參數的狀況。

Router.prototype.register

所以 如上代碼if判斷,if (typeof path === 'string' || path instanceof RegExp) {} 若是path是一個字符串,或者是一個正則表達式的實列的話,就從第二個參數截取,也就是說從第二個參數後,或者說把第三個參數賦值給 middleware 這個參數。不然的話,若是path它是一個函數的話,那麼就從第一個參數去截取,也就是說把第二個參數賦值給 middleware 這個變量。而後 path = name; name = null; 最後咱們會調用 register 方法去註冊路由;下面咱們來看看下 register 方法的代碼以下:

/*
 * 該方法有四個參數
 * @param {String} path 路由的路徑
 * @param {String} methods 'get、post、put、'等對應的方法
 * @param {Function} middleware 該參數是一個函數。會返回ctx對象。
 * @param {opts} {name: name} 若是隻有兩個參數,該name值爲null。不然就有值。
*/
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,
    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;
};

Router.prototype.register 該方法是註冊路由的核心函數。該方法直接掛載在Router的原型上,所以咱們在router的實列上也能夠訪問到該方法。所以在使用實列咱們以前是這樣註冊路由的:

router.get('/home', ctx => {
  ctx.body = '<h1>歡迎光臨home頁面</h1>';
});

其實上面的代碼至關於以下代碼:

router.register('/home', ['GET'], [(ctx, next) => {}], {name: null}); 

這樣的代碼。咱們能夠從上面的代碼傳進來的參數能夠理解成如上的代碼了。

咱們繼續看如上源碼,首先會判斷path是不是一個數組,若是是一個數組的話,會使用遞歸的方式依次調用router.register 這個方法。最後返回this對象。所以以下代碼也是能夠的:

router.get(['/home', '/xxx', '/yyy'], ctx => {
  ctx.body = '<h1>歡迎光臨home頁面</h1>';
});

如上 path路徑是一個數組也是支持的,http://localhost:3001/home, http://localhost:3001/xxx, http://localhost:3001/yyy 訪問的都會返回 "歡迎光臨home頁面" 頁面的顯示。以下所示:

代碼繼續日後看,實例化Layer函數,該函數咱們晚點再來分析,咱們繼續往下看代碼:以下所示:

if (this.opts.prefix) {
  route.setPrefix(this.opts.prefix);
}

理解Router.prototype.prefix

會判斷opts對象是否有 prefix 的前綴的key,若是有的話就會調用 route.setPrefix() 方法。咱們先來看看prefix的做用是什麼。基本源代碼以下:

Router.prototype.prefix = function (prefix) {
  prefix = prefix.replace(/\/$/, '');

  this.opts.prefix = prefix;

  this.stack.forEach(function (route) {
    route.setPrefix(prefix);
  });
  return this;
};

該prefix的做用就是給路由全局加前綴的含義;好比app.js改爲以下代碼:

const Koa = require('koa');
const app = new Koa();

const router = require('koa-router')({
  prefix: '/prefix'
});
// 添加路由
router.get('/', ctx => {
  ctx.body = '<h1>歡迎光臨index page 頁面</h1>';
});

router.get('/home', ctx => {
  ctx.body = '<h1>歡迎光臨home頁面</h1>';
});

router.get('user', '/users/:id', (ctx, next) => {
  ctx.body = 'hello world';
});
const r = router.url('user', 3);
console.log(r); // 生成路由 /users/3

// 加載路由中間件
app.use(router.routes());

app.use(router.allowedMethods());

app.listen(3001, () => {
  console.log('server is running at http://localhost:3001');
});

如今當咱們繼續訪問 http://localhost:3001/home 或 http://localhost:3001/ 的時候,頁面是訪問不到的,若是咱們加上前綴 '/prefix' 是能夠訪問的到的,如 http://localhost:3001/prefix/home 或 http://localhost:3001/prefix。其中代碼 route.setPrefix(this.opts.prefix);中的setPrefix的方法中的route就是new Layer的實列對象了,所以setPrefix的方法就是在Layer.js 裏面,setPrefix方法以下所示:

Layer.prototype.setPrefix

Layer.prototype.setPrefix = function (prefix) {
  if (this.path) {
    this.path = prefix + this.path;
    this.paramNames = [];
    this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
  }

  return this;
};

如上代碼能夠看到,若是有路由路徑的話,this.path = prefix + this.path; 所以路由路徑發生改變了。而後把路徑 使用 pathToRegExp 轉換成正則表達式保存到 this.regexp 中。最後返回Layer對象。

想要了解 pathToRegExp,能夠看我以前的一篇文章,瞭解pathToRegExp

下面咱們來看下Layer.js 代碼的結構以下:

var debug = require('debug')('koa-router');
var pathToRegExp = require('path-to-regexp');
var uri = require('urijs');

module.exports = Layer;

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

};

Layer.prototype.match = function (path) {
  // ...
}

Layer.prototype.params = function (path, captures, existingParams) {
  // ...
}

Layer.prototype.captures = function (path) {
  // ...
}

Layer.prototype.url = function (params, options) {
  // ...
}

Layer.prototype.param = function (param, fn) {
  // ...
}

Layer.prototype.setPrefix = function (prefix) {
  // ...
}

Layer.js 如上代碼結構晚點再折騰,咱們仍是回到 router.js中的register函數代碼上了;

/*
 * 該方法有四個參數
 * @param {String} path 路由的路徑
 * @param {String} methods 'get、post、put、'等對應的方法
 * @param {Function} middleware 該參數是一個函數。會返回ctx對象。
 * @param {opts} {name: name} 若是隻有兩個參數,該name值爲null。不然就有值。
*/
Router.prototype.register = function (path, methods, middleware, opts) {
  // 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
  });
}

在該函數內部會引用 Layer.js 進來,而後實列化該對象。所以咱們能夠理解Layer.js 的做用是:

注意:Layer類的做用能夠理解爲,建立一個實列對象來管理每個路由。也就是說每個路由都會實例化一個Layer對象。

注意:如上opts中的參數像 end、sensitive、strict、ignoreCaptures等這些參數是pathToRegExp庫中參數用法。
咱們能夠從opts這個配置上傳入進來後,在Layer.js 中會調用 pathToRegExp 將路徑字符串轉換爲正則表達式時,會將該這些參數傳入到 pathToRegExp 這個js中去。

所以對於app.js 中註冊路由這段代碼來說 router.get('/', ctx => {});的話,註冊路由實例化Layer對象。

/*
 path: '/',
 methods: 'GET',
 middleware: [Function]
*/
var route = new Layer(path, methods, middleware, {
  end: false,
  name: null,
  sensitive: false,
  strict: false,
  prefix: '',
  ignoreCaptures: opts.ignoreCaptures = undefined
});

就會調用Layer.js 的構造函數 Layer, 以下代碼:

/**
 * Initialize a new routing Layer with given `method`, `path`, and `middleware`.
 *
 * @param {String|RegExp} path Path string or regular expression.
 * @param {Array} methods Array of HTTP verbs.
 * @param {Array} middleware Layer callback/middleware or series of.
 * @param {Object=} opts
 * @param {String=} opts.name route name
 * @param {String=} opts.sensitive case sensitive (default: false)
 * @param {String=} opts.strict require the trailing slash (default: false)
 * @returns {Layer}
 * @private
 */

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

  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;
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);

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

如上是Layer.js 代碼,this.methods === Layer.methods 保存了全部http的方法,若是是Get請求的話,最後 this.methods = ['HEAD', 'GET'] 了。this.stack === Layer.stack 則保存的是 咱們的函數,以下:

// 添加路由
router.get('/', ctx => {
  ctx.body = '<h1>歡迎光臨index page 頁面</h1>';
});

中的 function(ctx) {} 這個函數了。最後 經過 pathToRegExp.js 會將咱們的路由字符串轉換成正則表達式,保存到 this.regexp === Layer.regexp 變量中。

如今咱們再回到 router.js中的 Router.prototype.register 方法中,再接着執行以下代碼:

// add parameter middleware
Object.keys(this.params).forEach(function (param) {
  route.param(param, this.params[param]);
}, this);

stack.push(route);

return route;

如上代碼,目前的 this.params 仍是 {}. 所以不會遍歷進去。最後代碼:stack.push(route); 含義是把當前的Layer對象的實列保存到 this.stack中。

注意:
1. Router.stack 的做用是保存每個路由,也就是Layer的實列對象。
2. Layer.stack 的做用是 保存的是 每一個路由的回調函數中間件。
二者的區別是:一個路由能夠添加多個回調函數的。

最後代碼返回了 route,也就是反回了 Layer對象的實列。

三:加載路由中間件

在app.js 中的代碼,以下調用:

// 加載路由中間件
app.use(router.routes());

如上代碼,咱們能夠分紅二步,第一步是:router.routes(); 這個方法返回值,再把返回值傳給 app.use(); 調用便可加載路由中間件了。所以咱們首先來看第一步:router.routes()方法內部到底作了什麼事情了。以下代碼:
理解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;

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

    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.prototype.routes = Router.prototype.middleware; router.routes 的別名也叫 router.middleware. 當咱們使用 router.routes() 調用的時候,返回了一個dispatch函數,dispatch.router = this;  Router實列對象也是dispatch中的router屬性。返回了一個dispatch函數後,咱們就使用 app.use(router.routes); 會將路由模塊添加到koa的中間件處理機制了。koa的中間件機制是以一個函數存在的,所以咱們routes函數也就返回了一個函數。

具體想要了解koa中的洋蔥型模型,能夠看我這篇文章。

app.use(fn); 會將全部的中間件函數存放到 this.middleware 數組中,當咱們在app.js中使用 app.listen()方法的時候,以下代碼:

app.listen(3001, () => {
  console.log('server is running at http://localhost:3001');
});

koa中的部分 listen方法代碼以下:

listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

最後當咱們在瀏覽器中訪問 http://localhost:3001/prefix/home 時候 會自動執行路由中的dispatch函數了。

咱們再回到 Router.prototype.routes = Router.prototype.middleware = function () {} 中的dispatch函數,看看該函數內部作了什麼事情了。

該dispatch 函數有兩個參數 ctx 和 next. 這兩個參數是koa中的基本知識,就很少介紹該兩個參數了。

var router = this; 保存Router實列對象。var path = router.opts.routerPath || ctx.routerPath || ctx.path; 這句代碼就拿到了 路由字符串了,好比當咱們訪問 http://localhost:3001/prefix/home 時候,ctx.path 就返回了 '/prefix/home'; 接着執行 var matched = router.match(path, ctx.method); 代碼,會進行路由匹配。
理解Router.prototype.match:

router.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) {
  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);
    // 這裏是使用由路由字符串生成的正則表達式判斷當前路徑是否符合該正則
    if (layer.match(path)) {

      // 將對應的 Layer 實例加入到結果集的 path 數組中
      matched.path.push(layer);

      // 若是對應的 layer 實例中 methods 數組爲空或者數組中有找到對應的方法
      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        // 將 layer 放入到結果集的 pathAndMethod 中
        matched.pathAndMethod.push(layer);
        if (layer.methods.length) matched.route = true;
      }
    }
  }

  return matched;
};

var matched = router.match(path, ctx.method); 調用該方法,會傳入兩個參數,第一個參數就是路由字符串 '/prefix/home'; 第二個參數 ctx.method, 也就是 'get' 方法。在match方法內部。this.stack === router.stack了,保存了每一個路由的實列對象,咱們能夠打印下 this.stack, 它的值是以下所示:

[ Layer {
    opts:
     { end: true,
       name: null,
       sensitive: false,
       strict: false,
       prefix: '/prefix',
       ignoreCaptures: undefined },
    name: null,
    methods: [ 'HEAD', 'GET' ],
    paramNames: [],
    stack: [ [Function] ],
    path: '/prefix/',
    regexp: { /^\/prefix(?:\/(?=$))?$/i keys: [] } },
  Layer {
    opts:
     { end: true,
       name: null,
       sensitive: false,
       strict: false,
       prefix: '/prefix',
       ignoreCaptures: undefined },
    name: null,
    methods: [ 'HEAD', 'GET' ],
    paramNames: [],
    stack: [ [Function] ],
    path: '/prefix/home',
    regexp: { /^\/prefix\/home(?:\/(?=$))?$/i keys: [] } },
  Layer {
    opts:
     { end: true,
       name: 'user',
       sensitive: false,
       strict: false,
       prefix: '/prefix',
       ignoreCaptures: undefined },
    name: 'user',
    methods: [ 'HEAD', 'GET' ],
    paramNames:
     [ { name: 'id',
         prefix: '/',
         delimiter: '/',
         optional: false,
         repeat: false,
         partial: false,
         asterisk: false,
         pattern: '[^\\/]+?' } ],
    stack: [ [Function] ],
    path: '/prefix/users/:id',
    regexp:
     { /^\/prefix\/users\/((?:[^\/]+?))(?:\/(?=$))?$/i
       keys:
        [ { name: 'id',
            prefix: '/',
            delimiter: '/',
            optional: false,
            repeat: false,
            partial: false,
            asterisk: false,
            pattern: '[^\\/]+?' } ] } } ]

能夠看到數組中有三個Layer對象,那是由於咱們註冊了三次路由,好比咱們的app.js代碼以下:

// 添加路由
router.get('/', ctx => {
  ctx.body = '<h1>歡迎光臨index page 頁面</h1>';
});

router.get('/home', ctx => {
  ctx.body = '<h1>歡迎光臨home頁面</h1>';
});

router.get('user', '/users/:id', (ctx, next) => {
  ctx.body = 'hello world';
});

註冊了多少次路由,Layer類就會實例化多少次,而咱們的Router.stack就是保存的是Layer實例化對象。保存的值是如上所示:

而後就遍歷循環 this.stack了。若是 if (layer.match(path)) {},若是其中一個Layer對象匹配到該路由路徑的話,就把該Layer對象存入到 matched.path.push(layer);matched對象中的path數組中了。具體的含義能夠看上面的代碼註釋。

經過上面返回的結果集, 咱們知道一個請求來臨的時候, 咱們可使用正則來匹配路由是否符合, 而後在 path 數組或者 pathAndMethod 數組中找到對應的 Layer 實例對象. 咱們再回到 Router.prototype.routes = function() {} 中的以下代碼:

if (ctx.matched) {
  ctx.matched.push.apply(ctx.matched, matched.path);
} else {
  ctx.matched = matched.path;
}

默認ctx.matched 爲undefined,所以使用 matched.path 賦值該 ctx.matched了。當咱們在瀏覽器訪問:http://localhost:3001/prefix/home 時候,那麼就會在match函數內部匹配到 '/prefix/home' 路由了,所以:matched.path 返回的值以下:

[ Layer {
    opts:
     { end: true,
       name: null,
       sensitive: false,
       strict: false,
       prefix: '/prefix',
       ignoreCaptures: undefined },
    name: null,
    methods: [ 'HEAD', 'GET' ],
    paramNames: [],
    stack: [ [Function] ],
    path: '/prefix/home',
    regexp: { /^\/prefix\/home(?:\/(?=$))?$/i keys: [] } } ]

最終 ctx.matched 值也就是上面的值了。

ctx.router = router; 代碼,也就是說把router對象掛載到 ctx中的router對象了。

if (!matched.route) return next(); 該代碼的含義是:若是沒有匹配到對應的路由的話,則直接跳過以下代碼,執行下一個中間件。
以下三句代碼的含義:

var matchedLayers = matched.pathAndMethod
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path;

matchedLayers 值是:

[ Layer {
    opts:
     { end: true,
       name: null,
       sensitive: false,
       strict: false,
       prefix: '/prefix',
       ignoreCaptures: undefined },
    name: null,
    methods: [ 'HEAD', 'GET' ],
    paramNames: [],
    stack: [ [Function] ],
    path: '/prefix/home',
    regexp: { /^\/prefix\/home(?:\/(?=$))?$/i keys: [] } } ]

所以 mostSpecificLayer 也是上面的值哦;而後 ctx._matchedRoute = mostSpecificLayer.path = '/prefix/home' 了。
接着代碼判斷:

if (mostSpecificLayer.name) {
  ctx._matchedRouteName = mostSpecificLayer.name;
}

如上咱們能夠看到 mostSpecificLayer.name 爲null,所以就不會進入if內部語句代碼。固然若是改對象的name不爲null的話,就會把該對應的name值保存到ctx對象上的_matchedRouteName屬性上了。

接着代碼以下

/*
 該函數的主要思想是:構建路徑對應路由的處理中間件函數數組,
 在每一個匹配的路由對應的中間件處理函數數組前添加一個用於處理。
*/
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);

理解 koa-compose 的思想,能夠看這篇文章

它的做用是將多箇中間件函數合併成一箇中間件函數,而後執行該函數。

matchedLayers.reduce中的reduce是將接收一個函數做爲累加器,數組中的每一個值(從左到右)開始縮減,最終計算爲一個值。

Router.prototype.allowedMethod 方法的做用就是用於處理請求的錯誤。 具體的能夠看下源碼,已經很晚了,Layer.js內部也有一些方法還未講解到,你們有空能夠去折騰下。koa-router源碼先分析到這裏了。

相關文章
相關標籤/搜索