嵌套路由

嵌套路由如何诞生

一些应用程序的 UI 由多层嵌套的组件组成。在这种情况下,URL 的片段通常对应于特定的嵌套组件结构,例如:

image-20251206130056766

父路由组件中通过 <router-view> 承载子路由组件,路由路径按层级嵌套,最终实现 URL 与页面结构的一一对应。

在单页应用(SPA)中,很多页面并非 “一次性渲染”,而是由父容器 + 子内容组成

  • 电商后台:/dashboard(父)→ /dashboard/order(子:订单页)、/dashboard/user(子:用户页);
  • 社交应用:/user/123(父:用户主页)→ /user/123/profile(子:个人资料)、/user/123/posts(子:用户动态)。

使用嵌套路由

routes 数组中,父路由通过 children 字段声明子路由,子路由支持与父路由相同的配置(name/component/props 等)。

定义嵌套路由规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

// 懒加载组件
const User = () => import('../views/User.vue') // 父组件
const UserProfile = () => import('../views/UserProfile.vue') // 子组件1
const UserPosts = () => import('../views/UserPosts.vue') // 子组件2
const UserSettings = () => import('../views/UserSettings.vue') // 子组件3

const routes = [
// 其他路由...
{
path: '/user/:id', // 父路由路径(支持动态参数)
name: 'user', // 父路由名称(可选,但推荐)
component: User, // 父路由组件
// 子路由配置(核心)
children: [
// 【默认子路由】:访问 /user/:id 时默认渲染此组件
{
path: '', // 空路径 = 默认子路由
name: 'userDefault',
component: UserProfile
},
// 子路由1:相对路径 → 最终 URL:/user/:id/profile
{
path: 'profile', // 无需写 /user/:id/profile,自动拼接父路径
name: 'userProfile', // 子路由名称
component: UserProfile
},
// 子路由2:相对路径 → 最终 URL:/user/:id/posts
{
path: 'posts',
name: 'userPosts',
component: UserPosts
},
// 子路由3:嵌套更深的子路由(多层嵌套)
{
path: 'settings',
name: 'userSettings',
component: UserSettings,
// 孙子路由(多层嵌套,逻辑同上)
children: [
{
path: 'account', // → /user/:id/settings/account
name: 'userSettingsAccount',
component: () => import('../views/UserSettingsAccount.vue')
},
{
path: 'security', // → /user/:id/settings/security
name: 'userSettingsSecurity',
component: () => import('../views/UserSettingsSecurity.vue')
}
]
}
]
}
]

const router = createRouter({
history: createWebHistory(),
routes
})

export default router
  • 在处理嵌套路由的时候,通常会给子路由命名

父组件中添加 <router-view>

父路由组件(如 User.vue)必须包含 <router-view> 标签,作为子路由组件的渲染容器(类似 “占位符”)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- views/User.vue(父组件) -->
<template>
<div class="user-page">
<!-- 父组件固定内容 -->
<div class="user-header">
<h1>用户 {{ $route.params.id }} 的主页</h1>
<!-- 子路由导航(可选) -->
<nav>
<router-link :to="{ name: 'userProfile' }">个人资料</router-link>
<router-link :to="{ name: 'userPosts' }">我的动态</router-link>
<router-link :to="{ name: 'userSettings' }">设置</router-link>
</nav>
</div>

<!-- 子路由组件渲染位置(核心) -->
<router-view></router-view>
</div>
</template>

<script setup>
// 可通过 useRoute 获取父路由参数
import { useRoute } from 'vue-router'
const route = useRoute()
console.log('用户ID:', route.params.id) // 父路由的 :id 参数
</script>

子路由组件无需特殊配置,只需按普通组件编写即可:

1
2
3
4
5
6
7
<!-- views/UserProfile.vue(子组件1) -->
<template>
<div class="user-profile">
<h2>个人资料</h2>
<p>姓名、年龄、头像等信息...</p>
</div>
</template>
1
2
3
4
5
6
7
<!-- views/UserSettingsAccount.vue(孙子组件) -->
<template>
<div class="user-settings-account">
<h3>账号设置</h3>
<p>修改密码、绑定手机...</p>
</div>
</template>

访问嵌套路由

访问 URL 渲染结构 说明
/user/123 User(父) + UserProfile(默认子) 触发默认子路由
/user/123/profile User(父) + UserProfile 匹配子路由 profile
/user/123/settings/account User + UserSettings + UserSettingsAccount 多层嵌套

忽略父组件

为什么会有 “忽略父组件” 的需求?

仅利用路由的父子关系,但不嵌套路由组件,这对于将具有公共路径前缀的路由分组在一起或使用更高级的功能时很有用

假设你定义了嵌套路由:

