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

前言

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

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

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

permission.ts 角色权限

文件 srcstoremodulespermission.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); },  // srcapisysuser.ts enum Api {    GetPermCode = '/getPermCode',  } export function getPermCode() {   return defHttp.get<string[]>({ url: Api.GetPermCode }); } 

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

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

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

权限模式

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

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

前端权限模式

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

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

// srcrouterroutesmodulesdemopermission.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;  // srcrouterroutesindex.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将多级路由转换为二级路由,下图是未处理前路由表信息:

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

下图是格式化后的二级路由表信息:
Vben Admin 源码学习:状态管理-角色权限

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; }; 

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

// srcrouterroutesmodulesdemofeat.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

关注专栏

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

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

发表评论

评论已关闭。

相关文章