WinddSnow

Vue-Router-4-Part2-Navigation-Guards-Route-Meta-Lazy-Loading-Transitions

字数统计: 5.1k阅读时长: 22 min
2026/08/01

第109课:Vue Router 4(下)——导航守卫、路由元信息、懒加载、过渡动画

上一课我们掌握了 Vue Router 4 的基础——路由配置、动态参数和导航方式。但真实的项目远不止路径映射那么简单:你需要在用户进入某些页面之前验证登录状态,在离开表单页面前提示保存数据,在路由跳转时显示加载进度条,为每个页面动态设置标题以利于 SEO,以及在页面切换时添加平滑的过渡动画。Vue Router 4 通过导航守卫(Navigation Guards)、路由元信息(Route Meta)、路由懒加载和与 <Transition> 组件的集成,提供了所有这些高级能力。本节课将逐一拆解这些进阶特性,并通过一个带权限验证的后台管理路由框架将所有知识点串联。


1. 导航守卫:拦截与放行

导航守卫是 Vue Router 提供的路由拦截器。当用户尝试导航到某个页面时,守卫函数在路由切换的不同阶段被调用,可以放行next 或返回 true)、重定向(返回路径字符串或对象)、或取消导航(返回 false)。导航守卫分为三个级别:全局守卫路由独享守卫组件内守卫

1.1 完整的导航解析流程

理解守卫的执行顺序至关重要。当触发一次路由跳转时,完整的流程如下:

  1. 导航被触发。
  2. 在失活的组件里调用 **beforeRouteLeave**(组件内守卫)。
  3. 调用全局的 **beforeEach**(全局前置守卫)。
  4. 在重用的组件里调用 **beforeRouteUpdate**(组件内守卫,仅当组件被复用时触发)。
  5. 调用路由配置中的 **beforeEnter**(路由独享守卫)。
  6. 解析异步路由组件(懒加载的 .vue 文件开始下载)。
  7. 在被激活的组件里调用 **beforeRouteEnter**(组件内守卫)。
  8. 调用全局的 **beforeResolve**(全局解析守卫)。
  9. 导航被确认。
  10. 调用全局的 **afterEach**(全局后置钩子,无 next,导航已完成)。
  11. DOM 更新。
  12. 调用 beforeRouteEnter 中传给 next 的回调函数,此时组件实例已可用。

记忆要点:组件内守卫先于全局守卫?不,是“失活组件离开守卫”最先触发,然后是全局前置守卫,然后才是“复用组件更新守卫”,最后是“激活组件进入守卫”。全局解析守卫在所有组件内守卫和路由独享守卫之后、导航确认之前触发,适合做最终的权限判定或数据预加载检查。

1.2 全局前置守卫:beforeEach

router.beforeEach 是使用最频繁的守卫。它常用于登录验证——在每次路由跳转前检查用户是否有权限访问目标页面。

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

const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true }, // 需要认证
},
{ path: '/login', component: Login },
],
});

// 全局前置守卫
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('token');

if (to.meta.requiresAuth && !isAuthenticated) {
// 未登录且目标页面需要认证 → 重定向到登录页
next({ path: '/login', query: { redirect: to.fullPath } });
} else if (to.path === '/login' && isAuthenticated) {
// 已登录却访问登录页 → 重定向到首页
next({ path: '/' });
} else {
next(); // 放行
}
});

守卫回调参数

  • to:目标路由对象(RouteLocationNormalized)。
  • from:来源路由对象。
  • next:函数,控制导航如何进行。在 Vue Router 4 中,next 仍然是有效 API,但也可以直接 return 一个值来替代 next 调用——返回 false 取消导航,返回路径字符串或对象进行重定向。两种风格均可,官方推荐使用 return 风格以获得更简洁的代码。
1
2
3
4
5
6
7
8
9
10
11
12
// Vue Router 4 推荐风格:return 替代 next
router.beforeEach((to, from) => {
const isAuthenticated = localStorage.getItem('token');

if (to.meta.requiresAuth && !isAuthenticated) {
return { path: '/login', query: { redirect: to.fullPath } };
}
if (to.path === '/login' && isAuthenticated) {
return { path: '/' };
}
// 返回 true 或 undefined 放行
});

1.3 路由独享守卫:beforeEnter

