Vue Router 路由守卫详解与面试指南
路由守卫是 Vue Router 提供的主要功能之一,用于在导航过程中进行控制和拦截。它们主要用于:
权限验证
数据预加载
导航确认
页面访问统计
路由守卫分类
1. 全局守卫
全局前置守卫 beforeEach
const router = createRouter({ ... })router.beforeEach((to, from, next) => {// to: 即将进入的目标路由对象// from: 当前导航正要离开的路由对象// next: 函数,必须调用以解析这个钩子if (to.meta.requiresAuth && !isAuthenticated()) {next({path: '/login',query: { redirect: to.fullPath }})} else {next() // 继续导航}
})
全局解析守卫 beforeResolve
router.beforeResolve(async (to, from, next) => {// 在导航被确认之前,组件内守卫和异步路由组件被解析之后调用if (to.meta.requiresCamera) {try {await askForCameraPermission()next()} catch (error) {next(false) // 取消导航}} else {next()}
})
全局后置钩子 afterEach
router.afterEach((to, from) => {// 导航已经完成,无法改变导航// 适合用于页面访问统计、页面标题设置等document.title = to.meta.title || '默认标题'sendToAnalytics(to.fullPath)
})
2. 路由独享守卫
const routes = [{path: '/admin',component: Admin,beforeEnter: (to, from, next) => {// 只在进入该路由时触发if (!isAdmin()) {next('/unauthorized')} else {next()}}}
]
3. 组件内守卫
beforeRouteEnter
export default {beforeRouteEnter(to, from, next) {// 在渲染该组件的对应路由被验证前调用// 不能获取组件实例 `this`,因为组件实例还没被创建next(vm => {// 通过回调参数访问组件实例vm.fetchData(to.params.id)})}
}
beforeRouteUpdate
export default {beforeRouteUpdate(to, from, next) {// 在当前路由改变,但是该组件被复用时调用// 可以访问组件实例 `this`this.userId = to.params.idthis.fetchUserData()next()}
}
beforeRouteLeave
export default {beforeRouteLeave(to, from, next) {// 在导航离开该组件的对应路由时调用// 可以访问组件实例 `this`const answer = window.confirm('确定要离开吗?未保存的更改将会丢失')if (answer) {next()} else {next(false) // 取消导航}}
}
完整的导航解析流程
导航被触发
调用失活组件的
beforeRouteLeave
守卫调用全局的
beforeEach
守卫在重用的组件里调用
beforeRouteUpdate
守卫调用路由配置里的
beforeEnter
守卫解析异步路由组件
在被激活的组件里调用
beforeRouteEnter
守卫调用全局的
beforeResolve
守卫导航被确认
调用全局的
afterEach
钩子触发 DOM 更新
调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入
面试题解答指南
常见面试题及解答
1. Vue Router 有哪些路由守卫?它们的执行顺序是怎样的?
解答:
Vue Router 提供了三类路由守卫:
全局守卫:
beforeEach
、beforeResolve
、afterEach
路由独享守卫:
beforeEnter
组件内守卫:
beforeRouteEnter
、beforeRouteUpdate
、beforeRouteLeave
执行顺序:
导航被触发
调用失活组件的
beforeRouteLeave
调用全局
beforeEach
调用重用组件的
beforeRouteUpdate
调用路由配置的
beforeEnter
解析异步路由组件
调用激活组件的
beforeRouteEnter
调用全局
beforeResolve
导航确认
调用全局
afterEach
DOM 更新
执行
beforeRouteEnter
中的 next 回调
2. beforeRouteEnter
中为什么不能访问 this
?如何解决?
解答:
beforeRouteEnter
守卫在导航确认前被调用,此时组件实例还没被创建,所以无法访问 this
。
解决方案:
通过 next
的回调函数来访问组件实例:
beforeRouteEnter(to, from, next) {next(vm => {// 通过 vm 访问组件实例vm.fetchData(to.params.id)})
}
3. 如何实现路由权限控制?
解答:
通常在全局前置守卫 beforeEach
中实现:
router.beforeEach((to, from, next) => {const isLoggedIn = checkAuth() // 检查用户是否登录const requiresAuth = to.matched.some(record => record.meta.requiresAuth)if (requiresAuth && !isLoggedIn) {next({path: '/login',query: { redirect: to.fullPath }})} else if (to.path === '/login' && isLoggedIn) {next('/dashboard') // 已登录用户访问登录页时重定向} else {next()}
})
4. 路由守卫中 next()
的不同用法?
解答:
next()
:进行管道中的下一个钩子next(false)
:中断当前导航next('/path')
或next({ path: '/path' })
:跳转到不同的位置next(error)
:传入 Error 实例,导航会被终止且错误会被传递给router.onError()
5. 如何在路由守卫中进行异步操作?
解答:
可以返回 Promise 或使用 async/await:
router.beforeEach(async (to, from, next) => {if (to.meta.requiresAuth) {try {const user = await fetchUserInfo()if (user.isAuthenticated) {next()} else {next('/login')}} catch (error) {next('/error')}} else {next()}
})
实战示例:完整的权限控制系统
// router.js
import { createRouter, createWebHistory } from 'vue-router'const routes = [{path: '/',component: () => import('@/views/Home.vue'),meta: { title: '首页', requiresAuth: false }},{path: '/login',component: () => import('@/views/Login.vue'),meta: { title: '登录', requiresAuth: false }},{path: '/dashboard',component: () => import('@/views/Dashboard.vue'),meta: { title: '控制台', requiresAuth: true,requiredRoles: ['admin', 'editor']}},{path: '/admin',component: () => import('@/views/Admin.vue'),meta: { title: '管理员', requiresAuth: true,requiredRoles: ['admin']},beforeEnter: (to, from, next) => {// 路由独享守卫if (!hasRole('admin')) {next('/unauthorized')} else {next()}}},{path: '/unauthorized',component: () => import('@/views/Unauthorized.vue'),meta: { title: '无权限' }}
]const router = createRouter({history: createWebHistory(),routes
})// 全局前置守卫
router.beforeEach(async (to, from, next) => {// 设置页面标题document.title = to.meta.title || '默认标题'// 检查是否需要认证if (to.meta.requiresAuth) {const token = localStorage.getItem('token')const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')if (!token) {next({path: '/login',query: { redirect: to.fullPath }})return}// 检查角色权限if (to.meta.requiredRoles) {const hasPermission = to.meta.requiredRoles.some(role => userInfo.roles?.includes(role))if (!hasPermission) {next('/unauthorized')return}}}next()
})// 全局后置钩子
router.afterEach((to, from) => {// 发送页面访问统计if (typeof gtag !== 'undefined') {gtag('config', 'GA_MEASUREMENT_ID', {page_path: to.path,page_title: to.meta.title})}
})export default router
面试技巧
理解原理:不仅要记住执行顺序,更要理解为什么是这样的顺序
结合实际:举例说明在实际项目中的应用场景
考虑边界情况:讨论错误处理、异步操作等复杂场景
性能考虑:提到路由守卫可能对性能的影响和优化方法
安全考虑:强调客户端路由守卫不能替代服务端验证
常见陷阱与最佳实践
避免无限循环:确保守卫逻辑不会导致无限重定向
错误处理:始终处理异步操作可能出现的错误
代码组织:将复杂的守卫逻辑抽离到单独的文件中
测试:编写单元测试来验证守卫逻辑的正确性