1
2
3
4
5
6
7
8
9
{
path: '/user/:id', // 父路由
name: 'user',
component: User, // 父组件(包含用户信息头部 + <router-view>)
children: [
{ path: 'profile', name: 'userProfile', component: UserProfile }, // 子路由1
{ path: 'settings', name: 'userSettings', component: UserSettings } // 子路由2
]
}

但是,我希望仅利用路由的父子关系,但不嵌套路由组件,为了实现这一点, 我们在父路由中省略了 componentcomponents 选项

1
2
3
4
5
6
7
8
9
10
11
const routes = [
{
path: '/user/:id', // 父路由
name: 'user',
component: User, // 父组件(包含用户信息头部 + <router-view>)
children: [
{ path: 'profile', name: 'userProfile', component: UserProfile }, // 子路由1
{ path: 'settings', name: 'userSettings', component: UserSettings } // 子路由2
]
},
]

嵌套路由传递参数

  • 父路由参数透传

    子路由可直接访问父路由的动态参数(如 :id),无需额外配置:

    1
    2
    3
    4
    5
    6
    <!-- UserPosts.vue(子组件) -->
    <script setup>
    import { useRoute } from 'vue-router'
    const route = useRoute()
    console.log('父路由的 id:', route.params.id) // 直接获取 /user/:id 中的 id
    </script>
  • 子路由自身的动态参数

    子路由也可定义自己的动态参数,与父参数共存:

    1
    2
    3
    4
    5
    6
    7
    8
    // 路由配置
    children: [
    {
    path: 'posts/:postId', // 子路由动态参数
    name: 'userPostDetail',
    component: () => import('../views/UserPostDetail.vue')
    }
    ]
    1
    2
    3
    4
    5
    6
    7
    <!-- 跳转时传递父 + 子参数 -->
    <router-link :to="{
    name: 'userPostDetail',
    params: { id: 123, postId: 456 }
    }">
    查看动态 456
    </router-link>
    1
    2
    3
    4
    5
    6
    7
    <!-- UserPostDetail.vue 中获取参数 -->
    <script setup>
    import { useRoute } from 'vue-router'
    const route = useRoute()
    console.log('用户ID:', route.params.id) // 父参数
    console.log('动态ID:', route.params.postId) // 子参数
    </script>
  • 通过 props 传递参数

    嵌套路由同样支持 props 配置,将参数转为组件 props,提升组件复用性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 路由配置
    {
    path: '/user/:id',
    component: User,
    props: true, // 父路由开启 props
    children: [
    {
    path: 'profile',
    component: UserProfile,
    props: (route) => ({
    userId: route.params.id, // 透传父参数
    isAdmin: false // 自定义参数
    })
    }
    ]
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!-- UserProfile.vue 接收 props -->
    <script setup>
    defineProps({
    userId: {
    type: String,
    required: true
    },
    isAdmin: {
    type: Boolean,
    default: false
    }
    })
    </script>

动态路由

什么是动态路由

根据不同用户角色来展示不同的菜单页面是很常见的需求,例如管理员,网站创作者和普通用户看到的个人主页应该就是不一样的,动态路由提供了灵活的路由规则,可以根据需要轻松添加、修改或者删除路由

动态路由是 Vue Router 中匹配可变路径片段的路由机制,这样就可以让同一路由规则匹配多个不同的 URL,并将 URL 中的可变部分作为参数传递给组件,从而实现 复用组件 + 根据参数渲染不同内容 的效果。

举个场景:电商网站的商品详情页,所有商品的详情页结构完全相同,只是商品 ID 不同(如 /goods/1/goods/2/goods/3)。如果为每个商品都定义一个路由规则,会极度冗余;而动态路由只需定义 /goods/:id 一条规则,就能匹配所有商品详情页,且能获取 URL 中的 id 参数。

也就是说,很多时候,动态路由将具有给定同一模式的路由映射到同一个组件

例如,我们可能有一个 User 组件,它应该为所有用户渲染,但使用不同的用户 ID。在 Vue Router 中,我们可以使用路径中的动态段来实现这一点,我们称之为 参数

1
2
3
4
5
6
7
import User from './User.vue'

// these are passed to `createRouter`
const routes = [
// dynamic segments start with a colon
{ path: '/users/:id', component: User },
]

现在,像 /users/johnny/users/jolyne 这样的 URL 都将映射到同一个路由。

那么动态路由如何写呢?: 标记动态参数

  • 在定义路由规则时,用 :参数名 标记 URL 中的动态片段,语法格式:

    1
    2
    3
    4
    5
    6
    {
    path: '/路由路径/:动态参数名', // 单个动态参数
    // path: '/路由路径/:param1/:param2', // 多个动态参数(支持多个)
    name: '路由名称', // 可选,命名路由
    component: 目标组件 // 要渲染的组件
    }

这样的路由跳转有两种形式

  • 声明式跳转(<router-link>:直接在模板中拼接动态参数
  • 编程式跳转($router.push:通过参数对象传递动态值

在目标组件中,通过 $route.params.参数名,或组合 setup + useRoute,获取 URL 中的动态值

而且当路由匹配时,其 参数 的值将作为 route.params 公开给每个组件。因此,我们可以通过将 User 的模板更新为以下内容来渲染当前用户 ID

1
2
3
4
5
6
<template>
<div>
<!-- The current route is accessible as $route in the template -->
User {{ $route.params.id }}
</div>
</template>

可以在同一个路由中拥有多个 参数,它们将映射到 route.params 上的对应字段。

动态路由匹配

来看一个实际的例子,对于动态路由如何匹配和拿到其参数

首先,定义路由规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import GoodsDetail from '@/components/GoodsDetail.vue'

const routes = [
{
path: '/goods/:id', // 动态参数:id
name: 'GoodsDetail',
component: GoodsDetail
}
]

const router = createRouter({
history: createWebHistory(), // Vue3 推荐的 history 模式
routes
})

export default router

路由跳转有两种方式

  • 声明式跳转<router-link>,在任意组件的模板中使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- 固定参数跳转:/goods/1 -->
    <router-link to="/goods/1">商品1详情</router-link>

    <!-- 动态参数跳转(绑定变量):比如 item.id 是从数据中获取的 -->
    <router-link :to="`/goods/${item.id}`">商品{{item.id}}详情</router-link>

    <!-- 命名路由跳转:通过 params 传动态参数 -->
    <router-link :to="{ name: 'GoodsDetail', params: { id: item.id } }">
    商品{{item.id}}详情
    </router-link>
  • 编程式跳转,常用于按钮点击和逻辑跳转,通常绑定为一个事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <template>
    <button @click="goToGoods(2)">跳转到商品2详情</button>
    </template>

    <script setup>
    import { useRouter } from 'vue-router'
    const router = useRouter()

    const goToGoods = (id) => {
    // 命名路由 + params
    router.push({
    name: 'GoodsDetail',
    params: { id: id }
    })

    // 或直接拼接路径
    // router.push(`/goods/${id}`)
    }
    </script>

然后,这样进行绑定之后,组件内就可以获取动态参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- src/components/GoodsDetail.vue -->
<template>
<div>
<h1>商品详情页</h1>
<p>当前商品ID:{{ route.params.id }}</p>
</div>
</template>

<script setup>
import { useRoute } from 'vue-router'
const route = useRoute() // 获取当前路由对象

// 打印动态参数
console.log('商品ID:', route.params.id)
</script>

对路由参数更改做出反应

当用户在同一路由规则、不同参数的 URL 间跳转(如 /users/johnny/users/jolyne)时,Vue Router 会复用同一个组件实例(而非销毁重建),目的是提升性能。但副作用是:

  • 组件的生命周期钩子(created/mounted 等)不会重新执行;
  • 如果组件逻辑依赖路由参数(比如根据 id 请求数据),参数变化后页面不会自动更新。

因此必须手动监听参数变化,触发逻辑更新。

假设你有一个「用户详情页」路由:/user/:id,当用户从 /user/1 跳转到 /user/2 时,如果不手动处理,页面还是显示「用户 1」的信息,不会更新为「用户 2」,那么可以这样让组件感知到 id 参数变化,并更新数据 / 控制跳转

  • 监听 route.params

    这是最直观的方式,利用 Vue 的 watch 监听路由参数的变化,参数更新时执行自定义逻辑(比如重新请求数据)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    <template>
    <div>
    <h1>用户详情:{{ userId }}</h1>
    <p>{{ userInfo.name }}</p>
    </div>
    </template>

    <script setup>
    import { ref, watch } from 'vue'
    import { useRoute } from 'vue-router'

    // 1. 获取当前路由对象
    const route = useRoute()
    // 2. 响应式数据:存储用户ID和信息
    const userId = ref(route.params.id) // 初始值是当前路由的id(比如1)
    const userInfo = ref({})

    // 3. 封装数据请求逻辑
    const fetchUser = async (id) => {
    // 模拟接口请求:根据ID获取用户数据
    const res = await fetch(`/api/user/${id}`)
    userInfo.value = await res.json()
    }

    // 4. 页面初始加载时,请求当前ID的用户数据
    fetchUser(userId.value)

    // 5. 监听路由参数id变化
    watch(
    // 监听的目标:路由参数id(需用函数返回,确保监听响应式)
    () => route.params.id,
    // 回调函数:newId=新值,oldId=旧值
    (newId, oldId) => {
    console.log(`从${oldId}切换到${newId}`)
    userId.value = newId // 更新响应式的userId
    fetchUser(newId) // 参数变化时重新请求数据
    },
    // 可选配置:immediate(初始执行)、deep(深度监听,参数是对象时用)
    { immediate: false }
    )
    </script>

    那么整个的执行流程还是很清晰的

    1. 用户点击跳转链接(比如 <router-link to="/user/2">);
    2. URL 变成 /user/2route.params.id1 变成 2
    3. watch 监听到这个变化,执行回调函数;
    4. 回调函数里更新 userId2,并调用 fetchUser(2) 请求新数据;
    5. userInfo 被更新为用户 2 的信息,页面自动渲染新内容。

    其中注意一些问题:

    • 为什么监听目标必须是 () => route.params.id,而不是直接 route.params.id
      • routeuseRoute() 返回的响应式对象,但 route.params.id 是响应式对象的属性值(字符串类型),本身不是响应式的;要是不写函数返回值形式就会丢失响应式,直接写 watch(route.params.id, ...),相当于监听一个 “固定的字符串”(比如初始值 1),这个字符串不会自己变,所以监听永远不会触发;
      • 写成 () => route.params.id,相当于每次「检查」时都去 route.params 里取最新的 id,能感知到参数变化,这是 Vue 监听 “对象属性” 的标准写法。
    • 若参数是复杂对象(如 /user/:infoinfo 是 JSON 字符串),需加 deep: true 开启深度监听。
  • 或者,使用 beforeRouteUpdate 导航守卫,它也允许你取消导航

    beforeRouteUpdate组件级导航守卫,专门用于监听 “当前组件对应的路由参数变化”,触发时机是 “路由更新前(路由参数变化、且组件复用时(比如 /user/1/user/2),跳转完成前触发)”,且支持取消导航(比如参数不合法时阻止跳转)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    <template>
    <div>用户ID:{{ route.params.id }}</div>
    </template>

    <script setup>
    import { ref } from 'vue'
    import { useRoute, onBeforeRouteUpdate } from 'vue-router'

    const route = useRoute()
    const userInfo = ref({})

    // 初始加载
    const fetchUser = async (id) => {
    userInfo.value = await fetch(`/api/user/${id}`).then(res => res.json())
    }
    fetchUser(route.params.id)

    // 路由参数更新前触发
    onBeforeRouteUpdate(async (to, from, next) => {
    // to:目标路由对象(跳转到哪里去)
    // from:当前路由对象(从哪里跳过来)
    // next:可选(Vue2 需手动调用,Vue3 组合式API中可省略,返回false/路由对象可取消/重定向)

    // 1. 校验参数:比如ID必须是数字,否则取消导航
    if (isNaN(Number(to.params.id))) {
    // 取消导航(停留在当前页面)
    return false
    // 或重定向:return { path: '/404' }
    }

    // 2. 参数合法,重新请求数据, 用目标路由的id请求数据
    await fetchUser(to.params.id)

    // Vue3 中无需手动调用 next(),逻辑执行完自动放行
    // 如果是Vue2,需要手动写 next() 才会完成跳转
    })
    </script>

除了 route.params 之外,route 对象还公开了其他有用的信息

路由匹配的语法

动态路由匹配规则

Vue Router 的动态路由匹配,本质是通过 参数占位符 + 可选正则约束,让一条路由规则匹配多个结构化相似的 URL

比如 /user/:id 能匹配 /user/1/user/abc,而 /user/:id(\\d+) 则仅匹配 /user/123(数字 ID),不匹配 /user/abc—— 这就是「基础匹配」和「正则约束匹配」的区别。

基础匹配规则

单个动态参数的匹配上面说了,就是/固定路径/:参数名,其中: 后的部分为动态参数,匹配任意非 / 的字符」(因为 / 是路径片段的分隔符)

多个动态参数和单个动态参数的思路类似

  • /固定路径/:参数1/:参数2

  • 匹配逻辑:多个参数占位符按顺序匹配对应的路径片段,每个参数仍仅匹配「非 / 的字符」。

    1
    2
    3
    const routes = [
    { path: '/post/:category/:id', component: Post }
    ]
    • 匹配成功:/post/tech/100category=techid=100)、/post/life/abccategory=lifeid=abc);

    • 匹配失败:/post/tech(缺少 id 参数)、/post/tech/100/comment(多了 comment 片段)。

可选动态参数

匹配语法/固定路径/:参数名?? 标记参数可选)

