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 实现一个简单的路由匹配。最后希望能够帮助到大家。

相关推荐
千里码aicood8 分钟前
【2025】springboot教学评价管理系统(源码+文档+调试+答疑)
java·spring boot·后端·教学管理系统
BigYe程普15 分钟前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H32 分钟前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍35 分钟前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai39 分钟前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默1 小时前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
liuxin334455661 小时前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端
2401_857297911 小时前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_1 小时前
meta标签作用/SEO优化
前端·javascript·html
Ink1 小时前
从底层看 path.resolve 实现
前端·node.js