在vue-router/react-router/koa-router等路由工具中,定义路由格式形如:/user/:name
或者其他更复杂的路由匹配。其实现均依赖path-to-regexp.js,本章将和读者一起深入探究path-to-regexp实现原理,同时加强对正则表达式的理解
path-to-regex项目地址https://github.com/pillarjs/path-to-regexp
使用方法
const pathToRegexp = require('path-to-regexp')
// pathToRegexp(path, keys?, options?)
// pathToRegexp.parse(path)
// pathToRegexp.compile(path)
options 配置项
sensitive 如果为 true
正则表达式大小写敏感 (默认: false
)
strict 如果为true
正则表达式允许可选的尾随分隔符匹配 (default: false
)
end 如果为 true
正则表达式将从字符串的结尾匹配 (default: true
)
start 如果为 true
正则表达式将从字符串的开头匹配 (default: true
)
delimiter 默认分割符号 (default: '/'
)
endsWith Optional character, or list of characters, to treat as "end" characters
whitelist 解析时要考虑分隔符的字符列表 (default: undefined
, any character)
const keys = []
const regexp = pathToRegexp('/foo/:bar', keys)
// regexp = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
暴露函数
使用module.exports对模块外暴露下述方法
// 获取路由路径对应的regexp
module.exports = pathToRegexp
// 解析路由路径,得到keys值
module.exports.parse = parse
module.exports.compile = compile
module.exports.tokensToFunction = tokensToFunction
module.exports.tokensToRegExp = tokensToRegExp
全局变量
全局变量中定义了路由默认分割符,以及路由匹配模式
/**
* 默认分隔符为正斜杠/
*/
var DEFAULT_DELIMITER = '/'
/**
* The main path matching regexp utility.
*
* @type {RegExp}
*/
var PATH_REGEXP = new RegExp([
// 匹配出现的转义字符
// 允许用户转义不会转换的特殊字符
'(\\\\.)',
// 将Express-style参数和未命名参数与前缀和可选后缀进行匹配
// 匹配结果数组形式:
// ":test(\\d+)?" => ["test", "\d+", undefined, "?"]
// "(\\d+)" => [undefined, undefined, "\d+", undefined]
'(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?'
].join('|'), 'g');
/**
PATH_REGEXP输出:
/(\\.)|(?:\:(\w+)(?:\(((?:\\.|[^\\()])+)\))?|\(((?:\\.|[^\\()])+)\))([+*?])?/g
**/
从上述正则可视化图中可知,该正则匹配可拆分为3种子正则表达式,分别是:“(\\\\.)”
,“(?:\\:(\\w+)(?:\\(((?:\\\\.|)+)\\))?)([+*?])?”
,“(?:\\(((?:\\\\.|)+)\\))([+*?])?”
;以下分别举例讲解:
匹配字符串中包含转义\字符的路径,如:"/user/\\?"
// 正则字面量中\\匹配双斜杆,.匹配任意一个字符,正则标志符g表示进行全局匹配
var regexp = new RegExp('(\\\\.)', g);
// /\\./g
regexp.exec('/user/\\?');
//匹配数组:["\?", index: 6, input: "/user/\?", groups: undefined]
该匹配模式中,有2种子类型匹配模式,分别是:key,以及限制key模式的正则;可匹配路径,如:“/user/:id”,“/user/:id?”,“/user/:id(\\d{8})”,“/user/:id(\\d{8})?”,其中:key(),括号中的为正则表达式字符串
var regexp = new RegExp('(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?)([+*?])?');
// /(?:\:(\w+)(?:\(((?:\\.|[^\\()])+)\))?)([+*?])?/
// 路径/user/:id
regexp.exec('/user/:id');
//匹配数组:[":id", "id", undefined, index: 5, input: "/use/:id", groups: undefined]
// 路径/user/:id?
regexp.exec('/user/:id?');
//匹配数组:[":id?", "id", undefined, "?", index: 6, input: "/user/:id?", groups: undefined]
// 路径/user/:id(\\d{8})
regexp.exec('/user/:id(\\d{8})');
//匹配数组:[":id(\d{8})", "id", "\d{8}", undefined, index: 6, input: "/user/:id(\d{8})", groups: undefined]
// 路径/user/:id(\\d{8})?
regexp.exec('/user/:id(\\d{8})?');
//匹配数组:[":id(\d{8})?", "id", "\d{8}", "?", index: 6, input: "/user/:id(\d{8})?", groups: undefined]
该模式可匹配模式,如:/user/([\d]{8}),/user/([\d]{8})?
var regexp = new RegExp('(?:\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?');
// /(?:\(((?:\\.|[^\\()])+)\))([+*?])?/
// 路径/user/([\\d]{8})
regexp.exec('/user/([\\d]{8})');
// 匹配数组:["([\d]{8})", "[\d]{8}", undefined, index: 6, input: "/user/([\d]{8})", groups: undefined]
// 路径/user/([\d]{8})?
regexp.exec('/user/([\d]{8})?');
// 匹配数组:["([\d]{8})?", "[\d]{8}", "?", index: 6, input: "/user/([\d]{8})?", groups: undefined]
公共函数
path-to-regexp中包含2个对字符串进行编码的函数:escapeString,escapeGroup
escapeString,对正则中所有特殊字符进行编码
var str = '/user(?:/(d{8}))?(?:/)?$/i';
str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
// \/user\(\?\:\/\(d\{8\}\)\)\?\(\?\:\/\)\?\$\/i
escapeGroup,对路由参数中自定义正则表达式进行编码
规定,在路由参数自定义正则表达式中不能存在=!:$/()等正则字符,固把这些字符进行转义
var str = '(d{8}$=)';
str.replace(/([=!:$/()])/g, '\\$1')
// "\(d{8}\$\=\)"
核心方法讲解
首先通过一个例子,了解pathToRegexp的输入和输出,如下输入“/user/:id(\d{8})?”
时,输出“/^\/user(?:\/(\d{8}))?(?:\/)?$/i”
var pathToRegexp = require('path-to-regexp')
var regexp = pathToRegexp('/user/:id(\d{8})?');
// 输出:/^\/user(?:\/(\d{8}))?(?:\/)?$/i
// 上述输出正则,可匹配路由/user/12345678
regexp.exec('/user/12345678');
// 匹配数组:["/user/12345678", "12345678", index: 0, input: "/user/12345678", groups: undefined]
pathToRegexp 处理流程
从下图可知,path解析为tokens(parse)与tokens转换为正则(tokensToRegExp)为核心逻辑,接下来的章节将围绕这两个函数进行讲解
parse
该函数把path路径解析为tokens数组,其中tokens结构为:
['无参数部分path路径',
{
"name": "路由参数名称", // "用户定义key" || 默认分配由0开始,
"prefix": "参数前缀", // 默认/
"delimiter": "路径分隔符", // 默认/
"optional": "路由参数可选,限定符", // 取值,?/*
"repeat": "路由参数重复,限定符", // 取值,+/*
"pattern": "路由参数,正则模式" // 用户自定义 或系统默认([^/])?
},{
}]
从上述结构可知,tokens由n个token组成,其中每个token中的元素由路由执行正则PATH_REGEXP
匹配并捕获而得到,那么匹配捕获项怎么和token对应起来呢?见示列:
var pathToRegexp = require('path-to-regexp')
var path = '/user/:id(\\d{8})?/([a-z]{4})';
var tokens = pathToRegexp.parse(path);
/**
# path解析过程捕获数组,与输出tokens
# 正则匹配结果数组,与tokens
# 第一次匹配数组:
[ ':id(\\d{8})?',
undefined,
'id',
'\\d{8}',
undefined,
'?',
index: 6,
input: '/user/:id(\\d{8})?/([a-z]{4})',
groups: undefined ]
# 第二次匹配数组:
[ '([a-z]{4})',
undefined,
undefined,
undefined,
'[a-z]{4}',
undefined,
index: 18,
input: '/user/:id(\\d{8})?/([a-z]{4})',
groups: undefined ]
# tokens
[ '/user',
{ name: 'id',
prefix: '/',
delimiter: '/',
optional: true,
repeat: false,
pattern: '\\d{8}' },
{ name: 0,
prefix: '/',
delimiter: '/',
optional: false,
repeat: false,
pattern: '[a-z]{4}' } ]
**/
见如下源码:
/**
@param {String} str 路由字符串
@param {Object} options 配置参数
**/
function parse (str, options) {
var tokens = []
/** 省略部分源码 **/
var res
// 循环,知道整个字符串匹配完为止
while ((res = PATH_REGEXP.exec(str)) !== null) {
// 匹配字符串
var m = res[0]
// 转义
var escaped = res[1]
var offset = res.index
// 第一个参数前的路径
path += str.slice(index, offset)
/** 省略部分源码 **/
var prev = ''
// 参数名称
var name = res[2]
// 命名路由参数捕获正则
var capture = res[3]
// 无命名路由参数捕获正则
var group = res[4]
// 参数捕获正则的限定符+?*
var modifier = res[5]
if (!pathEscaped && path.length) {
/** 省略部分源码 **/
if (matches) {
// 参数名称前的分隔符
prev = c
// 截取路径,不包含最后的分隔符/
path = path.slice(0, k)
}
}
// Push the current path onto the tokens.
if (path) {
// 把根路径,放到tokens第一个元素
tokens.push(path)
/** 省略部分源码 **/
}
// 是否重复
var repeat = modifier === '+' || modifier === '*'
// 是否可选
var optional = modifier === '?' || modifier === '*'
// 路由参数正则
var pattern = capture || group
// 路由路径分隔符
var delimiter = prev || defaultDelimiter
tokens.push({
name: name || key++, // 参数名称
prefix: prev, // 参数前分隔符
delimiter: delimiter, // 路由参数分隔符
optional: optional, // 路由参数可选标志
repeat: repeat, // 路由参数正则重复标志
pattern: pattern // 路由参数捕获正则表达式
? escapeGroup(pattern)
: '[^' + escapeString(delimiter === defaultDelimiter ? delimiter : (delimiter + defaultDelimiter)) + ']+?'
})
}
/** 省略部分源码 **/
return tokens
}
tokensToRegExp
该函数,把tokens数组转为路由正则表达式,继5.2. 中得到tokens,转换为正则表达式如下:
var pathToRegexp = require('path-to-regexp')
var path = '/user/:id(\\d{8})?/([a-z]{4})';
// 把path转为tokens
var tokens = pathToRegexp.parse(path);
/**
tokens 数组
[ '/user',
{ name: 'id',
prefix: '/',
delimiter: '/',
optional: true,
repeat: false,
pattern: '\\d{8}' },
{ name: 0,
prefix: '/',
delimiter: '/',
optional: false,
repeat: false,
pattern: '[a-z]{4}' } ]
**/
// 把tokens转为正则表达式
var pathRegExp = pathToRegexp.tokensToRegExp(tokens, keys, options);
/**
pathRegExp 值为:
/^\/user(?:\/(\d{8}))?\/([a-z]{4})(?:\/)?$/i
**/
见下述,通过分析源码,了解tokens转为正则的实现细节
/**
* @param {Array} tokens
* @param {Array} keys
* @param {Object} options
* @return {RegExp}
*/
function tokensToRegExp (tokens, keys, options) {
/** 省略部分源码 **/
// 路由默认开始为^
var route = start ? '^' : ''
// 遍历tokens,组装得到正则表达式字符串
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i]
// path根路径是字符串,进行转义处理即可
if (typeof token === 'string') {
route += escapeString(token)
} else { // token对象处理
// 路由参数,重复该参数正则模式拼接((?:[a-z]{4})(?:\/(?:[a-z]{4}))*))?
var capture = token.repeat
? '(?:' + token.pattern + ')(?:' + escapeString(token.delimiter) + '(?:' + token.pattern + '))*'
: token.pattern
// 记录token到keys中
if (keys) keys.push(token)
// 路由参数,可选该参数正则模式拼接(?:\/(\d{8}))?
if (token.optional) {
if (!token.prefix) {
route += '(' + capture + ')?'
} else {
route += '(?:' + escapeString(token.prefix) + '(' + capture + '))?'
}
} else {
// 必须的参数时,正则模式拼接\/(\d{8}))
route += escapeString(token.prefix) + '(' + capture + ')'
}
}
}
/** 省略部分源码 **/
// 把正则字符串转为RegExp表达式
return new RegExp(route, flags(options))
}