上面兩張圖主要將
koa-router
的總體代碼結構和大概的執行流程畫了出來,畫的不夠具體。那下面主要講koa-router
中的幾處的關鍵代碼解讀一下。javascript
讀代碼首先要找到入口文件,那幾乎全部的node
模塊的入口文件都會在package.json
文件中的main
屬性指明瞭。koa-router
的入口文件就是lib/router.js
。java
首先先講幾個第三方的node模塊瞭解一下,由於後面的代碼講解中會用到,不去看具體實現,只要知道其功能就行:
koa-compose:
提供給它一箇中間件數組, 返回一個順序執行全部中間件的執行函數。
methods:
node中支持的http動詞,就是http.METHODS,能夠在終端輸出看看。
path-to-regexp:
將路徑字符串轉換成強大的正則表達式,還能夠輸出路徑參數。node
Router
和 Layer
分別是兩個構造函數,分別在router.js
和 layer.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.stack
和Router
中的不一樣,這裏的是路由全部的中間件的數組。(一個路由能夠有多箇中間件)
做用:註冊路由
從上一篇的代碼結構圖中能夠看出,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; };
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; }; });
做用:啓動路由
這是在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); };
做用: 當請求出錯時的處理邏輯
一樣也是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 code
: 404
, 501
, 204
, 405
那這個函數其實就是當全部中間件函數執行完了,而且請求出錯了進行相應的處理:
若是請求的方法koa-router不支持而且沒有設置throw
選項,則返回 501(未實現)
若是是options
請求,則返回 204(無內容)
若是請求的方法支持但沒有設置throw
選項,則返回 405(不容許此方法 )
粗略淺析了這麼些,能大概知道了koa-router的工做原理。筆者能力有限,有錯誤還請指出。