导航守卫 #

一、导航守卫概述 #

导航守卫用于通过跳转或取消的方式守卫导航,常用于权限控制、登录验证等场景。

1.1 守卫分类 #

text
全局守卫
├── beforeEach    全局前置守卫
├── beforeResolve 全局解析守卫
└── afterEach     全局后置守卫

路由独享守卫
├── beforeEnter   路由前置守卫

组件内守卫
├── beforeRouteEnter    进入前
├── beforeRouteUpdate   更新时
└── beforeRouteLeave    离开前

1.2 导航解析流程 #

text
1. 触发导航
2. 调用失活组件的beforeRouteLeave
3. 调用全局beforeEach
4. 重用的组件调用beforeRouteUpdate
5. 路由配置中调用beforeEnter
6. 解析异步路由组件
7. 激活组件中调用beforeRouteEnter
8. 调用全局beforeResolve
9. 导航确认
10. 调用全局afterEach
11. DOM更新后调用beforeRouteEnter的next回调

二、全局守卫 #

2.1 全局前置守卫 #

javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes
})

// 全局前置守卫
router.beforeEach((to, from, next) => {
  // to: 即将进入的目标路由
  // from: 当前导航正要离开的路由
  // next: 必须调用以resolve钩子
  
  console.log('从', from.path, '到', to.path)
  
  // 权限验证
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

export default router

2.2 返回值替代next #

javascript
// Vue Router 4 推荐使用返回值
router.beforeEach((to, from) => {
  // 返回false取消导航
  if (!isAuthenticated()) {
    return false
  }
  
  // 返回路由地址重定向
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return '/login'
    // 或
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
  
  // 返回undefined或true继续导航
})

2.3 全局解析守卫 #

javascript
// 在导航被确认之前,所有组件内守卫和异步路由组件被解析后调用
router.beforeResolve((to, from) => {
  // 可以在这里进行最终检查
  console.log('导航即将完成')
})

2.4 全局后置守卫 #

javascript
router.afterEach((to, from, failure) => {
  // 导航完成后的操作
  // 可用于分析、更新标题等
  
  // 更新页面标题
  document.title = to.meta.title || '默认标题'
  
  // failure: 导航失败信息(如果导航失败)
  if (failure) {
    console.log('导航失败:', failure)
  }
})

三、路由独享守卫 #

3.1 beforeEnter #

javascript
const routes = [
  {
    path: '/admin',
    component: Admin,
    beforeEnter: (to, from) => {
      // 只有进入该路由时才触发
      if (!isAdmin()) {
        return '/403'
      }
    }
  },
  
  // 可以是函数数组
  {
    path: '/protected',
    component: Protected,
    beforeEnter: [checkAuth, checkPermission]
  }
]

function checkAuth(to, from) {
  if (!isAuthenticated()) {
    return '/login'
  }
}

function checkPermission(to, from) {
  if (!hasPermission(to.meta.permission)) {
    return '/403'
  }
}

四、组件内守卫 #

4.1 组合式API中使用 #

vue
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

// 离开守卫
onBeforeRouteLeave((to, from) => {
  const answer = window.confirm('确定要离开吗?未保存的数据将丢失。')
  if (!answer) return false
})

// 更新守卫
onBeforeRouteUpdate((to, from) => {
  // 路由参数变化时触发
  // 如从 /user/1 到 /user/2
  fetchUser(to.params.id)
})
</script>

4.2 选项式API中使用 #

vue
<script>
export default {
  beforeRouteEnter(to, from, next) {
    // 此时组件实例还未创建,无法访问this
    // 可以通过回调访问组件实例
    next(vm => {
      vm.fetchData(to.params.id)
    })
  },
  
  beforeRouteUpdate(to, from) {
    // 路由参数变化时
    this.fetchData(to.params.id)
  },
  
  beforeRouteLeave(to, from) {
    // 离开时
    if (this.hasUnsavedChanges) {
      const answer = window.confirm('确定要离开吗?')
      if (!answer) return false
    }
  }
}
</script>

五、实际应用示例 #

5.1 登录验证 #

javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    component: Home
  },
  {
    path: '/login',
    component: Login,
    meta: { guest: true }
  },
  {
    path: '/dashboard',
    component: Dashboard,
    meta: { requiresAuth: true }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// 登录验证守卫
router.beforeEach((to, from) => {
  const isAuthenticated = localStorage.getItem('token')
  
  // 需要登录但未登录
  if (to.meta.requiresAuth && !isAuthenticated) {
    return {
      path: '/login',
      query: { redirect: to.fullPath }
    }
  }
  
  // 已登录但访问登录页
  if (to.meta.guest && isAuthenticated) {
    return '/dashboard'
  }
})

export default router

5.2 权限控制 #

javascript
// 权限检查函数
function checkPermission(requiredRoles) {
  const userRoles = getUserRoles()
  return requiredRoles.some(role => userRoles.includes(role))
}

// 路由配置
const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: { roles: ['admin'] }
  },
  {
    path: '/editor',
    component: Editor,
    meta: { roles: ['admin', 'editor'] }
  }
]