匹配逻辑:参数可有可无,既匹配「带参数的 URL」,也匹配「不带参数的 URL」。

1
2
3
const routes = [
{ path: '/user/:id?', component: User }
]
  • 匹配成功:/user/123id=123)、/userid=undefined);
  • 匹配失败:/user/123/456(多片段)。

正则约束

基础匹配的参数范围太宽泛(比如 id 本应是数字,却能匹配字符串),因此可通过「自定义正则」约束参数格式,语法为:/:参数名(正则表达式)

约束需求 正则写法 路由示例 匹配成功 匹配失败
仅匹配数字 ID :id(\\d+) /user/:id(\\d+) /user/123 /user/abc
仅匹配字母 :name([a-zA-Z]+) /article/:name([a-zA-Z]+) /article/hello /article/123
匹配字母 + 数字 :code([a-zA-Z0-9]+) /order/:code([a-zA-Z0-9]+) /order/abc123 /order/abc-123
匹配固定长度(6 位数字) :code(\\d{6}) /verify/:code(\\d{6}) /verify/123456 /ver

注意:

  • 正则中的特殊字符(如 \d)需要转义:在 JavaScript 字符串中,\d 要写成 \\d(因为 \ 是转义符);

  • 正则仅约束「单个参数片段」(即 / 分隔的一段),无法跨片段约束;

  • 多个参数可分别加正则约束

    1
    2
    // category 仅匹配字母,id 仅匹配数字
    { path: '/post/:category([a-zA-Z]+)/:id(\\d+)', component: Post }

