乐趣区

关于前端: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<string[]>({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,性能如下:

权限模式

框架提供了欠缺的前后端权限治理计划,集成了三种权限解决形式:

  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 将多级路由转换为二级路由,下图是未解决前路由表信息:

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

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

关注专栏

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

此文章已收录到专栏中 👇,能够间接关注。

退出移动版