Vue 路由进阶:路由守卫权限控制、动态路由、懒加载、路由缓存

Hi,我是前端人类学

在 Vue 单页应用开发中,路由管理往往从“配置几个页面”演变为“构建一套复杂的访问控制系统”。随着应用规模的增长,权限控制、模块动态加载、性能优化和状态保持等问题会接踵而至。

本文将从路由守卫权限控制动态路由懒加载路由缓存四个维度,深入探讨 Vue Router 的进阶实践。


一、路由守卫:构建应用的权限防火墙

路由守卫(Navigation Guards)是 Vue Router 提供的一套钩子函数,用于在路由跳转前后执行逻辑。它是实现权限控制的核心机制。

1.1 全局守卫

全局前置守卫 router.beforeEach 是最常用的权限控制入口,每次路由跳转前都会触发:

import router from './router'

router.beforeEach(async (to, from, next) => {
  const token = localStorage.getItem('token')
  const userStore = useUserStore()
  
  // 白名单:登录页、注册页等无需认证
  const whiteList = ['/login', '/register']
  
  if (token) {
    if (to.path === '/login') {
      // 已登录,跳转到首页
      next('/')
    } else {
      // 获取用户信息(仅首次)
      if (!userStore.userInfo) {
        await userStore.fetchUserInfo()
      }
      next()
    }
  } else {
    if (whiteList.includes(to.path)) {
      next()
    } else {
      // 未登录,跳转登录页并携带回跳地址
      next(`/login?redirect=${to.fullPath}`)
    }
  }
})

全局解析守卫 router.beforeResolve 在导航确认前、组件渲染前触发,适用于在渲染前确保数据就绪。

全局后置钩子 router.afterEach 不接收 next 函数,适合日志记录、页面埋点等操作:

router.afterEach((to, from) => {
  // 页面访问统计
  trackPageView(to.fullPath)
  // 更新页面标题
  document.title = to.meta.title || '默认标题'
})

1.2 路由独享守卫

在路由配置中直接定义 beforeEnter 守卫,仅对该路由生效:

javascript

const routes = [
  {
    path: '/admin',
    component: () => import('@/views/Admin.vue'),
    beforeEnter: (to, from, next) => {
      const role = localStorage.getItem('role')
      if (role === 'admin') {
        next()
      } else {
        next('/403')
      }
    }
  }
]

1.3 组件内守卫

在组件内部使用 onBeforeRouteLeaveonBeforeRouteUpdate 守卫:

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

// 路由更新时(如 /posts/1 -> /posts/2)
onBeforeRouteUpdate((to, from, next) => {
  // 重新加载数据
  fetchPost(to.params.id)
  next()
})

// 离开当前页面时
onBeforeRouteLeave((to, from, next) => {
  if (formDataIsDirty.value) {
    if (confirm('有未保存的内容,确定离开吗?')) {
      next()
    } else {
      next(false)
    }
  } else {
    next()
  }
})
</script>

二、动态路由:运行时注册路由

动态路由是指应用在运行时根据用户权限、业务需求动态添加路由,而非在初始化时全部注册。这在 RBAC(基于角色的访问控制)系统中尤为重要。

2.1 路由初始化与动态添加

典型的动态路由流程:

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

// 基础路由:登录页、404、白名单页面
const constantRoutes = [
  { path: '/login', component: () => import('@/views/Login.vue') },
  { path: '/404', component: () => import('@/views/404.vue') }
]

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

export default router

在全局守卫中动态添加权限路由:

// 异步路由配置(按角色划分)
const asyncRouteMap = {
  admin: [
    { path: '/dashboard', component: () => import('@/views/Dashboard.vue') },
    { path: '/user-manage', component: () => import('@/views/UserManage.vue') }
  ],
  editor: [
    { path: '/dashboard', component: () => import('@/views/Dashboard.vue') },
    { path: '/content', component: () => import('@/views/Content.vue') }
  ],
  guest: [
    { path: '/dashboard', component: () => import('@/views/Dashboard.vue') }
  ]
}

