Vue3 动态路由实战:基于权限的动态路由管理与常见坑点解析

引言

在后台管理系统中,不同角色的用户看到的菜单和可访问的页面往往不同。传统的静态路由配置无法满足这种按需加载的需求,因此 动态路由 成为了现代前端工程的标配。Vue Router 提供了 addRoute 方法,允许我们在应用运行时动态添加路由,结合路由守卫,可以优雅地实现基于用户权限的路由控制。

本文将从零开始,带你掌握 Vue3 + JavaScript 环境下动态路由的核心概念、完整实现步骤,并深入剖析刷新后路由丢失、重复添加等经典坑点的解决方案。所有代码均采用组合式 API,可直接用于实战项目。

模块一:动态路由概念与使用场景

概念解析

动态路由 是指在应用运行期间,根据某些条件(如用户权限、角色)动态添加或移除的路由。与传统的静态路由 (在 routes 配置中一次性定义所有路由)相比,动态路由有以下特点:

  • 按需加载:只有具备权限的用户才能访问对应的页面,避免未授权访问。
  • 灵活性:路由表可由后端返回,前端动态生成,实现权限与路由的完全解耦。
  • 可扩展性:支持多角色、多租户等复杂场景。

在 Vue Router 4.x 中,动态添加路由主要通过两个方法实现:

  • router.addRoute(route: RouteRecordRaw):添加一条新路由。
  • router.removeRoute(name: string | symbol):移除已添加的路由。

使用场景

  • 权限控制:不同角色(管理员、普通用户)看到不同的菜单,访问不同的页面。
  • 多级菜单动态生成:根据后端返回的菜单结构,递归生成嵌套路由。
  • 功能模块按需加载:例如,某些模块仅在特定条件下启用(如节日活动页面)。

模块二:基于权限的动态路由实现

概念解析

实现权限动态路由的核心思路:

  1. 静态路由:所有用户都能访问的基础路由(如登录页、404、注册页等)。
  2. 异步路由:需要权限才能访问的路由,通常在后端定义,前端通过接口获取。
  3. 路由守卫:在路由跳转前判断用户是否登录、是否有权限,并动态添加异步路由。
  4. 状态管理:存储用户信息和权限路由表,防止刷新后丢失。

实战步骤

我们将实现一个简单的权限控制示例:管理员能看到"用户管理"和"仪表盘",普通用户只能看到"仪表盘"。

1. 定义静态路由和异步路由

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

// 静态路由(所有用户可访问)
export const constantRoutes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { hidden: true } // 不在菜单中显示
  },
  {
    path: '/404',
    name: '404',
    component: () => import('@/views/404.vue'),
    meta: { hidden: true }
  },
  {
    path: '/',
    redirect: '/dashboard'
  }
]

// 异步路由(需要权限)
export const asyncRoutes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { title: '仪表盘', icon: 'dashboard', roles: ['admin', 'user'] } // 允许的角色
  },
  {
    path: '/user',
    name: 'User',
    component: () => import('@/views/User.vue'),
    meta: { title: '用户管理', icon: 'user', roles: ['admin'] }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes: constantRoutes // 初始只挂载静态路由
})

export default router

2. 使用 Pinia 存储用户状态和权限路由

javascript 复制代码
// stores/user.js
import { defineStore } from 'pinia'
import { constantRoutes, asyncRoutes } from '@/router'
import router from '@/router'

// 模拟后端返回的权限路由名称
const mockFetchUserRoutes = (role) => {
  return asyncRoutes.filter(route => route.meta.roles.includes(role))
}

