前言
本文將對 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,功能如下:
權限模式
框架提供了完善的前后端權限管理方案,集成了三種權限處理方式:
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 后端權限模式
跟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
技術只有不沉浸其中才能對其更加公正看待! 沒了狂熱和浮躁,去體會下開源下的語法魅力!這里沒有技術宗教的狂熱和鄙夷,沒有瘋狂的個人崇拜,只是一個技術人探索之路上對于美麗與丑陋的隨筆和感悟!
本文著作權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/505535.html
標籤:其他
上一篇:promise