捕获所有参数

一般是实现 404 页面、匹配任意嵌套路径

匹配语法:/:参数名(.*)*

  • (.*):正则匹配「任意字符(包括 /)」;
  • 末尾 *:标记参数为「可重复、可选」,能匹配多段路径(参数会被解析为数组)。
1
2
3
4
const routes = [
// 捕获所有未定义的路由,用于404
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
]

所有未显式定义的路由都会匹配到这条规则,跳转到 404 组件。

匹配以固定字符开头的任意路径(前缀匹配)

语法/固定前缀:参数名(.*)

1
2
3
4
const routes = [
// 匹配所有以 /admin- 开头的路径
{ path: '/admin-:rest(.*)', component: AdminGeneric }
]
  • 匹配成功:/admin-dashboardrest: 'dashboard')、/admin-user/123rest: 'user/123');
  • 匹配失败:/admin(无后续字符)、/user-admin/123(前缀不是 /admin-)。

可重复参数

语法/:参数名++ 标记参数可重复)

匹配逻辑:匹配「至少一个」路径片段,参数解析为数组;若加 * 则匹配「零个或多个」片段。

1
2
3
4
5
6
const routes = [
// +:至少一个标签片段
{ path: '/tags/:tag+', component: Tags },
// *:零个或多个标签片段
{ path: '/tags/:tag*', component: Tags }
]
  • /:tag+ 匹配:/tags/vuetag: ['vue'])、/tags/vue/reacttag: ['vue', 'react']);不匹配 /tags
  • /:tag* 匹配:/tagstag: [])、/tags/vuetag: ['vue']);