export const useUserStore = defineStore('user', {
  state: () => ({
    token: localStorage.getItem('token') || '',
    role: '', // 当前用户角色
    routes: [] // 当前用户拥有的路由(静态+异步)
  }),
  actions: {
    // 登录
    async login(role) {
      // 模拟登录,存储 token 和 role
      this.token = 'mock-token'
      this.role = role
      localStorage.setItem('token', this.token)
      
      // 根据角色获取路由
      const dynamicRoutes = mockFetchUserRoutes(role)
      this.routes = [...constantRoutes, ...dynamicRoutes]
      
      // 动态添加路由
      dynamicRoutes.forEach(route => {
        router.addRoute(route)
      })
      // 添加 404 通配路由(必须最后添加)
      router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404', meta: { hidden: true } })
    },
    // 登出
    logout() {
      this.token = ''
      this.role = ''
      this.routes = []
      localStorage.removeItem('token')
      
      // 重置路由(移除所有动态添加的路由)
      const dynamicRoutes = mockFetchUserRoutes(this.role) // 此时 role 为空,获取空数组
      // 移除动态路由(需要遍历 remove)
      // 但更简单的方法是重新创建 router 实例,或者使用 resetRouter 函数
      resetRouter() // 自定义函数
      router.push('/login')
    }
  }
})

// 重置路由工具函数
function resetRouter() {
  // 获取所有动态路由的 name,并移除
  const dynamicRouteNames = asyncRoutes.map(route => route.name)
  dynamicRouteNames.forEach(name => {
    if (router.hasRoute(name)) {
      router.removeRoute(name)
    }
  })
  // 移除 404 通配路由(如果有)
  if (router.hasRoute('404')) {
    router.removeRoute('404')
  }
}

3. 路由守卫:判断权限并动态添加路由

javascript 复制代码
// router/permission.js
import router from './index'
import { useUserStore } from '@/stores/user'

// 白名单:不需要登录就能访问的路由
const whiteList = ['/login', '/404']

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  const hasToken = userStore.token
  
  if (hasToken) {
    if (to.path === '/login') {
      // 已登录,跳转到首页
      next('/')
    } else {
      // 判断是否已有角色信息(防止刷新后路由丢失)
      if (!userStore.role) {
        try {
          // 模拟从 token 中解析角色(实际应从后端获取用户信息)
          const role = 'admin' // 假设当前用户是 admin
          // 调用登录 action 动态添加路由
          await userStore.login(role)
          // 确保路由添加完成后再进入目标路由
          next({ ...to, replace: true })
        } catch (error) {
          // 获取用户信息失败,重置 token 并跳转登录
          userStore.logout()
          next(`/login?redirect=${to.path}`)
        }
      } else {
        // 已有角色,正常跳转
        next()
      }
    }
  } else {
    // 未登录,检查白名单
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
    }
  }
})

4. 生成动态菜单(侧边栏组件)

xml 复制代码
<!-- components/Sidebar.vue -->
<template>
  <ul>
    <li v-for="route in userRoutes" :key="route.path" v-if="!route.meta?.hidden">
      <router-link :to="route.path">{{ route.meta?.title }}</router-link>
    </li>
  </ul>
</template>

<script setup>
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// 只显示 meta.hidden 不为 true 的路由,且过滤掉重定向路由(如 '/')
const userRoutes = computed(() => {
  return userStore.routes.filter(route => !route.meta?.hidden && route.path !== '/')
})
</script>

5. 在 main.js 中引入路由守卫

javascript 复制代码
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './router/permission' // 引入守卫

const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

代码示例说明

  • 静态路由:包含登录页和404页,所有用户可访问。
  • 异步路由 :通过 meta.roles 标注允许的角色,模拟后端返回。
  • 路由守卫 :在每次跳转前检查 token 和角色,若角色为空则调用 login action 动态添加路由。
  • 菜单生成 :从 store 中获取 userRoutes 并渲染,自动过滤隐藏项。

注意事项

  1. 动态添加 404 路由:必须在所有动态路由添加完成后最后添加,否则会匹配到 404。
  2. 防止重复添加 :在 login action 中,应先移除之前添加的动态路由(如 resetRouter),或者判断 router.hasRoute 避免重复。
  3. 刷新路由丢失 :刷新页面会导致 store 中的 roleroutes 丢失,但 token 可能还存在。解决方案是在路由守卫中判断 !userStore.role 时重新获取用户信息并动态添加路由。
  4. 路由替换(replace: true) :动态添加路由后,需要使用 next({ ...to, replace: true }) 重走一遍导航,确保新路由生效。

模块三:常见坑点与解决方案

1. 路由重复添加

问题 :多次调用 router.addRoute 添加同名路由,会导致控制台警告,甚至路由混乱。