直接在路由配置中定义的守卫,仅对该路由生效。适合将特定路由的验证逻辑就近定义在路由表中,而非分散在全局守卫中。

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes = [
{
path: '/admin',
component: AdminPanel,
beforeEnter: (to, from) => {
const user = getUserFromStorage();
if (!user || user.role !== 'admin') {
// 不是管理员 → 重定向到 403 或首页
return { path: '/403' };
}
},
},
];

beforeEnter 的签名与 beforeEach 完全相同(接收 tofrom,可返回 false 或路径)。它仅在从不同路由进入该路由时触发——如果只是参数变化(如 /admin/users/1/admin/users/2),不会触发 beforeEnter

1.4 组件内守卫

在 Vue 组件内部(选项式 API 中作为选项,组合式 API 中通过 onBeforeRouteLeaveonBeforeRouteUpdate 函数)定义的守卫。它们与组件的生命周期紧密结合,最常用于表单脏数据保护(离开前提示保存)。

组件内守卫 触发时机 典型场景
onBeforeRouteLeave 当前组件即将被导航离开时。 表单未保存时提示用户是否放弃编辑。
onBeforeRouteUpdate 当前组件被复用但路由参数变化时(如 /users/1/users/2)。 根据新参数重新获取数据。
onBeforeRouteEnter 在进入该组件前触发(组合式 API 中无直接等价,可用 beforeRouteEnter 选项)。 在组件渲染前获取关键数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { ref, onBeforeRouteLeave } from 'vue-router';

const formDirty = ref(true);

// 组件内导航守卫
onBeforeRouteLeave((to, from) => {
if (formDirty.value) {
const answer = window.confirm('你有未保存的更改,确定离开吗?');
if (!answer) return false; // 取消导航
}
});
</script>

组合式 API 中的注意onBeforeRouteLeaveonBeforeRouteUpdate 是 Vue Router 提供的组合式函数,必须在 <script setup> 中直接调用(与组件实例绑定)。beforeRouteEnter 在组合式 API 中没有直接等价函数——因为该守卫触发时组件实例尚未创建,无法访问 thissetup 中的变量。如果需要在进入前执行逻辑,可以使用全局守卫或路由独享守卫。


2. 路由元信息:用 meta 携带附加数据

每个路由配置中的 meta 字段是一个自由对象,用于存储任意自定义数据。Vue Router 不会对 meta 做任何处理——它完全由开发者定义和消费。最常用于权限标记页面标题页面缓存控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const routes = [
{
path: '/dashboard',
component: Dashboard,
meta: {
requiresAuth: true,
title: '仪表板',
icon: 'dashboard',
},
},
{
path: '/settings',
component: Settings,
meta: {
requiresAuth: true,
roles: ['admin'], // 仅管理员可访问
title: '系统设置',
},
},
];

2.1 结合全局守卫动态设置页面标题

这是一个经典模式:在 afterEach 中读取 meta.title 并更新 document.title

1
2
3
4
5
6
7
8
router.afterEach((to) => {
const title = to.meta.title;
if (title) {
document.title = `${title} - My App`;
} else {
document.title = 'My App';
}
});

afterEach 是全局后置钩子,导航已经完成,因此它没有 next 参数,也不接受返回值——它纯粹用于副作用(如设置标题、滚动到顶部、埋点统计)。

2.2 基于 meta 的权限控制扩展

你可以扩展 beforeEach 守卫,使其读取 meta.roles 并与当前用户角色进行比对:

1
2
3
4
5
6
7
8
9
10
11
12
13
router.beforeEach((to, from) => {
const user = JSON.parse(localStorage.getItem('user') || 'null');

// 需要认证
if (to.meta.requiresAuth && !user) {
return { path: '/login', query: { redirect: to.fullPath } };
}

// 需要特定角色
if (to.meta.roles && !to.meta.roles.includes(user?.role)) {
return { path: '/403' }; // 无权限
}
});

3. 路由懒加载:按需分割代码

默认情况下,如果所有页面组件都在路由表中直接导入import Home from './Home.vue'),它们会被打包进同一个 JavaScript 文件,导致首屏加载体积巨大。路由懒加载使用动态 import() 将每个页面拆分为独立的 chunk,仅在用户访问时才下载对应代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 静态导入:所有页面打包进主 bundle
import Home from '../views/Home.vue';
import Dashboard from '../views/Dashboard.vue';
const routes = [
{ path: '/', component: Home },
{ path: '/dashboard', component: Dashboard },
];

