第109课:Vue Router 4(下)——导航守卫、路由元信息、懒加载、过渡动画
上一课我们掌握了 Vue Router 4 的基础——路由配置、动态参数和导航方式。但真实的项目远不止路径映射那么简单:你需要在用户进入某些页面之前验证登录状态,在离开表单页面前提示保存数据,在路由跳转时显示加载进度条,为每个页面动态设置标题以利于 SEO,以及在页面切换时添加平滑的过渡动画。Vue Router 4 通过导航守卫(Navigation Guards)、路由元信息(Route Meta)、路由懒加载和与 <Transition> 组件的集成,提供了所有这些高级能力。本节课将逐一拆解这些进阶特性,并通过一个带权限验证的后台管理路由框架将所有知识点串联。
1. 导航守卫:拦截与放行
导航守卫是 Vue Router 提供的路由拦截器。当用户尝试导航到某个页面时,守卫函数在路由切换的不同阶段被调用,可以放行(next 或返回 true)、重定向(返回路径字符串或对象)、或取消导航(返回 false)。导航守卫分为三个级别:全局守卫、路由独享守卫和组件内守卫。
1.1 完整的导航解析流程
理解守卫的执行顺序至关重要。当触发一次路由跳转时,完整的流程如下:
- 导航被触发。
- 在失活的组件里调用 **
beforeRouteLeave**(组件内守卫)。 - 调用全局的 **
beforeEach**(全局前置守卫)。 - 在重用的组件里调用 **
beforeRouteUpdate**(组件内守卫,仅当组件被复用时触发)。 - 调用路由配置中的 **
beforeEnter**(路由独享守卫)。 - 解析异步路由组件(懒加载的
.vue文件开始下载)。 - 在被激活的组件里调用 **
beforeRouteEnter**(组件内守卫)。 - 调用全局的 **
beforeResolve**(全局解析守卫)。 - 导航被确认。
- 调用全局的 **
afterEach**(全局后置钩子,无next,导航已完成)。 - DOM 更新。
- 调用
beforeRouteEnter中传给next的回调函数,此时组件实例已可用。
记忆要点:组件内守卫先于全局守卫?不,是“失活组件离开守卫”最先触发,然后是全局前置守卫,然后才是“复用组件更新守卫”,最后是“激活组件进入守卫”。全局解析守卫在所有组件内守卫和路由独享守卫之后、导航确认之前触发,适合做最终的权限判定或数据预加载检查。
1.2 全局前置守卫:beforeEach
router.beforeEach 是使用最频繁的守卫。它常用于登录验证——在每次路由跳转前检查用户是否有权限访问目标页面。
1 | // router/index.js |
守卫回调参数:
to:目标路由对象(RouteLocationNormalized)。from:来源路由对象。next:函数,控制导航如何进行。在 Vue Router 4 中,next仍然是有效 API,但也可以直接return一个值来替代next调用——返回false取消导航,返回路径字符串或对象进行重定向。两种风格均可,官方推荐使用return风格以获得更简洁的代码。
1 | // Vue Router 4 推荐风格:return 替代 next |
1.3 路由独享守卫:beforeEnter
直接在路由配置中定义的守卫,仅对该路由生效。适合将特定路由的验证逻辑就近定义在路由表中,而非分散在全局守卫中。
1 | const routes = [ |
beforeEnter 的签名与 beforeEach 完全相同(接收 to、from,可返回 false 或路径)。它仅在从不同路由进入该路由时触发——如果只是参数变化(如 /admin/users/1 → /admin/users/2),不会触发 beforeEnter。
1.4 组件内守卫
在 Vue 组件内部(选项式 API 中作为选项,组合式 API 中通过 onBeforeRouteLeave 和 onBeforeRouteUpdate 函数)定义的守卫。它们与组件的生命周期紧密结合,最常用于表单脏数据保护(离开前提示保存)。
| 组件内守卫 | 触发时机 | 典型场景 |
|---|---|---|
onBeforeRouteLeave |
当前组件即将被导航离开时。 | 表单未保存时提示用户是否放弃编辑。 |
onBeforeRouteUpdate |
当前组件被复用但路由参数变化时(如 /users/1 → /users/2)。 |
根据新参数重新获取数据。 |
onBeforeRouteEnter |
在进入该组件前触发(组合式 API 中无直接等价,可用 beforeRouteEnter 选项)。 |
在组件渲染前获取关键数据。 |
1 | <script setup> |
组合式 API 中的注意:onBeforeRouteLeave 和 onBeforeRouteUpdate 是 Vue Router 提供的组合式函数,必须在 <script setup> 中直接调用(与组件实例绑定)。beforeRouteEnter 在组合式 API 中没有直接等价函数——因为该守卫触发时组件实例尚未创建,无法访问 this 或 setup 中的变量。如果需要在进入前执行逻辑,可以使用全局守卫或路由独享守卫。
2. 路由元信息:用 meta 携带附加数据
每个路由配置中的 meta 字段是一个自由对象,用于存储任意自定义数据。Vue Router 不会对 meta 做任何处理——它完全由开发者定义和消费。最常用于权限标记、页面标题和页面缓存控制。
1 | const routes = [ |
2.1 结合全局守卫动态设置页面标题
这是一个经典模式:在 afterEach 中读取 meta.title 并更新 document.title。
1 | router.afterEach((to) => { |
afterEach 是全局后置钩子,导航已经完成,因此它没有 next 参数,也不接受返回值——它纯粹用于副作用(如设置标题、滚动到顶部、埋点统计)。
2.2 基于 meta 的权限控制扩展
你可以扩展 beforeEach 守卫,使其读取 meta.roles 并与当前用户角色进行比对:
1 | router.beforeEach((to, from) => { |
3. 路由懒加载:按需分割代码
默认情况下,如果所有页面组件都在路由表中直接导入(import Home from './Home.vue'),它们会被打包进同一个 JavaScript 文件,导致首屏加载体积巨大。路由懒加载使用动态 import() 将每个页面拆分为独立的 chunk,仅在用户访问时才下载对应代码。
1 | // ❌ 静态导入:所有页面打包进主 bundle |
打包效果:每个 () => import() 返回一个 Promise,Vue Router 仅在路由被激活时才触发 import,打包工具(Vite/Webpack)会自动将每个 import 的目标拆分为独立 chunk。懒加载的组件首次激活时需要下载 JS 文件,这期间可以配合 <Suspense> 显示加载状态。
3.1 命名 chunk:便于调试和分析
在 import() 中使用 /* webpackChunkName: "group-name" */ 魔术注释(Webpack 环境)可以自定义 chunk 名称。Vite 默认基于文件路径自动生成 chunk 名称,无需此注释。
1 | const routes = [ |
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 | <template> |
关键机制:
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 | // router/index.js |
1 | <!-- App.vue --> |
设计要点:
- 全局
beforeEach集中处理认证和角色权限,避免在每个组件中重复校验逻辑。 meta.requiresAuth标记需要登录的路由,meta.roles精细控制角色权限。meta.title在afterEach中统一设置document.title,无需各组件手动处理。- 所有页面组件使用动态
import()懒加载。 App.vue使用作用域插槽 +<Transition>包裹<component :is>,实现所有路由切换时的淡入淡出动画。- 根据
$route.meta.requiresAuth条件渲染侧边栏,登录页和 404 页不显示侧边栏。
课后练习
一、概念自测(选择题 / 填空题)
(单选) Vue Router 中,哪个导航守卫在所有组件内守卫和路由独享守卫之后、导航确认之前触发,适合做最终权限判定?
A.beforeEach
B.beforeEnter
C.beforeResolve
D.afterEach(单选) 要实现页面切换时的过渡动画,应该将
<Transition>包裹在哪个组件外部?
A.<router-link>
B.<router-view>内部的<component :is>
C.<nav>导航栏
D. 根<div id="app">(填空) 在路由配置中,要为某条路由添加附加信息(如权限标签、页面标题),应将这些数据放在
______字段中。(多选) 关于路由懒加载的描述,哪些是正确的?
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.js 和 App.vue。提示词中需明确指定全局守卫逻辑、meta 字段结构、过渡动画的 CSS 类名。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请为一个 Vue 3 电商后台管理系统创建 Vue Router 4 路由系统。需要创建以下文件:
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'。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; }。- 添加 JSDoc 注释,确保所有配置可直接使用。完成后列出所有文件内容。
四、面试真题与参考答案
题目(百度前端面试题):
请详细描述 Vue Router 的完整导航解析流程(从触发导航到 DOM 更新的每一步)。为什么需要将
<router-view>内部的<component :is>替换为显式的<component :is="Component">并设置key?如果不设置key,路由切换时<Transition>能正常工作吗?为什么?
参考答案:
完整的导航解析流程依次为:触发导航 → beforeRouteLeave(失活组件)→ beforeEach(全局前置)→ beforeRouteUpdate(复用组件)→ beforeEnter(路由独享)→ 解析异步路由组件 → beforeRouteEnter(激活组件)→ beforeResolve(全局解析)→ 导航确认 → afterEach(全局后置)→ DOM 更新 → beforeRouteEnter 中 next 的回调。
使用 <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.path 或 route.fullPath 作为 key,确保每次路由切换都创建新的元素标识。
课后练习答案
一、概念自测答案
C
- 解析:
beforeResolve在所有组件内守卫和路由独享守卫之后、导航确认之前触发。A 是全局前置,B 是路由独享,D 是后置钩子。
- 解析:
B
- 解析:
<Transition>必须包裹<router-view>内部显式渲染的<component :is>,才能检测组件切换并触发动画。
- 解析:
meta- 解析:
meta字段用于存储路由的自定义附加数据,如requiresAuth、title、roles等。
- 解析:
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 完整代码。”