简介
我在学习 vue-router@4 动态路由中自定义正则匹配规则的时候遇到了几处误区,以我"钻牛角尖"的性格,查阅相关文档后,初见其中原理,特写下这篇文章。本篇文章默认读者已初步了解动态路由及正则表达式的基本知识。
动态路由参数
以下面路由为例,当路径出现 /:[name] 时,表明该路由是动态路由,将匹配以/开头的任意路由,如 /a 、 /dasdas 等。在模板中通过 $route.params.pathName 可以获取动态参数。
js
const routes = [
{
// 动态字段以冒号开始,对应路径的参数将赋值给pathName
// 如路径 /abcd, 则pathName的值为abcd
path: '/:pathName'
}
]
由于动态路由在匹配的时候不区分动态参数名,因此以路径 /abcd 访问以下路由的时候,匹配的永远是第一条路由。
js
const routes = [
{
path: '/:pathName1'
},
{
path: '/:pathName2'
}
]
若要加以区分,则可添加路径前缀,如下所示。
js
const routes = [
{
// 匹配 /a/abcd
path: '/a/:pathName1'
},
{
// 匹配 /b/abcd
path: '/b/:pathName2'
}
]
但这样就平白多出了路由前缀,不够简洁,因此引出了自定义正则。
在参数中自定义正则
若定义的路由出现 :[name],则在底层中会将其转化为正则表达式 ([^/]+) (匹配至少一个不是/的字符),如下。
当出现路由 /abcd ,则该路由就会与转化的正则相匹配,然后根据动态参数的位置,将对应位置的动态参数abcd赋值给pathName。
js
const routes = [
{
// 底层转化为 /^\/([^/]+)/i,注意表达式并非完整,只是便于观看
path: '/:pathName' // 模板中$route.params.pathName -> abcd
}
]
由上面例子所知,如果有多条一级的动态路由,我们又不想添加路由前缀,则可以使用自定义正则,自定义正则的形式为/:pathName(正则),通过在动态路径后添加括号,在括号中为参数指定一个自定义的正则,指定的自定义正则将会替代默认的 ([^/]+), 如下所示。
js
const routes = [
{
// 底层转化为 /^\/(\d+)/i,注意表达式并非完整,只是便于观看
path: '/:pathName(\\d+)' // 仅匹配数字
},
{
// 底层转化为 /^\/(.*)/i,注意表达式并非完整,只是便于观看
path: '/:pathName(.*)' // 匹配任意字符
},
{
// 底层转化为 /^\/([^/]+)/i,注意表达式并非完整,只是便于观看
path: '/:pathName' // 匹配至少有一个不是斜杠的字符
}
]
TIP
确保转义反斜杠(
\) ,就像我们对\d( 变成\\d)所做的那样,在 JavaScript 中实际传递字符串中的反斜杠字符。
可重复参数
到这里我们就基本了解了什么是自定义正则了,但是我们在写下动态路由时,在底层中转化的正则表达式并非是全局匹配的,这也是为什么在上面的正则中出现i的原因。
从下面的例子看,我们以/assdf访问的时候,可以正确的匹配到 /:pathName ,但我们以 /asss/sss访问的时候,则无法匹配到 /:pathName 了,
js
const routes = [
{
// 底层转化为 /^\/([^/]+)/i,注意表达式并非完整,只是便于观看
path: '/:pathName' // 匹配至少有一个不是斜杠的字符
// /asss/sss 当出现了另一个斜杠,则匹配终止了,因此整个表达式不匹配
}
]
为了匹配具有多个部分的路由,则需要使用到自定义正则的重复参数。 如 /abcd/ab, 我们需要用 *(0 个或多个)和 +(1 个或多个)将参数标记为可重复:
js
const routes = [
{
// 底层转化为 /^\/((?:[^/]+?)(?:\/(?:[^/]+?))*)\/?$/i 完整表达式
path: '/:pathName+' // 多部分匹配至少有一个不是斜杠的字符(1个或多个)
// 匹配 /a,/a/b/c 。。。
},
{
// 底层转化为 /^(?:\/((?:[^/]+?)(?:\/(?:[^/]+?))*))?\/?$/i 完整表达式
path: '/:pathName*' // 多部分匹配至少有一个不是斜杠的字符(0个或多个)
// 匹配 / ,/a,/a/b/c 。。。
}
]
TIP
正则表达式中
(?:x)为非捕获括号,如果表达式是/foo{1,2}/,{1,2}将只应用于 'foo' 的最后一个字符 'o'。如果使用非捕获括号,则{1,2}会应用于整个 'foo' 单词。
?如果紧跟在任何量词*、+、?或{}的后面 ,将会使量词变为非贪婪 (匹配尽量少的字符),和缺省使用的贪婪模式 (匹配尽可能多的字符)正好相反。例如,对 "123abc" 使用/\d+/将会匹配 "123",而使用/\d+?/则只会匹配到 "1"。如果不是跟着量词后面的话,则表示为0次或1次。详情请移步MDN查阅:(developer.mozilla.org/zh-CN/docs/...)
当采用重复参数后,动态参数则以数组的形式接受多部分动态参数,如下:
js
const routes = [
{
// 底层转化为 /^\/((?:[^/]+?)(?:\/(?:[^/]+?))*)\/?$/i 完整表达式
// 匹配 /a/b/c
path: '/:pathName*' // $route.params.pathName -> ['a','b','c']
}
]
/:path* 、/:path(.*)与 /:path(.*)*
好了,到了"钻牛角尖"的时候了,请问上面小标题的三个例子有什么区别? 如果看完这篇文章的话那么心里大致就有了答案了。
/:pathName*是多部分匹配动态参数,若访问路径为/a/b/c,则获取到的参数为['a','b','c'];若访问路径为/a////,则无法正常匹配。/:pathName(.*)是自定义正则,用(.*)去替换([^/]+),因此匹配的字符是包括/的,所以哪怕路径是/ab//b//, 那么也是可以成功匹配的,得到的参数为['ab//b//']。/:pathName(.*)*同理,也是可以匹配到/ab//b//,但是匹配的是多部分的,区别就在于,匹配得到的参数为[ "ab", "", "b", "", "" ]
这也是为什么官方推荐用 /:pathName(.*)* 来捕获404路由,这可以极大限度地来防止 "老六" 乱输路径。
js
const routes = [
{
// 底层转化为 /^\/((?:[^/]+?)(?:\/(?:[^/]+?))*)\/?$/i 完整表达式
path: '/:pathName*'
},
{
// 底层转化为 /^\/(.*)\/?$/i 完整表达式
path: '/:pathName(.*)'
},
{
// 底层转化为 /^(?:\/((?:.*)(?:\/(?:.*))*))?\/?$/i 完整表达式
path: '/:pathName(.*)*'
}
]
相关参考
- 本篇文章针对路径的正则转化均来自官方推荐网站:Vue Router Path Parser (esm.dev)
- 正则相关知识:正则表达式 - JavaScript | MDN (mozilla.org)
- VueRouter官方文档:路由的匹配语法 | Vue Router (vuejs.org)