【Vue2手录14】导航守卫
一、导航守卫基础认知
1.1 核心定义与作用
导航守卫是 Vue Router 提供的路由跳转拦截与控制机制,可在路由“跳转前、跳转中、跳转后”插入自定义逻辑,实现权限验证、数据预加载、导航拦截等核心需求。
类比理解:如同“城门守卫”——所有路由跳转需经过守卫检查,符合条件则“放行”,不符合则“拦截”或“重定向”。
1.2 核心特性
- 拦截时机全覆盖:覆盖导航全生命周期(前/中/后),适配不同业务场景。
- 作用范围灵活:支持“全局(所有路由)、局部(特定路由/组件)”,兼顾通用性与针对性。
- 控制能力强:可直接决定导航结果(放⾏/拦截/重定向),并获取路由上下文信息(目标/来源路由)。
二、全局导航守卫(影响所有路由)
全局导航守卫是项目级别的拦截逻辑,任何路由跳转都会触发,核心包括三类:全局前置守卫、全局解析守卫、全局后置钩子。其中全局前置守卫是实战中最常用的类型。
2.1 全局前置守卫(router.beforeEach
)
全局前置守卫是“路由跳转前的第一道关卡”,常用于登录验证、权限控制等核心场景,必须调用 next()
函数才能完成导航,否则路由会被永久阻塞。
1. 基础用法(注册与参数)
(1)规范注册流程(企业级实践)
为避免代码臃肿,需单独创建 router/guard.js
文件(符合“单一职责原则”),步骤如下:
// 1. 新建 router/guard.js(守卫专用文件)
import router from './index' // 关键:先创建路由实例,再注册守卫// 2. 注册全局前置守卫
router.beforeEach((to, from, next) => {// to:即将进入的目标路由对象(含 path、name、params、query 等)// from:当前正要离开的路由对象(来源路由信息)// next:必须调用的“导航解析函数”,控制导航结果console.log(`从 ${from.path} 跳转到 ${to.path}`)// 业务逻辑后必须调用 next()next() // 无参 = 无条件放⾏
})// 3. 在 main.js 中引入守卫文件(确保执行顺序:Vue → Router → 路由实例 → 守卫)
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import './router/guard' // 仅需执行注册,无需接收返回值new Vue({router,render: h => h(App)
}).$mount('#app')
(2)next()
函数详解(核心!)
next()
是控制导航的关键,不同调用方式对应不同行为,需严格区分:
next() 调用方式 | 作用与适用场景 |
---|---|
next() | 无条件放⾏,进入下一个导航钩子(正常流程,如已登录用户访问首页) |
next(false) | 中断当前导航,URL 重置为 from 路由地址(如阻止未登录用户访问敏感页) |
next('/path') 或 next({ name: 'RouteName' }) | 重定向到指定路由,中断原导航(如未登录时跳转到登录页) |
next(error) | 传入 Error 实例,终止导航并触发 router.onError() 回调(用于错误监控与提示) |
2. 实战场景:登录验证(含白名单防死循环)
未登录用户仅能访问登录页,已登录用户可自由访问——这是最典型的全局前置守卫场景,必须添加白名单(避免跳转登录页时再次触发守卫,导致死循环)。
// router/guard.js
import router from './index'// 1. 模拟登录状态(实际项目中从 localStorage/cookie 读取 token)
const isLogin = () => {return localStorage.getItem('token') !== null // token 存在 = 已登录
}// 2. 白名单:无需登录即可访问的路由(必须包含登录页)
const whiteList = ['/login']// 3. 登录验证逻辑
router.beforeEach((to, from, next) => {if (isLogin()) {// 已登录:若目标是登录页,重定向到首页(避免重复登录)to.path === '/login' ? next('/home') : next()} else {// 未登录:若目标在白名单内,放⾏;否则重定向到登录页whiteList.includes(to.path) ? next() : next('/login')}
})
3. 常见问题与排查
错误现象 | 原因分析 | 解决方案 |
---|---|---|
路由被阻塞,页面空白 | 未调用 next() 函数,导航无法完成 | 确保所有分支逻辑中都有 next() 调用 |
死循环(Maximum call stack size exceeded) | 未加白名单,重定向路由(如登录页)再次触发守卫 | 新增白名单,包含无需验证的路由(如 /login ) |
router 实例未定义 | 守卫注册早于路由实例创建 | 先执行 new VueRouter() ,再引入守卫文件 |
2.2 全局解析守卫(router.beforeResolve
)
全局解析守卫与前置守卫功能类似,但执行时机更晚:在“所有组件内守卫和异步路由组件解析完成后”触发,确保路由跳转前所有依赖已准备就绪。
// router/guard.js
router.beforeResolve((to, from, next) => {console.log('所有组件内守卫与异步组件已解析')next() // 仍需调用 next() 放⾏
})
应用场景:需等待异步数据加载完成后再跳转(如获取用户权限信息后决定是否放⾏),实际开发中使用频率低于前置守卫。
2.3 全局后置钩子(router.afterEach
)
全局后置钩子是“路由跳转完成后”的回调,无 next()
函数,仅用于监听导航结果(无法控制导航行为),类比“导航后的收尾工作”。
1. 基础用法
// router/guard.js
router.afterEach((to, from) => {// 仅接收 to(目标)和 from(来源)参数,无 next()console.log(`导航完成:从 ${from.path} 到 ${to.path}`)// 典型场景:// 1. 页面访问统计(上报用户行为)// reportAnalytics(to.path, from.path)// 2. 关闭全局 loading 状态// store.commit('closeGlobalLoading')// 3. 重置页面滚动位置(避免跳转后停留在原位置)window.scrollTo(0, 0)
})
2. 与“守卫”的核心区别
对比维度 | 守卫(如 beforeEach ) | 钩子(如 afterEach ) |
---|---|---|
执行时机 | 导航过程中(跳转前/中) | 导航完成后(跳转后) |
控制能力 | 可控制导航(放⾏/拦截/重定向) | 无控制能力,仅执行回调 |
参数差异 | 接收 to 、from 、next 三个参数 | 仅接收 to 、from 两个参数 |
三、路由独享守卫(仅影响特定路由)
路由独享守卫是“绑定在单个路由上的拦截逻辑”,仅当访问该路由时触发,适合保护敏感页面(如财务模块、管理员页面),避免全局守卫的“无差别拦截”,提升性能。
3.1 基础用法(配置与参数)
在路由配置对象中通过 beforeEnter
属性定义,参数与全局前置守卫一致(to
、from
、next
)。
// router/index.js
const routes = [{path: '/home',name: 'Home',component: () => import('@/views/Home.vue')},// 路由独享守卫:仅访问 /finance(财务页)时触发{path: '/finance',name: 'Finance',component: () => import('@/views/Finance.vue'),beforeEnter: (to, from, next) => {// 业务逻辑:仅管理员可访问财务页const userRole = localStorage.getItem('role')if (userRole === 'admin') {next() // 管理员:放⾏} else {next('/403') // 非管理员:跳转 403 无权限页}}},{path: '/403',component: () => import('@/views/Forbidden.vue') // 403 组件}
]
3.2 与全局守卫的区别
对比维度 | 全局前置守卫(beforeEach ) | 路由独享守卫(beforeEnter ) |
---|---|---|
作用范围 | 所有路由跳转 | 仅配置该守卫的特定路由 |
执行时机 | 所有路由跳转前 | 访问特定路由前 |
性能优势 | 可能存在无效拦截(如普通页面也触发) | 仅拦截敏感路由,减少不必要判断 |
死循环风险 | 高(需白名单) | 低(仅影响单个路由,不会反复触发) |
四、组件内守卫(绑定在组件上)
组件内守卫是“绑定在 Vue 组件上的路由拦截逻辑”,仅当组件与路由关联时触发(如进入/离开该组件对应的路由),适合组件级别的权限控制或数据预加载。
4.1 三种组件内守卫对比
守卫类型 | 执行时机 | 能否访问 this | 核心场景 |
---|---|---|---|
beforeRouteEnter | 路由进入组件前(组件未创建) | ❌ 不能 | 组件渲染前预加载数据、组件级权限验证 |
beforeRouteUpdate | 路由变化但组件复用(如 /user/1 →/user/2 ) | ✅ 能 | 动态路由参数变化时更新数据(避免组件重建) |
beforeRouteLeave | 路由离开组件前(组件已存在) | ✅ 能 | 阻止未保存表单的意外离开、清理组件状态 |
4.2 实战示例
1. beforeRouteEnter
:预加载数据(无 this
处理)
因组件未创建,无法直接访问 this
,需通过 next()
的回调函数获取组件实例。
<!-- src/views/UserDetail.vue(用户详情页) -->
<template><div><h1>用户详情</h1><p>用户名:{{ user.name }}</p><p>用户ID:{{ user.id }}</p></div>
</template>
<script>
export default {name: 'UserDetail',data() {return {user: {} // 存储预加载的用户数据}},// 组件内守卫:进入路由前预加载数据beforeRouteEnter(to, from, next) {// 1. 模拟异步请求(实际项目中调用接口)const fetchUser = () => {return new Promise(resolve => {setTimeout(() => {// 从路由参数(to.params)获取用户IDresolve({ id: to.params.id, name: '张三' })}, 500)})}// 2. 预加载完成后,通过 next 回调注入组件实例fetchUser().then(userData => {next(vm => {// vm 等同于组件实例(this)vm.user = userData})})}
}
</script>
2. beforeRouteLeave
:阻止未保存表单离开
用户编辑表单未提交时,离开路由会弹出确认框,避免数据丢失。
<!-- src/views/EditForm.vue(编辑表单页) -->
<template><div><textarea v-model="form.content" placeholder="请输入内容" style="width: 300px; height: 100px;"></textarea><button @click="submitForm" style="margin-top: 10px;">提交表单</button></div>
</template>
<script>
export default {name: 'EditForm',data() {return {form: { content: '' },isSaved: false // 标记表单是否已提交}},methods: {submitForm() {// 模拟表单提交console.log('表单提交:', this.form.content)this.isSaved = truethis.$router.push('/home') // 提交后跳转到首页}},// 组件内守卫:离开路由前检查表单状态beforeRouteLeave(to, from, next) {if (!this.isSaved && this.form.content.trim()) {// 未保存且有内容:弹出确认框const isConfirm = window.confirm('表单未提交,确定离开吗?')isConfirm ? next() : next(false) // 确认则放⾏,否则中断} else {next() // 已保存或无内容:直接放⾏}}
}
</script>
五、补充重点知识(面试与实战必备)
5.1 导航守卫的执行顺序(面试高频)
当触发路由跳转时,各类守卫的执行顺序严格遵循“从全局到局部、从跳转前到跳转后”:
- 触发全局前置守卫(
beforeEach
); - 触发目标路由的独享守卫(
beforeEnter
); - 触发**目标组件的 **
beforeRouteEnter
; - 触发全局解析守卫(
beforeResolve
); - 导航确认,组件渲染;
- 触发全局后置钩子(
afterEach
); - 触发目标组件的
mounted
生命周期。
实战小练习
6.1 题目
- 全局前置守卫实战:实现“多角色权限控制”,要求:
- 普通用户(
role: 'user'
):可访问/home
、/profile
、/login
; - 管理员(
role: 'admin'
):可访问所有路由(含/admin
、/finance
); - 未登录用户:仅可访问
/login
,否则跳转登录页; - 无权限用户:跳转
/403
无权限页(需创建Forbidden.vue
组件)。
- 普通用户(
- 路由独享守卫实战:双重保护
/finance
路由,要求:- 即使全局守卫失效,仅管理员可访问
/finance
,普通用户跳转/403
(双重保险)。
- 即使全局守卫失效,仅管理员可访问
- 组件内守卫实战:优化“编辑表单”组件,要求:
- 表单内容变化后(未提交),离开路由时弹出确认框;
- 表单提交后,允许直接离开,且跳转时清空表单内容。
- 全局后置钩子实战:实现“页面访问统计与滚动重置”,要求:
- 每次路由跳转后,在控制台打印“[时间] 用户从 [来源路径] 跳转到 [目标路径]”;
- 自动重置页面滚动位置到顶部(平滑滚动)。
6.2 参考答案
1. 全局前置守卫:多角色权限控制
(1)创建 403 组件(Forbidden.vue
)
<template><div style="text-align: center; margin-top: 50px; color: #f44336;"><h1>403 - 无访问权限</h1><router-link to="/home" style="color: #2196f3; margin-top: 20px; display: inline-block;">返回首页</router-link></div>
</template>
(2)配置守卫逻辑(router/guard.js
)
import router from './index'// 1. 角色-路由权限映射(key:角色,value:可访问路由列表)
const roleRouteMap = {user: ['/home', '/profile', '/login'],admin: ['/home', '/profile', '/admin', '/finance', '/login']
}// 2. 白名单:未登录用户可访问的路由
const whiteList = ['/login']// 3. 权限控制逻辑
router.beforeEach((to, from, next) => {const token = localStorage.getItem('token') // 登录标识const role = localStorage.getItem('role') || 'user' // 默认普通用户if (!token) {// 未登录:仅白名单路由放⾏whiteList.includes(to.path) ? next() : next('/login')} else {// 已登录:检查角色权限const allowRoutes = roleRouteMap[role]if (allowRoutes.includes(to.path)) {next() // 有权限:放⾏} else {next('/403') // 无权限:跳转 403 页}}
})
2. 路由独享守卫:保护 /finance
路由
// router/index.js
const routes = [{path: '/finance',name: 'Finance',component: () => import('@/views/Finance.vue'),// 路由独享守卫:双重验证管理员权限beforeEnter: (to, from, next) => {const role = localStorage.getItem('role')role === 'admin' ? next() : next('/403')}}
]
3. 组件内守卫:编辑表单离开确认
<!-- src/views/EditForm.vue -->
<template><div><textarea v-model="form.content" placeholder="请输入内容" @input="markChanged"style="width: 300px; height: 100px;"></textarea><button @click="submitForm" style="margin-top: 10px;">提交表单</button></div>
</template>
<script>
export default {name: 'EditForm',data() {return {form: { content: '' },isSaved: false, // 标记是否提交isChanged: false // 标记内容是否变化}},methods: {markChanged() {this.isChanged = true // 内容变化时更新标记},submitForm() {// 模拟提交逻辑console.log('表单提交:', this.form.content)this.isSaved = truethis.form.content = '' // 提交后清空表单this.$router.push('/home') // 跳转到首页}},// 组件内守卫:离开前检查状态beforeRouteLeave(to, from, next) {if (this.isChanged && !this.isSaved) {const isConfirm = window.confirm('内容已修改但未提交,确定离开吗?')isConfirm ? next() : next(false)} else {next()}}
}
</script>
4. 全局后置钩子:访问统计与滚动重置
// router/guard.js
router.afterEach((to, from) => {// 1. 页面访问统计(格式:[时间] 用户从 来源 跳转到 目标)const time = new Date().toLocaleString()console.log(`[${time}] 用户从 ${from.path} 跳转到 ${to.path}`)// 2. 平滑重置滚动位置到顶部window.scrollTo({top: 0,behavior: 'smooth' // 平滑滚动(可选,移除则为瞬间跳转)})
})