router.beforeEach(async (to, from, next) => {
  const token = localStorage.getItem('token')
  if (token) {
    if (!router.hasRoute('dashboard')) {
      // 获取用户角色
      const role = await getUserRole()
      const asyncRoutes = asyncRouteMap[role] || []
      
      // 动态添加路由
      asyncRoutes.forEach(route => {
        router.addRoute(route)
      })
      
      // 添加 404 兜底(需放在最后)
      router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' })
      
      // 重新导航,确保动态路由生效
      return next({ ...to, replace: true })
    }
    next()
  } else {
    // 未登录处理...
  }
})

2.2 路由删除与重置

在某些场景下(如退出登录),需要重置路由:

// 重置路由的方法
export function resetRouter() {
  // 获取所有动态添加的路由
  const routes = router.getRoutes()
  routes.forEach(route => {
    // 保留基础路由(根据 name 过滤)
    if (route.name !== 'Login' && route.name !== '404') {
      router.removeRoute(route.name)
    }
  })
}

// 退出登录
function logout() {
  resetRouter()
  localStorage.clear()
  router.push('/login')
}

2.3 路由权限的精细化控制

除了控制路由是否存在,还可以通过路由元信息 meta 配合守卫实现更细粒度的权限控制:

const routes = [
  {
    path: '/system',
    component: () => import('@/views/System.vue'),
    meta: {
      roles: ['admin', 'super_admin'],
      permissions: ['system:read', 'system:write']
    }
  }
]

router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  
  // 检查角色
  if (to.meta.roles) {
    const hasRole = to.meta.roles.some(role => userStore.roles.includes(role))
    if (!hasRole) return next('/403')
  }
  
  // 检查权限
  if (to.meta.permissions) {
    const hasPermission = to.meta.permissions.some(
      perm => userStore.permissions.includes(perm)
    )
    if (!hasPermission) return next('/403')
  }
  
  next()
})

三、懒加载:路由级代码分割

在大型 SPA 中,首屏加载效率直接影响用户体验。路由懒加载通过代码分割将不同路由的代码拆分为独立的 chunk,只在访问对应路由时才加载。

3.1 基本用法

const routes = [
  {
    path: '/dashboard',
    // 使用动态 import
    component: () => import('@/views/Dashboard.vue')
  },
  {
    path: '/settings',
    component: () => import('@/views/Settings.vue')
  }
]

3.2 命名 chunk

通过 Webpack/Vite 的魔法注释指定 chunk 名称,便于管理和缓存:

component: () => import(
  /* webpackChunkName: "dashboard" */
  '@/views/Dashboard.vue'
)

3.3 预加载与预获取

结合路由元信息,实现智能预加载:

// 使用 webpackPrefetch 预获取(低优先级)
component: () => import(
  /* webpackChunkName: "dashboard" */
  /* webpackPrefetch: true */
  '@/views/Dashboard.vue'
)

// 使用 webpackPreload 预加载(高优先级,适用于首屏关键路由)
component: () => import(
  /* webpackChunkName: "home" */
  /* webpackPreload: true */
  '@/views/Home.vue'
)

3.4 加载状态与错误处理

<template>
  <RouterView>
    <template #default="{ Component, route }">
      <Suspense>
        <template #default>
          <component :is="Component" />
        </template>
        <template #fallback>
          <div class="loading-spinner">加载中...</div>
        </template>
      </Suspense>
    </template>
  </RouterView>
</template>

结合 Vue 的 <Suspense> 组件,可以优雅地处理异步组件的加载状态。

四、路由缓存:KeepAlive 与 RouterView

在多标签页或频繁切换的场景中,希望保留页面状态(如列表滚动位置、表单输入内容),避免重复渲染和接口请求。

4.1 基本缓存

<template>
  <RouterView v-slot="{ Component }">
    <KeepAlive>
      <component :is="Component" />
    </KeepAlive>
  </RouterView>
</template>

4.2 精细控制缓存

通过 includeexclude 属性控制哪些路由组件需要缓存:

<template>
  <RouterView v-slot="{ Component }">
    <KeepAlive :include="['Dashboard', 'UserList']" :max="5">
      <component :is="Component" />
    </KeepAlive>
  </RouterView>