// ✅ 动态导入:每个页面独立为 chunk
const routes = [
{ path: '/', component: () => import('../views/Home.vue') },
{ path: '/dashboard', component: () => import('../views/Dashboard.vue') },
];

打包效果:每个 () => import() 返回一个 Promise,Vue Router 仅在路由被激活时才触发 import,打包工具(Vite/Webpack)会自动将每个 import 的目标拆分为独立 chunk。懒加载的组件首次激活时需要下载 JS 文件,这期间可以配合 <Suspense> 显示加载状态。

3.1 命名 chunk:便于调试和分析

import() 中使用 /* webpackChunkName: "group-name" */ 魔术注释(Webpack 环境)可以自定义 chunk 名称。Vite 默认基于文件路径自动生成 chunk 名称,无需此注释。

1
2
3
4
5
6
const routes = [
{
path: '/dashboard',
component: () => import(/* webpackChunkName: "dashboard" */ '../views/Dashboard.vue'),
},
];

3.2 批量懒加载:将相关页面合并到同一 chunk

有时你希望多个相关页面打包在同一个 chunk 中(如所有后台管理页面),可以减少 HTTP 请求数。这需要在构建工具层面配置(如 Vite 的 rollupOptions.output.manualChunks 或 Webpack 的 SplitChunksPlugin),而非在路由定义层面实现。


4. 路由过渡动画:<Transition> 包裹 <router-view>

Vue 的 <Transition> 组件可以与 <router-view> 配合,为每次路由切换添加平滑的过渡效果。核心原理是:<router-view> 渲染的组件被 <Transition> 包裹后,Vue 在切换时自动为进入和离开的组件应用 CSS 类名。

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
<template>
<router-view v-slot="{ Component, route }">
<Transition :name="route.meta.transition || 'fade'" mode="out-in">
<component :is="Component" :key="route.path" />
</Transition>
</router-view>
</template>

<style>
/* 淡入淡出 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}

/* 滑动(右进左出) */
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease;
}
.slide-enter-from {
transform: translateX(100%);
}
.slide-leave-to {
transform: translateX(-100%);
}
</style>

关键机制

  • v-slot="{ Component, route }"<router-view> 的作用域插槽暴露了当前匹配的组件和路由对象。
  • :is="Component":使用 <component :is> 动态渲染当前路由组件,而非直接由 <router-view> 隐式渲染。这是使用 <Transition> 包裹的必需步骤——<Transition> 需要直接包裹切换的组件。
  • :key="route.path":确保不同路径的组件被视为不同元素,触发完整的进入/离开动画(而非仅复用同一元素)。
  • mode="out-in":先离开再进入,避免新旧组件同时存在导致布局抖动。
  • 过渡名称可通过 route.meta.transition 在每个路由上自定义,实现不同路由使用不同动画效果。

5. 综合实战:带权限验证与过渡动画的后台管理路由

以下代码将全局守卫、路由元信息、懒加载和过渡动画组合为一个完整的后台管理路由系统。

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
{
path: '/',
name: 'home',
component: () => import('../views/Home.vue'),
meta: { title: '首页' },
},
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue'),
meta: { title: '登录', guest: true },
},
{
path: '/dashboard',
component: () => import('../views/DashboardLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'dashboard',
component: () => import('../views/DashboardHome.vue'),
meta: { title: '仪表板', requiresAuth: true },
},
{
path: 'users',
name: 'users',
component: () => import('../views/Users.vue'),
meta: { title: '用户管理', requiresAuth: true, roles: ['admin'] },
},
{
path: 'settings',
name: 'settings',
component: () => import('../views/Settings.vue'),
meta: { title: '设置', requiresAuth: true },
},
],
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('../views/NotFound.vue'),
meta: { title: '页面未找到' },
},
];

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

// ---- 全局前置守卫:权限验证 ----
router.beforeEach((to, from) => {
const token = localStorage.getItem('token');
const user = JSON.parse(localStorage.getItem('user') || 'null');

// 1. 需要认证但未登录 → 重定向到登录页
if (to.meta.requiresAuth && !token) {
return { path: '/login', query: { redirect: to.fullPath } };
}

// 2. 需要特定角色权限
if (to.meta.roles && !to.meta.roles.includes(user?.role)) {
return { path: '/' }; // 无权限,回首页(或 /403)
}

// 3. 已登录但访问登录页 → 重定向到首页
if (to.meta.guest && token) {
return { path: '/' };
}
});

