Node 第二彈:Node 服務(wù)端應(yīng)用路由解析
大家好,我是山月。在上篇文章介紹了 HTTP 報(bào)文及簡(jiǎn)單的服務(wù)端框架要素,如如何接受請(qǐng)求參數(shù)等。這篇文章介紹另一個(gè)常見(jiàn)的主題:路由。
簡(jiǎn)單路由
最簡(jiǎn)單的路由可使用 req.url 進(jìn)行路由分發(fā)不同的邏輯,代碼如下所示。
但是對(duì)于一個(gè)非Demo式的頁(yè)面,業(yè)務(wù)邏輯都堆在一起,這顯得太為簡(jiǎn)陋。
- const http = require('http')
- const server = http.createServer((req, res) => {
- console.log(req.url)
- let data = ''
- if (req.url === '/') {
- data = 'hello, world'
- res.end(data)
- } else if (req.url === '/json') {
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
- data = JSON.stringify({ username: '山月' })
- res.end(data)
- } else if (req.url === '/input') {
- let body = ''
- req.on('data', chunk => body += chunk)
- req.on('end', () => {
- data = body
- res.end(data)
- })
- }
- })
- server.listen(3000)
復(fù)雜路由
作為一個(gè)能夠在生產(chǎn)環(huán)境使用,較為復(fù)雜的路由至少能夠解析以下路由,并為單獨(dú)路由配置單獨(dú)的業(yè)務(wù)邏輯處理函數(shù)
- Method: app.post('/', handler)
- Param Path: app.post('/users/:userId', handler)
基于正則路由
目前,絕大部分服務(wù)端框架的路由都是基于正則進(jìn)行匹配,如 koa、express 等。另外,前端框架的路由 vue-router 與 react-router 也是基于正則匹配。
而這些框架基于正則匹配的路由,都離不開(kāi)一個(gè)庫(kù): path-to-regexp,它將把一個(gè)路由如 /user/:name 轉(zhuǎn)化為正則表達(dá)式。
https://github.com/pillarjs/path-to-regexp
標(biāo)題:path-to-regexp
它的 API 十分簡(jiǎn)單:
- pathToRegexp: 可將路由轉(zhuǎn)化為正則表達(dá)式
- match: 可匹配參數(shù)
- const { pathToRegexp, match, parse, compile } = require('path-to-regexp')
- pathToRegexp('/api/users/:userId')
- //=> /^\/api\/users(?:\/([^\/#\?]+?))[\/#\?]?$/i
- const toParams = match('/api/users/:userId')
- toParams('/api/users/10')
- //=> {
- // index: 0
- // params: {userId: "12"}
- // path: "/api/users/12"
- // }
那這些 Node 服務(wù)器框架基于正則路由的原理是什么?
- 注冊(cè)路由。每一個(gè)路由都作為一個(gè) Layer (在 express、koa 中),并使用 path-to-regexp 把路由路徑轉(zhuǎn)化為正則,作為 Layer 的屬性。
- 匹配路由。當(dāng)一次請(qǐng)求來(lái)臨時(shí),對(duì)比路由表中每一條路由,找到匹配正則的多條路由,執(zhí)行多條路由所對(duì)應(yīng)的業(yè)務(wù)處理邏輯。
從上可以看出它沒(méi)進(jìn)行一次路由匹配的時(shí)間復(fù)雜度為: 「O(n) X 正則匹配復(fù)雜度」
基于正則路由的一些問(wèn)題
性能問(wèn)題先不談,先看一個(gè)問(wèn)題:
「當(dāng)我們請(qǐng)求 /api/users/10086,有兩條路由可供選擇: /api/users/10086 與 /api/users/:userId,此時(shí)將會(huì)匹配哪一條路由?」
以下是由 koa/koa-router 書(shū)寫(xiě), 「由于是正則匹配,此時(shí)極易出現(xiàn)路由沖突問(wèn)題,匹配路由時(shí)與順序極為相關(guān)。」
- const Koa = require("koa");
- const Router = require("@koa/router");
- const app = new Koa();
- const router = new Router();
- router.get("/api/users/10086", (ctx, next) => {
- console.log(ctx.router);
- ctx.body = {
- userId: 10086,
- direct: true
- };
- });
- router.get("/api/users/:userId", (ctx, next) => {
- console.log(ctx.router);
- ctx.body = {
- userId: ctx.params.userId
- };
- });
基于前綴樹(shù)路由 (Trie、Radix Tree、Prefix Tree)
相對(duì)于正則匹配路由而言,基于前綴樹(shù)匹配更加高效,且無(wú)上述路由沖突問(wèn)題。
- find-my-way
https://github.com/delvedor/find-my-way
標(biāo)題:find-my-way
- const http = require('http')
- const router = require('find-my-way')()
- const server = http.createServer((req, res) => {
- router.lookup(req, res)
- })
- router.on('GET', '/api', () => {})
- router.on('GET', '/api/users/:id', (req, res) => { res.end('id') })
- router.on('GET', '/api/users/10086', (req, res) => { res.end('10086') })
- router.on('GET', '/api/users-friends', () => {})
- console.log(router.prettyPrint())
- server.listen(3000)
在上述代碼中,將把所有路由路徑構(gòu)成前綴樹(shù)。前綴樹(shù),顧名思義,將會(huì)把字符串的公共前綴提取出來(lái)。
- └── /api (GET)
- └── /users
- ├── /
- │ ├── 10086 (GET)
- │ └── :id (GET)
- └── -friends (GET)
可以看出,前綴樹(shù)路由的匹配時(shí)間復(fù)雜度明顯小于 O(n),且每次不會(huì)有正則路由進(jìn)行正則匹配的復(fù)雜度。這決定了它相比正則路由更高的性能。
Node 中最快的框架 fastify,便是內(nèi)置了基于前綴樹(shù)的路由。
- const fastify = require('fastify')()
- fastify.get('/api/users/10086', async (request, reply) => {
- return { userId: 10086, direct: true }
- })
- fastify.get('/api/users/:id', async (request, reply) => {
- const id = request.params.id
- return { userId: id }
- })
- fastify.listen(3000)
405
在 HTTP 狀態(tài)碼中,與路由相關(guān)的狀態(tài)碼為 404、405,作為一個(gè)專業(yè)的路由庫(kù),實(shí)現(xiàn)一個(gè) 405 也是分內(nèi)之事。
- 301
- 302
- 307
- 308
- 404: Not Found
- 405: Method Not Allowed