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,功能如下:

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

关注专栏

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

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