</template>

4.3 基于路由动态决定缓存

利用路由元信息动态决定是否缓存:

<template>
  <RouterView v-slot="{ Component, route }">
    <KeepAlive :include="cachedViews">
      <component :is="Component" :key="route.fullPath" />
    </KeepAlive>
  </RouterView>
</template>

<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'

const cachedViews = ref([])

const route = useRoute()

// 根据路由 meta 动态添加缓存
watch(() => route.meta.keepAlive, (newVal) => {
  if (newVal && !cachedViews.value.includes(route.name)) {
    cachedViews.value.push(route.name)
  }
}, { immediate: true })
</script>

4.4 缓存下的生命周期

<KeepAlive> 缓存的组件会经历 activateddeactivated 生命周期钩子:

<script setup>
import { onActivated, onDeactivated } from 'vue'

// 组件从缓存中激活时执行
onActivated(() => {
  // 刷新数据
  fetchData()
  // 恢复滚动位置
  restoreScrollPosition()
})

// 组件被缓存、进入非活跃状态时执行
onDeactivated(() => {
  // 保存当前状态
  saveCurrentState()
})
</script>

4.5 标签页缓存方案

实现类似浏览器的多标签页缓存,需要结合 KeepAliveRouterView 和标签页管理:

<template>
  <div class="tabs-layout">
    <!-- 标签页栏 -->
    <div class="tabs-bar">
      <div
        v-for="tab in tabs"
        :key="tab.path"
        class="tab-item"
        :class="{ active: tab.path === currentPath }"
        @click="switchTab(tab)"
      >
        {{ tab.title }}
        <span class="close-btn" @click.stop="closeTab(tab)">×</span>
      </div>
    </div>
    
    <!-- 路由视图 + 缓存 -->
    <RouterView v-slot="{ Component, route }">
      <KeepAlive :include="cachedTabNames">
        <component :is="Component" :key="route.path" />
      </KeepAlive>
    </RouterView>
  </div>
</template>

五、综合实践:完整权限路由方案

将上述四部分整合,形成一个完整的权限路由管理方案:

// stores/permission.js
import { defineStore } from 'pinia'

export const usePermissionStore = defineStore('permission', {
  state: () => ({
    routes: [],
    permissions: []
  }),
  actions: {
    generateRoutes(role) {
      // 根据角色获取动态路由
      const asyncRoutes = getAsyncRoutesByRole(role)
      // 存储到 store
      this.routes = asyncRoutes
      return asyncRoutes
    }
  }
})

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { usePermissionStore } from '@/stores/permission'

const constantRoutes = [
  { path: '/login', name: 'Login', component: () => import('@/views/Login.vue') },
  { path: '/403', name: '403', component: () => import('@/views/403.vue') }
]

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

// 全局前置守卫
router.beforeEach(async (to, from, next) => {
  const token = localStorage.getItem('token')
  const permissionStore = usePermissionStore()
  
  if (token) {
    if (to.path === '/login') {
      next('/')
      return
    }
    
    // 检查是否已加载权限路由
    if (!permissionStore.routes.length) {
      try {
        const userInfo = await getUserInfo()
        const routes = permissionStore.generateRoutes(userInfo.role)
        
        routes.forEach(route => router.addRoute(route))
        router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' })
        
        // 确保动态路由加载完成后再跳转
        return next({ ...to, replace: true })
      } catch (error) {
        // 权限获取失败,跳转登录
        localStorage.removeItem('token')
        next(`/login?redirect=${to.fullPath}`)
      }
    } else {
      next()
    }
  } else {
    if (to.meta.requiresAuth !== false) {
      next(`/login?redirect=${to.fullPath}`)
    } else {
      next()
    }
  }
})

export default router

路由管理是 Vue 应用架构的核心环节之一。合理的权限控制保证系统安全,动态路由提供灵活的扩展能力,懒加载优化性能,缓存提升用户体验。在实际项目中,这四者往往需要协同配合,共同构建一个健壮、高效的路由系统。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端人类学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值