简介
我在学习 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)