Vben|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({ 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 {
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
关注专栏 如果本文对您有所帮助请关注?、 点赞、 收藏?!您的认可就是对我的最大支持!
【Vben|Vben Admin 源码学习:状态管理-角色权限】此文章已收录到专栏中 ,可以直接关注。
推荐阅读
- 高级前端进阶必修(自主打造高扩展的业务组件库含源码PPT)
- 源码(chan|源码(chan,map,GMP,mutex,context)
- 小程序|微信小程序游戏开发│石头剪刀布游戏(附源码)
- JUC锁:核心类AQS源码详解
- 阿里架构师从应用、原理、集群、拓展、源码等方面深入解析Redis
- Spring5源码5-Bean生命周期后置处理器
- inquirer命令行交互原理((三)inquirer源码实现流程)
- k8s驱逐篇(3)-kubelet节点压力驱逐-源码分析篇
- 【小小的项目|【基于Java和Socket等技术的聊天室系统的设计与实现-----这或许就是QQ的原型之一吧!(效果+源码+论文+视频介绍等获取~!)】
- SSM框架解析|【SSM框架】Mybatis详解10(源码自取)之入参、返回值map