匹配优先级规则

当多条路由规则都能匹配同一个 URL 时,Vue Router 遵循「先定义的规则优先级更高」的原则。

  1. 精准匹配优先于动态匹配:固定路径(如 /user/me)比带动态参数的路径(如 /user/:id)优先级更高;
  2. 先定义的规则优先于后定义的规则:路由数组中排在前面的规则,会先被匹配,匹配成功后立即终止匹配流程;
  3. 普通动态参数优先于通配符 / 捕获所有参数/user/:id/user/:pathMatch(.*)* 优先级更高;
  4. 嵌套路由的匹配深度优先:嵌套路由的子规则会先匹配,再匹配父规则(仅针对嵌套结构)。

简单来说:Vue Router 会按路由数组的定义顺序,从第一条开始逐个匹配 URL,找到第一个符合条件的规则后立即停止,不再匹配后续规则

例如

  • 同时定义精准路径和动态参数路径,精准路径会优先匹配。

    1
    2
    3
    4
    5
    6
    const routes = [
    // 规则1:精准匹配 /user/me
    { path: '/user/me', component: UserProfile },
    // 规则2:动态匹配 /user/:id
    { path: '/user/:id(\\d+)', component: UserDetail }
    ]
    • 访问 /user/me:匹配规则 1(而非规则 2),因为规则 1 定义在前;

    • 访问 /user/123:匹配规则 2(规则 1 不匹配)。

  • 先定义的规则 > 后定义的规则

    1
    2
    3
    4
    5
    6
    const routes = [
    // 规则1:先定义,匹配以 /user/admin 开头的路径
    { path: '/user/admin', component: AdminPanel },
    // 规则2:后定义,匹配所有 /user/:role 路径
    { path: '/user/:role', component: UserRole }
    ]
    • 访问 /user/admin:匹配规则 1(先定义),即使规则 2 也能匹配,也不会触发;
    • 访问 /user/editor:匹配规则 2(规则 1 不匹配);
    • 注意:如果把规则 2 放在规则 1 前面,那么 /user/admin 会匹配规则 2(role=admin
  • 普通动态参数 > 通配符 / 捕获所有参数

    1
    2
    3
    4
    5
    6
    const routes = [
    // 规则1:普通动态参数
    { path: '/user/:id', component: UserDetail },
    // 规则2:捕获所有参数(404)
    { path: '/:pathMatch(.*)*', component: NotFound }
    ]
    • 访问 /user/123:匹配规则 1(普通动态参数优先级更高);
    • 访问 /abc/123:规则 1 不匹配,才会匹配规则 2(404 页面);
    • 核心原则:捕获所有参数的规则必须放在路由数组的最后,否则会覆盖前面的所有规则(比如把规则 2 放在规则 1 前面,那么 /user/123 会被规则 2 匹配,直接跳 404)。404 路由必须放在最后
  • 嵌套路由的子规则会先匹配,再匹配父规则。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const routes = [
    {
    path: '/user',
    component: UserLayout, // 父组件
    children: [
    // 子规则1:嵌套路由,先匹配
    { path: 'profile', component: UserProfile },
    // 子规则2:嵌套动态参数
    { path: ':id', component: UserDetail }
    ]
    },
    // 父级规则:后匹配
    { path: '/user/:id', component: UserDetailV2 }
    ]
    • 访问 /user/profile:先匹配嵌套路由的子规则 1(/user/profile),渲染 UserLayout + UserProfile
    • 访问 /user/123:先匹配嵌套路由的子规则 2(/user/:id),渲染 UserLayout + UserDetail,而非外层的 /user/:idUserDetailV2);
    • 核心逻辑:嵌套路由的 children 规则是「更深度的匹配」,优先级高于外层同路径规则。