// ---- 全局后置守卫:设置页面标题 ----
router.afterEach((to) => {
document.title = to.meta.title ? `${to.meta.title} - AdminPro` : 'AdminPro';
});

export default router;
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
<!-- App.vue -->
<template>
<div id="app">
<nav v-if="$route.meta.requiresAuth" class="sidebar">
<router-link to="/dashboard">仪表板</router-link>
<router-link to="/dashboard/users">用户管理</router-link>
<router-link to="/dashboard/settings">设置</router-link>
</nav>

<main :class="{ 'with-sidebar': $route.meta.requiresAuth }">
<router-view v-slot="{ Component, route }">
<Transition name="fade" mode="out-in">
<component :is="Component" :key="route.path" />
</Transition>
</router-view>
</main>
</div>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

设计要点

  • 全局 beforeEach 集中处理认证和角色权限,避免在每个组件中重复校验逻辑。
  • meta.requiresAuth 标记需要登录的路由,meta.roles 精细控制角色权限。
  • meta.titleafterEach 中统一设置 document.title,无需各组件手动处理。
  • 所有页面组件使用动态 import() 懒加载。
  • App.vue 使用作用域插槽 + <Transition> 包裹 <component :is>,实现所有路由切换时的淡入淡出动画。
  • 根据 $route.meta.requiresAuth 条件渲染侧边栏,登录页和 404 页不显示侧边栏。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) Vue Router 中,哪个导航守卫在所有组件内守卫和路由独享守卫之后、导航确认之前触发,适合做最终权限判定?
    A. beforeEach
    B. beforeEnter
    C. beforeResolve
    D. afterEach

  2. (单选) 要实现页面切换时的过渡动画,应该将 <Transition> 包裹在哪个组件外部?
    A. <router-link>
    B. <router-view> 内部的 <component :is>
    C. <nav> 导航栏
    D. 根 <div id="app">

  3. (填空) 在路由配置中,要为某条路由添加附加信息(如权限标签、页面标题),应将这些数据放在 ______ 字段中。

  4. (多选) 关于路由懒加载的描述,哪些是正确的?
    A. 使用 () => import('./Page.vue') 可以将该页面拆分为独立 chunk。
    B. 懒加载的页面首次访问时需要额外的网络请求。
    C. 所有页面必须懒加载,不能使用静态导入。
    D. 懒加载可以减少首屏 JavaScript 体积。

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要为一个电商后台管理系统配置 Vue Router 4。要求如下:

  • 路由包含:/login(登录页)、/products(商品列表)、/products/:id(商品详情)、/orders(订单列表)、/settings(设置页)、*(404 页面)。
  • 除登录页外,所有页面都需要登录验证(meta.requiresAuth: true),未登录重定向到 /login 并携带 redirect 参数。
  • 已登录用户访问 /login 自动重定向到 /products
  • 使用 afterEach 动态设置 document.title(从 meta.title 读取)。
  • 所有页面组件使用懒加载。
  • App.vue 中使用 <Transition name="fade" mode="out-in"> 包裹 <router-view> 实现页面切换淡入淡出。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成完整的路由配置文件 router/index.jsApp.vue。提示词中需明确指定全局守卫逻辑、meta 字段结构、过渡动画的 CSS 类名。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请为一个 Vue 3 电商后台管理系统创建 Vue Router 4 路由系统。需要创建以下文件:

  1. src/router/index.js
    • 使用 createRouter + createWebHistory
    • 定义 6 条路由:/login(meta: { title: ‘登录’, guest: true })、/products(meta: { title: ‘商品管理’, requiresAuth: true })、/products/:id(meta: { title: ‘商品详情’, requiresAuth: true })、/orders(meta: { title: ‘订单管理’, requiresAuth: true })、/settings(meta: { title: ‘设置’, requiresAuth: true })、/:pathMatch(.*)*(meta: { title: ‘页面未找到’ })。所有页面使用 () => import() 懒加载。
    • beforeEach:从 localStorage 读取 token。如果目标路由 meta.requiresAuth 且无 token,return { path: '/login', query: { redirect: to.fullPath } }。如果 to.meta.guest 且有 token,return { path: '/products' }
    • afterEach:设置 document.title = to.meta.title ? ${to.meta.title} - ShopAdmin : 'ShopAdmin'
  2. src/App.vue
    • 模板使用 <router-view v-slot="{ Component, route }">,内部 <Transition name="slide" mode="out-in"> 包裹 <component :is="Component" :key="route.path" />
    • <style> 定义 .slide-enter-active / .slide-leave-active { transition: transform 0.3s ease, opacity 0.3s ease; }.slide-enter-from { transform: translateX(20px); opacity: 0; }.slide-leave-to { transform: translateX(-20px); opacity: 0; }
  3. 添加 JSDoc 注释,确保所有配置可直接使用。完成后列出所有文件内容。

