本文主要探索:路径转正则的库
path-to-regexp
一、开始之前
在开始之前,我们要先聊一聊 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 实现一个简单的路由匹配。最后希望能够帮助到大家。