标签:前端、Vue3、VueRouter4、动态路由、RBAC 权限、addRoute、Pinia 适合面试复习、后台管理系统开发,区分【参数动态路由】和【权限动态路由】两个容易混淆概念
前言
很多人刚接触路由的时候,会把两种东西混在一起:
- 动态路径参数 :
/detail/:id,同一个组件根据参数渲染不同数据(简单参数匹配) - 权限型动态路由(本文重点) :登录之后,根据当前用户角色,从后端接口拿到菜单路由列表,运行时动态添加路由,不同用户看到不同页面、不同侧边菜单,这也是后台系统最常用的 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() |
权限控制、不同角色菜单隔离 |
面试高频坑:面试官问动态路由,优先回答第二种权限动态路由,简单提一句参数路由做区分。
二、整体实现流程
整体链路:
- 初始化路由:只放静态白名单路由(登录页、404、首页),不写任何业务权限路由
- 登录成功,拿到 token,调用接口获取当前用户的菜单路由数据
- 后端返回树形菜单,前端递归转换为
RouteRecordRaw标准路由结构,处理组件懒加载 - 使用
router.addRoute()批量添加路由到路由实例 - 存储路由到 Pinia,渲染侧边菜单
- 路由前置守卫
beforeEach拦截:页面刷新时重新走一遍拉取路由逻辑,防止刷新丢失动态路由 - 处理收尾:添加路由后
next({ ...to, replace:true })触发一次重定向,解决路由添加完成后匹配不到页面的 404 白屏问题 - 登出时调用
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,递归渲染菜单组件即可,菜单和路由同源,自动和权限保持一致。
四、常见大坑 & 解决方案(面试高频)
- 页面刷新后白屏 / 404 No match found 原因:
addRoute是运行时加到内存里的,刷新路由实例会重置,动态路由丢失。 ✅ 方案:在beforeEach判断,没有动态路由时重新请求接口生成路由,并且next({...to, replace:true})触发新一轮导航匹配 - Vite 动态 import 报错,找不到组件 原因:webpack 和 vite 的动态导入规则不一样,不能写完全变量
import(path)✅ 方案:固定前缀模板字符串import(@/views/${path}.vue) - 登出之后再次登录,路由重复添加 原因:addRoute 重复注册同名路由 ✅ 方案:登出调用
resetRouter,循环removeRoute删除,清空 pinia 状态 - 404 路由放前面,动态路由添加后永远 404 ✅ 方案:404 路由不要写在 constantRoutes 里,等所有动态路由 addRoute 完成之后,最后再 addRoute 添加 404 通配路由
- 死循环问题
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 校验、接口异步、路由注册时机、刷新重置、登出清理这一套闭环。很多新手只写了登录时添加路由,忽略页面刷新场景,上线直接翻车。