前端动态路由实现方案(Vue3 + VueRouter4)|后台权限路由实战

标签:前端、Vue3、VueRouter4、动态路由、RBAC 权限、addRoute、Pinia 适合面试复习、后台管理系统开发,区分【参数动态路由】和【权限动态路由】两个容易混淆概念

前言

很多人刚接触路由的时候,会把两种东西混在一起:

  1. 动态路径参数 :/detail/:id,同一个组件根据参数渲染不同数据(简单参数匹配)
  2. 权限型动态路由(本文重点) :登录之后,根据当前用户角色,从后端接口拿到菜单路由列表,运行时动态添加路由,不同用户看到不同页面、不同侧边菜单,这也是后台系统最常用的 RBAC 权限方案。

传统写死在 router/index.ts 的静态路由,所有用户都能访问,无法满足权限隔离。Vue Router4 提供 router.addRoute()、router.removeRoute() 来完成运行时注册路由,是这套方案的核心 APIVue Router。

技术栈:Vue3 + Vite + VueRouter4 + Pinia + ElementPlus 需求:登录成功→请求后端菜单接口→把后端返回数据转成标准路由结构→动态注册路由→渲染侧边菜单;刷新页面路由不丢失,404、白屏问题处理。

一、两种动态路由概念区分

表格

类型 示例 核心 API 用途
动态参数路由 /article/:articleId useRoute().params 详情页,复用组件,读取路径参数
权限动态路由 后端返回菜单列表,运行时添加 router.addRoute() 权限控制、不同角色菜单隔离

面试高频坑:面试官问动态路由,优先回答第二种权限动态路由,简单提一句参数路由做区分。

二、整体实现流程

整体链路:

  1. 初始化路由:只放静态白名单路由(登录页、404、首页),不写任何业务权限路由
  2. 登录成功,拿到 token,调用接口获取当前用户的菜单路由数据
  3. 后端返回树形菜单,前端递归转换为 RouteRecordRaw 标准路由结构,处理组件懒加载
  4. 使用 router.addRoute() 批量添加路由到路由实例
  5. 存储路由到 Pinia,渲染侧边菜单
  6. 路由前置守卫 beforeEach 拦截:页面刷新时重新走一遍拉取路由逻辑,防止刷新丢失动态路由
  7. 处理收尾:添加路由后 next({ ...to, replace:true }) 触发一次重定向,解决路由添加完成后匹配不到页面的 404 白屏问题
  8. 登出时调用 router.removeRoute()、重置路由,清除权限信息

三、代码实战

1. 基础路由文件 router/index.ts

只保留静态路由(白名单),不要写权限业务路由

复制代码
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { usePermissionStore } from '@/stores/permission'

// 静态路由:所有用户都能访问
export const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/login/index.vue'),
    meta: { title: '登录' }
  },
  {
    path: '/404',
    name: 'NotFound',
    component: () => import('@/views/error/404.vue'),
    meta: { title: '页面不存在' }
  }
]

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

// 重置路由方法:登出时清空动态路由
export function resetRouter() {
  const permissionStore = usePermissionStore()
  permissionStore.dynamicRoutes.forEach(item => {
    router.removeRoute(item.name as string)
  })
  permissionStore.$reset()
}

export default router

2. Pinia 权限仓库 stores/permission.ts

负责存储后端菜单、转换路由结构

复制代码
import { defineStore } from 'pinia'
import { RouteRecordRaw } from 'vue-router'
import router from '@/router'
import { getMenuListApi } from '@/api/menu'

// vite下动态导入组件工具函数
const loadView = (viewPath: string) => {
  return () => import(`@/views/${viewPath}.vue`)
}

// 递归转换后端菜单 → 标准路由
export function formatMenuToRoute(menuList: any[]): RouteRecordRaw[] {
  return menuList.map(menu => {
    const route: RouteRecordRaw = {
      path: menu.path,
      name: menu.name,
      component: loadView(menu.component),
      meta: {
        title: menu.title,
        icon: menu.icon
      }
    }
    if (menu.children && menu.children.length > 0) {
      route.children = formatMenuToRoute(menu.children)
    }
    return route
  })
}

export const usePermissionStore = defineStore('permission', {
  state: () => ({
    dynamicRoutes: [] as RouteRecordRaw[],
    menuList: [] as any[]
  }),
  actions: {
    // 获取后端菜单,生成动态路由
    async generateRoutes() {
      const res = await getMenuListApi()
      this.menuList = res.data
      // 格式化
      this.dynamicRoutes = formatMenuToRoute(res.data)
      // 循环添加路由
      this.dynamicRoutes.forEach(route => {
        router.addRoute(route)
      })
      return this.dynamicRoutes
    }
  }
})

⚠️ Vite 注意:动态导入字符串不能拼接变量外层,固定模板 import(@/views/${xxx}.vue) 是 Vite 识别规则,不能写成变量直接传字符串,否则打包失效。

