Skip to content

8. 进阶实现:处理未命名参数、修饰符与自定义模式

在前面的章节中,我们已经构建了 parsetokensToRegexp 的核心骨架,能够处理简单的命名参数。然而,path-to-regexp 的强大之处在于它能够灵活地处理各种复杂的路径模式。本章将深入探讨如何扩展我们的实现,以支持未命名参数、各种修饰符(?, *, +)以及用户自定义的正则表达式模式。

好消息是,如果前几章的基础打得足够牢固,支持这些进阶功能并不需要对现有代码做颠覆性的修改,而更多的是在 parsetokensToRegexp 的逻辑上进行自然的扩展和完善。

8.1. 支持未命名参数

挑战: 如何处理像 /post/(\d{4}-\d{2}-\d{2}) 这样的路径,其中包含一个没有名字的参数?

实现策略: 这个挑战主要在 parse 函数中解决。

  1. 正则表达式扩展: 我们在第四章讨论的 PATH_REGEXP 已经考虑了这种情况。它的第二部分 |\\(((?:\\([^)]+\\)|[^\\()]+)+)\\)) 就是专门用来捕获未命名参数的。当这部分匹配成功时,name 捕获组将是 undefined

  2. Key 对象创建: 在 parse 函数创建 Key 对象时,我们已经有了相应的逻辑:

    typescript
    // ... in parse function ...
    const token: Key = {
      name: name || key++, // 如果 name 不存在,就使用自增的数字 key
      // ... other properties
    };

    这里的 key++ 就是关键。我们维护一个从 0 开始的计数器 key。每当遇到一个未命名参数时,就将当前的 key 值作为其 name,然后将 key 自增。这样,第一个未命名参数的 name 就是 0,第二个是 1,以此类推。

tokensToRegexp 的影响: 无。tokensToRegexp 的设计是通用的,它只关心 Key 对象的结构,不关心 name 是字符串还是数字。后续在使用匹配结果时,可以通过数字索引来访问未命名参数的值。

8.2. 支持修饰符 (?, *, +)

挑战: 如何处理 /user/:id? (可选), /files/:path* (零或多个), /tags/:tag+ (一或多个)?

实现策略: 这个挑战同时涉及 parsetokensToRegexp

  1. parse 函数: PATH_REGEXP 的末尾部分 ([+*?])? 就是用来捕获修饰符的。在 parse 函数中,我们只需将捕获到的修饰符(如果存在)存入 Key 对象的 modifier 字段即可。

    typescript
    // ... in parse function ...
    const [match, ..., modifier] = res;
    const token: Key = {
      // ...
      modifier: modifier || ""
    };
  2. tokensToRegexp 函数: 在生成正则表达式时,我们需要根据 modifier 的值来改变生成的正则片段。这需要对 tokensToRegexp 中处理 Key 对象的逻辑进行扩展。

    typescript
    // ... in tokensToRegexp ...
    else {
      // ... (prefix, suffix, keys logic)
    
      let pattern = token.pattern;
      let route = "";
    
      // 关键:根据 modifier 包装 pattern
      if (token.modifier === "?") {
        route = `${prefix}(?:${pattern})${suffix}?`;
      } else if (token.modifier === "*") {
        route = `${prefix}(?:${pattern})${suffix}*`;
      } else if (token.modifier === "+") {
        route = `${prefix}(?:${pattern})${suffix}+`;
      } else {
        route = `${prefix}(${pattern})${suffix}`;
      }
    
      // ...
    }

    这里的核心改动是,我们将原本简单的 (pattern) 结构,根据 modifier 的不同,包装成了更复杂的形态。例如,对于可选参数 ?,我们需要将整个“前缀 + 参数”部分都变成可选的,即 (?:/([^/]+?))?。这需要将 prefixpattern 一起包裹在一个非捕获组 (?:...) 中,然后再加上量词。

    一个更精确的实现如下:

    typescript
    // A more precise implementation in tokensToRegexp
    const prefix = escapeString(token.prefix || "");
    const suffix = escapeString(token.suffix || "");
    const captureGroup = `(${token.pattern})`;
    
    if (token.modifier === "?") {
      route += `(?:${prefix}${captureGroup})?${suffix}?`;
    } else if (token.modifier === "*") {
      route += `(?:${prefix}${captureGroup})*${suffix}*`;
    } else if (token.modifier === "+") {
      route += `(?:${prefix}${captureGroup})+${suffix}+`;
    } else {
      route += `${prefix}${captureGroup}${suffix}`;
    }

8.3. 支持自定义模式

挑战: 如何处理 /user/:id(\d+),其中 :id 参数只接受数字?

实现策略: 这同样涉及 parsetokensToRegexp

  1. parse 函数: PATH_REGEXP(?:\\((?:\\([^)]+\\)|[^\\()]+)+\\))? 这部分就是用来捕获括号内的自定义模式的。parse 函数需要做的,就是将这个捕获组(如果存在)的值存入 Key 对象的 pattern 字段。

    typescript
    // ... in parse function ...
    const [match, ..., customPattern, ...] = res;
    const token: Key = {
      // ...
      pattern: customPattern || "[^\\/]+?", // 如果没有自定义模式,使用默认值
      // ...
    };
  2. tokensToRegexp 函数: 我们在第七章的实现已经考虑了这一点!

    typescript
    // ... in tokensToRegexp ...
    if (token.pattern) { // 实际上这里应该直接使用 token.pattern
      route += `${prefix}(${token.pattern})${suffix}${token.modifier}`;
    } else { 
      // 这段逻辑实际上可以合并,因为 parse 已经填好了默认 pattern
      route += `${prefix}([^${escapeString(delimiter)}]+?)${suffix}${token.modifier}`;
    }

    我们的 tokensToRegexp 函数已经天然地支持了自定义模式。它直接从 Key 对象中读取 pattern 属性,并将其作为捕获组的内容。parse 函数则保证了 pattern 字段要么是用户提供的自定义模式,要么是库设置的默认模式。这是一个非常好的例子,说明了 Token 作为中间表示,是如何有效地解耦了分析和生成两个阶段的。

通过对 parsetokensToRegexp 进行上述的扩展,我们的 mini-path-to-regexp 现在已经拥有了处理绝大多数复杂路径模式的能力。这些进阶功能的实现,完美地展示了 path-to-regexp 设计的优雅和可扩展性:所有的变化都被局限在“如何构造 Key 对象”和“如何解释 Key 对象”这两个清晰的环节中,而整体的“解析-生成”架构保持不变。

8. 进阶实现:处理未命名参数、修饰符与自定义模式 has loaded