path-to-regexp, 是一个将URL路径转为正则(正则表达式可以查看之前的文章)的工具, 在很多项目中都会运用到, 比如 Express
, Vue-Router
等等。这篇文章将会 解析 path-to-regexp
是如何工作的。
基本使用
- 安装
npm install path-to-regexp --save
// 或者
yarn add path-to-regexp
- 引入使用
const { pathToRegexp } = require("path-to-regexp");
const regexp = pathToRegexp("/foo/:bar");
console.log(regexp) // /^/foo(?:/([^/#?]+?))[/#?]?$/i 返回了正则表达式
源码分析
path-to-regexp
代码的基本结构:
接口类型 interface
- LexToken
interface LexToken {
type:
| "OPEN" // {
| "CLOSE" // }
| "PATTERN" // ()
| "NAME" // :
| "CHAR" // a-z A-Z _ 0-9
| "ESCAPED_CHAR" //
| "MODIFIER" // + * ?
| "END";
index: number;
value: string;
}
- ParseOptions
export interface ParseOptions {
/**
* 设置分隔符. (默认: '/')
*/
delimiter?: string;
/**
* 解析时自动考虑前缀的字符列表
*/
prefixes?: string;
}
- TokensToFunctionOptions
export interface TokensToFunctionOptions {
/**
* 当为真时,正则表达式将区分大小写. (默认: `false`)
*/
sensitive?: boolean;
/**
* 对字符串进行编码的函数
*/
encode?: (value: string, token: Key) => string;
/**
* 当 `false` 函数可以产生一个无效(不匹配的)路径 (默认: `true`)
*/
validate?: boolean;
}
- RegexpToFunctionOptions
export interface RegexpToFunctionOptions {
/**
* 为参数解码字符串的函数
*/
decode?: (value: string, token: Key) => string;
}
- MatchResult
export interface MatchResult<P extends object = object> {
path: string;
index: number;
params: P;
}
- Key
export interface Key {
name: string | number;
prefix: string;
suffix: string;
pattern: string;
modifier: string;
}
- TokensToRegexpOptions
export interface TokensToRegexpOptions {
/**
* 当`true`时,正则表达式将区分大小写. (默认: `false`)
*/
sensitive?: boolean;
/**
* 当 `true` 时,正则表达式将不允许可选的尾随分隔符匹配. (默认: `false`)
*/
strict?: boolean;
/**
* 当 `true` 时,正则表达式将匹配到字符串的末尾. (默认: `true`)
*/
end?: boolean;
/**
* 当 `true` 时,正则表达式将从字符串的开头匹配. (默认: `true`)
*/
start?: boolean;
/**
* 设置非结束匹配的最终字符. (default: `/`)
*/
delimiter?: string;
/**
* “结束”字符的字符列表
*/
endsWith?: string;
/**
* 对路径标记进行编码以在 `RegExp` 中使用
*/
encode?: (value: string) => string;
}
类型别名 type
- PathFunction
export type PathFunction<P extends object = object> = (data?: P) => string;
- Match
export type Match<P extends object = object> = false | MatchResult<P>;
- MatchFunction
export type MatchFunction<P extends object = object> = (
path: string
) => Match<P>;
- Token
export type Token = string | Key;
- Path
export type Path = string | RegExp | Array<string | RegExp>;
函数 function
pathToRegexp
export function pathToRegexp(
path: Path, // 可以是 字符串, 正则表达式,或者 (字符串或正则)数组
keys?: Key[], // 也就是上面提到的 interface Key 的结构
options?: TokensToRegexpOptions & ParseOptions // TokensToRegexpOptions 和 ParseOptions
) {
// 如果是 RegExp 正则表达式, 则使用 regexpToRegexp函数
if (path instanceof RegExp) return regexpToRegexp(path, keys);
// 如果是 数组 , 则使用 arrayToRegexp 函数
if (Array.isArray(path)) return arrayToRegexp(path, keys, options);
// 如果是 字符串, 则使用 stringToRegexp 函数
return stringToRegexp(path, keys, options);
}
keys: 是一个 Key类型的数组, 是用来存储路径中的 比如 :id
, (.*)
等格式的 关键字;
import { pathToRegexp, Key } from './src/index'
const keys: Key[] = []
const str = '/foo/:id/(.*)'
const path = pathToRegexp(str, keys)
console.log(keys, path)
// keys = [ { name: 'id', prefix: '/', suffix: '', pattern: '[^\\/#\\?]+?', modifier: '' }, { name: 0, prefix: '/', suffix: '', pattern: '.*', modifier: '' } ]
// path = /^\/foo(?:\/([^\/#\?]+?))(?:\/(.*))[\/#\?]?$/i
regexpToRegexp
function regexpToRegexp(path: RegExp, keys?: Key[]): RegExp {
// 如果 keys 不存在则直接 返回
if (!keys) return path;
// 否则 从正则中 提取出 keys
const groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g;
let index = 0;
let execResult = groupsRegex.exec(path.source);
while (execResult) {
keys.push({
// Use parenthesized substring match if available, index otherwise
name: execResult[1] || index++,
prefix: "",
suffix: "",
modifier: "",
pattern: "",
});
execResult = groupsRegex.exec(path.source);
}
return path;
}
- 如果 keys 不存在,则直接返回
- 否则 从用户定义的正则中,通过 正则
/\((?:\?<(.*?)>)?(?!\?)/g
提取出 具名捕获,然后添加到keys
。 通过可视化可以看到, 其实在匹配正则中的具名捕获(?<Name>x)
, 提取出Name
。
const keys: Key[] = []
const str = /(?<Name>x)/
regexpToRegexp(str, keys)
//keys = [ { name: 'Name', prefix: '', suffix: '', modifier: '', pattern: '' } ]
arrayToRegexp
对数组进行遍历调用 pathToRegexp
方法, 最后将 处理后的结果使用 |
连接起来,并且转成 正则
function arrayToRegexp(
paths: Array<string | RegExp>,
keys?: Key[],
options?: TokensToRegexpOptions & ParseOptions
): RegExp {
const parts = paths.map((path) => pathToRegexp(path, keys, options).source);
return new RegExp(`(?:${parts.join("|")})`, flags(options));
}
例如:
import { pathToRegexp, Key } from './src/index'
const keys: Key[] = []
const path = pathToRegexp(['/path/:userId', '/path/path2'], keys)
console.log(path)
// /(?:^\/path\/path1[\/#\?]?$|^\/path\/path2[\/#\?]?$)/i
// ^\/path\/path1[\/#\?]?$ 和 ^\/path\/path2[\/#\?]?$ 组成
stringToRegexp
对于所有的字符串都会 调用 该函数; 将字符串进行分词token, 转成 AST(抽象语法树),再根据 AST 转成 正则,可以看到 下面的代码
- 调用
parse
对path进行解析成AST - 调用
tokensToRegexp
将解析后的AST转为正则表达式;
function stringToRegexp(
path: string,
keys?: Key[],
options?: TokensToRegexpOptions & ParseOptions
) {
return tokensToRegexp(parse(path, options), keys, options);
}
parse
将字符串进行词法分析获取到token, 然后转成AST;
在 parse
函数中 可以看到一个 lexer
函数, 就是将 传入的 字符串 转成 token;
- 我们先分析
lexer
函数
function lexer(str: string): LexToken[] {
const tokens: LexToken[] = []; // LexToken类型的数组
let i = 0; // 从字符串第一个开始
while (i < str.length) { // 对字符串进行遍历
const char = str[i];
// 对于 * , + ,? 被描述为 MODIFIER(修饰符)
if (char === "*" || char === "+" || char === "?") {
tokens.push({ type: "MODIFIER", index: i, value: str[i++] });
continue;
}
// 对于 \ 被描述为 ESCAPED_CHAR(转义字符)
if (char === "\\") {
tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] });
continue;
}
// 对于 { 被描述为 OPEN
if (char === "{") {
tokens.push({ type: "OPEN", index: i, value: str[i++] });
continue;
}
// 对于 } 被描述为 CLOSE
if (char === "}") {
tokens.push({ type: "CLOSE", index: i, value: str[i++] });
continue;
}
// 对于 : 被描述为 NAME 用于 具名
if (char === ":") {
let name = "";
let j = i + 1;
while (j < str.length) {
const code = str.charCodeAt(j);
if (
// `0-9`
(code >= 48 && code <= 57) ||
// `A-Z`
(code >= 65 && code <= 90) ||
// `a-z`
(code >= 97 && code <= 122) ||
// `_`
code === 95
) {
name += str[j++];
continue;
}
break;
}
if (!name) throw new TypeError(`Missing parameter name at ${i}`);
tokens.push({ type: "NAME", index: i, value: name });
i = j;
continue;
}
// 对于 (...)被描述为 PATTERN (
if (char === "(") {
let count = 1;
let pattern = "";
let j = i + 1;
if (str[j] === "?") {
throw new TypeError(`Pattern cannot start with "?" at ${j}`);
}
while (j < str.length) {
if (str[j] === "\\") {
pattern += str[j++] + str[j++];
continue;
}
if (str[j] === ")") {
count--;
if (count === 0) {
j++;
break;
}
} else if (str[j] === "(") {
count++;
if (str[j + 1] !== "?") {
throw new TypeError(`Capturing groups are not allowed at ${j}`);
}
}
pattern += str[j++];
}
if (count) throw new TypeError(`Unbalanced pattern at ${i}`);
if (!pattern) throw new TypeError(`Missing pattern at ${i}`);
tokens.push({ type: "PATTERN", index: i, value: pattern });
i = j;
continue;
}
// 其他都描述为 CHAR
tokens.push({ type: "CHAR", index: i, value: str[i++] });
}
// 结束分析
tokens.push({ type: "END", index: i, value: "" });
return tokens;
}
从上面简易的流程图可以看出, 将字符串的类型分为:
标志 | 匹配 |
---|---|
MODIFIER | *,+,? |
ESCAPED_CHAR | \ |
OPEN | { |
CLOSE | } |
NAME | : |
PATTERN | () |
CHAR | 非以上字符都是CHAR |
END | 匹配结束后 |
比如: /p/{param}/:id/(a*)
转为token
[{ type: "CHAR", index: 0, value: "/" },
{ type: "CHAR", index: 1, value: "p" },
{ type: "CHAR", index: 2, value: "/" },
{ type: "OPEN", index: 3, value: "{" },
{ type: "CHAR", index: 4, value: "p" },
{ type: "CHAR", index: 5, value: "a" },
{ type: "CHAR", index: 6, value: "r" },
{ type: "CHAR", index: 7, value: "a" },
{ type: "CHAR", index: 8, value: "m" },
{ type: "CLOSE", index: 9, value: "}" },
{ type: "CHAR", index: 10, value: "/" },
{ type: "CHAR", index: 11, value: "a" },
{ type: "CHAR", index: 12, value: "/" },
{ type: "NAME", index: 13, value: "id" },
{ type: "CHAR", index: 16, value: "/" },
{ type: "PATTERN", index: 17, value: "a*" },
{ type: "END", index: 21, value: "" }]
- escapeString
在使用特殊字符的时候, 比如
*
, 我们就需要转义,escapeString
函数 将输入的str中特殊的字符转义为正则表达式中的一个字面字符串, 比如 默认的/#?
转义 为\\/#\\?
function escapeString(str: string) {
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
}
export function parse(str: string, options: ParseOptions = {}): Token[] {
const tokens = lexer(str);
const { prefixes = "./" } = options;
const defaultPattern = `[^${escapeString(options.delimiter || "/#?")}]+?`;
const result: Token[] = [];
let key = 0;
let i = 0;
let path = "";
// 获取指定的 LexToken 类型
const tryConsume = (type: LexToken["type"]): string | undefined => {
if (i < tokens.length && tokens[i].type === type) return tokens[i++].value;
};
const mustConsume = (type: LexToken["type"]): string => {
const value = tryConsume(type);
if (value !== undefined) return value;
const { type: nextType, index } = tokens[i];
throw new TypeError(`Unexpected ${nextType} at ${index}, expected ${type}`);
};
const consumeText = (): string => {
let result = "";
let value: string | undefined;
// tslint:disable-next-line
while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) {
result += value;
}
return result;
};
while (i < tokens.length) {
const char = tryConsume("CHAR"); // 如果 tokens[i] 是 CHAR 则返回,并且 i自增
const name = tryConsume("NAME"); // 如果 tokens[i] 是 NAME 则返回,并且 i自增
const pattern = tryConsume("PATTERN"); // 如果 tokens[i] 是 PATTERN 则返回,并且 i自增
// 如有有 name 或者 pattern
if (name || pattern) {
let prefix = char || "";
if (prefixes.indexOf(prefix) === -1) {
path += prefix;
prefix = "";
}
if (path) {
result.push(path);
path = "";
}
// 将 NAME 或者 PATTERN 添加到 result 中
result.push({
name: name || key++,
prefix,
suffix: "",
pattern: pattern || defaultPattern,
modifier: tryConsume("MODIFIER") || "",
});
continue;
}
// 如果 字符串 是 CHAR 或者 ESCAPED_CHAR 则 添加 到 path, 否则 将 path 追加到 result 数组中, 并且 将 path 赋值 空字符串
const value = char || tryConsume("ESCAPED_CHAR");
if (value) {
path += value;
continue;
}
if (path) {
result.push(path);
path = "";
}
// 调用 consumeText 函数 , 设置 prefix(前缀)
// 获取 NAME 或者 PATTERN
// 调用 consumeText 函数 , 获取 suffix 后缀
const open = tryConsume("OPEN");
if (open) {
const prefix = consumeText();
const name = tryConsume("NAME") || "";
const pattern = tryConsume("PATTERN") || "";
const suffix = consumeText();
mustConsume("CLOSE");
result.push({
name: name || (pattern ? key++ : ""),
pattern: name && !pattern ? defaultPattern : pattern,
prefix,
suffix,
modifier: tryConsume("MODIFIER") || "",
});
continue;
}
mustConsume("END");
}
return result;
}
以上代码实现了将 lexer
函数返回的 tokens 进行解析成 AST; 继续拿 /p/{param}/:id/(a*)
来当例子, 返回的是 :
[ "/p/",
{ name: "", pattern: "", prefix: "param", suffix: "", modifier: ""},
"/a",
{ name: "id", prefix: "/", suffix: "", pattern: "[^\\/#\\?]+?", modifier: ""},
{ name: 0, prefix: "/", suffix: "", pattern: "a*", modifier: "" }]
更多情况
- 纯字符串
/p/test
[ '/p/test' ]
// /^\/p\/test[\/#\?]?$/i
- 命名参数
/:id
[{ name: 'id', prefix: '/', suffix: '', pattern: '[^\\/#\\?]+?', modifier: '' }]
// /^(?:\/([^\/#\?]+?))[\/#\?]?$/i
- 命名参数 正则
/:id(\\d+)
[{ name: 'id', prefix: '/', suffix: '', pattern: '\\d+', modifier: '' }]
// /^(?:\/(\d+))[\/#\?]?$/i
- 前后缀
/{前缀:id(\\d+)后缀}*
[ '/',
{ name: 'id', pattern: '\\d+', prefix: '前缀', suffix: '后缀', modifier: '*' } ]
// /^\/(?:前缀((?:\d+)(?:后缀前缀(?:\d+))*)后缀)?[\/#\?]?$/i
对于 命名参数 如果用户自定义 正则匹配, 那么 采用自定义的正则, 如果没哟, 则采用 默认的 [^\/#\?]+?)
非 /
, #
, ?
的其他字符
tokensToRegexp
将 parse
解析的 AST
, 转成 Regexp
export function tokensToRegexp(
tokens: Token[],
keys?: Key[],
options: TokensToRegexpOptions = {}
) {
// 设置 转换参数
const {
strict = false,
start = true,
end = true,
encode = (x: string) => x,
} = options;
// 结尾
const endsWith = `[${escapeString(options.endsWith || "")}]|$`;
// 分隔符
const delimiter = `[${escapeString(options.delimiter || "/#?")}]`;
// 路由开始
let route = start ? "^" : "";
// Iterate over the tokens and create our regexp string.
// 遍历 tokens
for (const token of tokens) {
// 如果 token 是字符串, 那么 转义特殊字符就好了
if (typeof token === "string") {
route += escapeString(encode(token));
} else {
// 如果有 prefix 或者 suffix
const prefix = escapeString(encode(token.prefix));
const suffix = escapeString(encode(token.suffix));
// 如果 pattern 存在,
if (token.pattern) {
if (keys) keys.push(token); // 如果 keys存在 这将 token 添加到 keys
// 如有 prefix 或者 suffix
if (prefix || suffix) {
// 如果 有修饰符 + 或 *
if (token.modifier === "+" || token.modifier === "*") {
const mod = token.modifier === "*" ? "?" : ""; // 将 "*" 转为 "?"
route += `(?:${prefix}((?:${token.pattern})(?:${suffix}${prefix}(?:${token.pattern}))*)${suffix})${mod}`;
} else {
route += `(?:${prefix}(${token.pattern})${suffix})${token.modifier}`;
}
} else {
if (token.modifier === "+" || token.modifier === "*") {
route += `((?:${token.pattern})${token.modifier})`;
} else {
route += `(${token.pattern})${token.modifier}`;
}
}
} else {
route += `(?:${prefix}${suffix})${token.modifier}`;
}
}
}
if (end) {
if (!strict) route += `${delimiter}?`;
route += !options.endsWith ? "$" : `(?=${endsWith})`;
} else {
const endToken = tokens[tokens.length - 1];
const isEndDelimited =
typeof endToken === "string"
? delimiter.indexOf(endToken[endToken.length - 1]) > -1
: // tslint:disable-next-line
endToken === undefined;
if (!strict) {
route += `(?:${delimiter}(?=${endsWith}))?`;
}
if (!isEndDelimited) {
route += `(?=${delimiter}|${endsWith})`;
}
}
// 返回正则
return new RegExp(route, flags(options));
}
在这里最复杂的就是:
route += `(?:${prefix}((?:${token.pattern})(?:${suffix}${prefix}(?:${token.pattern}))*)${suffix})${mod}`;
其实就是确保了,在有前后缀 以及 修饰符 ‘*’ 或者 ‘+’的情况下能够 完整的匹配到;比如: /{-:id(\\d+)}+
转成的 正则 /^\/(?:-((?:\d+)(?:-(?:\d+))*))[\/#\?]?$/i
, 我们看下 正则表达式可视化, 对于 路由是 /-12-13-14
, 直接获取到 12-13-14
以上就是 对 pathToRegexp
对路由的解析分析;
compile
将指定的路径字符串转为模版函数, 返回函数用来编译成路径; 比如:
const str = '/{-:id(\\d+)}+'
const fn = compile(str)
const s = fn({
id: [12, 13]
})
// /-12-13
parse
我们在前面讲过, 核心在tokensToFunction
函数
export function compile<P extends object = object>(
str: string,
options?: ParseOptions & TokensToFunctionOptions
) {
return tokensToFunction<P>(parse(str, options), options);
}
tokensToFunction
export function tokensToFunction<P extends object = object>(
tokens: Token[],
options: TokensToFunctionOptions = {}
): PathFunction<P> {
const reFlags = flags(options);
const { encode = (x: string) => x, validate = true } = options;
// 将所有token为对象的转成 正则.
const matches = tokens.map((token) => {
if (typeof token === "object") {
return new RegExp(`^(?:${token.pattern})$`, reFlags);
}
});
return (data: Record<string, any> | null | undefined) => {
let path = "";
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (typeof token === "string") {
path += token;
continue;
}
const value = data ? data[token.name] : undefined;
const optional = token.modifier === "?" || token.modifier === "*";
const repeat = token.modifier === "*" || token.modifier === "+";
if (Array.isArray(value)) {
if (!repeat) {
throw new TypeError(
`Expected "${token.name}" to not repeat, but got an array`
);
}
if (value.length === 0) {
if (optional) continue;
throw new TypeError(`Expected "${token.name}" to not be empty`);
}
for (let j = 0; j < value.length; j++) {
const segment = encode(value[j], token);
if (validate && !(matches[i] as RegExp).test(segment)) {
throw new TypeError(
`Expected all "${token.name}" to match "${token.pattern}", but got "${segment}"`
);
}
path += token.prefix + segment + token.suffix;
}
continue;
}
if (typeof value === "string" || typeof value === "number") {
const segment = encode(String(value), token);
if (validate && !(matches[i] as RegExp).test(segment)) {
throw new TypeError(
`Expected "${token.name}" to match "${token.pattern}", but got "${segment}"`
);
}
path += token.prefix + segment + token.suffix;
continue;
}
if (optional) continue;
const typeOfMessage = repeat ? "an array" : "a string";
throw new TypeError(`Expected "${token.name}" to be ${typeOfMessage}`);
}
return path;
};
}
match
从 path-to-regexp
创建一个函数,用来匹配路由
例如:
const str = '/{-:id(\\d+)}+'
const p = match(str)
p('/-13-14-15')
// { path: '/-13-14-15', index: 0, params: { id: [ '13', '14', '15' ] } }
export function match<P extends object = object>(
str: Path,
options?: ParseOptions & TokensToRegexpOptions & RegexpToFunctionOptions
) {
const keys: Key[] = [];
const re = pathToRegexp(str, keys, options);
return regexpToFunction<P>(re, keys, options);
}
regexpToFunction
export function regexpToFunction<P extends object = object>(
re: RegExp,
keys: Key[],
options: RegexpToFunctionOptions = {}
): MatchFunction<P> {
const { decode = (x: string) => x } = options;
return function (pathname: string) {
const m = re.exec(pathname); // 对路径进行正则执行
if (!m) return false;
const { 0: path, index } = m;
const params = Object.create(null);
for (let i = 1; i < m.length; i++) {
// tslint:disable-next-line
if (m[i] === undefined) continue;
const key = keys[i - 1];
// 对于修饰符 ‘*’ 或者 '+', 进行 根据 前后缀 分割
if (key.modifier === "*" || key.modifier === "+") {
params[key.name] = m[i].split(key.prefix + key.suffix).map((value) => {
return decode(value, key);
});
} else {
params[key.name] = decode(m[i], key);
}
}
return { path, index, params };
};
}
至此, path-to-regex
p 源码解析完成, 区区六百多行的代码, 能够实现如此强大的功能,真心佩服作者