前言
本文将對 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
,功能如下:
權限模式
架構提供了完善的前後端權限管理方案,內建了三種權限處理方式:
-
通過使用者角色來過濾菜單(前端方式控制),菜單和路由分開配置。ROLE
-
通過使用者角色來過濾菜單(前端方式控制),菜單由路由配置自動生成。ROUTE_MAPPING
-
通過背景來動态生成路由表(後端方式控制)。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
将多級路由轉換為二級路由,下圖是未處理前路由表資訊:
下圖是格式化後的二級路由表資訊:
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;