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

關注專欄

如果本文對您有所幫助請關注➕、 點贊👍、 收藏⭐!您的認可就是對我的最大支援!

此文章已收錄到專欄中 👇,可以直接關注。