// 权限守卫
router.beforeEach((to, from) => {
  const requiredRoles = to.meta.roles
  
  if (requiredRoles) {
    if (!checkPermission(requiredRoles)) {
      return '/403'
    }
  }
})

5.3 页面加载进度条 #

javascript
// 使用NProgress
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

router.beforeEach((to, from) => {
  NProgress.start()
})

router.afterEach(() => {
  NProgress.done()
})

5.4 页面标题 #

javascript
router.beforeEach((to, from) => {
  // 设置页面标题
  const title = to.meta.title
  if (title) {
    document.title = `${title} - 我的应用`
  } else {
    document.title = '我的应用'
  }
})

5.5 防止重复导航 #

javascript
let lastPath = ''

router.beforeEach((to, from) => {
  if (to.path === lastPath) {
    return false
  }
  lastPath = to.path
})

5.6 数据预加载 #

javascript
// 路由配置
const routes = [
  {
    path: '/user/:id',
    component: User,
    meta: {
      preload: (to) => fetchUser(to.params.id)
    }
  }
]

// 预加载守卫
router.beforeEach(async (to, from) => {
  if (to.meta.preload) {
    try {
      to.meta.data = await to.meta.preload(to)
    } catch (error) {
      return '/error'
    }
  }
})

六、完整的守卫配置示例 #

javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue'),
    meta: { title: '首页' }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('../views/Login.vue'),
    meta: { title: '登录', guest: true }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('../views/Dashboard.vue'),
    meta: { title: '控制台', requiresAuth: true }
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('../views/Admin.vue'),
    meta: { title: '管理后台', requiresAuth: true, roles: ['admin'] }
  },
  {
    path: '/403',
    name: 'Forbidden',
    component: () => import('../views/403.vue'),
    meta: { title: '无权限' }
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('../views/NotFound.vue'),
    meta: { title: '页面未找到' }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// 全局前置守卫
router.beforeEach((to, from) => {
  // 开始进度条
  NProgress.start()
  
  // 设置页面标题
  document.title = to.meta.title ? `${to.meta.title} - MyApp` : 'MyApp'
  
  // 获取token
  const token = localStorage.getItem('token')
  
  // 需要登录的页面
  if (to.meta.requiresAuth && !token) {
    return {
      path: '/login',
      query: { redirect: to.fullPath }
    }
  }
  
  // 已登录访问登录页
  if (to.meta.guest && token) {
    return '/dashboard'
  }
  
  // 权限检查
  if (to.meta.roles) {
    const userRoles = JSON.parse(localStorage.getItem('roles') || '[]')
    const hasRole = to.meta.roles.some(role => userRoles.includes(role))
    if (!hasRole) {
      return '/403'
    }
  }
})

// 全局解析守卫
router.beforeResolve((to, from) => {
  // 可以在这里进行最终检查
})

// 全局后置守卫
router.afterEach((to, from, failure) => {
  // 结束进度条
  NProgress.done()
  
  // 滚动到顶部
  window.scrollTo(0, 0)
  
  // 记录导航日志
  if (!failure) {
    logNavigation(to, from)
  }
})

export default router

七、导航失败处理 #

7.1 检测导航失败 #

javascript
import { isNavigationFailure, NavigationFailureType } from 'vue-router'

router.afterEach((to, from, failure) => {
  if (isNavigationFailure(failure)) {
    console.log('导航失败:', failure)
    
    if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
      console.log('导航被中止')
    }
    
    if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
      console.log('重复导航')
    }
    
    if (isNavigationFailure(failure, NavigationFailureType.cancelled)) {
      console.log('导航被取消')
    }
  }
})

7.2 处理导航结果 #

vue
<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

async function navigate() {
  const result = await router.push('/dashboard')
  
  if (result) {
    console.log('导航失败:', result)
  }
}
</script>

八、总结 #

守卫使用场景 #

守卫 使用场景
beforeEach 全局权限验证、登录检查
beforeResolve 最终检查、数据预加载
afterEach 更新标题、进度条结束
beforeEnter 路由级别权限控制
beforeRouteLeave 防止数据丢失、清理工作
beforeRouteUpdate 路由参数变化处理

最佳实践:

  • 使用返回值替代next
  • 合理组织守卫逻辑
  • 避免在守卫中进行耗时操作
  • 使用meta存储路由元信息
  • 组合使用多种守卫实现复杂逻辑
最后更新:2026-03-26