• Vben Admin 源码学习:状态管理-角色权限


    前言

    本文将对 Vue-Vben-Admin 角色权限的状态管理进行源码解读,耐心读完,相信您一定会有所收获!

    更多系列文章详见专栏 👉 📚 Vben Admin 项目分析&实践 。

    本文涉及到角色权限之外的较多内容(路由相关)会一笔带过,具体功能实现将在后面专题中详细讨论。为了更好的理解本文内容,请先阅读官方的文档说明 # 权限

    permission.ts 角色权限

    文件 src\store\modules\permission.ts 声明导出一个store实例 usePermissionStore 、一个方法 usePermissionStoreWithOut()用于没有使用 setup 组件时使用。

    // 角色权限信息存储
    export const usePermissionStore = defineStore({
      id: 'app-permission',
      state: { /*...*/ },
      getters: { /*...*/ }
      actions:{ /*...*/ }   
    });
    
    export function usePermissionStoreWithOut() {
      return usePermissionStoreWithOut(store);
    }
    

    State/Getter

    状态对象定义了权限代码列表、是否动态添加路由、菜单最后更新时间、后端角色权限菜单列表以及前端角色权限菜单列表。同时提供了对应getter用于获取状态值。

    // 权限状态
    interface PermissionState { 
      permCodeList: string[] | number[]; // 权限代码列表 
      isDynamicAddedRoute: boolean; // 是否动态添加路由 
      lastBuildMenuTime: number; // 菜单最后更新时间 
      backMenuList: Menu[]; // 后端角色权限菜单列表
      frontMenuList: Menu[]; // 前端角色权限菜单列表
    }
    
    // 状态定义及初始化
    state: (): PermissionState => ({
      permCodeList: [], 
      isDynamicAddedRoute: false, 
      lastBuildMenuTime: 0, 
      backMenuList: [], 
      frontMenuList: [],
    }),
    getters: { 
      getPermCodeList(): string[] | number[] {
        return this.permCodeList; // 获取权限代码列表
      },
      getBackMenuList(): Menu[] {
        return this.backMenuList; // 获取后端角色权限菜单列表
      },
      getFrontMenuList(): Menu[] {
        return this.frontMenuList; // 获取前端角色权限菜单列表
      },
      getLastBuildMenuTime(): number {
        return this.lastBuildMenuTime; // 获取菜单最后更新时间
      },
      getIsDynamicAddedRoute(): boolean {
        return this.isDynamicAddedRoute; // 获取是否动态添加路由
      },
    }, 
    

    Actions

    以下方法用于更新状态属性。

    // 更新属性 permCodeList
    setPermCodeList(codeList: string[]) {
      this.permCodeList = codeList;
    },
    // 更新属性 backMenuList
    setBackMenuList(list: Menu[]) {
      this.backMenuList = list;
      list?.length > 0 && this.setLastBuildMenuTime(); // 记录菜单最后更新时间
    },
    // 更新属性 frontMenuList
    setFrontMenuList(list: Menu[]) {
      this.frontMenuList = list;
    },
    // 更新属性 lastBuildMenuTime
    setLastBuildMenuTime() {
      this.lastBuildMenuTime = new Date().getTime(); // 一个代表时间毫秒数的数值
    },
    // 更新属性 isDynamicAddedRoute
    setDynamicAddedRoute(added: boolean) {
      this.isDynamicAddedRoute = added;
    },
    // 重置状态属性
    resetState(): void {
      this.isDynamicAddedRoute = false;
      this.permCodeList = [];
      this.backMenuList = [];
      this.lastBuildMenuTime = 0;
    },
    

    方法 changePermissionCode 模拟从后台获得用户权限码,常用于后端权限模式下获取用户权限码。项目中使用了本地 Mock服务模拟。

    async changePermissionCode() {
      const codeList = await getPermCode();
      this.setPermCodeList(codeList);
    },
    
    // src\api\sys\user.ts
    enum Api { 
      GetPermCode = '/getPermCode', 
    }
    export function getPermCode() {
      return defHttp.get({ url: Api.GetPermCode });
    }
    

    使用到的 mock 接口和模拟数据。

    // mock\sys\user.ts
    {
      url: '/basic-api/getPermCode',
      timeout: 200,
      method: 'get',
      response: (request: requestParams) => {
        // ...  
        const checkUser = createFakeUserList().find((item) => item.token === token); 
        const codeList = fakeCodeList[checkUser.userId];
        // ...
        return resultSuccess(codeList);
      },
    },
    
    const fakeCodeList: any = {
      '1': ['1000', '3000', '5000'], 
      '2': ['2000', '4000', '6000'],
    };
    

    动态路由&权限过滤

    方法buildRoutesAction用于动态路由及用户权限过滤,代码逻辑结构如下:

    async buildRoutesAction(): Promise<AppRouteRecordRaw[]> {
      const { t } = useI18n(); // 国际化
      const userStore = useUserStore(); // 用户信息存储
      const appStore = useAppStoreWithOut(); // 项目配置信息存储
    
      let routes: AppRouteRecordRaw[] = [];
      // 用户角色列表
      const roleList = toRaw(userStore.getRoleList) || [];
      // 获取权限模式
      const { permissionMode = projectSetting.permissionMode } = appStore.getProjectConfig; 
      
      // 基于角色过滤方法
      const routeFilter = (route: AppRouteRecordRaw) => { /*...*/ };
      // 基于 ignoreRoute 属性过滤
      const routeRemoveIgnoreFilter = (route: AppRouteRecordRaw) => { /*...*/ }; 
      
      
      // 不同权限模式处理逻辑
      switch (permissionMode) {
        // 前端方式控制(菜单和路由分开配置)
        case PermissionModeEnum.ROLE: /*...*/ 
        // 前端方式控制(菜单由路由配置自动生成)
        case PermissionModeEnum.ROUTE_MAPPING: /*...*/ 
        // 后台方式控制
        case PermissionModeEnum.BACK: /*...*/ 
      }
    
      routes.push(ERROR_LOG_ROUTE); // 添加`错误日志列表`页面路由
      
      // 根据设置的首页path,修正routes中的affix标记(固定首页)
      const patchHomeAffix = (routes: AppRouteRecordRaw[]) => { /*...*/ };
      patchHomeAffix(routes);
      
      return routes; // 返回路由列表
    },
    

    页面“错误日志列表”路由地址/error-log/list,功能如下:

    image.png

    权限模式

    框架提供了完善的前后端权限管理方案,集成了三种权限处理方式:

    1. ROLE 通过用户角色来过滤菜单(前端方式控制),菜单和路由分开配置。
    2. ROUTE_MAPPING通过用户角色来过滤菜单(前端方式控制),菜单由路由配置自动生成。
    3. BACK 通过后台来动态生成路由表(后端方式控制)。
    // src\settings\projectSetting.ts
    // 项目配置 
    const setting: ProjectConfig = { 
      permissionMode: PermissionModeEnum.ROUTE_MAPPING, // 权限模式  默认前端模式
      permissionCacheType: CacheTypeEnum.LOCAL, // 权限缓存存放位置 默认存放于localStorage
      // ...
    }
    
    // src\enums\appEnum.ts
    // 权限模式枚举
    export enum PermissionModeEnum { 
      ROLE = 'ROLE', // 前端模式(菜单路由分开)
      ROUTE_MAPPING = 'ROUTE_MAPPING', // 前端模式(菜单由路由生成) 
      BACK = 'BACK', // 后端模式  
    }
    

    前端权限模式

    前端权限模式提供了 ROLEROUTE_MAPPING两种处理逻辑,接下来将一一分析。

    在前端会固定写死路由的权限,指定路由有哪些权限可以查看。系统定义路由记录时指定可以访问的角色RoleEnum.SUPER

    // src\router\routes\modules\demo\permission.ts
    {
      path: 'auth-pageA',
      name: 'FrontAuthPageA',
      component: () => import('/@/views/demo/permission/front/AuthPageA.vue'),
      meta: {
        title: t('routes.demo.permission.frontTestA'),
        roles: [RoleEnum.SUPER],
      },
    },
    

    系统使用meta属性在路由记录上附加自定义数据,它可以在路由地址和导航守卫上都被访问到。本方法中使用到的配置属性如下:

    export interface RouteMeta {  
      // 可以访问的角色,只在权限模式为Role的时候有效
      roles?: RoleEnum[]; 
      // 是否固定标签
      affix?: boolean; 
      // 菜单排序,只对第一级有效
      orderNo?: number;
      // 忽略路由。用于在ROUTE_MAPPING以及BACK权限模式下,生成对应的菜单而忽略路由。
      ignoreRoute?: boolean; 
      // ...
    } 
    

    ROLE

    初始化通用的路由表asyncRoutes,获取用户角色后,通过角色去遍历路由表,获取该角色可以访问的路由表,然后对其格式化处理,将多级路由转换为二级路由,最终返回路由表。

    // 前端方式控制(菜单和路由分开配置)
    import { asyncRoutes } from '/@/router/routes';
    
    // ...
    
    case PermissionModeEnum.ROLE:
      // 根据角色过滤路由
      routes = filter(asyncRoutes, routeFilter);
      routes = routes.filter(routeFilter);
      // 将多级路由转换为二级路由
      routes = flatMultiLevelRoutes(routes);
      break;
    
    // src\router\routes\index.ts
    export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList];
    

    在路由钩子内动态判断,调用方法返回生成的路由表,再通过 router.addRoutes 添加到路由实例,实现权限的过滤。

    // src/router/guard/permissionGuard.ts
    const routes = await permissionStore.buildRoutesAction(); 
    routes.forEach((route) => {
      router.addRoute(route as unknown as RouteRecordRaw);
    }); 
    // ....
    
    routeFilter

    过滤方法routeFilter通过角色去遍历路由表,获取该角色可以访问的路由表。

    const userStore = useUserStore(); // 用户信息存储  
    const roleList = toRaw(userStore.getRoleList) || []; // 用户角色列表
    
    const routeFilter = (route: AppRouteRecordRaw) => {
      const { meta } = route;
      const { roles } = meta || {};
      if (!roles) return true;
      return roleList.some((role) => roles.includes(role));
    };
    
    flatMultiLevelRoutes

    方法flatMultiLevelRoutes将多级路由转换为二级路由,下图是未处理前路由表信息:

    image.png

    下图是格式化后的二级路由表信息:
    image.png

    ROUTE_MAPPING

    ROUTE_MAPPINGROLE逻辑一样,不同之处会根据路由自动生成菜单。

    // 前端方式控制(菜单由路由配置自动生成)
    case PermissionModeEnum.ROUTE_MAPPING:
      // 根据角色过滤路由
      routes = filter(asyncRoutes, routeFilter);
      routes = routes.filter(routeFilter);
      // 通过转换路由生成菜单
      const menuList = transformRouteToMenu(routes, true);
      // 移除属性 meta.ignoreRoute 路由
      routes = filter(routes, routeRemoveIgnoreFilter);
      routes = routes.filter(routeRemoveIgnoreFilter);
      menuList.sort((a, b) => {
        return (a.meta?.orderNo || 0) - (b.meta?.orderNo || 0);
      });
    
      // 通过转换路由生成菜单
      this.setFrontMenuList(menuList);
      // 将多级路由转换为二级路由
      routes = flatMultiLevelRoutes(routes);
      break;
    

    调用方法 transformRouteToMenu 将路由转换成菜单,调用过滤方法routeRemoveIgnoreFilter忽略设置ignoreRoute属性的路由菜单。

    const routeRemoveIgnoreFilter = (route: AppRouteRecordRaw) => {
      const { meta } = route;
      const { ignoreRoute } = meta || {};
      return !ignoreRoute;
    };
    

    系统示例,路由下不同的路径参数生成一个菜单。

    // src\router\routes\modules\demo\feat.ts
    {
      path: 'testTab/:id',
      name: 'TestTab',
      component: () => import('/@/views/demo/feat/tab-params/index.vue'),
      meta: { 
        hidePathForChildren: true,
      },
      children: [
        {
          path: 'testTab/id1',
          name: 'TestTab1',
          component: () => import('/@/views/demo/feat/tab-params/index.vue'),
          meta: { 
            ignoreRoute: true,
          },
        },
        {
          path: 'testTab/id2',
          name: 'TestTab2',
          component: () => import('/@/views/demo/feat/tab-params/index.vue'),
          meta: { 
            ignoreRoute: true,
          },
        },
      ],
    },
    

    BACK 后端权限模式

    ROUTE_MAPPING逻辑处理相似,只不过路由表数据来源是调用接口从后台获取。

    // 后台方式控制
    case PermissionModeEnum.BACK:  
      let routeList: AppRouteRecordRaw[] = []; // 获取后台返回的菜单配置
      this.changePermissionCode();  // 模拟从后台获取权限码 
      routeList = (await getMenuList()) as AppRouteRecordRaw[]; // 模拟从后台获取菜单信息
      // 基于路由动态地引入相关组件
      routeList = transformObjToRoute(routeList); 
      // 通过路由列表转换成菜单
      const backMenuList = transformRouteToMenu(routeList);
      // 设置菜单列表
      this.setBackMenuList(backMenuList);
    
      // 移除属性 meta.ignoreRoute 路由
      routeList = filter(routeList, routeRemoveIgnoreFilter);
      routeList = routeList.filter(routeRemoveIgnoreFilter);
    
      // 将多级路由转换为二级路由
      routeList = flatMultiLevelRoutes(routeList);
      routes = [PAGE_NOT_FOUND_ROUTE, ...routeList];
      break;
    

    📚参考&关联阅读

    "routelocationnormalized",vue-router
    "Meta 配置说明",vvbin.cn
    "Date/getTime",MDN
    "toraw",vuejs

    关注专栏

    如果本文对您有所帮助请关注➕、 点赞👍、 收藏⭐!您的认可就是对我的最大支持!

    此文章已收录到专栏中 👇,可以直接关注。

  • 相关阅读:
    蓝桥杯STM32G431RBT6实现按键的单击、双击、长按的识别
    AIRIOT亮相IOTE2023深圳物联网展,产品创新力再获“IOTE金奖”
    传奇开服教程——legend/blue引擎替换和登陆器生成教程
    【前端】响应式布局笔记——rem
    C语言 - 通讯录详解
    【四】将vue部署到k8s中
    【GitLab私有仓库】在Linux上用Gitlab搭建自己的私有库并配置cpolar内网穿透
    用MES系统预警监控
    27、Java高级特性——集合、ArrayList集合、LinkedList集合、List接口、ArrayList和LinkedList的区别
    VUE3 之 状态动画 - 这个系列的教程通俗易懂,适合新手
  • 原文地址:https://www.cnblogs.com/anduril/p/16664946.html