敏感和严格路由选项

默认情况下,所有路由都区分大小写,并且匹配带有或不带有尾部斜杠的路由。假如,我希望路由 /users 能匹配 /users/users/ 甚至 /Users/。此行为可以使用 strictsensitive 选项配置,它们可以在路由器和路由级别设置

1
2
3
4
5
6
7
8
9
10
11
12
const router = createRouter({
history: createWebHistory(),
routes,
strict: true, // 严格模式(可选)
sensitive: true // 区分大小写(可选)
})

// 打印匹配过程(调试用)
router.beforeEach((to, from) => {
console.log('匹配的目标路由:', to.fullPath)
console.log('匹配的规则:', to.matched) // 显示匹配到的所有规则(数组)
})

这种情况一般在路由调试时候用的最多

例如上述就是一个路由匹配调试,用于测试路由匹配的优先级的,访问目标 URL,查看 to.matched 数组:数组第一个元素就是优先匹配的规则。

命名路由

命名路由是 Vue Router 中为路由规则赋予唯一名称的特性,核心作用是替代硬编码的 URL 路径

创建路由时,我们可以选择性地为路由指定一个 name

为什么需要命名路由?

  • 避免硬编码路径:如果业务路径变更(如 /user/member),只需修改路由配置,无需逐个修改跳转处的路径;
  • 简化动态路由跳转:带参数的路由(如 /user/:id)通过名称跳转更直观;
  • 明确语义:路由名称比路径更易理解(如 name: 'userDetail'/user/123 更清晰)。

在创建路由实例时,为 routes 数组中的路由对象添加 name 属性,即为该路由赋予唯一名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import UserDetail from '../views/UserDetail.vue'

const routes = [
{
path: '/',
name: 'home', // 命名路由:名称为 home
component: Home
},
{
path: '/user/:id', // 动态参数路由
name: 'userDetail', // 命名路由:名称为 userDetail
component: UserDetail
}
]

const router = createRouter({
history: createWebHistory(),
routes
})

export default router
  • 其中,命名路由的name属性值不能重复,必须保证是唯一的。

  • params 与 path 不可混用

    • 如果通过 name 跳转,params 会被正确解析到 URL 中;

    • 如果通过 path 跳转,params 会被忽略(Vue Router 4 明确规定)

      1
      2
      3
      4
      5
      6
      // 错误:path 跳转时 params 无效
      router.push({ path: '/user', params: { id: 123 } })
      // 正确:name 跳转 + params
      router.push({ name: 'userDetail', params: { id: 123 } })
      // 正确:path 跳转时直接拼接参数
      router.push({ path: `/user/${123}` })

当路由匹配规则有了路由名称后,在定义路由链接或执行某些跳转操作时,可以直接通过路由名称表示相应的路由,不再需要通过路由路径表示相应的路由。

