天天看點

Vben Admin 源碼學習:狀态管理-角色權限

前言

本文将對 ​​Vue-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​

​,功能如下:

Vben Admin 源碼學習:狀态管理-角色權限

權限模式

架構提供了完善的前後端權限管理方案,內建了三種權限處理方式:

  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', // 後端模式  
}      

前端權限模式

前端權限模式提供了 ​

​ROLE​

​​ 和 ​

​ROUTE_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​

​将多級路由轉換為二級路由,下圖是未處理前路由表資訊:

Vben Admin 源碼學習:狀态管理-角色權限

下圖是格式化後的二級路由表資訊:

Vben Admin 源碼學習:狀态管理-角色權限

ROUTE_MAPPING

​ROUTE_MAPPING​

​​跟​

​ROLE​

​邏輯一樣,不同之處會根據路由自動生成菜單。

// 前端方式控制(菜單由路由配置自動生成)
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 後端權限模式

// 背景方式控制
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;