Path-to-RegExp 模块学习笔记
一个对URL进行解析的模块,支持正则表达式验证、匹配、反向解析,可用于增加 koa 框架对于 koa-router 对 path 的解析工作。
包安装
npm install path-to-regexp --save
加载方法
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
// pathToRegexp(path, keys?, options?)
// match(path)
// parse(path)
// compile(path)
// 也可以直接加载包:
const PathToRegexp = require('path-to-regexp');
pathToRegexp 方法
创建一个正则表达式匹配对象
const PathToRegexp = require('path-to-regexp');
const keys = [];
const regexp = pathToRegexp('/foo/:bar', keys);
console.log(regexp); // /^/foo(?:/([^/#?]+?))[/#?]?$/i
console.log(keys)
/*
[
{
name: 'bar',
prefix: '/',
suffix: '',
pattern: '[^\\/#\\?]+?',
modifier: ''
}
]
*/
创建的正则表达式:
/^/foo(?:/([^/#?]+?))[/#?]?$/i
1、/.../i 不区分大小写搜索。
2、^...$ 从字符串开头匹配到字符串结束
3、/foo(?:...)[/#?]? 匹配 /foo 后面是(?:...) 非捕获组 然后匹配 [/#?]? /、#、? 出现 或 出现任何一个
4、\/(...) 非捕获组中的内容 \/() 表示匹配 特殊字符 / 后面是个捕获组
5、捕获组中的内容:[^/#?]+? 非 /、#、? 的字符 +?非贪婪匹配
创建的数组:
[
{
name: 'bar', 捕获组名称
prefix: '/', 前缀
suffix: '', 后缀
pattern: '[^\\/#\\?]+?', 匹配表达式 [^\/#\?]+?
modifier: '' 修饰符
}
]
pathToRegexp 函数创建了一个对 url /foo/:bar 的正则表达式匹配对象, 并对使用 :冒号标识的URL命名参数创建了一个命名参数对象 添加到 keys(参数2) 数组中。
由上例可知:
path-to-regexp函数所创建的正则表达式,功能仅能匹配一个url路径、包含主机名称的url路径。 其中的\/规则、[^/#?]及 匹配规则,都是为解析url路径所定制功能,不能匹配任意的非url字符串 或 json。- 使用
path-ro-regexp函数 创建对象时,如果要匹配?key1=value&key2=value...GET 请求参数,需要对?进行转义处理。
执行URL匹配测试
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const regexp = pathToRegexp('/foo/:bar', keys);
console.log(regexp.exec('/foo/test_a'));
/*
[
'/foo/test_a', 匹配的字符串
'test_a', 捕获组中子匹配
index: 0, 匹配位置
input: '/foo/test_a', 进行匹配的字符串
groups: undefined 正则表达式中的 捕获组 是个非命名捕获组所以没有 groups 对象
]
*/
注意:
命名URL参数名称 仅能使用单词字符,也就是[A-Za-z0-9_].
自定义匹配参数
命名URL参数可以有一个自定义的正则表达式,它会覆盖默认匹配:([^\/#\?]+?):
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const regexp_number = pathToRegexp('/icon-:foo(\\d+).png', keys);
// 注意:字符串表示正则字符: \d 必须对 \ 进行转义: \\d
console.log(regexp_number); // /^/icon-(\d+).png[/#?]?$/i
console.log(keys);
/*
[
{
name: 'foo',
prefix: '',
suffix: '',
pattern: '\\d+', // 默认的 [^\\/#\\?]+? 被替换掉了
modifier: ''
}
]
*/
javascript 中
\符号具有特殊意义,所以在字符串中添加\d必须对\进行转义:\\d
测试匹配:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const regexp_number = pathToRegexp('/icon-:foo(\d+).png', keys);
console.log(regexp_number.exec('/icon-123.png'));
/*
[
'/icon-123.png',
'123', // 子组捕获 123 匹配 \d+
index: 0,
input: '/icon-123.png',
groups: undefined
]
*/
console.log(regexp_number.exec('/icon-abc.png')); // abc 不匹配 \d+ 所以返回 null
// null
匹配字符串的例子:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const regexp_word = pathToRegexp('/(user|u)', keys);
console.log(regexp_word.exec('/u')); // [ '/u', 'u', index: 0, input: '/u', groups: undefined ]
console.log(regexp_word.exec('/user')); // [ '/user', 'user', index: 0, input: '/user', groups: undefined ]
自定义匹配参数添加修饰符
自定义参数的重复,可以添加正则表达式量词修饰符:
例如匹配: http://127.0.0.1/test/test/test/test 中的 test
URL中多个参数的重复
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const repeat_param1 = pathToRegexp('/:num(\d)?');
const repeat_param2 = pathToRegexp('/:num(\d)*');
const repeat_param3 = pathToRegexp('/:num(\d)+');
console.log(repeat_param1); // /^(?:/(\d))?[/#?]?$/i
console.log(repeat_param2); // /^(?:/((?:\d)(?:/(?:\d))*))?[/#?]?$/i
console.log(repeat_param3); // /^(?:/((?:\d)(?:/(?:\d))*))[/#?]?$/i
const url_0 = '/';
const url_1 = '/1';
const url_2 = '/1/2';
const url_3 = '/1/2/3/4/5';
console.log(repeat_param1.exec(url_0)); // [ '/', undefined, index: 0, input: '/', groups: undefined ]
console.log(repeat_param1.exec(url_1)); // [ '/1', '1', index: 0, input: '/1', groups: undefined ]
console.log(repeat_param1.exec(url_2)); // null
console.log(repeat_param1.exec(url_3)); // null
console.log(repeat_param2.exec(url_0)); // [ '/', undefined, index: 0, input: '/', groups: undefined ]
console.log(repeat_param2.exec(url_1)); // [ '/1', '1', index: 0, input: '/1', groups: undefined ]
console.log(repeat_param2.exec(url_2)); // [ '/1/2', '1/2', index: 0, input: '/1/2', groups: undefined ]
console.log(repeat_param2.exec(url_3)); // ['/1/2/3/4/5', '1/2/3/4/5', index: 0, input: '/1/2/3/4/5', groups: undefined]
console.log(repeat_param3.exec(url_0)); // null
console.log(repeat_param3.exec(url_1)); // [ '/1', '1', index: 0, input: '/1', groups: undefined ]
console.log(repeat_param3.exec(url_2)); // [ '/1/2', '1/2', index: 0, input: '/1/2', groups: undefined ]
console.log(repeat_param3.exec(url_3)); // ['/1/2/3/4/5', '1/2/3/4/5', index: 0, input: '/1/2/3/4/5', groups: undefined]
给自定义匹配参数添加修饰符 可以使 URL的匹配变的更加灵活,除添加修饰符?外,还支持 * 表示参数可以出现零次或多次, +表示参数至少出现一次 或多次。
自定义匹配参数前缀、后缀
使用 {} 包装参数,可以创建自定义的 前缀、后缀, 假设有以下需求:
http://127.0.0.1/image/1000.png 请求正常尺寸图像
http://127.0.0.1/image/1000.png@small 请求小尺寸图像
http://127.0.0.1/image/1000.png@large 请求大尺寸图像
定义: /image/:id(\d+).png@:size(small|large)? 将导致 @符产生硬性匹配:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const image_regexp = pathToRegexp('/image/:id(\d+).png@:size(small|large)?', keys);
console.log(image_regexp); // /^\/image(?:\/(\d+))\.png@(small|large)?[\/#\?]?$/i
console.log(keys);
// [
// {
// name: 'id',
// prefix: '/',
// suffix: '',
// pattern: '\d+',
// modifier: ''
// },
// {
// name: 'size',
// prefix: '',
// suffix: '',
// pattern: 'small|large',
// modifier: '?'
// }
// ]
console.log(image_regexp.exec('/image/1000.png')); // null
console.log(image_regexp.exec('/image/1000.png@small'));
// [
// '/image/1000.png@small',
// '1000',
// 'small',
// index: 0,
// input: '/image/1000.png@small',
// groups: undefined
// ]
console.log(image_regexp.exec('/image/1000.png@large'));
// [
// '/image/1000.png@large',
// '1000',
// 'large',
// index: 0,
// input: '/image/1000.png@large',
// groups: undefined
// ]
为自定义参数添加前缀:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const image_regexp = pathToRegexp('/image/:id(\d+).png{@:size(small|large)}?', keys);
console.log(image_regexp); // /^/image(?:/(\d+)).png(?:@(small|large))?[/#?]?$/i
// 重点在: (?:@(small|large))? 将 @:size(small|large) 放在一个非捕获组中,并添加 修饰符 ?
// 可出现,可不出现
console.log(keys);
// [
// {
// name: 'id',
// prefix: '/',
// suffix: '',
// pattern: '\d+',
// modifier: ''
// },
// {
// name: 'size',
// pattern: 'small|large',
// prefix: '@', // 子捕获组添加前缀
// suffix: '',
// modifier: '?'
// }
// ]
// [
// '/image/1000.png',
// '1000',
// undefined,
// index: 0,
// input: '/image/1000.png',
// groups: undefined
// ]
// [
// '/image/1000.png@small',
// '1000',
// 'small',
// index: 0,
// input: '/image/1000.png@small',
// groups: undefined
// ]
// [
// '/image/1000.png@large',
// '1000',
// 'large',
// index: 0,
// input: '/image/1000.png@large',
// groups: undefined
// ]
console.log(image_regexp.exec('/image/1000.png')); // 成功匹配
// [
// '/image/1000.png',
// '1000',
// undefined,
// index: 0,
// input: '/image/1000.png',
// groups: undefined
// ]
console.log(image_regexp.exec('/image/1000.png@small'));
// [
// '/image/1000.png@small',
// '1000',
// 'small', // 子捕获组 匹配正确
// index: 0,
// input: '/image/1000.png@small',
// groups: undefined
// ]
console.log(image_regexp.exec('/image/1000.png@large'));
// [
// '/image/1000.png@large',
// '1000',
// 'large',
// index: 0,
// input: '/image/1000.png@large',
// groups: undefined
// ]
处理查询字符串时,如果要包含GET请求参数 ? 需要对其进行转义处理:
如下例,请求中带有GET发送的code码:
http://127.0.0.1/email-activation?code=EAD5-DER4-EFZ2-21DE-565A
code格式:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const url = '/email-activation?code=EAD5-DER4-EFZ2-21DE-565A';
const code_regexp1 = '(?:[A-Z0-9]{4}-){4}[A-Z0-9]{4}';
const email_activation_regexp = pathToRegexp(`/email-activation{\?code=:code(${code_regexp1})}?`, keys);
// url中出现?匹配 必须对其进行转义
console.log(email_activation_regexp); // /^/email-activation?code=((?:[A-Z0-9]{4}-){4,5}[A-Z0-9]{4})[/#?]?$/i
console.log(keys);
// [
// {
// name: 'code',
// prefix: '',
// suffix: '',
// pattern: '(?:[A-Z0-9]{4}-){4}[A-Z0-9]{4}', // 子捕获组中不允许再次使用 捕获组,只能使用非捕获组
// modifier: ''
// }
// ]
console.log(email_activation_regexp.exec(url));
// [
// '/email-activation?code=EAD5-DER4-EFZ2-21DE-565A',
// 'EAD5-DER4-EFZ2-21DE-565A',
// index: 0,
// input: '/email-activation?code=EAD5-DER4-EFZ2-21DE-565A',
// groups: undefined
// ]
//
console.log(email_activation_regexp.exec('/email-activation'));
// [
// '/email-activation',
// undefined,
// index: 0,
// input: '/email-activation',
// groups: undefined
// ]
注意: 在自定义匹配参数的
捕获组中不允许再次出现()捕获组,应使用(?:)非捕获组添加量词修饰符。
未命名参数
可以编写仅由正则表达式组成的未命名参数。它与命名参数的工作方式相同,除了它将被数字索引:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const regexp = pathToRegexp("/:foo/(.*)", keys);
console.log(regexp); // /^(?:/([^/#?]+?))(?:/(.*))[/#?]?$/i
console.log(keys);
// [
// {
// name: 'foo',
// prefix: '/',
// suffix: '',
// pattern: '[^\/#\?]+?',
// modifier: ''
// },
// { name: 0, prefix: '/', suffix: '', pattern: '.*', modifier: '' }
// 非命名捕获组所捕获的对象 name 为数字索引
// ]
console.log(regexp.exec("/test/route"));
// [
// '/test/route',
// 'test',
// 'route',
// index: 0,
// input: '/test/route',
// groups: undefined
// ]
正则对象可选项
path字符串、字符串数组 或 正则表达式keys用路径中找到的键填充的数组。options配置选项sensitive正则表达式是否区分大小写 默认为falsestrict正则表达式匹配时是否使用默认分隔符falseend正则表达式将匹配到字符串的未尾 默认为truestart正则表达式将从字符串开头开始匹配 默认为truedelimiter定义URL段的默认分隔符, 默认为[/#?]endsWith定义URL结尾断言encode一个函数,用于在插入之前对RegExp字符串进行编码 默认为 `x => xprefixes解析时自动考虑前缀的字符列表。 默认为./默认的标志位配置:
const keys = [];
const regexp = pathToRegexp("/:foo/(.*)");
console.log(regexp1);
console.log(keys);
/^(?:\/([^\/#\?]+?))(?:\/(.*))[\/#\?]?$/i <<< 存在 i 标志位 默认不区分大小写匹配
[
{
name: 'foo',
prefix: '/',
suffix: '',
pattern: '[^\\/#\\?]+?',
modifier: ''
},
{ name: 0, prefix: '/', suffix: '', pattern: '.*', modifier: '' }
]
sensitive 正则是否区分大小写:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const options = {
sensitive: true
}
const regexp = pathToRegexp("/:foo/(.*)", keys, options);
console.log(regexp);
console.log(keys);
/^(?:\/([^\/#\?]+?))(?:\/(.*))[\/#\?]?$/ << 默认的标志位 i 不见了
[
{
name: 'foo',
prefix: '/',
suffix: '',
pattern: '[^\\/#\\?]+?',
modifier: ''
},
{ name: 0, prefix: '/', suffix: '', pattern: '.*', modifier: '' }
]
strict 正则表达式匹配时是否使用自定义分段分隔符:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const options = {
strict: true
}
const regexp = pathToRegexp("/:foo/(.*)", keys, options);
console.log(regexp);
console.log(keys);
/^(?:\/([^\/#\?]+?))(?:\/(.*))$/i << 默认的尾分隔符是 [\/#\?]?
[
{
name: 'foo',
prefix: '/',
suffix: '',
pattern: '[^\\/#\\?]+?',
modifier: ''
},
{ name: 0, prefix: '/', suffix: '', pattern: '.*', modifier: '' }
]
end 正则表达式将匹配到字符串的未尾:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const options = {
end: false
}
const regexp = pathToRegexp("/:foo/(.*)", keys, options);
console.log(regexp);
console.log(keys);
/^(?:\/([^\/#\?]+?))(?:\/(.*))(?:[\/#\?](?=[]|$))?(?=[\/#\?]|[]|$)/i <<< 结尾的$/i 中的 $ 去掉了
[
{
name: 'foo',
prefix: '/',
suffix: '',
pattern: '[^\\/#\\?]+?',
modifier: ''
},
{ name: 0, prefix: '/', suffix: '', pattern: '.*', modifier: '' }
]
start 正则表达式将从字符串开头开始匹配 :
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const options = {
start: false
}
const regexp = pathToRegexp("/:foo/(.*)", keys, options);
console.log(regexp);
console.log(keys);
/(?:\/([^\/#\?]+?))(?:\/(.*))[\/#\?]?$/i <<< 开头部分的 /^ 中的 ^ 被去掉了
[
{
name: 'foo',
prefix: '/',
suffix: '',
pattern: '[^\\/#\\?]+?',
modifier: ''
},
{ name: 0, prefix: '/', suffix: '', pattern: '.*', modifier: '' }
]
delimiter 定义URL段的默认分隔符:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const options = {
delimiter: '[@%]'
}
const regexp = pathToRegexp("/:foo/(.*)", keys, options);
console.log(regexp);
console.log(keys);
/^(?:\/([^\[@%\]]+?))(?:\/(.*))[\[@%\]]?$/i
[
{
name: 'foo',
prefix: '/',
suffix: '',
pattern: '[^\\[@%\\]]+?',
modifier: ''
},
{ name: 0, prefix: '/', suffix: '', pattern: '.*', modifier: '' }
]
影响三处位置:
1、捕获组中的 非 @% 分隔符
2、捕获组外分隔 @% 匹配
3、默认子组中的正则匹配表达式
endsWith 定义URL尾部断言:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const options = {
endsWith: '@%'
}
const regexp = pathToRegexp("/:foo/(.*)", keys, options);
console.log(regexp);
console.log(keys);
/^(?:\/([^\/#\?]+?))(?:\/(.*))[\/#\?]?(?=[@%]|$)/i << 添加先行断言 (?=[@%]) 尾部匹配/、@、?且后面必须紧跟 @、% 符号 或者直接$ 结尾。
[
{
name: 'foo',
prefix: '/',
suffix: '',
pattern: '[^\\/#\\?]+?',
modifier: ''
},
{ name: 0, prefix: '/', suffix: '', pattern: '.*', modifier: '' }
]
encode 一个函数,用于在插入之前对RegExp字符串进行编码:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const options = {
encode: x => {console.log(x, '只有硬性的匹配字符可以被函数编码'); return x}
}
const regexp = pathToRegexp("/test/:foo", keys, options);
console.log(regexp);
console.log(keys);
/test 只有硬性的匹配字符可以被函数编码
/ 只有硬性的匹配字符可以被函数编码
只有硬性的匹配字符可以被函数编码 <<< 自定义URL参数 均被忽略为 空 字符串
/^\/test(?:\/([^\/#\?]+?))[\/#\?]?$/i
[
{
name: 'foo',
prefix: '/',
suffix: '',
pattern: '[^\\/#\\?]+?',
modifier: ''
}
]
prefixes 解析时自动考虑前缀的字符列表:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const keys = [];
const options = {
prefixes: '\'
}
const regexp = pathToRegexp("/:foo/(.*)", keys, options);
console.log(regexp);
console.log(keys);
/^\/([^\/#\?]+?)\/(.*)[\/#\?]?$/i <<< 无论设置什么字符串,最终都是把 每段URL中的匹配 (?:\/(...)) 外的 (?:) 非捕获组去掉
[
{
name: 'foo',
prefix: '',
suffix: '',
pattern: '[^\\/#\\?]+?',
modifier: ''
},
{ name: 0, prefix: '', suffix: '', pattern: '.*', modifier: '' }
]
URL中的默认分隔符就是/ 不能修改,修改只是去掉了非命名捕获组。
parse 解析函数
parse函数将从路径字符串返回字符串和键的列表:
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const tokens = parse("/route/:foo/(.*)");
console.log(tokens[0]);
//=> "/route"
// 硬性匹配字符 /route
console.log(tokens[1]);
//=> { name: 'foo', prefix: '/', suffix: '', pattern: '[^\/#\?]+?', modifier: '' }
// 自定义URL参数 foo
console.log(tokens[2]);
//=> { name: 0, prefix: '/', suffix: '', pattern: '.*', modifier: '' }
// 未命名URL参数
Compile(反向解析 pathToRegExp 对象)
该compile函数将返回一个用于将参数转换为有效路径的函数.
encode函数的反向解析
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
// 确保对路径段进行一致的编码。
const toPath = compile ( "/user/:id" , { encode : encodeURIComponent } );
// 传递参数为一个字典,字典中的字符 通过 options 中的 encode 进行转码,最终合成 url。
console.log(toPath({id: '123'}));
// "/user/123"
console.log(encodeURIComponent('123'));
console.log(toPath({id: 'café'}));
// "/user/caf%C3%A9"
console.log(encodeURIComponent('café'));
// caf%C3%A9
console.log(toPath({id: '/'}));
// "/user/%2F"
console.log(encodeURIComponent('/'));
// %2F
console.log(toPath({id: ':/'}));
// "/user/%3A%2F"
console.log(encodeURIComponent(':/'));
// %3A%2F
// 如果没有 `encode`,您需要确保输入正确编码。
const toPathRaw = compile ( "/user/:id" ) ;
// 没有任何转码函数的 pathToRegexp 对象, 直接设置转码后的字符串
toPathRaw ({id : "%3A%2F"}) ;
// "/user/%3A%2F"
量词匹配的反向解析
const {pathToRegexp, match, parse, compile } = require('path-to-regexp');
const toPathRepeated = compile("/:segment+");
// 正则表达式中涉及多次匹配
console.log(toPathRepeated({segment: 'foo'}));
// /foo
console.log(toPathRepeated({segment: ['a', 'b', 'c']}));
// /a/b/c
// 对正则表达式匹配多次的,可以通过数组传递参数
反向解析中的正则验证
const toPathRegexp = compile("/user/:id(\d+)");
// 传递不正确的匹配:
console.log(toPathRegexp({id: 123}));
// /user/123
// 传递类型非字符串,自动调用 toString 转码后匹配
console.log(toPathRegexp({id: "123"}));
// /user/123
// console.log(toPathRegexp({id: 'abc'})); // 报错
const toPathRegexpValidate = compile("/user/:id(\d+)", {validate: false});
// 不进行正则验证的反向解析解析
console.log(toPathRegexpValidate({id: 'abc'})); // 报错
// 'abc' 并不匹配 :id(\d+),但也被解析了.