那么使用路由时,一般会先在 routes 属性中配置路由匹配规则,然后在页面中使用<router-link>的 to 属性跳转到指定目标地址。所以命名路由也是一样的

<router-link>标签中使用命名路由时,需要动态绑定 to 属性的值为对象。当使用对象作为 to 属性的值时,to 前面要加一个冒号,也就是使用 v-bind指令进行绑定。

在对象中,通过name属性指定要跳转到的路由名称,使用 params 属性指定跳转时携带的路由参数,语法格式如下:

1
<router-link :to="{ name: 路由名称, params: { 参数名: 参数值 } }"></router-link>

也就是

1
2
<!-- 替代 <router-link to="/">首页</router-link> -->
<router-link :to="{ name: 'home' }">首页</router-link>

带动态参数

1
2
3
4
5
6
7
<!-- 替代 <router-link to="/user/123">用户123</router-link> -->
<router-link :to="{
name: 'userDetail',
params: { id: 123 } // 匹配路由的 path: /user/:id
}">
用户123
</router-link>

带查询参数(query)

命名路由也可结合 query(查询参数,如 /user/123?tab=info

1
2
3
4
5
6
router.push({
name: 'userDetail',
params: { id: 123 },
query: { tab: 'info' }
})
// 最终 URL:/user/123?tab=info

嵌套路由中的命名路由

嵌套路由(子路由)也可命名,适用于复杂页面结构:

先定义嵌套命名路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// router/index.js
const routes = [
{
path: '/user/:id',
name: 'user', // 父路由名称
component: () => import('../views/User.vue'),
children: [
{
path: 'profile', // 子路由路径(相对路径)
name: 'userProfile', // 子路由名称
component: () => import('../views/UserProfile.vue')
},
{
path: 'settings',
name: 'userSettings',
component: () => import('../views/UserSettings.vue')
}
]
}
]

跳转嵌套命名路由

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()

// 跳转到 /user/123/profile
const goUserProfile = () => {
router.push({
name: 'userProfile', // 直接使用子路由名称
params: { id: 123 } // 父路由的参数仍需传递
})
}
</script>

而且命名路由和 alias(别名)是互补的:

  • name 用于跳转时的标识;
  • alias 用于 URL 路径的别名(如 path: '/user', alias: '/member',访问 /member 等同于 /user)。

编程导航使用命名路由

而且,下面会讲编程式导航,命名路由也是编程式导航的重要使用

通过 router.push() / router.replace() 跳转时,使用命名路由对象替代路径字符串:

1
2
3
4
5
6
7
8
9
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()

// 跳转首页:替代 router.push('/')
const goHome = () => {
router.push({ name: 'home' })
}
</script>

带动态参数

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()

// 跳转到用户详情:替代 router.push('/user/123')
const goUserDetail = (userId) => {
router.push({
name: 'userDetail',
params: { id: userId } // 必须匹配路由定义的参数名
})
}
</script>

编程导航

什么是编程导航

在 Vue Router 中,路由跳转分为两种核心方式:声明式导航(<router-link>编程式导航(通过 JS 代码触发跳转)

前者适合模板中静态 / 简单的导航场景,后者适合动态逻辑(如点击按钮、请求成功后跳转)

其实以前一直在用,只不过现在单独拿出来说一下

编程式导航指通过调用 Vue Router 提供的 API(如 router.pushrouter.replace 等),在 JavaScript 代码中主动触发路由跳转,而非通过模板中的 <router-link> 组件。

<router-link> 只能处理 “点击链接跳转” 的静态场景,但实际开发中,很多跳转需要结合业务逻辑:

  • 表单提交成功后跳转到列表页;
  • 点击按钮时先验证权限,再决定是否跳转;
  • 定时器延迟跳转(如点击下载后 5 秒跳转到下载链接);
  • 批量操作完成后跳转并携带提示信息。

这些动态逻辑无法通过 <router-link> 实现,必须依赖编程式导航。

使用编程式导航前,需先获取 router 实例:

  • 选项式 API:组件内可直接通过 this.$router 访问;

  • 组合式 API(setup 语法):需导入useRouter钩子获取:

    1
    2
    3
    4
    <script setup>
    import { useRouter } from 'vue-router'
    const router = useRouter() // 获取 router 实例
    </script>

<router-link>声明式导航的核心组件

<router-link> 是 Vue Router 提供的声明式导航组件,本质是对 <a> 标签的封装,用于在模板中实现路由跳转,无需编写 JS 代码。

替代原生 <a> 标签:避免页面刷新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<!-- 基础跳转(字符串路径) -->
<router-link to="/">首页</router-link>

<!-- 命名路由 + 动态参数(推荐) -->
<router-link
:to="{
name: 'userDetail',
params: { id: userId },
query: { tab: 'profile' }
}"
>
用户详情
</router-link>

<!-- 嵌套路由跳转 -->
<router-link :to="{ name: 'userProfile', params: { id: 123 } }">
个人资料
</router-link>

<!-- 替换历史记录(登录后跳转) -->
<router-link to="/home" replace>
跳转到首页(不保留历史)
</router-link>
</template>

<script setup>
const userId = 123
</script>

核心属性

属性名 作用 示例
to 目标路由(必填),支持字符串 / 对象 to="/user":to="{ name: 'user' }"
replace 是否替换当前历史记录(布尔值),等同于 router.replace() <router-link to="/" replace>
active-class 自定义激活类名(替代默认的 router-link-active active-class="active"
exact 精确匹配路由(只有路径完全一致时才激活) <router-link to="/" exact>
exact-active-class 精确匹配的激活类名 exact-active-class="exact-active"
target 跳转目标(如 _blank 打开新标签页) <router-link to="/" target="_blank">

其中,涉及到两个激活类名

  • router-link-active:模糊匹配(默认):如 /user/123 会匹配 /user<router-link>
  • router-link-exact-active:精确匹配:只有路径完全一致时才激活(如 / 仅匹配首页,不会匹配 /user)。

编程式导航的核心 API

Vue Router 4 提供了 5 个核心编程式导航 API,覆盖所有跳转场景:

  • router.push()

    最常用的跳转

    它触发路由跳转,而且向浏览器历史记录栈中添加一条新记录(类似点击 <a> 标签),用户点击浏览器 “后退” 可回到上一页。

    写法类型 示例 说明
    字符串路径 router.push('/user/123') 直接传入目标路径
    路由对象(path) router.push({ path: '/user/123', query: { tab: 'info' } }) 带查询参数的路径跳转
    路由对象(name) router.push({ name: 'userDetail', params: { id: 123 } }) 命名路由跳转(推荐)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <script setup>
    import { useRouter } from 'vue-router'
    const router = useRouter()

    // 场景1:基础跳转(字符串路径)
    const goToHome = () => {
    router.push('/')
    }

    // 场景2:带查询参数(path + query)
    const goToUserWithQuery = () => {
    router.push({
    path: '/user/123',
    query: { tab: 'profile', page: 1 } // URL:/user/123?tab=profile&page=1
    })
    }

    // 场景3:命名路由 + 动态参数
    const goToUserDetail = (userId) => {
    router.push({
    name: 'userDetail',
    params: { id: userId }, // 匹配路由 path: /user/:id
    query: { tab: 'settings' } // 可同时带 query
    })
    }

    // 场景4:嵌套路由跳转
    const goToUserProfile = () => {
    router.push({
    name: 'userProfile', // 子路由名称
    params: { id: 123 } // 父路由参数需传递
    })
    }
    • pathparams 不可混用规则依旧生效
    • 动态参数必填:如果路由定义了 :id,跳转时必须传递 params.id,否则 URL 会异常(如 /user/undefined)。
  • router.replace()

    替换当前历史记录,与 router.push() 功能一致,但不会新增历史记录,而是替换当前历史记录(用户点击 “后退” 会回到上上个页面)。

    登录页跳转首页最常用的这个API

    1
    2
    3
    4
    5
    6
    7
    // 登录成功后跳转首页,替换当前历史记录
    const login = () => {
    // 模拟登录请求
    api.login().then(() => {
    router.replace('/home') // 而非 push,避免后退回登录页
    })
    }
  • router.go(n)

    控制历史记录前进 / 后退,基于浏览器历史记录栈,向前 / 向后跳转指定步数,正数前进,负数后退

    • router.go(0):刷新当前页面(不推荐,建议用 window.location.reload())。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 后退到上一页
    const goBack = () => {
    router.go(-1)
    }

    // 前进到下一页(如果有)
    const goForward = () => {
    router.go(1)
    }
  • router.back() / router.forward()

    快捷前进后退,语法糖,router.back() 等同于 router.go(-1)(后退),router.forward() 等同于 router.go(1)(前进)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 后退
    const goBack = () => {
    router.back()
    }

    // 前进
    const goForward = () => {
    router.forward()
    }
  • router.push() 的 Promise 回调

    Vue Router 4 中,router.push() / router.replace() 会返回一个 Promise,可用于处理跳转成功 / 失败的逻辑(如路由守卫拦截、路径不存在等)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const goToUser = () => {
    router.push({ name: 'userDetail', params: { id: 123 } })
    .then(() => {
    console.log('跳转成功')
    })
    .catch((err) => {
    console.log('跳转失败:', err) // 如路由守卫拒绝、路径不存在
    })
    }