Appearance
4. 词法分析:parse 函数的核心正则表达式
欢迎来到本书的核心实现部分。从本章开始,我们将深入 path-to-regexp 的源码世界,亲手构建我们自己的 mini-path-to-regexp。我们的第一站,是整个转换流程的入口——parse 函数,它的核心职责就是进行词法分析,将路径字符串分解为 Token 数组。
而要实现 parse 函数,我们首先需要打造它的引擎:一个强大而精妙的正则表达式。
4.1. parse 函数的目标
在上一章我们已经知道,parse 函数的目标是将一个路径字符串,例如 /user/:id(\d+)?,转换成一个 Token 数组。为了实现这个目标,parse 函数需要在一个循环中,不断地从路径字符串的头部“切割”出一个个有意义的片段(Token),直到整个字符串被消耗完毕。
这些“有意义的片段”主要有两种:
- 静态路径片段: 如
/user/或/about。 - 动态参数片段: 如
:id、:id(\d+)或:id?。
parse 函数的核心挑战在于,如何在一个循环中,高效地识别并捕获这两种完全不同的片段?答案就是利用一个精心设计的正则表达式,它能够一次性地匹配出“下一个”参数片段,或者在没有参数片段时,匹配出尽可能长的静态路径片段。
4.2. 核心正则表达式的构建与解析
path-to-regexp 源码中的 parse 函数,其核心驱动是一个名为 PATH_REGEXP 的正则表达式。让我们先一睹它的真容,然后再逐一拆解它的构成:
javascript
// path-to-regexp/src/index.ts
const PATH_REGEXP = new RegExp(
[
// Match escaped characters that would otherwise appear in future matches.
// This allows the user to escape special characters that won't transform.
"(\\\\.)",
// Match Express-style parameters and un-named parameters with a prefix
// and optional suffixes. Matches appear as:
//
// ":test(\\.+)?" => [":test(\\.+)?", ":", "test", "(\\.+)?", "\\.", "?", undefined]
// ":test?" => [":test?", ":", "test", undefined, undefined, "?", undefined]
"([\\.](?:(?::\\w+)(?:\\((?:\\([^)]+\\)|[^\\()]+)+\\))?|\\))|\\(((?:\\([^)]+\\)|[^\\()]+)+)\\))([+*?])?",
// Match regexp special characters that are always escaped.
"([.{}])"
].join("|"),
"g"
);这个正则表达式看起来非常复杂,但别担心,它是由三个主要部分通过 |(或)连接而成的。g 标志意味着它会进行全局匹配。让我们逐一分析这三个部分。
第一部分:转义字符 (\\.)
- 表达式:
(\\.) - 解释: 这部分非常简单,它匹配一个反斜杠
\后面跟着任意一个字符。例如\(或\.。 - 作用:
path-to-regexp允许用户通过反斜杠来转义那些具有特殊语法意义的字符(如(,),:,*等),使它们被当作普通的静态文本处理。例如,在/user\(:id\)中,括号()将被视为字面量,而不是参数的自定义模式。这个部分就是用来捕获这些被转义的字符序列的。
第二部分:参数匹配(核心)
这是整个正则表达式中最核心、最复杂的部分。它自身又由两个子部分通过 | 连接而成,分别用于匹配命名参数和未命名参数。
- 表达式:
([\\.](?:(?::\\w+)(?:\\((?:\\([^)]+\\)|[^\\()]+)+\\))?|\\))|\\(((?:\\([^)]+\\)|[^\\()]+)+)\\))([+*?])?
让我们把它拆开来看:
命名参数部分:
([\\.](?:(?::\\w+)(?:\\((?:\\([^)]+\\)|[^\\()]+)+\\))?|\\))([\\.]): 匹配并捕获一个前缀字符,通常是/或.。这是参数的“分隔符”。(?: ... ): 一个非捕获组,内部包含了匹配参数主体逻辑。(?::\\w+): 匹配参数名,如:id。\\w+匹配一个或多个字母、数字或下划线。(?:\\((?:\\([^)]+\\)|[^\\()]+)+\\))?: 这是一个非常巧妙的结构,用于匹配括号内的自定义模式,如(\d+)。它甚至能够正确处理嵌套的括号!我们暂时无需深究其细节,只需知道它能完整地捕获括号内的内容即可。这部分是可选的。
未命名参数部分:
\\(((?:\\([^)]+\\)|[^\\()]+)+)\\))- 这部分与上面类似,但它不要求有
:前缀,直接匹配括号内的正则表达式,用于捕获未命名参数。
- 这部分与上面类似,但它不要求有
修饰符部分:
([+*?])?([+*?]): 匹配并捕获一个修饰符+,*, 或?。?: 表示修饰符本身是可选的。
当这个核心部分成功匹配时,它的捕获组会包含参数的各个组成部分:前缀、名称、自定义模式、修饰符等。parse 函数正是利用这些捕获组来构建 Token 对象的。
第三部分:特殊的正则字符 ([.{}])
- 表达式:
([.{}]) - 解释: 匹配那些在正则表达式中有特殊意义,但在这里我们希望它们被自动转义的单个字符,如
.或{}。
4.3. 工作流程模拟
现在,让我们来模拟一下 parse 函数如何使用这个 PATH_REGEXP 来处理字符串 /user/:id?。
- 初始化:
path = "/user/:id?",tokens = [],index = 0。 - 循环开始:
PATH_REGEXP.lastIndex被设置为index(0)。 - 执行匹配:
PATH_REGEXP.exec(path)开始从头扫描。- 它无法匹配到任何参数。
exec返回null。
- 它无法匹配到任何参数。
- 处理静态文本: 当
exec返回null时,意味着从当前index到字符串末尾都没有参数了。parse函数会截取从index到字符串末尾的所有内容 (/user/:id?),但它发现这其中可能包含参数。正确的做法是,当匹配失败时,应该找到下一个参数出现的位置。
让我们换一个更清晰的思路来描述 parse 的逻辑:
- 循环执行
PATH_REGEXP.exec(path): 在全局模式下,每次调用exec都会从上一次匹配结束的位置继续向后查找。 - 处理匹配结果: 当
exec找到一个匹配项(例如,它在/user/:id?中找到了:id?):- 捕获静态部分: 从上一个
index到当前匹配开始的位置之间的所有文本(即/user),都是一个静态路径Token。将其创建并推入tokens数组。 - 捕获动态部分: 利用
exec返回的捕获组(包含前缀、名称、修饰符等信息),创建一个参数Token。将其推入tokens数组。 - 更新
index: 将index更新为当前匹配结束的位置。
- 捕获静态部分: 从上一个
- 处理尾部静态文本: 当循环结束后,如果
index还没有到达字符串的末尾,那么从index到末尾的所有内容都是最后一段静态路径Token。
通过这种“查找-截取”的循环模式,parse 函数就能够利用 PATH_REGEXP 这个强大的引擎,将任意复杂的路径字符串,一步步地分解成结构清晰的 Token 序列。
在下一章,我们将把这个逻辑翻译成具体的代码,并完整地实现 Token 数据结构。