前一篇文章介紹了 fastify
經過 schema 來序列化 JSON,爲 Node.js 服務提高性能的方法。今天的文章會介紹 fastify
使用的路由庫,翻閱其源碼(lib/route.js
)能夠發現,fastify
的路由庫並非內置的,而是使用了一個叫作 find-my-way
的路由庫。node
這個路由庫的簡介也頗有意思,號稱「超級無敵快」的 HTTP 路由。git
看上去 fastify
像是依賴了第三方的路由庫,其實這兩個庫的做者是同一批人。github
find-my-way
經過 on
方法綁定路由,而且提供了 HTTP 全部方法的簡寫。web
const router = require('./index')() router.on('GET', '/a', (req, res, params) => { res.end('{"message": "GET /a"}') }) router.get('/a/b', (req, res, params) => { res.end('{"message": "GET /a/b"}') }))
其實內部就是經過遍歷全部的 HTTP 方法名,而後在原型上擴展的。算法
Router.prototype.on = function on (method, path, opts, handler) { if (typeof opts === 'function') { // 若是 opts 爲函數,表示此時的 opts 爲 handler handler = opts opts = {} } // ... } for (var i in http.METHODS) { const m = http.METHODS[i] const methodName = m.toLowerCase() // 擴展方法簡寫 Router.prototype[methodName] = function (path, handler) { return this.on(m, path, handler) } }
綁定的路由能夠經過 lookup
調用,只要將原生的 req 和 res 傳入 lookup 便可。數據結構
const http = require('http') const server = http.createServer((req, res) => { // 只要將原生的 req 和 res 傳入 lookup 便可 router.lookup(req, res) }) server.listen(3000)
find-my-way
會經過 req.method
/req.url
找到對應的 handler,而後進行調用。框架
Router.prototype.lookup = function lookup (req, res) { var handle = this.find(req.method, sanitizeUrl(req.url)) if (handle === null) { return this._defaultRoute(req, res, ctx) } // 調用 hendler return handle.handler(req, res, handle.params) }
路由的添加和查找都基於樹結構來實現的,下面咱們來看看具體的實現。koa
find-my-way
採用了名爲 Radix Tree
(基數樹) 的算法,也被稱爲 Prefix Tree
(前綴樹)。Go 語言裏經常使用的 web 框架echo和gin都使用了Radix Tree
做爲路由查找的算法。函數
在計算機科學中,基數樹,或稱壓縮前綴樹,是一種更節省空間的Trie( 前綴樹)。對於基數樹的每一個節點,若是該節點是肯定的子樹的話,就和父節點合併。
在 find-my-way
中每一個 HTTP 方法(GET
、POST
、PUT
...)都會對應一棵前綴樹。性能
// 方法有所簡化... function Router (opts) { opts = opts || {} this.trees = {} this.routes = [] } Router.prototype.on = function on (method, path, opts, handler) { if (typeof opts === 'function') { // 若是 opts 爲函數,表示此時的 opts 爲 handler handler = opts opts = {} } this._on(method, path, opts, handler) } Router.prototype._on = function on (method, path, opts, handler) { this.routes.push({ method, path, opts, handler, }) // 調用 _insert 方法 this._insert(method, path, handler) } Router.prototype._insert = function _insert (method, path, handler) { // 取出方法對應的 tree var currentNode = this.trees[method] if (typeof currentNode === 'undefined') { // 首次插入構造一個新的 Tree currentNode = new Node({ method }) this.trees[method] = currentNode } while(true) { // 爲 currentNode 插入新的節點... } }
每一個方法對應的樹在第一次獲取不存在的時候,都會先建立一個根節點,根節點使用默認字符(/
)。
每一個節點的數據結構以下:
// 只保留了一些重要參數,其餘的暫時忽略 function Node(options) { options = options || {} this.prefix = options.prefix || '/' // 去除公共前綴以後的字符,默認爲 / this.label = this.prefix[0] // 用於存放其第一個字符 this.method = options.method // 請求的方法 this.handler = options.handler // 請求的回調 this.children = options.children || {} // 存放後續的子節點 }
當咱們插入了幾個路由節點後,樹結構的具體構造以下:
router.on('GET', '/a', (req, res, params) => { res.end('{"message":"hello world"}') }) router.on('GET', '/aa', (req, res, params) => { res.end('{"message":"hello world"}') }) router.on('GET', '/ab', (req, res, params) => { res.end('{"message":"hello world"}') })
Node { label: 'a', prefix: 'a', method: 'GET', children: { a: Node { label: 'a', prefix: 'a', method: 'GET', children: {}, handler: [Function] }, b: Node { label: 'b', prefix: 'b', method: 'GET', children: {}, handler: [Function] } }, handler: [Function] }
若是咱們綁定一個名爲 /axxx
的路由,爲了節約內存,不會生成三個 label 爲x
的節點,只會生成一個節點,其 label 爲 x
,prefix 爲 xxx
。
router.on('GET', '/a', (req, res, params) => { res.end('{"message":"hello world"}') }) router.on('GET', '/axxx', (req, res, params) => { res.end('{"message":"hello world"}') })
Node { label: 'a', prefix: 'a', method: 'GET', children: { a: Node { label: 'x', prefix: 'xxx', method: 'GET', children: {}, handler: [Function] } }, handler: [Function] }
經過以前的代碼能夠看到, on
方法最後會調用內部的 _insert
方法插入新的節點,下面看看其具體的實現方式:
Router.prototype._insert = function _insert (method, path, handler) { // 取出方法對應的 tree var currentNode = this.trees[method] if (typeof currentNode === 'undefined') { // 首次插入構造一個新的 Tree currentNode = new Node({ method }) this.trees[method] = currentNode } var len = 0 var node = null var prefix = '' var prefixLen = 0 while(true) { prefix = currentNode.prefix prefixLen = prefix.length len = prefixLen path = path.slice(len) // 查找是否存在公共前綴 node = currentNode.findByLabel(path) if (node) { // 公共前綴存在,複用 currentNode = node continue } // 公共前綴不存在,建立一個 node = new Node({ method: method, prefix: path }) currentNode.addChild(node) } }
插入節點會調用 Node 原型上的 addChild
方法。
Node.prototype.getLabel = function () { return this.prefix[0] } Node.prototype.addChild = function (node) { var label = node.getLabel() // 取出第一個字符作爲 label this.children[label] = node return this }
本質是遍歷路徑的每一個字符,而後判斷當前節點的子節點是否已經存在一個節點,若是存在就繼續向下遍歷,若是不存在,則新建一個節點,插入到當前節點。
find-my-way
對外提供了 lookup
方法,用於查找路由對應的方法並執行,內部是經過 find
方法查找的。
Router.prototype.find = function find (method, path, version) { var currentNode = this.trees[method] if (!currentNode) return null while (true) { var pathLen = path.length var prefix = currentNode.prefix var prefixLen = prefix.length var len = prefixLen var previousPath = path // 找到了路由 if (pathLen === 0 || path === prefix) { var handle = currentNode.handler if (handle !== null && handle !== undefined) { return { handler: handle.handler } } } // 繼續向下查找 path = path.slice(len) currentNode = currentNode.findChild(path) } } Node.prototype.findChild = function (path) { var child = this.children[path[0]] if (child !== undefined || child.handler !== null)) { if (path.slice(0, child.prefix.length) === child.prefix) { return child } } return null }
查找節點也是經過遍歷樹的方式完成的,找到節點以後還須要放到 handle 是否存在,存在的話須要執行回調。
本文主要介紹了 fastify
的路由庫經過 Radix Tree
進行提速的思路,相比於其餘的路由庫經過正則匹配(例如 koa-router 就是經過 path-to-regexp 來解析路徑的),效率上仍是高不少的。