67. 路由守卫深度解析

📖 概述

路由守卫是Vue Router中用于控制路由导航的钩子函数,允许我们在路由导航的不同阶段执行特定逻辑,如权限检查、数据预加载、页面标题设置等。Vue Router 4.x提供了多种类型的路由守卫,包括全局守卫、路由独享守卫和组件内守卫。本集将深入讲解Vue Router 4.x中路由守卫的类型、用法、执行顺序以及最佳实践,帮助你构建更安全、更灵活的单页应用。

✨ 核心知识点

1. 路由守卫概述

什么是路由守卫

  • 路由守卫是在路由导航过程中触发的钩子函数
  • 允许在路由导航的不同阶段执行特定逻辑
  • 用于权限控制、数据预加载、页面标题设置等场景
  • 支持异步操作和导航控制

路由守卫的类型

  1. 全局守卫:应用于所有路由

    • beforeEach:导航开始前触发
    • beforeResolve:导航确认前触发
    • afterEach:导航完成后触发
  2. 路由独享守卫:应用于特定路由

    • beforeEnter:路由进入前触发
  3. 组件内守卫:应用于组件内部

    • 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. 路由守卫的执行顺序

路由导航过程中,守卫的执行顺序如下:

  1. 触发router.beforeEach()全局前置守卫
  2. 触发即将离开组件的onBeforeRouteLeave()组件内守卫
  3. 触发router.beforeEnter()路由独享守卫
  4. 解析异步路由组件
  5. 触发即将进入组件的onBeforeRouteEnter()组件内守卫
  6. 触发router.beforeResolve()全局解析守卫
  7. 导航确认
  8. 触发router.afterEach()全局后置钩子
  9. 触发DOM更新
  10. 执行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 3

3. 导航取消与重定向

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. 路由守卫的最佳实践

  1. 权限控制

    • 使用全局前置守卫实现集中式权限控制
    • 结合路由元信息和用户权限进行检查
    • 支持动态权限更新
  2. 数据预加载

    • 使用全局解析守卫或路由独享守卫进行数据预加载
    • 避免在组件内重复加载数据
    • 处理数据加载失败的情况
  3. 页面标题与SEO

    • 在全局前置守卫中统一设置页面标题
    • 支持国际化的页面标题
    • 结合meta标签进行SEO优化
  4. 导航取消与确认

    • 使用组件内守卫实现表单未保存提示
    • 提供清晰的用户反馈
    • 避免过度打扰用户
  5. 性能优化

    • 避免在守卫中执行复杂的同步操作
    • 异步操作要设置合理的超时时间
    • 缓存守卫的计算结果
  6. 错误处理

    • 在守卫中处理各种错误情况
    • 提供友好的错误页面
    • 记录错误日志
  7. 代码组织

    • 将守卫逻辑按照功能模块拆分
    • 保持守卫函数的简洁性
    • 使用TypeScript类型增强类型安全性

8. 常见问题与解决方案

1. 导航守卫无限循环

问题:导航守卫中使用next({ name: &#39;route&#39; })导致无限循环

解决方案

  • 检查导航条件,确保不会重复触发相同的重定向
  • 使用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()
  })
})

📝 最佳实践总结

  1. 选择合适的守卫类型

    • 全局逻辑使用全局守卫
    • 特定路由逻辑使用路由独享守卫
    • 组件特定逻辑使用组件内守卫
  2. 保持守卫简洁

    • 每个守卫只负责一个功能
    • 复杂逻辑拆分为多个守卫
    • 使用辅助函数封装复杂逻辑
  3. 处理异步操作

    • 确保异步操作完成后调用next()
    • 处理异步操作的错误情况
    • 设置合理的超时时间
  4. 避免无限循环

    • 检查导航条件,避免重复重定向
    • 使用条件判断防止循环
    • 测试守卫的各种情况
  5. 使用TypeScript增强类型安全

    • 为守卫函数添加类型注解
    • 扩展路由元信息类型
    • 使用接口定义路由参数类型
  6. 测试守卫逻辑

    • 编写单元测试测试守卫逻辑
    • 测试各种边界情况
    • 测试异步操作的处理

💡 常见问题与解决方案

  1. 守卫执行顺序错误

    • 检查守卫的注册顺序
    • 了解守卫的执行流程
    • 避免在守卫中修改路由配置
  2. 守卫不触发

    • 检查路由配置是否正确
    • 确保守卫已正确注册
    • 检查导航是否被其他守卫阻止
  3. next()被多次调用

    • 确保每个分支只调用一次next()
    • 使用return语句避免多次调用
    • 检查异步操作是否多次调用next()

📚 进一步学习资源

🎯 课后练习

  1. 基础练习

    • 实现全局前置守卫进行权限控制
    • 实现组件内守卫进行表单未保存提示
    • 实现全局解析守卫进行数据预加载
  2. 进阶练习

    • 实现动态添加和移除守卫
    • 实现守卫链,处理复杂的导航逻辑
    • 实现异步守卫,处理异步操作
  3. 实战练习

    • 在实际项目中使用路由守卫进行权限控制
    • 实现基于角色的访问控制
    • 实现数据预加载和错误处理
  4. 性能优化练习

    • 优化守卫的执行性能
    • 缓存守卫的计算结果
    • 异步操作设置合理的超时时间

通过本集的学习,你已经掌握了Vue Router 4.x中路由守卫的类型、用法、执行顺序以及最佳实践。在实际项目中,合理运用路由守卫,能够实现权限控制、数据预加载、页面标题设置等功能,构建更安全、更灵活、更易于维护的单页应用。下一集我们将深入学习滚动行为与过渡动画,进一步提升Vue 3路由系统的使用能力。

« 上一篇 Vue3 + TypeScript 系列教程 - 第66集:路由元信息与权限控制 下一篇 » Vue3 + TypeScript 系列教程 - 第68集:滚动行为与过渡动画