本文兩個目的
path-to-regexp用法簡介。
想一想若是咱們要識別路由的話,咱們能夠怎麼作?
最直觀確定是路徑字符串全匹配javascript
'/string' => '/string'
當路由全匹配 /string 的時候咱們能夠作出一些反饋操做。例如執行一個callback等。java
咱們還能夠利用正則匹配特性node
這樣子匹配模式顯然可操做方式更多元,匹配路徑也更多git
例如對路徑path:github
/^\/string\/.*?\/xixi$ // => '/string/try/xixi'
path-to-regexp就是一種這樣的工具
試想一下若是咱們要對路徑解析匹配,咱們須要本身再去寫正則表達式。從而達到匹配效果。正則表達式
能夠寫嗎?後端
確定能夠,但是太費時了。api
path-to-regexp 它能夠幫助咱們簡單地完成這種操做。數組
how to use it ???
const pathToRegexp = require('path-to-regexp') // pathToRegexp(path, keys?, options?) // pathToRegexp.parse(path) // pathToRegexp.compile(path)
// pathToRegexp(path, keys?, options?) // path 能夠是string/字符串數組/正則表達式 // keys 存放路徑中找到的鍵數組 // options 是一些匹配規則的填充 例如是否爲全匹配 分割符等
// 一個demo 若是咱們要實現正常的匹配某些鍵值 eg: /user/:name 咱們實現這樣子的正則如何實現 前部是全匹配,後部用正則分組提取值 eg: /\/user\/((?!\/).*?)\/?$/.exec('/user/zwkang') 查找匹配正則的字符串 返回一個數組/無值返回一個null pathToRegexp就是乾的這個活。生成須要的正則表達式匹配。固然裏面還有一些封裝操做,可是本質就是乾的這個。
pathToRegexp('/user/:name').exec('/user/zwkang') path option ? 表示無關緊要 pathToRegexp('/:foo/:bar?').exec('/test') pathToRegexp('/:foo/:bar?').exec('/test/route') * 表明來多少均可以 + 表明一個或者多個 仔細看你能夠發現 這些詞跟正則中的量詞幾乎一致 也能夠匹配未命名參數 存儲keys時會根據序列下標存儲 同時也支持正則表達式 parse方法 對path 生成匹配的tokens數組 也就是上文的keys數組 方法適用於string類型 Compile 方法 用compile傳入一個path 返回一個能夠填充的函數 生成與path匹配的值 pathToRegexp.compile('/user/:id')({id: 123}) => "/user/123" 適用於字符串 pathToRegexp.tokensToRegExp(tokens, keys?, options?) pathToRegexp.tokensToFunction(tokens) 名字上能夠看出 一個將tokens數組轉化爲正則表達式 一個將tokens數組轉化爲compile方法生成的函數
pathToRegexp =返回=> regexp parse =解析=> path =匹配tokens=> keys token compile => path => generator function => value => full path string
不知道你是否曾使用過koa-router服務器
notic: 注意如今的koa-router的維護權限變動問題
router實現實際上也是一種基於正則的訪問路徑匹配。
例子:
匹配路徑/simple 返回一個body爲 {name:'zwkang'}的body string
一個簡單的例子,如
假設咱們匹配路由 使用一個簡單的中間件匹配ctx.url app.use(async (ctx, next) => { const url = ctx.url if(/^\/simple$/i.test(url)) { ctx.body = { name: 'ZWkang' } } else { ctx.body = { errorCode: 404, message: 'NOT FOUND' } ctx.status = 404 } return await next() }) 測試代碼 describe('use normal koa path', () => { it('use error path', (done) => { request(http.createServer(app.callback())) .get('/simple/s') .expect(404) .end(function (err, res) { if (err) return done(err); expect(res.body).to.be.an('object'); expect(res.body).to.have.property('errorCode', 404) done(); }); }) it('use right path', (done) => { request(http.createServer(app.callback())) .get('/simple') .expect(200) .end(function (err, res) { if (err) return done(err); expect(res.body).to.be.an('object'); expect(res.body).to.have.property('name', 'ZWkang') done(); }); }) })
以上咱們本身實現url的模式就是這樣,單一的匹配,若是多元化匹配,甚至匹配參數,須要考慮正則的書寫。
缺點,較爲單一,設定方法較爲簡陋,功能弱小
// 一個簡單的用法 it('simple use should work', (done) => { router.get('/simple', (ctx, next) => { ctx.body = { path: 'simple' } }) app.use(router.routes()).use(router.allowedMethods()); request(http.createServer(app.callback())) .get('/simple') .expect(200) .end(function (err, res) { if (err) return done(err); expect(res.body).to.be.an('object'); expect(res.body).to.have.property('path', 'simple'); done(); }); })
上方測試代碼的一些點解釋
callback是koa的運行機制。方法表明了啥? 表明了其setup的過程
而咱們的經常使用listen方法 實際上也是調用了http.createServer(app.callback()) 這麼一步惟一
讓咱們來看看這koa-router到底作了些什麼
以上面簡單例子咱們能夠看出,理解koa運行機制,內部中間件處理模式。
調用koa時候調用的實例方法包括
router.allowedMethods ===> router.routes ===> router.get
考慮由於是koa,use調用,那麼咱們能夠確定是標準的koa中間件模式
返回的函數相似於
async (ctx, next) => { // 處理路由邏輯 // 處理業務邏輯 }
源碼的開頭註釋給咱們講述了基本的一些用法
咱們能夠簡單提煉一下
router.verb()
根據http方法指定對應函數
例如router.get().post().put()
.all 方法支持全部http 方法
當路由匹配時,ctx._matchedRoute能夠在這裏獲取路徑,若是他是命名路由,這裏能夠獲得路由名ctx._matchedRouteName
請求匹配的時候不會考慮querystring(?xxxx)
在開發時候能夠快速定位路由
* router.get('user', '/users/:id', (ctx, next) => { * // ... * }); * * router.url('user', 3); * // => "/users/3"
* router.get( * '/users/:id', * (ctx, next) => { * return User.findOne(ctx.params.id).then(function(user) { * ctx.user = user; * next(); * }); * }, * ctx => { * console.log(ctx.user); * // => { id: 17, name: "Alex" } * } * );
* var forums = new Router(); * var posts = new Router(); * * posts.get('/', (ctx, next) => {...}); * posts.get('/:pid', (ctx, next) => {...}); * forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods()); * * // responds to "/forums/123/posts" and "/forums/123/posts/123" * app.use(forums.routes());
var router = new Router({ prefix: '/users' }); router.get('/', ...); // responds to "/users" router.get('/:id', ...); // responds to "/users/:id"
router.get('/:category/:title', (ctx, next) => { console.log(ctx.params); // => { category: 'programming', title: 'how-to-node' } });
代碼設計上有些點挺巧妙
不妨先從layer文件理解。
前面說了,這個文件主要是用來處理對path-to-regexp庫的操做
文件只有300行左右 方法較少,直接截取方法作詳細解釋。
function Layer(path, methods, middleware, opts) { this.opts = opts || {}; this.name = this.opts.name || null; // 命名路由 this.methods = []; // 容許方法 // [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }] this.paramNames = []; this.stack = Array.isArray(middleware) ? middleware : [middleware]; // 中間件堆 // 初始化參數 // tips : forEach 第二個參數能夠傳遞this // forEach push數組之後 可使用數組[l-1]進行判斷末尾元素 // push方法返回值是該數組push後元素個數 // 外部method參數傳入內部 methods.forEach(function(method) { var l = this.methods.push(method.toUpperCase()); // 若是是GET請求 支持HEAD請求 if (this.methods[l-1] === 'GET') { this.methods.unshift('HEAD'); } }, this); // ensure middleware is a function // 保證每個middleware 爲函數 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; // 利用pathToRegExp 生成路徑的正則表達式 // 與params相關的數組回落入到咱們的this.paramNames中 // this.regexp一個生成用來切割的數組 this.regexp = pathToRegExp(path, this.paramNames, this.opts); debug('defined route %s %s', this.methods, this.opts.prefix + this.path); };
咱們能夠關注在輸入與輸出。
輸入:path, methods, middleware, opts
輸出:對象 屬性包括(opts, name, methods, paramNames, stack, path, regexp)
咱們以前說過了 layer是根據route path 作處理 判斷是否匹配,鏈接庫path-to-regexp,這一點很重要。
stack應該與傳入的middleware一致。stack是數組形式,以此可見咱們的path對應的route是容許多個的。
咱們接下來關注下
根據path-to-regexp結合自身須要的middleware, koa-router給咱們處理了什麼封裝
原型鏈上掛載方法有
// 獲取路由參數鍵值對 Layer.prototype.params = function (path, captures, existingParams) { var params = existingParams || {}; for (var len = captures.length, i=0; i<len; i++) { if (this.paramNames[i]) { // 得到捕獲組相對應 var c = captures[i]; // 得到參數值 params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c; // 填充鍵值對 } } // 返回參數鍵值對對象 return params; };
在構造函數初始化的時候,咱們生成this.regexp的時候經過傳入this.paramNames從而將其根據path解析出的param填出
輸入: 路徑,捕獲組,已存在的參數組
輸出: 一個參數鍵值對對象
處理方式很普通。由於params與captures 是位置相對應的。因此直接能夠循環便可。
// 判斷是否匹配 Layer.prototype.match = function (path) { return this.regexp.test(path); };
首先看的也是輸入值與返回值
輸入: path
輸出: 是否匹配的boolean
咱們能夠看這個this.regexp是屬性值,證實咱們是有能力隨時改變this.regexp 從而影響這個函數的返回值
// 返回參數值 Layer.prototype.captures = function (path) { if (this.opts.ignoreCaptures) return []; // 忽略捕獲返回空 // match 返回匹配結果的數組 // 從正則能夠看出生成的正則是一段全匹配。 /** * eg: * var test = [] * pathToRegExp('/:id/name/(.*?)', test) * * /^\/((?:[^\/]+?))\/name\/((?:.*?))(?:\/(?=$))?$/i * * '/xixi/name/ashdjhk'.match(/^\/((?:[^\/]+?))\/name\/((?:.*?))(?:\/(?=$))?$/i) * * ["/xixi/name/ashdjhk", "xixi", "ashdjhk"] */ return path.match(this.regexp).slice(1); // [value, value .....] };
輸入: path路徑
輸出: 捕獲組數組
返回整個捕獲組內容
Layer.prototype.url = function(params, options) { var args = params; console.log(this); var url = this.path.replace(/\(\.\*\)/g, ""); var toPath = pathToRegExp.compile(url); // var replaced; if (typeof params != "object") { args = Array.prototype.slice.call(arguments); if (typeof args[args.length - 1] == "object") { options = args[args.length - 1]; args = args.slice(0, args.length - 1); } } var tokens = pathToRegExp.parse(url); var replace = {}; if (args instanceof Array) { for (var len = tokens.length, i = 0, j = 0; i < len; i++) { if (tokens[i].name) replace[tokens[i].name] = args[j++]; } } else if (tokens.some(token => token.name)) { replace = params; // replace = params } else { options = params; // options = params } replaced = toPath(replace); // 默認狀況下 replace 是默認傳入的鍵值對 //匹配事後就是完整的url if (options && options.query) { // 是否存在query var replaced = new uri(replaced); // replaced.search(options.query); //添加query 路由查詢 return replaced.toString(); } return replaced; // 返回URL串 };
layer實例的url方法
實際上一個例如/name/:id
咱們解析後會得到一個{id: xxx}的params對象
根據/name/:id 跟params對象咱們是否是能夠反推出實際的url?
這個url方法提供的就是這種能力。
Layer.prototype.param = function(param, fn) { var stack = this.stack; var params = this.paramNames; var middleware = function(ctx, next) { return fn.call(this, ctx.params[param], ctx, next); }; middleware.param = param; var names = params.map(function(p) { return String(p.name); }); var x = names.indexOf(param); // 得到index if (x > -1) { stack.some(function(fn, i) { // param handlers are always first, so when we find an fn w/o a param property, stop here // if the param handler at this part of the stack comes after the one we are adding, stop here // 兩個策略 // 1. param處理器老是在最前面的,當前fn.param不存在。則直接插入 [a,b] mid => [mid, a, b] // 2. [mid, a, b] mid2 => [mid, mid2, a, b]保證按照params的順序排列 // 保證在正常中間件前 // 保證按照params順序排列 if (!fn.param || names.indexOf(fn.param) > x) { // 在當前注入中間件 stack.splice(i, 0, middleware); return true; // 中止some迭代。 } }); } return this; };
這個方法的做用是在當前的stack中添加針對單個param的處理器
實際上就是對layer的stack進行一個操做
Layer.prototype.setPrefix = function(prefix) { // 調用setPrefix至關於將layer的一些構造重置 if (this.path) { this.path = prefix + this.path; this.paramNames = []; this.regexp = pathToRegExp(this.path, this.paramNames, this.opts); } return this; };
對當前的path加上前綴而且重置當前的一些實例屬性
function safeDecodeURIComponent(text) { try { return decodeURIComponent(text); } catch (e) { return text; } }
保證safeDecodeURIComponent 不會拋出任何錯誤
layer的stack主要是存儲實際的middleware[s]。
主要的功能是針對pathToRegexp作設計。
提供能力給上層的Router作調用實現的。
Router主要是對上層koa框架的響應(ctx, status等處理),以及連接下層layer實例。
function Router(opts) { // 自動new if (!(this instanceof Router)) { return new Router(opts); } this.opts = opts || {}; // methods用於對後面allowedMethod作校驗的 this.methods = this.opts.methods || [ "HEAD", "OPTIONS", "GET", "PUT", "PATCH", "POST", "DELETE" ]; // 初始化http方法 this.params = {}; // 參數鍵值對 this.stack = []; // 存儲路由實例 }
methods.forEach(function(method) { // 給原型上附加全部http method 方法 Router.prototype[method] = function(name, path, middleware) { var middleware; // 兼容參數 // 容許path爲字符串或者正則表達式 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; } // 註冊到當前實例上 // 主要是設置一個通用的install middleware 的方法。(mark. tag: function) this.register(path, [method], middleware, { name: name }); // 鏈式調用 return this; }; });
給Router原型註冊上
http method 的方法,如:Router.prototype.get = xxx
當咱們使用實例的時候能夠更方便準確使用
router.get('name', path, cb)
這裏的middleware顯然是能夠多個。例如router.get(name, path, cb)
咱們能夠留意到,這裏的主要是調用了另外一個方法
notic:
register方法。而這個方法的入參,咱們能夠留意下。與Layer實例初始化入參極爲類似。
帶着疑惑咱們能夠進入到register方法內。
Router.prototype.register = function(path, methods, middleware, opts) { opts = opts || {}; var router = this; var stack = this.stack; if (Array.isArray(path)) { path.forEach(function(p) { router.register.call(router, p, methods, middleware, opts); }); return this; } var route = new Layer(path, methods, middleware, { end: opts.end === false ? opts.end : true, // 須要明確聲明爲end name: opts.name, // 路由的名字 sensitive: opts.sensitive || this.opts.sensitive || false, // 大小寫區分 正則加i strict: opts.strict || this.opts.strict || false, // 非捕獲分組 加(?:) prefix: opts.prefix || this.opts.prefix || "", // 前綴字符 ignoreCaptures: opts.ignoreCaptures || false // 給layer使用 忽略捕獲 }); 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); // 當前Router實例stack push單個layer實例 stack.push(route); return route; };
咱們能夠看到整個register方法,是設計給註冊單一路徑的。
針對多路徑在forEach調用register方法。這種寫法在koa-router實現裏並很多見。。
看了register方法,咱們的疑惑獲得了證明,果真入參大可能是用來初始化layer實例的。
初始化layer實例後,咱們將它放置到router實例下的stack中。
根據一些opts再進行處理判斷。很少大抵是無傷大雅的。
這樣一來咱們就知道了register的用法。
咱們知道咱們調用router實例時候。
要使用中間件 咱們每每須要完成兩步
咱們知道一個極簡的中間件調用形式老是
app.use(async (ctx, next) => { await next() })
咱們的無論koa-body 仍是koa-router
傳入app.use老是一個
async (ctx, next) => { await next() }
這樣的函數,是符合koa 中間件需求的。
帶着這樣的想法
咱們能夠來到routes方法中一探究竟。
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; // matched已是進行過處理了 得到了layer對象承載 var matched = router.match(path, ctx.method); var layerChain, layer, i; // 考慮多個router實例的狀況 if (ctx.matched) { // 由於matched老是一個數組 // 這裏用apply相似於concat ctx.matched.push.apply(ctx.matched, matched.path); } else { // 匹配的路徑 ctx.matched = matched.path; } // 當前路由 ctx.router = router; // 若是存在匹配的路由 if (!matched.route) return next(); // 方法與路徑都匹配的layer var matchedLayers = matched.pathAndMethod; // 最後一個layer var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]; // ctx._matchedRoute = mostSpecificLayer.path; // 若是layer存在命名 if (mostSpecificLayer.name) { ctx._matchedRouteName = mostSpecificLayer.name; } // 匹配的layer進行compose操做 // update capture params routerName等 // 例如咱們使用了多個路由的話。 // => ctx.capture, ctx.params, ctx.routerName => layer Stack[s] // => ctx.capture, ctx.params, ctx.routerName => next layer Stack[s] 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; };
咱們知道路由匹配的本質是實際路由與定義路徑相匹配。
那麼routes生成的中間件實際上就是在考慮作這種匹配的處理。
從返回值咱們能夠看到
=> dispatch方法。
這個dispacth方法實際上就是咱們前面說的極簡方式。
function dispatch(ctx, next) {}
能夠說是相差無幾。
咱們知道stack當前存儲的是多個layer實例。
而根據路徑的匹配,咱們能夠知道
一個後端路徑,簡單能夠分爲http方法,與路徑定義匹配。
例如:/name/:id
這個時候來了個請求/name/3
是否是匹配了。(params = {id: 3})
可是請求方法若是是get呢? 定義的這個/name/:id是個post的話。
則此時雖然路徑匹配,可是實際並不能徹底匹配。
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)) { //若是路徑匹配 matched.path.push(layer); // matched中壓入layer if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) { // 校驗方法 matched.pathAndMethod.push(layer); // 路徑與方法中都壓入layer if (layer.methods.length) matched.route = true; // 證實沒有支持的方法。route爲true 後面跳過中間件處理 } } } return matched; };
看看這個match方法吧。
對stack中的layaer進行判斷。
返回的matched對象中
path屬性: 僅僅路徑匹配便可。
pathAndMethod屬性: 僅僅http方法與路徑匹配便可。
route屬性: 須要layer的方法長度不爲0(有定義方法。)
因此dispatch中咱們首先
ctx.matched = matched.path
獲得路徑匹配的layer
實際中間件處理的,是http方法且路徑匹配的layer
這種狀況下。而實際上,所謂中間件就是一個個數組
它的堆疊方式多是多維的,也多是一維的。
若是一個route進行了匹配
ctx._matchedRoute表明了它的路徑。
這裏ctx._matchedRoute是方法且路徑匹配數組的layer的最後一個。
相信取最後一個你們也知道爲何。多個路徑,除開當前處理,在下一個中間件處理時候,老是返回最後一個便可。
最後將符合的layer組合起來
例如 若是有多個layer的狀況下,layer也有多個stack的狀況下
// 例如咱們使用了多個路由的話。 // => ctx.capture, ctx.params, ctx.routerName => layer Stack[?s] // => ctx.capture, ctx.params, ctx.routerName => next layer Stack[?s]
運行順序就會如上所示
至關於在將多個layer實例的stack展平,且在每個layer實例前,添加ctx屬性進行使用。
最後用compose將這個展平的數組一塊兒拿來使用。
其實在這裏咱們能夠留意到,所謂的中間件也不過是一堆數組罷了。
可是這裏的在每一個layer實例前使用ctx屬性卻是個不錯的想法。
對中間件的操做例如prefix等。就是不斷的對內部的stack位置屬性的調整。
Router.prototype.allowedMethods = function(options) { options = options || {}; var implemented = this.methods; // 返回一箇中間件用於 app.use註冊。 return function allowedMethods(ctx, next) { return next().then(function() { var allowed = {}; // 判斷ctx.status 或者狀態碼爲404 console.log(ctx.matched, ctx.method, implemented); if (!ctx.status || ctx.status === 404) { // routes方法生成的ctx.matched // 就是篩選出來的layer匹配組 ctx.matched.forEach(function(route) { route.methods.forEach(function(method) { allowed[method] = method; }); }); var allowedArr = Object.keys(allowed); // 實現了的路由匹配 if (!~implemented.indexOf(ctx.method)) { // 位運算符 ~(-1) === 0 !0 == true // options參數 throw若是爲true的話則直接扔出錯誤 // 這樣能夠給上層中間價作處理 // 默認是拋出一個HttpError if (options.throw) { var notImplementedThrowable; if (typeof options.notImplemented === "function") { notImplementedThrowable = options.notImplemented(); // set whatever the user returns from their function } else { notImplementedThrowable = new HttpError.NotImplemented(); } throw notImplementedThrowable; } else { // 不然跑出501 // 501=>服務器未實現方法 ctx.status = 501; ctx.set("Allow", allowedArr.join(", ")); } // 若是容許的話 } else if (allowedArr.length) { // 對options請求進行操做。 // options請求與get請求相似,可是請求沒有請求體 只有頭。 // 經常使用語查詢操做 if (ctx.method === "OPTIONS") { ctx.status = 200; ctx.body = ""; ctx.set("Allow", allowedArr.join(", ")); } else if (!allowed[ctx.method]) { // 若是容許方法 if (options.throw) { var notAllowedThrowable; if (typeof options.methodNotAllowed === "function") { notAllowedThrowable = options.methodNotAllowed(); // set whatever the user returns from their function } else { notAllowedThrowable = new HttpError.MethodNotAllowed(); } throw notAllowedThrowable; } else { // 405 方法不被容許 ctx.status = 405; ctx.set("Allow", allowedArr.join(", ")); } } } } }); }; };
這個方法主要是默認的給咱們路由中間件添加404 405 501的這些狀態控制。
咱們也能夠在高層中間件統一處理也能夠。
使用位運算符+indexOf也是一種常見的用法。
至此整篇的koa-router源碼基本就解析完畢了。
雖然Router的源碼還有不少方法本文沒有寫出,可是大多都是給上層提供layer實例的方法鏈接,歡迎到github連接從源碼處查看。
總的來講能吸取的點多是挺多的。
若是看完了整篇。