67. 路由守卫深度解析
📖 概述
路由守卫是Vue Router中用于控制路由导航的钩子函数,允许我们在路由导航的不同阶段执行特定逻辑,如权限检查、数据预加载、页面标题设置等。Vue Router 4.x提供了多种类型的路由守卫,包括全局守卫、路由独享守卫和组件内守卫。本集将深入讲解Vue Router 4.x中路由守卫的类型、用法、执行顺序以及最佳实践,帮助你构建更安全、更灵活的单页应用。
✨ 核心知识点
1. 路由守卫概述
什么是路由守卫
- 路由守卫是在路由导航过程中触发的钩子函数
- 允许在路由导航的不同阶段执行特定逻辑
- 用于权限控制、数据预加载、页面标题设置等场景
- 支持异步操作和导航控制
路由守卫的类型
全局守卫:应用于所有路由
- beforeEach:导航开始前触发
- beforeResolve:导航确认前触发
- afterEach:导航完成后触发
路由独享守卫:应用于特定路由
- beforeEnter:路由进入前触发
组件内守卫:应用于组件内部
- onBeforeRouteEnter:组件进入前触发
- onBeforeRouteUpdate:组件更新前触发
- onBeforeRouteLeave:组件离开前触发
2. 全局守卫
1. router.beforeEach()
全局前置守卫,在导航开始前触发,是最常用的守卫。
// src/router/index.ts
router.beforeEach((to, from, next) => {
// to: 即将进入的目标路由对象
// from: 当前离开的路由对象
// next: 导航控制函数
// 示例:权限控制
const requiresAuth = to.meta.requiresAuth || false
if (requiresAuth && !isAuthenticated()) {
// 重定向到登录页
next({ name: 'login', query: { redirect: to.fullPath } })
return
}
// 示例:页面标题设置
if (to.meta.title) {
document.title = `${to.meta.title} - Vue App`
}
// 允许导航
next()
})2. router.beforeResolve()
全局解析守卫,在导航确认前触发,所有组件内守卫和异步路由组件都已解析完成。
router.beforeResolve((to, from, next) => {
// 在所有组件内守卫和异步路由组件被解析之后调用
// 常用于数据预加载
// 示例:数据预加载
if (to.meta.requiresData) {
fetchData(to.params.id)
.then(data => {
// 将数据存储到全局状态或路由元信息中
to.meta.data = data
next()
})
.catch(error => {
// 数据加载失败,重定向到错误页
next({ name: 'error', params: { message: error.message } })
})
} else {
next()
}
})3. router.afterEach()
全局后置钩子,在导航完成后触发,不接收next函数,不能改变导航。
router.afterEach((to, from) => {
// 导航完成后触发
// 常用于统计、埋点、日志记录等
// 示例:统计页面访问量
trackPageView(to.path)
// 示例:滚动到顶部
window.scrollTo(0, 0)
// 示例:清除页面缓存
if (!to.meta.keepAlive) {
clearPageCache(from.name)
}
})3. 路由独享守卫
路由独享守卫是在路由配置中定义的守卫,只应用于特定路由。
// src/router/index.ts
const routes: Array<RouteRecordRaw> = [
{
path: '/admin',
name: 'admin',
component: () => import('../views/AdminView.vue'),
meta: {
requiresAuth: true
},
// 路由独享守卫
beforeEnter: (to, from, next) => {
// 只应用于/admin路由
const hasAdminPermission = checkAdminPermission()
if (hasAdminPermission) {
next()
} else {
next({ name: '403' })
}
}
},
{
path: '/user/:id',
name: 'user',
component: () => import('../views/UserView.vue'),
// 路由独享守卫,支持异步操作
beforeEnter: async (to, from, next) => {
const userExists = await checkUserExists(to.params.id)
if (userExists) {
next()
} else {
next({ name: '404' })
}
}
}
]4. 组件内守卫
组件内守卫是在组件内部定义的守卫,只应用于当前组件。
1. onBeforeRouteEnter
在组件进入前触发,此时组件实例尚未创建,无法访问this。
<script setup lang="ts">
import { onBeforeRouteEnter } from 'vue-router'
onBeforeRouteEnter((to, from, next) => {
// 组件实例尚未创建,无法访问this
// 可以通过next的回调函数访问组件实例
next(vm => {
// vm是组件实例,可以访问组件的data、methods等
vm.fetchData(to.params.id)
})
// 示例:表单未保存提示
if (from.meta.requiresUnsavedCheck) {
if (confirm('您有未保存的更改,确定要离开吗?')) {
next()
} else {
next(false) // 取消导航
}
}
})
</script>2. onBeforeRouteUpdate
在组件更新前触发,当路由参数变化但组件被复用时触发。
<script setup lang="ts">
import { ref, onBeforeRouteUpdate } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const userData = ref(null)
// 初始加载数据
fetchData(route.params.id)
// 路由参数变化时更新数据
onBeforeRouteUpdate((to, from, next) => {
// 组件已创建,可以访问组件实例
fetchData(to.params.id)
next()
})
async function fetchData(id) {
userData.value = await getUserData(id)
}
</script>3. onBeforeRouteLeave
在组件离开前触发,用于确认离开或清理资源。
<script setup lang="ts">
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave((to, from, next) => {
// 示例:确认离开
if (hasUnsavedChanges.value) {
if (confirm('您有未保存的更改,确定要离开吗?')) {
next()
} else {
next(false) // 取消导航
}
} else {
// 示例:清理资源
cleanupResources()
next()
}
})
</script>5. 路由守卫的执行顺序
路由导航过程中,守卫的执行顺序如下:
- 触发
router.beforeEach()全局前置守卫 - 触发即将离开组件的
onBeforeRouteLeave()组件内守卫 - 触发
router.beforeEnter()路由独享守卫 - 解析异步路由组件
- 触发即将进入组件的
onBeforeRouteEnter()组件内守卫 - 触发
router.beforeResolve()全局解析守卫 - 导航确认
- 触发
router.afterEach()全局后置钩子 - 触发DOM更新
- 执行
onBeforeRouteEnter()守卫中的next回调,访问组件实例
6. 路由守卫的高级应用
1. 异步路由守卫
// 异步全局前置守卫
router.beforeEach(async (to, from, next) => {
// 异步权限检查
const hasPermission = await checkPermission(to.meta.permissions || [])
if (hasPermission) {
next()
} else {
next({ name: '403' })
}
})2. 守卫链
// 多个全局前置守卫会按照注册顺序执行
router.beforeEach((to, from, next) => {
console.log('Guard 1')
next()
})
router.beforeEach((to, from, next) => {
console.log('Guard 2')
next()
})
router.beforeEach((to, from, next) => {
console.log('Guard 3')
next()
})
// 输出顺序:Guard 1 -> Guard 2 -> Guard 33. 导航取消与重定向
router.beforeEach((to, from, next) => {
// 取消导航
if (to.path === '/forbidden') {
next(false)
return
}
// 重定向到其他路由
if (to.path === '/old-path') {
next({ name: 'new-path' })
return
}
// 带查询参数的重定向
if (to.meta.requiresAuth && !isAuthenticated()) {
next({ name: 'login', query: { redirect: to.fullPath } })
return
}
next()
})4. 动态添加守卫
// 动态添加全局前置守卫
const removeGuard = router.beforeEach((to, from, next) => {
// 守卫逻辑
next()
})
// 移除守卫
removeGuard()7. 路由守卫的最佳实践
权限控制
- 使用全局前置守卫实现集中式权限控制
- 结合路由元信息和用户权限进行检查
- 支持动态权限更新
数据预加载
- 使用全局解析守卫或路由独享守卫进行数据预加载
- 避免在组件内重复加载数据
- 处理数据加载失败的情况
页面标题与SEO
- 在全局前置守卫中统一设置页面标题
- 支持国际化的页面标题
- 结合meta标签进行SEO优化
导航取消与确认
- 使用组件内守卫实现表单未保存提示
- 提供清晰的用户反馈
- 避免过度打扰用户
性能优化
- 避免在守卫中执行复杂的同步操作
- 异步操作要设置合理的超时时间
- 缓存守卫的计算结果
错误处理
- 在守卫中处理各种错误情况
- 提供友好的错误页面
- 记录错误日志
代码组织
- 将守卫逻辑按照功能模块拆分
- 保持守卫函数的简洁性
- 使用TypeScript类型增强类型安全性
8. 常见问题与解决方案
1. 导航守卫无限循环
问题:导航守卫中使用next({ name: 'route' })导致无限循环
解决方案:
- 检查导航条件,确保不会重复触发相同的重定向
- 使用
next(false)取消导航,而不是重定向到同一路由 - 在重定向前添加条件判断,避免循环
// 错误示例:无限循环
router.beforeEach((to, from, next) => {
if (to.name !== 'login') {
next({ name: 'login' }) // 会导致无限循环
} else {
next()
}
})
// 正确示例:添加条件判断
router.beforeEach((to, from, next) => {
const requiresAuth = to.meta.requiresAuth || false
if (requiresAuth && !isAuthenticated() && to.name !== 'login') {
next({ name: 'login' }) // 只有在需要认证且未认证且当前不是登录页时才重定向
} else {
next()
}
})2. 异步操作未正确处理
问题:守卫中的异步操作未调用next(),导致导航卡住
解决方案:
- 确保异步操作完成后调用next()
- 处理异步操作的错误情况
- 设置合理的超时时间
// 错误示例:异步操作未调用next()
router.beforeEach((to, from, next) => {
fetchData() // 异步操作,但未调用next()
})
// 正确示例:处理异步操作
router.beforeEach((to, from, next) => {
fetchData()
.then(() => {
next() // 成功时调用next()
})
.catch(() => {
next({ name: 'error' }) // 失败时调用next()
})
})3. 组件内守卫无法访问this
问题:在onBeforeRouteEnter中无法访问组件实例
解决方案:
- 使用next的回调函数访问组件实例
- 在
onBeforeRouteUpdate中访问组件实例
// 正确示例:使用next的回调函数
onBeforeRouteEnter((to, from, next) => {
next(vm => {
// vm是组件实例,可以访问组件的data、methods等
vm.fetchData()
})
})📝 最佳实践总结
选择合适的守卫类型
- 全局逻辑使用全局守卫
- 特定路由逻辑使用路由独享守卫
- 组件特定逻辑使用组件内守卫
保持守卫简洁
- 每个守卫只负责一个功能
- 复杂逻辑拆分为多个守卫
- 使用辅助函数封装复杂逻辑
处理异步操作
- 确保异步操作完成后调用next()
- 处理异步操作的错误情况
- 设置合理的超时时间
避免无限循环
- 检查导航条件,避免重复重定向
- 使用条件判断防止循环
- 测试守卫的各种情况
使用TypeScript增强类型安全
- 为守卫函数添加类型注解
- 扩展路由元信息类型
- 使用接口定义路由参数类型
测试守卫逻辑
- 编写单元测试测试守卫逻辑
- 测试各种边界情况
- 测试异步操作的处理
💡 常见问题与解决方案
守卫执行顺序错误
- 检查守卫的注册顺序
- 了解守卫的执行流程
- 避免在守卫中修改路由配置
守卫不触发
- 检查路由配置是否正确
- 确保守卫已正确注册
- 检查导航是否被其他守卫阻止
next()被多次调用
- 确保每个分支只调用一次next()
- 使用return语句避免多次调用
- 检查异步操作是否多次调用next()
📚 进一步学习资源
🎯 课后练习
基础练习
- 实现全局前置守卫进行权限控制
- 实现组件内守卫进行表单未保存提示
- 实现全局解析守卫进行数据预加载
进阶练习
- 实现动态添加和移除守卫
- 实现守卫链,处理复杂的导航逻辑
- 实现异步守卫,处理异步操作
实战练习
- 在实际项目中使用路由守卫进行权限控制
- 实现基于角色的访问控制
- 实现数据预加载和错误处理
性能优化练习
- 优化守卫的执行性能
- 缓存守卫的计算结果
- 异步操作设置合理的超时时间
通过本集的学习,你已经掌握了Vue Router 4.x中路由守卫的类型、用法、执行顺序以及最佳实践。在实际项目中,合理运用路由守卫,能够实现权限控制、数据预加载、页面标题设置等功能,构建更安全、更灵活、更易于维护的单页应用。下一集我们将深入学习滚动行为与过渡动画,进一步提升Vue 3路由系统的使用能力。