一、开始之前
在开始之前,我们要先聊一聊 restful
规范,也就是如何根据 url 从服务器获取资源。
1.1)以商品获取为例
sh
复制代码
GET /api/products
我们要通过 HTTP 协议的 GET 方法获取 url 在 /api
下的 products
列表。这很简单程序很容易识别固定的 url。
1.2)获取具体的内容
sh
复制代码
GET /api/product/1 GET /api/product/2 GET /api/product/3 GET /api/product/9999
我们看到使用枚举的方式也可以轻松的完成,但是这些枚举的路径在程序中都是可以重复的,可以抽象的。
我们可以认为:
/product/id
中 id 是一个传递的参数,是动态的。我们尝试将这个动态的用别的形式展示
/product/:id
/product/{id}
/product/$id
- ...
等等不同的表现形式。其中 express 就选择了第一种方式进行解析动态路径。
关联了 restful 规范和路径动态表现形式,下面就需要一些方式来解析路径,path-to-regexp 就一个应用于 express 中的路径解释库。下面我们就开始进入它讲解。
二、哪些库在使用 path-to-regexp
- express
- koa
- vue-router2
- nestjs
2.1)express 路径的写法
经常写 expressjs 或者类似框架小伙伴,可能都会写如下的代码:
sh
复制代码
app.get('/users/:id', (req, res) => { res.send(`获取用户 ${req.params.id}`); });
/users/:id
路径的写法,代表 id 是一个 动态参数
,可以匹配不同的 id,来经过这个路由函数进行处理。那么 express 如何如何在内部关联上 path-to-regexp 的呢?
2.2)express 在 Layer 使用使用
express 中 path-to-regexp 也只在 一个
地方用到了 Layer
类中。
js
复制代码
var pathRegexp = require('path-to-regexp'); function Layer(path, options, fn) { if (!(this instanceof Layer)) { return new Layer(path, options, fn); } // *** this.regexp = pathRegexp(path, this.keys = [], opts); // *** }
这样就简单了。path-to-regexp 与 layer 进行关联。所有使用实例化 layer 位置就需要解析 path 为 regexp。
2.3)nestjs 路径的写法
ts
复制代码
import { Controller, Get, Param } from '@nestjs/common'; @Controller('cats') export class CatsController { @Get(':id') findOne(@Param('id') id: string) { return `This action returns a #${id} cat`; } }
三、path-to-regexp API 设计
使用 Object.keys 快速获取 path-to-regexp
的 api。
js
复制代码
import * as ptr from 'path-to-regexp' const keys = Object.keys(ptr) console.log(keys) [ '__esModule', 'compile', 'default', 'match', 'parse', 'pathToRegexp', 'regexpToFunction', 'tokensToFunction', 'tokensToRegexp' ]
以下是 api 整体的输出情况:
项目 | 说明 |
---|---|
__esModule | 表示是一个 ES 模块 |
compile | 将路径字符串 编译为生成 URL 的函数 |
default | 默认 导出的模块或函数 |
match | 生成匹配 URL 的函数 matchFn |
parse | 将路径字符串 解析为令牌 数组 |
pathToRegexp | 将路径字符串 转换为正则表达式 ,并提取参数 |
regexpToFunction | 将正则表达式转换 为匹配 URL函数 |
tokensToFunction | 将令牌数组 转换为生成URL函数 |
tokensToRegexp | 将令牌数组 转换为正则表达式 |
四、pathToRegexp
pathToRegexp('/user/:id') -> regexpObject
pathToRegexp 迅速将 url 转换成正则。
js
复制代码
import { pathToRegexp } from 'path-to-regexp' const keys = []; const regexp = pathToRegexp('/user/:id', keys); console.log(regexp); // /^\/user\/([^\/]+?)\/?$/i console.log(keys);
有了正则就可以做其他的匹配事情了:
js
复制代码
regexp.exec('/user/23')
五、match 匹配函数
match('/user/:id') -> matchFn -> matchObject/false
match 函数用于创建一个新的匹配函数 matchFn,以下是一个获取用户 id 的例子:
sh
复制代码
const matchUserFn = match("/user/:id") const result = matchUserFn("/user/123") const userId = result.params.id // 123 const result = matchFn('/user/123/ad?d=123'); // false
从抽象的地址到具体的地址,本质就是 :id
与 params.id
的一个映射的过程。能够方便的以 js 对象的形式获取数据。当然 match 还是传入 options,对正则表达式进行约束,这里就不再赘述了。如果把 match 立即为正面的匹配的话,那么它的反面是如何呈现的呢?
六、compile
compile('/user/:id') -> toPath -> url
将对象转换 path, 与 match 相反 compile 是已知 id 需要将 id 转换成路径:
js
复制代码
const toPath = compile("/user/:id") const user = { id: 123 } const userPath = toPath(user) // /user/123
七、parse
parse('/user/:id') -> tokens
7.1)词法对象
path-to-regexp 实现 lexer 词法分析,使用 parse 对 url 进行词法分析产生 tokens,词法对象:
ts
复制代码
{ type: "MODIFIER" | "ESCAPED_CHAR" | "OPEN" || "CLOSE" || "NAME" || "PATTERN" || "CHAR" || "END", index, value, }
7.2)token 对象
parse 解析完毕词法之后,在此数组的基础上,将其解析为 tokens 数组,下面是 token 数组的大致数据形式:
js
复制代码
{ name: name || key++, prefix: prefix, suffix: "", pattern: pattern || defaultPattern, modifier: tryConsume("MODIFIER") || "", }
7.3)示例
js
复制代码
import { parse } from 'path-to-regexp' const tokens = parse('/user/:id'); console.log(tokens); /** * [ '/user', { name: 'id', prefix: '/', suffix: '', pattern: '[^\\/#\\?]+?', modifier: '' } ] */
八、tokenTo 相关转换函数
前面已经提到了词法解析与 tokens 的解析 path-to-regexp 也提供了两个方法用于奖励 token 与 url 和正则的转换关系
8.1)tokensToFunction token 转换成函数
parse('/user/:id') -> tokens -> tokensToFunction({ id: 123}) -> path_url
ts
复制代码
import { parse, tokensToFunction } from 'path-to-regexp' const tokens = parse('/user/:id'); const toPath = tokensToFunction(tokens); console.log(toPath({ id: 123 })); // /user/123
8.2)tokensToRegexp
parse('/user/:id') -> tokens -> tokensToRegexp -> regexp
ts
复制代码
import { parse, tokensToRegexp } from 'path-to-regexp'; const tokens = parse('/user/:id'); const keys = []; const regexp = tokensToRegexp(tokens, keys); console.log(regexp); // /^\/user\/([^\/]+?)\/?$/i console.log(keys); // [ { name: 'id', prefix: '/', ... }]
九、与 node.js http 模块
ts
复制代码
const http = require("http"); const { pathToRegexp } = require("path-to-regexp"); // 定义路由和处理函数 const routes = [ { method: "GET", path: "/", handler: (req, res) => { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello World!"); }, }, { method: "GET", path: "/user/:id", handler: (req, res, params) => { res.writeHead(200, { "Content-Type": "text/plain" }); res.end(`User ID is ${req.params.id}`); }, }, { method: "POST", path: "/submit", handler: (req, res) => { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Form submitted!"); }, }, ]; function matchRoute(req) { const route = routes.find((route) => { const keys = []; const re = pathToRegexp(route.path, keys); const result = re.exec(req.url); if (result && req.method === route.method) { req.params = {}; for (let i = 1; i < result.length; i++) { req.params[keys[i - 1].name] = result[i]; } return true; } return false; }); return route; } // 创建服务器 const server = http.createServer((req, res) => { // 找到匹配的路由 const route = matchRoute(req) // 执行路由处理函数 if (route) { route.handler(req, res); } else { res.writeHead(404, { "Content-Type": "text/plain" }); res.end("Not Found"); } }); // 启动服务器 server.listen(3000, () => { console.log("Server is running on port 3000"); });
大致分为三个部分:
- 定义路由
- 路由匹配与处理
- 启动服务
这是一个简单的路由匹配,但是也能够帮助我们理解 http 中路由匹配的基本用法。
十、如何构建
- 基于 typescript 编写
- 使用 @borderless/ts-scripts 进行构建
- 使用 size-limit 进行尺寸大小测试
十一、其他语言版本
语言 | 使用 |
---|---|
go | go get -u github.com/soongo/path-to-regexp |
rust | cargo add path2regex |
path-to-regexp-rust | |
python | python-repath |
十二、小结
本文从 restful 获取服务器资源的 url 开始到,express 中常用获取用户 id 的资源到 path-to-regexp 的api 设计使用方式,使用原生 Node.js + path-to-regexp 实现一个简单的路由匹配。最后希望能够帮助到大家。