一、概述
项目地址:github
本文档详细梳理了基于RBAC(基于角色的访问控制)的动态路由实现方案,包括路由拦截机制和动态路由生成过程。该方案通过前端路由守卫与后端权限系统结合,实现了根据用户角色动态加载路由的功能,提高了系统的安全性和灵活性。
二、路由钩子拦截流程
路由守卫是Vue Router提供的钩子函数,用于在路由跳转过程中进行权限控制。在本项目中,主要通过beforeEach
钩子实现路由拦截。
2.1 路由拦截流程图
flowchart TD
A[路由跳转请求] --> B{是否有Token?}
B -->|是| C{是否访问登录页?}
B -->|否| D{是否在白名单中?}
C -->|是| E[重定向到首页]
C -->|否| F{路由是否已加载?}
D -->|是| G[直接放行]
D -->|否| H[重定向到登录页]
F -->|是| I{是否匹配到路由?}
F -->|否| J[获取用户菜单]
I -->|是| K[直接放行]
I -->|否| L[重定向到404页面]
J --> M{获取成功?}
M -->|是| N[生成动态路由]
M -->|否| O[清空Token并跳转登录页]
N --> P[重新导航到目标页面]
2.2 关键代码分析
路由守卫实现位于src/router/permission.ts
文件中,主要逻辑如下:
// 白名单路由(不需要登录就可以访问)
const whiteList = ['/login']
// 路由前置守卫
router.beforeEach(async (to, from, next) => {
// 获取权限Store
const permissionStore = usePermissionStore()
// 获取Token
const token = permissionStore.getToken()
// 判断是否有Token
if (token) {
// 已登录
if (to.path === '/login') {
// 如果已登录,访问登录页则重定向到首页
next({ path: '/' })
} else {
// 判断是否已获取用户信息
if (permissionStore.isRoutesLoaded) {
// 已获取用户信息,直接放行
next()
} else {
try {
// 获取用户菜单
const menuRes = await authApi.getUserMenus()
// 生成动态路由
permissionStore.generateDynamicRoutes(menuRes)
// 确保动态路由已添加完成
next({ ...to, replace: true })
} catch (error) {
// 获取用户信息失败,清空Token并跳转到登录页
permissionStore.resetPermission()
redirectToLogin(to, next)
}
}
}
} else {
// 未登录
if (whiteList.includes(to.path)) {
// 在白名单中,直接放行
next()
} else {
// 不在白名单中,重定向到登录页
redirectToLogin(to, next)
}
}
})
三、动态路由生成流程
动态路由生成是指根据后端返回的菜单数据,动态创建前端路由配置并添加到路由实例中的过程。
3.1 动态路由生成流程图
flowchart TD
A[获取后端菜单数据] --> B[调用generateDynamicRoutes方法]
B --> C[调用generateRoutes转换菜单数据]
C --> D[遍历菜单项]
D --> E{处理组件类型}
E -->|Layout组件| F[使用Layout组件]
E -->|普通组件| G{组件是否存在?}
G -->|是| H[使用对应组件]
G -->|否| I[使用404组件]
D --> J{是否有子菜单?}
J -->|是| K[递归处理子菜单]
J -->|否| L[完成当前菜单处理]
L --> M[构建路由配置]
K --> M
H --> M
I --> M
F --> M
M --> N[保存动态路由]
N --> O[使用router.addRoute添加路由]
O --> P[设置isRoutesLoaded为true]
3.2 关键代码分析
动态路由生成的核心代码位于src/stores/permission.ts
文件中:
// 使用 import.meta.glob 动态导入所有视图组件
const modules = import.meta.glob('../views/**/*.vue')
/**
* 将后端返回的菜单数据转换为路由配置
* @param menus 后端返回的菜单数据
*/
function generateRoutes(menus: ServerMenuItem[]): RouteRecordRaw[] {
const result: RouteRecordRaw[] = []
for (const menu of menus) {
const route: Partial<RouteRecordRaw> = {
path: menu.path,
name: menu.name,
meta: {
title: menu.meta.title,
icon: menu.meta.icon,
hidden: menu.meta.hidden,
keepAlive: menu.meta.keepAlive,
alwaysShow: menu.meta.alwaysShow,
},
}
// 如果有重定向,添加到路由配置中
if (menu.redirect) {
route.redirect = menu.redirect
}
// 处理组件
if (menu.component?.toString() === 'Layout') {
route.component = Layout
} else if (menu.component) {
// 检查组件是否存在
const modulePath = `../views/${menu.component}.vue`
if (modules[modulePath]) {
route.component = modules[modulePath]
} else {
console.warn(`Component not found: ${menu.component}`)
route.component = () => import('@/views/error/404.vue')
}
}
// 处理子路由
if (menu.children?.length) {
route.children = generateRoutes(menu.children)
}
result.push(route as RouteRecordRaw)
}
return result
}
// 在Store中的方法
function generateDynamicRoutes(menus: ServerMenuItem[]) {
// 生成路由配置
const routes = generateRoutes(menus)
dynamicRoutes.value = routes
// 添加路由
routes.forEach((route) => {
router.addRoute(route)
})
isRoutesLoaded.value = true
}
四、关键技术点分析
4.1 import.meta.glob 动态导入
使用Vite提供的import.meta.glob
功能实现组件的动态导入,避免了手动导入每个组件的繁琐过程:
// 使用 import.meta.glob 动态导入所有视图组件
const modules = import.meta.glob('../views/**/*.vue')
这行代码会扫描views
目录下所有的.vue
文件,并返回一个对象,键是文件路径,值是导入该文件的函数。
4.2 Layout组件处理
布局组件是动态路由中的特殊组件,通常作为父级路由的容器:
// 布局组件
export const Layout = () => import('@/layout/index.vue')
// 处理组件
if (menu.component?.toString() === 'Layout') {
route.component = Layout
}
4.3 容错处理
对于后端返回的不存在的组件路径,提供了容错处理,自动使用404页面代替:
if (modules[modulePath]) {
route.component = modules[modulePath]
} else {
console.warn(`Component not found: ${menu.component}`)
route.component = () => import('@/views/error/404.vue')
}
4.4 静态路由与动态路由结合
系统中的路由分为静态路由和动态路由两部分:
// 静态路由定义在 src/router/index.ts 中
export const constantRoutes: RouteRecordRaw[] = [
// 登录页、404页等基础路由
]
// 在Store中合并静态路由和动态路由
const routes = computed(() => [...constantRoutes, ...dynamicRoutes.value])
五、总结
本项目实现的动态路由方案具有以下特点:
- 基于RBAC模型:根据用户角色和权限动态加载路由,实现精细化的权限控制。
- 前后端分离:前端负责路由拦截和动态生成,后端负责提供权限数据,职责明确。
- 灵活性高:可以通过后端配置动态调整菜单结构,无需修改前端代码。
- 容错性好:对于不存在的组件提供了容错处理,避免系统崩溃。
- 性能优化:使用
import.meta.glob
实现组件的按需加载,提高了应用性能。
通过这种动态路由方案,系统可以根据不同用户的权限动态生成菜单和路由,既保证了系统的安全性,又提高了开发效率和用户体验。