四、面试真题与参考答案

题目(百度前端面试题):

请详细描述 Vue Router 的完整导航解析流程(从触发导航到 DOM 更新的每一步)。为什么需要将 <router-view> 内部的 <component :is> 替换为显式的 <component :is="Component"> 并设置 key?如果不设置 key,路由切换时 <Transition> 能正常工作吗?为什么?

参考答案

完整的导航解析流程依次为:触发导航 → beforeRouteLeave(失活组件)→ beforeEach(全局前置)→ beforeRouteUpdate(复用组件)→ beforeEnter(路由独享)→ 解析异步路由组件 → beforeRouteEnter(激活组件)→ beforeResolve(全局解析)→ 导航确认 → afterEach(全局后置)→ DOM 更新 → beforeRouteEnternext 的回调。

使用 <router-view> 的作用域插槽并显式渲染 <component :is="Component"> 是因为 <Transition> 需要直接包裹它负责切换的组件元素<router-view> 自身是一个函数式组件,<Transition> 无法直接感知其内部组件的切换。通过作用域插槽获取当前匹配的 Component,并在 v-slot 内手动用 <component :is> 渲染,<Transition> 就能检测到子元素的进入和离开。

设置 :key="route.path" 是关键。如果不设置 key,当从 /users/1 导航到 /users/2 时,<component :is> 接收到的组件类型相同,Vue 会认为是同一元素并尝试复用,<Transition> 不会触发进入/离开动画。key 告诉 Vue 这两个渲染是不同的实例,从而强制触发完整的过渡动画。通常使用 route.pathroute.fullPath 作为 key,确保每次路由切换都创建新的元素标识。


课后练习答案

一、概念自测答案

  1. C

    • 解析:beforeResolve 在所有组件内守卫和路由独享守卫之后、导航确认之前触发。A 是全局前置,B 是路由独享,D 是后置钩子。
  2. B

    • 解析:<Transition> 必须包裹 <router-view> 内部显式渲染的 <component :is>,才能检测组件切换并触发动画。
  3. meta

    • 解析:meta 字段用于存储路由的自定义附加数据,如 requiresAuthtitleroles 等。
  4. A、B、D

    • 解析:C 错误,并非所有页面必须懒加载——首屏关键页面可以使用静态导入以更快显示。A、B、D 正确。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请用 Vue 3 + Vue Router 4 创建电商后台路由系统。要求:

  • 路由:/login/products/products/:id/orders/settings*
  • 全局守卫:未登录重定向到登录页(携带 redirect),已登录访问 login 重定向到 products。
  • afterEach 设置页面标题(从 meta.title 读取)。
  • 所有组件懒加载。
  • App.vue 使用 <Transition> 包裹 <component :is> 并设 key,实现淡入淡出。
  • 输出 router/index.js 和 App.vue 完整代码。”
CATALOG
  1. 1. 第109课:Vue Router 4(下)——导航守卫、路由元信息、懒加载、过渡动画
    1. 1.1. 1. 导航守卫:拦截与放行
      1. 1.1.1. 1.1 完整的导航解析流程
      2. 1.1.2. 1.2 全局前置守卫:beforeEach
      3. 1.1.3. 1.3 路由独享守卫:beforeEnter
      4. 1.1.4. 1.4 组件内守卫
    2. 1.2. 2. 路由元信息:用 meta 携带附加数据
      1. 1.2.1. 2.1 结合全局守卫动态设置页面标题
      2. 1.2.2. 2.2 基于 meta 的权限控制扩展
    3. 1.3. 3. 路由懒加载:按需分割代码
      1. 1.3.1. 3.1 命名 chunk:便于调试和分析
      2. 1.3.2. 3.2 批量懒加载:将相关页面合并到同一 chunk
    4. 1.4. 4. 路由过渡动画:<Transition> 包裹 <router-view>
    5. 1.5. 5. 综合实战:带权限验证与过渡动画的后台管理路由
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)