Vue3基于RBAC的动态路由实现方案

308 阅读4分钟

一、概述

项目地址: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])

五、总结

本项目实现的动态路由方案具有以下特点:

  1. 基于RBAC模型:根据用户角色和权限动态加载路由,实现精细化的权限控制。
  2. 前后端分离:前端负责路由拦截和动态生成,后端负责提供权限数据,职责明确。
  3. 灵活性高:可以通过后端配置动态调整菜单结构,无需修改前端代码。
  4. 容错性好:对于不存在的组件提供了容错处理,避免系统崩溃。
  5. 性能优化:使用import.meta.glob实现组件的按需加载,提高了应用性能。

通过这种动态路由方案,系统可以根据不同用户的权限动态生成菜单和路由,既保证了系统的安全性,又提高了开发效率和用户体验。