Express 路径转正则 path-to-regexp

本文主要探索:路径转正则的库 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

从抽象的地址到具体的地址,本质就是 :idparams.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");
});

大致分为三个部分:

  1. 定义路由
  2. 路由匹配与处理
  3. 启动服务

这是一个简单的路由匹配,但是也能够帮助我们理解 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 实现一个简单的路由匹配。最后希望能够帮助到大家。

相关推荐
confiself6 分钟前
deer-flow前端分析
前端
刘宇琪7 分钟前
Vite 生产环境代码分割与懒加载优化
前端
mldlds17 分钟前
Spring Boot应用关闭分析
java·spring boot·后端
zjjsctcdl19 分钟前
Spring Boot与MyBatis
spring boot·后端·mybatis
恋猫de小郭19 分钟前
让你的 OpenClaw 带你学习,清华开源 AI 私人导师 OpenMAIC
前端·人工智能·ai编程
石小石Orz29 分钟前
AI焦虑下,前端该何去何从
前端
何中应30 分钟前
<el-tree>标签问题
前端·vue.js·elementui
坚持学习前端日记30 分钟前
ComfyUI模型管理与集成方案
前端·人工智能·python
En^_^Joy33 分钟前
JavaScript Web API:DOM操作全解析
开发语言·前端·javascript