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

Hi,我是前端人类学

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

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

文章目录

    • 一、路由守卫:构建应用的权限防火墙
      • [1.1 全局守卫](#1.1 全局守卫)
      • [1.2 路由独享守卫](#1.2 路由独享守卫)
      • [1.3 组件内守卫](#1.3 组件内守卫)
    • 二、动态路由:运行时注册路由
      • [2.1 路由初始化与动态添加](#2.1 路由初始化与动态添加)
      • [2.2 路由删除与重置](#2.2 路由删除与重置)
      • [2.3 路由权限的精细化控制](#2.3 路由权限的精细化控制)
    • 三、懒加载:路由级代码分割
      • [3.1 基本用法](#3.1 基本用法)
      • [3.2 命名 chunk](#3.2 命名 chunk)
      • [3.3 预加载与预获取](#3.3 预加载与预获取)
      • [3.4 加载状态与错误处理](#3.4 加载状态与错误处理)
    • [四、路由缓存:KeepAlive 与 RouterView](#四、路由缓存:KeepAlive 与 RouterView)
      • [4.1 基本缓存](#4.1 基本缓存)
      • [4.2 精细控制缓存](#4.2 精细控制缓存)
      • [4.3 基于路由动态决定缓存](#4.3 基于路由动态决定缓存)
      • [4.4 缓存下的生命周期](#4.4 缓存下的生命周期)
      • [4.5 标签页缓存方案](#4.5 标签页缓存方案)
    • 五、综合实践:完整权限路由方案

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

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

1.1 全局守卫

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

javascript 复制代码
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 函数,适合日志记录、页面埋点等操作:

javascript 复制代码
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 守卫:

javascript 复制代码
<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 路由初始化与动态添加

典型的动态路由流程:

javascript 复制代码
// 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

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

javascript 复制代码
// 异步路由配置(按角色划分)
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 路由删除与重置

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

javascript 复制代码
// 重置路由的方法
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 配合守卫实现更细粒度的权限控制:

javascript 复制代码
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 基本用法

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

3.2 命名 chunk

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

javascript 复制代码
component: () => import(
  /* webpackChunkName: "dashboard" */
  '@/views/Dashboard.vue'
)

3.3 预加载与预获取

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

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

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

3.4 加载状态与错误处理

vue 复制代码
<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 基本缓存

vue 复制代码
<template>
  <RouterView v-slot="{ Component }">
    <KeepAlive>
      <component :is="Component" />
    </KeepAlive>
  </RouterView>
</template>

4.2 精细控制缓存

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

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

4.3 基于路由动态决定缓存

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

vue 复制代码
<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 生命周期钩子:

javascript 复制代码
<script setup>
import { onActivated, onDeactivated } from 'vue'

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

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

4.5 标签页缓存方案

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

html 复制代码
<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>

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

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

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

相关推荐
Cobyte1 小时前
Vite & Rollup 插件开发实践
前端·javascript·vue.js
ShuiShenHuoLe2 小时前
vue如何实现国际化,它来了!
前端·javascript·vue.js
其美杰布-富贵-李2 小时前
Vue Router 实战教程
前端·javascript·vue.js
名字还没想好☜2 小时前
React 用 IntersectionObserver 实现图片懒加载与无限滚动:封装一个 useInView Hook
前端·javascript·vue.js·react.js·react
天天码行空19 小时前
vkeyboardhand:零依赖虚拟键盘指法组件
前端·javascript·vue.js
我要两颗404西柚19 小时前
Stage four:VUE项目上线
前端·javascript·vue.js
TinssonTai1 天前
Vite 8 版 Chrome 插件全家桶,popup/options/sidepanel 一次集齐
前端·vue.js
A24207349301 天前
Vue 3 + TypeScript:核心优势与开发注意事项详解
vue.js·ubuntu·typescript
sugar__salt1 天前
Vue3 自定义指令与插槽(Slot)技术详解
前端·javascript·vue.js·前端框架·vue