两种路由模式

Vue Router 使用两种主要的方式来监听 URL 的变化:一种是基于浏览器的 history API,另一种是基于 hash 变化。这两种方式分别对应 history 模式和 hash 模式。

不管是 hash 模式还是 history 模式,核心目的都是在不刷新整个页面的前提下,实现前端路由的切换(单页应用 SPA 的核心需求),只是监听 URL 变化的方式和 URL 表现形式不同。

history 模式

history 模式下,Vue Router 利用 HTML5 的 history API 来实现路由的切换。(pushStatereplaceStatepopstate 事件)

  • 这些 API 允许开发者在不刷新页面的情况下,修改浏览器的历史记录栈,并且 URL 中不再包含 # 符号
  • Vue Router 会监听浏览器的 popstate 事件(当用户点击浏览器的前进 / 后退按钮时触发),同时通过 pushState/replaceState 方法修改 URL 并渲染对应组件。
  • 当用户导航到新的路由时,Vue Router 会使用 history.pushStatehistory.replaceState 方法来更改浏览器的历史记录

因为它不包含 # 符号。所以 URL 更美观、对 SEO 更友好,但需要后端配置重定向,仅支持现代浏览器,适合生产环境的正式项目

若项目部署在常规的 Web 服务器(Nginx/Apache)上,优先选择 history 模式并配置后端重定向

需要使用 createWebHistory 来创建 history 模式的路由实例:

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
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]

// 创建路由实例:使用 createWebHistory 对应 history 模式
const router = createRouter({
history: createWebHistory(), // 显式指定 history 模式(可传入基础路径,如 createWebHistory('/app/'))
routes
})

export default router

hash 模式

hash 模式,它不会改变浏览器的 URL,但页面会重新加载。它是默认模式,兼容性好、而且无需后端配置,但 URL 带 #,适合对 URL 美观度要求不高、需要兼容老旧浏览器的项目。

hash 指的是 URL 中 # 符号后面的部分(比如 http://localhost:5173/#/home 中,#/home 就是 hash)。

  • 浏览器在处理 hash 时,不会将 # 及后面的内容发送给服务器,它只是浏览器端的一个标识。
  • Vue Router 的 hash 模式会监听浏览器的 hashchange 事件(当 URL 中的 hash 发生变化时触发),从而根据新的 hash 值渲染对应的组件。

貌似没看到过不支持 hash 模式的老东西,它貌似支持所有浏览器,因为 hashchange 事件是很早就支持的特性。

而且因为 hash 部分不会发送到服务器,所以即使直接刷新页面,服务器也不会处理 hash,不会出现 404 错误。

在创建路由实例时,默认就是 hash 模式,也可以显式指定:

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
// src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

// 定义路由规则
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]

// 创建路由实例:使用 createWebHashHistory 对应 hash 模式
const router = createRouter({
history: createWebHashHistory(), // 显式指定 hash 模式
routes
})

export default router

此时访问的 URL 形式是:http://localhost:5173/#/http://localhost:5173/#/about

其他的路由模式

Memory 模式

他会假设自己不在浏览器中,所以我们不考虑它是一种主流的浏览器路由模式

它的核心特点是路由的历史记录只存储在内存中,不与浏览器的 URL、历史记录栈交互,也不依赖浏览器环境

Memory 模式完全 “脱离” 浏览器的 URL 和历史记录,路由的变化只在程序内存中发生,外部完全感知不到。

正因为它不依赖浏览器环境,所以主要用于非浏览器场景

  1. Node.js 环境:比如在 Node 端执行路由相关的逻辑(如数据预取),此时没有浏览器的 URL 和 history API,Memory 模式是最佳选择。
  2. 服务端渲染(SSR):Vue 服务端渲染时,服务端没有浏览器环境,需要用 Memory 模式来处理路由,避免依赖浏览器的 API。

