前言

本文将对 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

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

// 更新属性 permCodeListsetPermCodeList(codeList: string[]) {  this.permCodeList = codeList;},// 更新属性 backMenuListsetBackMenuList(list: Menu[]) {  this.backMenuList = list;  list?.length > 0 && this.setLastBuildMenuTime(); // 记录菜单最初更新工夫},// 更新属性 frontMenuListsetFrontMenuList(list: Menu[]) {  this.frontMenuList = list;},// 更新属性 lastBuildMenuTimesetLastBuildMenuTime() {  this.lastBuildMenuTime = new Date().getTime(); // 一个代表工夫毫秒数的数值},// 更新属性 isDynamicAddedRoutesetDynamicAddedRoute(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.tsenum 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.tsexport const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList];

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

// src/router/guard/permissionGuard.tsconst 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

关注专栏

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

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