前言

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

关注专栏

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

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

作者:Anduril

简书传送门:简书/Anduril

掘金传送门:掘金/Andurils

个人小站:anduril.cn

技术只有不沉浸其中才能对其更加公正看待! 没了狂热和浮躁,去体会下开源下的语法魅力!这里没有技术宗教的狂热和鄙夷,没有疯狂的个人崇拜,只是一个技术人探索之路上对于美丽与丑陋的随笔和感悟!

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。