Vben Admin 源碼學習:狀態管理-角色許可權
- 2022 年 9 月 7 日
- 筆記
- 0x02.FrontEnd, vben-admin, VUE, 前端, 源碼分析
前言
本文將對 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
關注專欄
如果本文對您有所幫助請關注➕、 點贊👍、 收藏⭐!您的認可就是對我的最大支援!
此文章已收錄到專欄中 👇,可以直接關注。