3. 路由守卫 permission.ts(核心,处理刷新逻辑)

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

const whiteList = ['/login', '/404']

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  const permissionStore = usePermissionStore()

  // 有token
  if (userStore.token) {
    if (to.path === '/login') {
      next('/')
    } else {
      // 判断是否已经生成过动态路由
      if (permissionStore.dynamicRoutes.length === 0) {
        try {
          await permissionStore.generateRoutes()
          // 关键:添加完路由,重新进入一次目标地址,让路由表更新匹配
          next({ ...to, replace: true })
        } catch (err) {
          // token失效,清空信息跳登录
          userStore.logout()
          next('/login')
        }
      } else {
        next()
      }
    }
  } else {
    // 无token,白名单放行,否则跳登录
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next('/login')
    }
  }
})

4. 侧边菜单渲染

直接读取 permissionStore.menuList,递归渲染菜单组件即可,菜单和路由同源,自动和权限保持一致。

四、常见大坑 & 解决方案(面试高频)

  1. 页面刷新后白屏 / 404 No match found 原因:addRoute 是运行时加到内存里的,刷新路由实例会重置,动态路由丢失。 ✅ 方案:在 beforeEach 判断,没有动态路由时重新请求接口生成路由,并且 next({...to, replace:true}) 触发新一轮导航匹配
  2. Vite 动态 import 报错,找不到组件 原因:webpack 和 vite 的动态导入规则不一样,不能写完全变量 import(path) ✅ 方案:固定前缀模板字符串 import(@/views/${path}.vue)
  3. 登出之后再次登录,路由重复添加 原因:addRoute 重复注册同名路由 ✅ 方案:登出调用 resetRouter,循环 removeRoute 删除,清空 pinia 状态
  4. 404 路由放前面,动态路由添加后永远 404 ✅ 方案:404 路由不要写在 constantRoutes 里,等所有动态路由 addRoute 完成之后,最后再 addRoute 添加 404 通配路由
  5. 死循环问题 next({...to, replace:true}) 会再次触发 beforeEach,必须加判断标记,避免无限调用 generateRoutes。

五、两种方案对比(选型参考)

方案 A:后端返回菜单,前端动态 addRoute(本文方案,推荐)

  • ✅ 优点:权限控制在后端,菜单维护在数据库,新增页面不用改前端代码,多租户、复杂 RBAC 友好
  • ❌ 缺点:要处理刷新、路由重置、组件动态导入各种坑

方案 B:前端写全所有路由,根据角色过滤(若依早期方案)

  • ✅ 优点:简单稳定,没有动态导入问题
  • ❌ 缺点:路由硬编码在前端,新增菜单必须发版,权限粒度弱,不适合大型系统

企业中后台主流选择:方案 A。

六、React 补充(简单对比)

React Router v6 没有 addRoute 这种运行时添加路由的 API,主流做法:

  • 拿到权限菜单数组,存在 state,用 map 条件渲染 <Route>;
  • 封装权限守卫组件 RequireAuth 做访问拦截;
  • 思路和 Vue 不一样,不是运行时注册路由表,是条件渲染路由节点React Rout...。

七、面试一句话总结背诵版

动态路由(权限版): 首先在路由初始化只配置登录等静态白名单路由;登录成功后,在路由前置守卫中请求后端菜单接口,将返回的树形菜单递归转换成标准路由结构,使用 router.addRoute() 动态注册路由,存入状态库渲染侧边菜单;页面刷新时重新拉取路由,添加完成后 next({...to, replace:true}) 重新触发导航匹配;登出时调用 removeRoute 重置路由,解决路由重复、刷新 404 白屏问题。

结尾

动态路由是后台权限系统的核心能力,难点不在于 addRoute API 本身,而在于时序控制:token 校验、接口异步、路由注册时机、刷新重置、登出清理这一套闭环。很多新手只写了登录时添加路由,忽略页面刷新场景,上线直接翻车。


相关推荐
一切皆是因缘际会1 小时前
大模型应用
前端·人工智能
SCandL1521 小时前
【无标题】
前端
你挚爱的强哥2 小时前
如何设置 Chrome 打开默认页面不要是360的主页https://hao.360.com/?src=lm&ls=n6205869099
前端·chrome·360首页劫持
总要学点什么2 小时前
第75篇:右键菜单控件——把一张会做事的菜单钉在鼠标落下的那点
前端·信息可视化·echarts·cesium·空间分析
计算机魔术师2 小时前
国办说AI要"缓"着上,但你公司可能已经全上完了
前端
IT_陈寒3 小时前
SpringBoot性能优化完全指南
前端·人工智能·后端
溪语流沙3 小时前
【每天一个CSS | Day33】聚焦即弹出的命令面板
前端·css·html5·组件·每日一练
燕落南枝3 小时前
CSS 用户行为伪类::hover 与 :focus 详解
前端·css
风骏时光牛马3 小时前
业务基础信息管理库
前端