我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!
前言:
我们今天讲解的设计是框架层
对于权限管理
的包装。
提供给应用层使用。
我之前写过一篇你真的了解项目中的权限控制吗 一文中,也提到过权限相关的,内容主要是针对业务应用的解决方案,牵扯到权限分类,以及开发场景的应用。
可以作为本次的辅助思考。
设计思路:
和上述几个设计大同小异
系统中,对于菜单的数据,在应用层一般作为路由权限控制以及菜单的显示。
但是在实际开发中,肯定会遇到这样的场景:
后端服务器部署没到位,前后端本地联调,没有菜单显示,怎么办
一般菜单数据权限维护,都是在服务器进行统一维护,当开发初期没有用户相关接口的支持,可能会比较麻烦,
我们在应用层加载的时候,创建了静态文件夹,router.dev.js
开发人员可以临时在文件内写菜单和路由,配置好后,登录进入后,会将静态数据
与服务端数据
进行合并操作
/**
* 功能模块配置说明---此文件只针对开发环境使用---【不要提交】
* pageId:页面ID
* pageName: 页面名称
* alias:页面别名(pageType==privilege生效 按钮---权限别名)
* parentId:父页面ID
* parentIds:父页面ID集合(-1,001,001001)
* pageType:
* entry 入口页面(1、2、3级菜单显示)
* cover 非入口页面(不显示在菜单的隐藏路由页面--如详情页面)
* portal 门户页面
* privilege 按钮
* path: 路由地址
* component: 模块文件地址
* sortNo: 排序--Number
* systemId: 系统ID
*/
// 静态路由---本地开发测试使用
export default [{
pageId: '002221',
pageName: '考试1',
alias: '课程管理',
parentId: '-1',
parentIds: '-1',
dependencyIds: '-1',
pageType: 'entry',
icon: 'icon-picture1',
path: '/examination',
component: 'views/examination',
systemId: "100000001",
sortNo: '1',
}]
业务应用:
权限的数据以及注册到框架层权限管理器,主要是在业务组件 InitialRequest 中
import { BaseStore } from '@basic-library'
import { cache } from '@basic-utils'
import { queryPageList, queryAccountDetail } from './service'
import staticRouter from "@/router/router.dev";
async function initialization({accountId }) {
const [userPrivileges = { returnObj: [] }, userInfo = { returnObj: {} }] = await Promise.all([
queryPageList(),
queryAccountDetail({accountId}),
]);
return { userPrivileges, userInfo}
}
let _BASE_DATA_INIT = false
function InitialRequest(isMode) {
const accountId = cache.getCache('accountId', "local")
// const userInfo = BaseStore.app.userInfo
return initialization({accountId})
.then((res) => {
const { userPrivileges, userInfo } = res;
let globalAuth = []
if(process.env.NODE_ENV === 'development'){
globalAuth = staticRouter
}
const userAuth = userPrivileges.returnObj
BaseStore.app.updateUserInfo(userInfo.returnObj);
BaseStore.auth.initAuthData([...userAuth, ...globalAuth]);
cache.setCache('_AUTH_STORE_',[...userAuth, ...globalAuth],'session')
_BASE_DATA_INIT = true;
})
.catch((e) => console.error('接口初始化失败!', e));
}
export default InitialRequest
请求逻辑,在一个独立函数initialization中,传递用户ID,进行数据请求
- 菜单数据 queryPageList
- 用户详情 queryAccountDetail
本地静态菜单数据加载
import staticRouter from "@/router/router.dev";
if(process.env.NODE_ENV === 'development'){ globalAuth = staticRouter }
服务端菜单数据加载
BaseStore.auth.initAuthData([...userAuth, ...globalAuth]);
权限管理器注册完成后,进行本地存储
cache.setCache('_AUTH_STORE_',[...userAuth, ...globalAuth],'session')
存储的目的:
- 路由加载使用
- 菜单显示
动态路由注册
/**
* registerDynamicRoutes 注册动态路由
* @param {*} router 路由
* 场景: 刷新页面
*/
function registerDynamicRoutes(router){
Window._EDU_SYSTEM_ROUTERS = router.getRoutes()
const _AUTH_STORE = cache.getCache('_AUTH_STORE_','session') || []
_AUTH_STORE.forEach( menus => {
const routerName = menus.path?.substr(1)
if(routerName && !router.hasRoute(routerName)){
router.addRoute('home',{
path: menus.path,
name: routerName,
meta: {
id: menus.pageId,
title: menus.pageName,
isAsync: true,
icon: menus.icon || 'icon-wenjian'
},
component: ()=> import(`@/${menus.component}`)
})
}
})
}
cache.getCache('_AUTH_STORE_','session')
获取存储的菜单数据
菜单显示
/**
* 左侧导航-加载器
* @param {*} routerUrl
* @returns 1/2/3...级菜单 pageType=1
*/
const getSidebarMenu = (routerUrl) => {
const item = BaseStore.auth.getInfoByRoute(routerUrl)
if (!item) {
console.warn(`进入非入口页面,当前访问模块${routerUrl}未授权,请联系管理员进行授权!`)
return
}
let pageId = ''
if (item.parentId == '-1') {
pageId = item.pageId
} else {
pageId = item.parentIds.split(',')[1]
}
console.debug('查询pageId=' + pageId + "下的页面")
return BaseStore.auth.getInfoByNotEntryTree(pageId)
}
主要是左侧的菜单加载使用,其中用到了 BaseStore.auth
这部分就是框架层的权限管理器
权限管理器:
业务使用:
//示例为BaseStore方式
import { BaseStore } from '@basic-library';
function MenuView() {
const {auth} = BaseStore
const menuList = auth.userMenuList.filter((v) => v.pageType !== 'entry');
const isBool= auth.getInfoByName(item.id));
}
API
参数 | 说明 |
---|---|
userMenuList | 用户菜单列表 |
userAuthList | 用户菜单及功能全列表 |
userEntryMenuList | 用户入口页面菜单列表-一般是一级菜单 |
userTreeMenuList | 用户菜单列表-Tree结构 |
isAuth | 根据别名判断是否有权限 |
getInfoByCode | 根据 code 返回当前菜单功能信息 |
getInfoByNames | 根据 codes 批量返回当前菜单功能信息 |
getInfoByRoute | 根据 路由地址 返回当前菜单功能信息 |
getInfoByNotEntryTree | 根据 父页面ID 返回非入口页面的信息--转化为Tree结构 |
数据结构
- pageId:页面ID
- pageName: 页面名称
- alias:页面别名(按钮---权限别名)
- parentId:父页面ID
- parentIds:父页面ID集合(-1,001,001001)
- pageType:entry入口页面(entry 入口页面) cover 非入口页面(不显示在菜单的隐藏路由页面)...
- path 路由地址
- component 模块文件地址
- sortNo 排序--Number
- systemId 系统ID
代码实践
index.js
import produce, { enableMapSet } from "immer";
import { formatAuthData, computeTreeList } from "./utils";
import { orderBy } from "lodash";
enableMapSet();
// auth
// parentId为-1时,是一级菜单 pageType entry 入口页面(1、2、3级菜单显示) cover 非入口页面(不显示在菜单的隐藏路由页面) privilege 按钮
const _AUTH_STORE_ = {
dataMap: new Map(),
nameToCode: new Map(),
codeToName: new Map(),
};
let authStore = produce(_AUTH_STORE_, () => {});
window._AUTH_STORE_ = authStore;
class menu {
constructor() {
this.updateTime = 0;
}
updateDepHolder() {
this.updateTime = Date.now();
}
/**
* 计算当前路由集合的树结构,过滤操作权限
*/
get userMenuList() {
return orderBy(
Array.from(authStore.dataMap.values()).filter(this.filterMenus),
["sortNo"],
["asc"]
);
}
get userAuthList() {
console.debug("权限最近更新时间->", this.updateTime);
return Array.from(authStore.dataMap.values());
}
get userEntryMenuList() {
const authArr = Array.from(authStore.dataMap.values()).filter(
this.filterEntryMenus
);
return orderBy(authArr, ["sortNo"], ["asc"]);
}
get userTreeMenuList() {
const authArr = Array.from(authStore.dataMap.values()).filter(
this.filterMenus
);
return orderBy(computeTreeList(authArr), ["sortNo"], ["asc"]);
}
getMenuListByType(type) {
const authArr = Array.from(authStore.dataMap.values()).filter(
(v) => v.pageType == type
);
return orderBy(authArr, ["sortNo"], ["asc"]);
}
getInfoByName(name) {
return authStore.dataMap.get(authStore.nameToCode.get(name));
}
getInfoByNames(names) {
return names.map((name) => this.getInfoByName(name));
}
getInfoByCode(code) {
return authStore.dataMap.get(code);
}
getInfoByRoute(pathname) {
let item;
const authArr = Array.from(authStore.dataMap.values());
item = authArr.find((v) => v.path === pathname);
if (!item) {
item = authArr.find((v) => v.path && v.path.includes(pathname));
}
return item;
}
getInfoByNotEntryTree(parentId) {
let authArr = Array.from(authStore.dataMap.values()).filter(
this.filterNotEntryMenus
);
if (parentId) {
authArr = authArr.filter((v) => {
return v.parentIds.split(",").find((r) => r == parentId);
});
}
return orderBy(computeTreeList(authArr), ["sortNo"], ["asc"]);
}
isAuth(name) {
return !!this.getInfoByName(name);
}
// 过滤页面菜单(包括1/2/3级)--显示
filterMenus(v) {
return v.pageType == "entry";
}
// 过滤非1级菜单---(2/3级等)显示
filterNotEntryMenus(v) {
return v.parentId != "-1" && v.pageType == "entry";
}
// 过滤入口页面菜单---显示--1级菜单
filterEntryMenus(v) {
return v.parentId == "-1" && v.pageType == "entry";
}
// 过滤操作权限-按钮
filterPrivilege(v) {
return v.pageType == "privilege";
}
// 过滤非入口页面---叶子菜单下---详情页面等
filterCover(v) {
return v.pageType == "cover";
}
// 过滤全局路由--用户中心、门户等地址
filterPortal(v) {
return v.pageType == "portal";
}
/**
* 操作权限--按钮
* @param path 当前路径
* @param alias 权限别名
*
*/
getAuthPrivilege(path, alias) {
const currPageItem = this.getInfoByRoute(path);
return currPageItem?.alias;
}
/**
* 初始化权限数据
* @param {*} menus
* @param {*} privileges
*/
initAuthData(features = []) {
authStore = produce(authStore, (draftState) => {
const { formatMap, nameToCode, codeToName } = formatAuthData(features);
draftState.dataMap = new Map(formatMap);
draftState.nameToCode = new Map(nameToCode);
draftState.codeToName = new Map(codeToName);
});
this.updateDepHolder();
}
}
export { authStore };
export default new menu();
utils.js
主要是权限数据格式转换使用
formatAuthData
数据按照ID、名称进行格式化多个集合computeTreeList
扁平集合数据转换Tree树形结构数据
import { cloneDeep } from 'lodash';
export function formatAuthData(features) {
const formatMap = [];
const nameToCode = [];
const codeToName = [];
features.forEach((feature) => {
formatMap.push([feature.pageId, feature]);
nameToCode.push([feature.pageName, feature.pageId]);
codeToName.push([feature.pageId, feature.pageName]);
});
return { formatMap, nameToCode, codeToName };
}
export function computeTreeList(list, id = 'pageId', pid = 'parentId', isNoDeep) {
let treeData;
if (!isNoDeep) {
treeData = cloneDeep(list);
} else {
treeData = list;
}
let treeMap = {};
treeData.forEach((v) => {
treeMap[v[id]] = v;
});
let arr = [];
for (let i = 0, l = treeData.length; i < l; i++) {
const item = treeData[i];
let hasParent = false;
if (item[pid] && treeMap[item[pid]]) {
hasParent = true;
const item2 = treeMap[item[pid]];
!Array.isArray(item2.children) && (item2.children = []);
item2.children.push(item);
}
!hasParent && arr.push(i);
}
treeMap = null;
return treeData.filter((_, index) => arr.includes(index));
}
核心代码解读
1、菜单数据的装置,对List集合数据,按照code、name进行集合拆分,方便语法传参提取
2、提供了基本的菜单数据获取,比如全部菜单数据以及树形数据格式、入口页面菜单集合、根据code或者name获取菜单信息
设计的目的,从系统层考虑,减少业务耦合即可。
老铁们,我们一起关注系列设计:
深入挖掘前端基础服务&中间件设计-basic-library
深入挖掘前端基础服务&中间件设计-字典设计
深入挖掘前端基础服务&中间件设计-配置设计
深入挖掘前端基础服务&中间件设计-权限控制
深入挖掘前端基础服务&中间件设计-请求封装