解决方案

  • 添加前使用 router.hasRoute(route.name) 检查是否存在。
  • 或在添加前统一移除所有动态路由(如上面的 resetRouter)。

javascript

scss 复制代码
// 安全添加
if (!router.hasRoute(route.name)) {
  router.addRoute(route)
}

2. 刷新后路由丢失

问题 :刷新页面后,store 中的 roleroutes 被重置,但 token 可能还在,此时用户访问非静态路由会报错。

解决方案 :在路由守卫中判断若 token 存在但角色为空,则调用接口获取用户信息并重新添加路由。如上面的 beforeEach 实现。

3. 动态添加的路由在菜单中不显示

原因 :菜单组件直接使用 router.options.routes 获取路由表,但 addRoute 添加的路由不会自动合并到 options.routes 中。

解决方案:将动态路由保存在 store 中,菜单组件基于 store 中的路由渲染,而非直接从 router 实例获取。

4. 404 路由匹配问题

问题:如果在动态路由之前添加了 404 路由,所有未匹配的路由都会跳到 404,导致动态路由无法访问。

解决方案 :确保 404 路由在所有动态路由之后 添加,并且只添加一次。可以在登录成功后添加,并配合 resetRouter 在登出时移除。

5. 嵌套路由的动态添加

问题addRoute 支持添加嵌套路由,但需要指定父路由的 name

示例

php 复制代码
router.addRoute('Parent', {
  path: 'child',
  name: 'Child',
  component: () => import('...')
})

注意父路由必须已存在。


扩展思考

更细粒度的权限控制

除了路由级别的权限,实际项目中还常需要按钮级别 的权限控制。可以通过在 meta 中添加 permissions 数组,或在 store 中存储权限标识,然后在组件中使用自定义指令或函数判断。

javascript 复制代码
// 自定义指令 v-permission
app.directive('permission', {
  mounted(el, binding) {
    const userStore = useUserStore()
    const required = binding.value
    if (!userStore.permissions.includes(required)) {
      el.parentNode?.removeChild(el)
    }
  }
})

动态路由与菜单联动

当后端返回的菜单结构可能包含多级时,需要递归生成路由和菜单。可以定义一个递归函数,将后端返回的 JSON 转换为 Vue Router 支持的 RouteRecordRaw 数组。

结合路由元信息(meta)进行更多控制

meta 中可以存放标题、图标、缓存标识等,配合 router.beforeEach 实现页面标题动态更新、页面缓存控制等功能。


总结

本文详细讲解了 Vue3 动态路由的核心概念、实现步骤以及常见问题的解决方案。通过实战代码,你学会了如何根据用户权限动态添加路由,如何处理刷新后路由丢失,以及如何避免重复添加路由。动态路由是构建大型后台管理系统的基础,掌握它将使你的前端工程更具灵活性和可维护性。

在实际项目中,你可能还需要结合后端接口、WebSocket 通知等实时更新权限,但本文提供的模式已经足够应对绝大多数场景。希望你能将这些知识应用到自己的项目中,构建出健壮、安全的前端应用。

相关推荐
许留山2 小时前
前端 PDF 导出:从文件流下载到自动分页
前端·react.js
蓝鲸有腿2 小时前
项目部署后->这样通知用户刷新
前端
少卿2 小时前
OpenClaw github 技能:让 GitHub 操作像聊天一样简单
前端
Ekehlaft2 小时前
同题画图大考,AiPy 章鱼适配性拉满,OpenClaw 龙虾全程 “哑火”
前端
掘金酱2 小时前
小册上新|玩🦐吗?ai 编程全栈指南了解一下?
前端·人工智能·ai编程
小小小小宇2 小时前
富文本编辑器知识体系(一)
前端
发现一只大呆瓜3 小时前
深度拆解 fetch-event-source库实现原理
前端·javascript·面试
2601_953465613 小时前
HLS.js 原生开发!m3u8live.cn打造最贴合项目的 M3U8 在线播放器
开发语言·前端·javascript·python·json·ecmascript·前端开发工具
前端Hardy3 小时前
为什么资深前端都在悄悄学 WebAssembly?
前端·javascript·面试