通过 createMemoryHistory() 创建 Memory 模式的路由实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/router/index.js
import { createRouter, createMemoryHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

const routes = [
{ path: '/', component: Home, name: 'Home' },
{ path: '/about', component: About, name: 'About' }
]

// 创建 Memory 模式的路由实例
const router = createRouter({
history: createMemoryHistory(), // 核心:使用 createMemoryHistory
routes
})

export default router

Memory 模式不会自动触发初始的路由导航(比如默认跳转到 /),所以需要在挂载应用后手动调用 router.push() 来指定初始路由:

1
2
3
4
5
6
7
8
9
10
11
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')

// 手动触发初始导航:跳转到根路径 /
router.push('/')

在浏览器中使用时,用户无法通过后退 / 前进按钮切换路由,因为 Memory 模式没有修改浏览器的历史记录栈。

即使在浏览器中,路由切换时地址栏的 URL 也不会发生任何变化(始终是页面初始的 URL)。

官方明确不建议在普通的浏览器端应用中使用 Memory 模式,因为它会丢失用户熟悉的 URL 导航、历史记录操作等体验,只有非浏览器场景才需要用。

命名视图

回顾视图

在 Vue Router 中,视图(View) 本质上是路由渲染组件的占位容器

视图就是一个坑位,路由会根据匹配的规则,把对应的组件渲染到这个 坑位 里(byd厕所雅间)

在 Vue 组件模板中,基础视图对应的是 <router-view> 标签,这是 Vue Router 提供的内置组件,也是最核心的视图载体。

当你配置了路由规则后,访问对应的路径时,Vue Router 会自动将路由规则中 component 对应的组件渲染到 <router-view> 的位置。

一个典型的单视图场景

1
2
3
4
5
6
7
<!-- App.vue(根组件) -->
<template>
<div id="app">
<!-- 这就是「基础视图」的占位符 -->
<router-view></router-view>
</div>
</template>

对应的路由配置:

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

const routes = [
{ path: '/', component: Home }, // 访问 / 时,Home 组件渲染到 <router-view>
{ path: '/about', component: About } // 访问 /about 时,About 组件渲染到 <router-view>
]

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

export default router

为什么说是坑位,是因为<router-view> 本身不渲染任何内容,只是作为组件的挂载点。而且视图的内容由路由规则的 component(或 components)属性决定。

嵌套路由就是可以在组件内部再次使用 <router-view>实现

命名视图

命名视图是解决「同一页面渲染多个组件」的问题

基础视图(默认的 <router-view>)只能在页面中渲染一个组件,但实际开发中,你可能会遇到需要在同一级路由下,同时渲染多个组件到不同位置的场景,比如:

  • 页面分为头部、侧边栏、主内容区三个部分,且这三个部分的内容都由路由控制。

此时用基础视图无法实现,因为一个 <router-view> 只能放一个组件,这时候就需要命名视图

命名视图就是给 <router-view> 标签添加一个 name 属性,使其成为一个具名的占位容器。同时,在路由规则中使用 components(注意是复数,它有个s)属性,为不同名称的视图指定对应的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- App.vue -->
<template>
<div id="app">
<!-- 命名视图:头部 -->
<router-view name="header"></router-view>
<div class="container">
<!-- 命名视图:侧边栏 -->
<router-view name="sidebar"></router-view>
<!-- 默认视图:没有 name 属性,默认名称是 default -->
<router-view></router-view>
</div>
</template>

<style>
.container {
display: flex;
}
</style>
</template>

这里定义了三个视图:

  • 命名视图 header:渲染头部组件
  • 命名视图 sidebar:渲染侧边栏组件
  • 默认视图 default:渲染主内容组件(没有 name 属性时,默认名称是 default

然后在路由规则中配置 components(复数)

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
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

// 导入需要渲染的组件
import HeaderView from '../views/HeaderView.vue'
import SidebarView from '../views/SidebarView.vue'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'

const routes = [
{
path: '/',
// 注意:这里是 components(复数),对应多个命名视图
components: {
header: HeaderView, // 名称为 header 的视图,渲染 HeaderView 组件
sidebar: SidebarView, // 名称为 sidebar 的视图,渲染 SidebarView 组件
default: HomeView // 默认视图(default),渲染 HomeView 组件
}
},
{
path: '/about',
components: {
header: HeaderView, // about 页面的头部还是用 HeaderView
sidebar: SidebarView, // about 页面的侧边栏还是用 SidebarView
default: AboutView // 默认视图渲染 AboutView 组件
}
}
]

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

export default router

然后创建对应的组件就行了

嵌套命名视图

和普通视图一样,命名视图也可以嵌套,这个场景多用于广告上,即在一个命名视图渲染的组件内部,再定义新的命名视图(或默认视图),实现更复杂的布局。

1
2
3
4
5
6
7
8
9
/settings/emails                                       /settings/profile
+-----------------------------------+ +------------------------------+
| UserSettings | | UserSettings |
| +-----+-------------------------+ | | +-----+--------------------+ |
| | Nav | UserEmailsSubscriptions | | +------------> | | Nav | UserProfile | |
| | +-------------------------+ | | | +--------------------+ |
| | | | | | | | UserProfilePreview | |
| +-----+-------------------------+ | | +-----+--------------------+ |
+-----------------------------------+ +------------------------------+
  • Nav 只是一个常规组件。
  • UserSettings 是一个视图组件。
  • UserEmailsSubscriptionsUserProfileUserProfilePreview 是嵌套的视图组件。

示例:在 HomeView 组件中嵌套命名视图

1
2
3
4
5
6
7
8
9
<!-- src/views/HomeView.vue -->
<template>
<main class="main">
<h2>首页内容</h2>
<!-- 嵌套的命名视图 -->
<router-view name="homeSub1"></router-view>
<router-view name="homeSub2"></router-view>
</main>
</template>

对应的路由配置(添加子路由):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const routes = [
{
path: '/',
components: {
header: HeaderView,
sidebar: SidebarView,
default: HomeView
},
// 子路由配置
children: [
{
path: '', // 匹配 / 时,默认渲染子组件
components: {
homeSub1: HomeSub1View,
homeSub2: HomeSub2View
}
}
]
},
// 其他路由...
]

重定向

重定向

重定向是指当用户访问某个路径(如 /home)时,Vue Router 会自动将其导航到另一个指定的路径(如 /),URL 地址栏会显示目标路径的地址。简单来说,重定向是 “用户访问 A,系统强制跳转到 B”,且地址栏会发生变化。

Vue Router 的重定向完全通过 routes 配置实现,主要有三种写法

  • 静态字符串重定向

    直接指定重定向的目标路径字符串,一般就是固定的跳转场景。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import { createRouter, createWebHistory } from 'vue-router'
    import Home from '../views/Home.vue'
    import About from '../views/About.vue'

    const routes = [
    { path: '/', component: Home, name: 'home' }, // 根路径
    { path: '/about', component: About },
    // 访问 /home 时,重定向到根路径 /
    { path: '/home', redirect: '/' }
    ]

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

    export default router

    当用户输入 http://localhost:5173/home 时,地址栏会立刻变成 http://localhost:5173/,并渲染 Home 组件。

  • 命名路由重定向

    通过路由的 name 属性指定重定向目标,避免硬编码路径

    1
    2
    3
    4
    const routes = [
    { path: '/', component: Home, name: 'home' }, // 给路由命名为 home
    { path: '/home', redirect: { name: 'home' } } // 重定向到命名路由 home
    ]

    如果后续修改了根路径的 path(比如改成 /index),只需修改路由的 path 属性,重定向配置无需改动

  • 动态函数重定向

    这个支持自定义的逻辑,通过一个函数返回重定向目标,函数接收目标路由对象 to 作为参数(包含当前访问的路径、参数、查询参数等信息),可以根据这些信息动态决定跳转方向,适用于复杂的业务场景。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const routes = [
    // 场景:/search/screens -> /search?q=screens(把路径参数转为查询参数)
    {
    path: '/search/:searchText', // 带路径参数的路由
    redirect: (to) => {
    // to.params 包含路径参数,to.query 包含查询参数
    console.log(to.params.searchText); // 比如访问 /search/screens,这里输出 screens
    // 返回重定向的路径对象(也可以返回字符串,如 `/search?q=${to.params.searchText}`)
    return { path: '/search', query: { q: to.params.searchText } };
    }
    },
    { path: '/search', component: () => import('../views/Search.vue') } // 搜索页面
    ]

    访问 /search/手机 时,会自动跳转到 /search?q=手机,地址栏显示后者的 URL。

虽然还没讲到,但是注意

重定向的原路由(如 /home)上的导航守卫(如 beforeEnter不会被触发,因为用户访问原路由时会直接跳转到目标路由,只有目标路由的守卫会生效。

1
2
3
4
5
6
7
8
9
// 错误示例:/home 的 beforeEnter 不会执行
{
path: '/home',
redirect: '/',
beforeEnter: (to, from, next) => {
console.log('这行代码永远不会执行');
next();
}
}

重定向的路由记录不需要配置 component,因为它从来不会被直接访问,也就没有组件需要渲染。唯一例外:如果路由有 children(嵌套路由)和 redirect,则必须配置 component(作为嵌套视图的容器)。

1
2
3
4
5
6
7
8
9
// 嵌套路由+重定向:必须有 component
{
path: '/user',
component: UserLayout, // 必须配置,作为子路由的容器
redirect: '/user/profile', // 重定向到子路由
children: [
{ path: 'profile', component: UserProfile }
]
}

相对重定向

可以基于当前路由的路径,通过函数返回相对路径实现相对重定向,适用于需要修改路径部分内容的场景。

1
2
3
4
5
6
7
8
9
10
11
const routes = [
// 访问 /users/123/posts 时,重定向到 /users/123/profile
{
path: '/users/:id/posts',
redirect: (to) => {
// to.path 是当前访问的完整路径,这里替换 posts 为 profile
return to.path.replace(/posts$/, 'profile');
}
},
{ path: '/users/:id/profile', component: UserProfile }
]

别名

URL 的 “别名昵称”

别名是指为一个路由路径设置一个或多个替代路径,当用户访问别名路径时,URL 地址栏不会发生变化,但 Vue Router 会将其视为访问原路径。简单来说,别名是 “用户访问 A,系统可以通过别名认为用户访问的是 B,但地址栏还是 A”。

比如将 / 别名为 /home

  • 用户访问 /home,地址栏仍然显示 /home,但系统会渲染 / 对应的组件。
  • 这和重定向的核心区别:重定向会改 URL,别名不会改 URL

单个别名就是直接为路由配置 alias 属性,指定单个别名路径。

1
2
3
4
const routes = [
// 原路径是 /,别名为 /hooome,访问 /hooome 等同于访问 /
{ path: '/', component: Home, alias: '/hooome' }
]

访问 /hooome 时,地址栏显示 /hooome,但页面渲染 Home 组件(和访问 / 的效果完全一样)

还可以通过数组为路由设置多个别名,适用于需要多个路径映射到同一个组件的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes = [
{
path: '/users',
component: UsersLayout,
children: [
// 以下 3 个 URL 都渲染 UserList 组件:
// - /users(原路径)
// - /users/list(相对别名,基于父路径 /users)
// - /people(绝对别名,以 / 开头)
{ path: '', component: UserList, alias: ['/people', 'list'] }
]
}
]

注意

  • 别名以 / 开头为绝对别名(基于根路径);
  • 不以 / 开头为相对别名(基于父路由的路径)。

如果路由路径包含参数(如 :id),别名中也需要包含对应的参数,确保参数能正常传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes = [
{
path: '/users/:id',
component: UsersByIdLayout,
children: [
// 以下 3 个 URL 都渲染 UserDetails 组件:
// - /users/24/profile(原路径)
// - /users/24(相对别名 '')
// - /24(绝对别名 /:id,包含参数)
{ path: 'profile', component: UserDetails, alias: ['/:id', ''] }
]
}
]

使用别名时,同一个组件可能对应多个 URL(如 //home),这会导致搜索引擎认为是重复内容,影响 SEO。

但是可以为页面设置规范链接(canonical link),告诉搜索引擎哪个 URL 是官方的主路径。

在 Vue 组件中可以通过 meta 标签设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- Home.vue -->
<template>
<div>首页内容</div>
</template>

<script setup>
import { useHead } from '@vueuse/head' // 可使用 VueUse 的 head 插件,或直接操作 DOM

// 设置规范链接,指定主路径为 /
useHead({
link: [
{ rel: 'canonical', href: 'https://yourdomain.com/' }
]
})
</script>

将 props 传递给路由组件

我们知道,在 Vue Router 中,路由组件默认可以通过 $route(选项式 API)或 useRoute()(组件式 API)获取路由参数(如 paramsquery

1
2
3
4
5
6
7
8
9
10
<template>
<div>用户ID:{{ $route.params.id }}</div>
</template>

<script setup>
// 或用 Composition API
import { useRoute } from 'vue-router'
const route = useRoute()
const id = route.params.id
</script>

但是,想没想过,该组件只能在 /user/:id 这个路由下使用,无法在其他地方复用(比如直接在页面中写 <User id="123" />

组件内部直接依赖路由对象,违反了 组件与路由解耦 的设计原则。

使用 Vue Router 提供的 props 配置,将路由参数作为组件的 props 传入,让组件从路由的固定配置中解放

Vue Router 提供了布尔模式、对象模式、函数模式、命名视图模式(以及额外的 RouterView 插槽传参方式)

布尔模式

传递 route.params 到组件 props

props: true 时,路由的 params 参数会被自动映射为组件的 props,适用于 动态路径参数(/user/:id)的场景

首先,定义路由组件,声明对应的 props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- src/views/User.vue -->
<template>
<!-- 直接使用 props 中的 id,不再依赖 $route -->
<div>用户ID:{{ id }}</div>
</template>

<script setup>
// Composition API:声明 props
defineProps({
id: {
type: String | Number, // 对应路由参数的类型
required: true // 因为路由中是 /user/:id,所以必传
}
})
</script>

在路由配置中开启 props: true

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 User from '../views/User.vue'

const routes = [
{
path: '/user/:id', // 动态路径参数:id
component: User,
props: true // 布尔模式:开启 params 转 props
}
]

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

export default router

当访问 /user/123 时,路由的 route.params.id = '123' 会被自动传入 User 组件的 id prop。

但是布尔模式只处理 route.params,不会处理 route.query(比如 /user?id=123 中的 query.id 不会被传递)。如果路由没有动态参数(如 /user),props: true 不会传递任何参数(因为 params 为空)。

对象模式

props 是一个静态对象时,该对象会被原样作为 props 传入组件,适用于传递静态数据(不依赖路由参数的固定值)的场景。

定义路由组件,声明静态 props
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- src/views/Promotion.vue -->
<template>
<div>
<h2>促销活动</h2>
<p>是否显示弹窗:{{ newsletterPopup ? '是' : '否' }}</p>
<p>活动名称:{{ activityName }}</p>
</div>
</template>

<script setup>
defineProps({
// 静态 props:新闻通讯弹窗开关
newsletterPopup: {
type: Boolean,
default: true
},
// 静态 props:活动名称
activityName: {
type: String,
default: '默认活动'
}
})
</script>

在路由配置中设置 props 为静态对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/router/index.js
import Promotion from '../views/Promotion.vue'

const routes = [
{
path: '/promotion/from-newsletter', // 固定路径
component: Promotion,
// 对象模式:传递静态的 props
props: {
newsletterPopup: false, // 它会覆盖默认值
activityName: '新年促销' // 自定义静态值
}
}
]

访问 /promotion/from-newsletter 时,组件会收到 { newsletterPopup: false, activityName: '新年促销' } 的 props。

静态对象中的值不会随路由变化而变化,适合传递固定配置。

但是很少有人用这个,因为无法获取路由的 paramsquery 等动态数据

如果同时需要静态数据和动态路由数据,不要用对象模式,改用函数模式。

函数模式

props 是一个函数,接收路由对象 route 作为参数,返回一个 props 对象。这是最灵活的模式

例如,我们假如需要将 query 参数映射为 props

比如访问 /search?q=vue,需要把 route.query.q 传递给 SearchUser 组件的 query prop。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- src/views/SearchUser.vue -->
<template>
<div>搜索关键词:{{ query }}</div>
</template>

<script setup>
defineProps({
query: {
type: String,
default: '' // 搜索关键词
}
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/router/index.js
import SearchUser from '../views/SearchUser.vue'

const routes = [
{
path: '/search', // 路径无 params,有 query 参数
component: SearchUser,
// 函数模式:接收 route 对象,返回 props
props: (route) => ({
query: route.query.q // 把 query.q 映射为 props.query
})
}
]
  • 访问 /search?q=vue 时,组件收到 query: 'vue'
  • 访问 /search 时,组件收到 query: ''(默认值)

还可以处理更复杂的参数逻辑,比如根据路由的 params.type 动态设置 props:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
props: (route) => {
const { type, id } = route.params
// 业务逻辑:不同类型返回不同 props
if (type === 'admin') {
return {
id: Number(id),
role: 'admin',
permissions: ['edit', 'delete']
}
} else {
return {
id: Number(id),
role: 'user',
permissions: ['view']
}
}
}

注意,props 函数应只依赖 route 参数,不要依赖外部状态,因为 props 函数只在路由变化时执行,如果依赖外部状态,外部状态变化时 props 不会更新。

而且函数必须返回一个纯对象,不能返回响应式对象(如 reactiveref 包装的对象),因为 Vue Router 会自动处理响应式。

命名视图的 props 配置

为多个视图分别设置 props

当路由使用命名视图时,需要为每个命名视图单独配置 props,格式为 { 视图名: 布尔/对象/函数 }

定义多个命名视图组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- src/views/UserMain.vue -->
<template>
<div>用户ID:{{ id }}</div>
</template>
<script setup>
defineProps({ id: [String, Number] })
</script>

<!-- src/views/UserSidebar.vue -->
<template>
<div>用户等级:{{ level }}</div>
</template>
<script setup>
defineProps({ level: { type: Number, default: 1 } })
</script>
1
2
3
4
5
<!-- App.vue -->
<template>
<router-view></router-view> <!-- default 视图:UserMain -->
<router-view name="sidebar"></router-view> <!-- sidebar 视图:UserSidebar -->
</template>

路由配置(命名视图 + 分别设置 props)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const routes = [
{
path: '/user/:id',
// 命名视图:default(UserMain)、sidebar(UserSidebar)
components: {
default: UserMain,
sidebar: UserSidebar
},
// 为每个命名视图配置 props
props: {
default: true, // default 视图:布尔模式,传递 params.id
sidebar: (route) => ({ // sidebar 视图:函数模式,传递动态 level
level: Number(route.params.id) % 10 + 1 // 模拟业务逻辑
})
}
}
]

通过 <RouterView> 插槽传递 props

除了路由配置中的 props 选项,还可以通过 <RouterView>作用域插槽直接传递 props 给路由组件

这种方式适用于需要给所有路由组件传递公共 props 的场景,官方不推荐滥用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- App.vue -->
<template>
<!-- RouterView 插槽:获取组件实例 Component -->
<RouterView v-slot="{ Component }">
<!-- 动态组件:传递 props -->
<component
:is="Component"
// 传递公共 props所有路由组件都会收到
common-prop="这是公共属性"
// 也可以传递响应式数据
:user-info="userInfo"
/>
</RouterView>
</template>

<script setup>
import { reactive } from 'vue'
// 响应式数据
const userInfo = reactive({ name: '张三', age: 20 })
</script>

解读一下官方的警告

  1. 所有组件都会接收 props:这种方式会让所有路由组件都收到传递的 props,即使组件不需要这些 props,这会导致组件的 props 声明冗余,违反 “按需接收” 的原则。
  2. 优先级问题:如果路由配置中的 props 和插槽传递的 props 有重名,插槽传递的 props 会覆盖路由配置的 props(因为组件的 props 是浅合并的)。
  3. 适用场景:仅在需要传递全局公共数据(如用户信息、全局配置)时使用,优先使用路由配置的 props 方式处理路由相关的参数。

匹配当前路由的链接

在 Vue 项目中,我们通常用 <RouterLink> 组件创建导航链接,用于替代原生 <a> 标签

假如一个很常见的情况,就是需要对当前正在访问的路由对应的链接进行视觉区分

Vue Router 为 <RouterLink> 提供了自动添加激活 CSS 类的功能,无需手动判断路由状态,这就是路由链接激活状态的核心作用。

<RouterLink> 会为匹配当前路由的链接添加两个 CSS 类,分别对应普通匹配精确匹配,这是理解激活状态的关键。

我们先通过一个实际的路由例子,理解普通匹配精确匹配的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 路由配置
const routes = [
{
path: '/user/:username', // 父路由
component: User,
children: [
{
path: 'role/:roleId', // 子路由
component: Role,
}
]
}
]

对应的导航链接:

1
2
3
<!-- 导航链接 -->
<RouterLink to="/user/erina">User</RouterLink>
<RouterLink to="/user/erina/role/admin">Role</RouterLink>

当当前路径是 /user/erina/role/admin 时:

  • 普通匹配<RouterLink to="/user/erina"><RouterLink to="/user/erina/role/admin"> 都会被判定为匹配,都会添加 router-link-active 类。
  • 精确匹配:只有 <RouterLink to="/user/erina/role/admin"> 会被判定为精确匹配,会添加 router-link-exact-active 类;<RouterLink to="/user/erina"> 不会添加这个类。

简单来说:

  • router-link-active包含性匹配,当前路径是该链接路径的子路径(或相等)时触发,会包含祖先路由的链接。
  • router-link-exact-active完全性匹配,当前路径与该链接路径完全一致时才触发,不包含祖先路由的链接。

Vue Router 判断一个 <RouterLink> 是否匹配当前路由,遵循以下严格的规则,这些规则决定了激活类是否会被添加

匹配的核心依据是:路由记录 + params 参数

一个链接被判定为匹配当前路由,必须满足两个核心条件:

  1. 路由记录匹配:链接的 to 路径解析后,对应到同一个路由配置记录(即 routes 数组中的某一项)。
  2. params 参数匹配:链接的 params(如 /user/:username 中的 username)与当前路由的 params 完全相同。

注意

  • query 参数(如 /user/erina?tab=profile 中的 tab不会影响匹配结果,即使 query 不同,只要路由记录和 params 匹配,链接仍会被判定为匹配。
  • hash 部分(如 /user/erina#top 中的 #top)也不会影响匹配结果

嵌套路由中,祖先路由的链接会被匹配

在嵌套路由场景下,指向祖先路由的链接,只要 params 匹配,就会被判定为匹配当前路由(这也是 router-link-active 会作用于祖先链接的原因)。

比如上面的例子中,/user/erina/user/erina/role/admin 的祖先路由,所以前者的链接会被匹配。

别名(alias)会被视为匹配

如果路由配置了别名(如 path: '/', alias: '/home'),那么指向别名的链接(<RouterLink to="/home">)和指向原路径的链接(<RouterLink to="/">),都会被判定为匹配当前路由(只要解析到同一个路由记录)。

重定向(redirect)不会被跟随

如果一个路由配置了重定向(如 path: '/home', redirect: '/'),那么指向 /home 的链接,在判断匹配时不会跟随重定向到 /。也就是说,当当前路径是 / 时,<RouterLink to="/home"> 不会被判定为匹配。

规则 5:路径不需要完全一致(但 params 必须一致)

只要路由记录和 params 匹配,即使路径的表现形式不同(比如通过别名、相对路径),也会被判定为匹配。

自定义激活类名

Vue Router 允许你自定义激活类名,替代默认的 router-link-activerouter-link-exact-active,满足项目的 CSS 命名规范(比如 Tailwind CSS、BEM 命名法)。

通过 <RouterLink>activeClassexactActiveClass 属性,为单个链接设置自定义类名。这样是局部的自定义

1
2
3
4
5
6
7
8
<!-- 局部自定义激活类名 -->
<RouterLink
to="/user/erina"
activeClass="nav-active" // 替代 router-link-active
exactActiveClass="nav-exact-active" // 替代 router-link-exact-active
>
User
</RouterLink>

在创建路由实例时,通过 linkActiveClasslinkExactActiveClass 选项,全局设置所有 <RouterLink> 的激活类名

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

const routes = [/* 你的路由配置 */]

const router = createRouter({
history: createWebHistory(),
routes,
// 全局自定义激活类名
linkActiveClass: 'nav-active', // 全局替代 router-link-active
linkExactActiveClass: 'nav-exact-active' // 全局替代 router-link-exact-active
})

export default router