近 2 年一直使用蚂蚁金服的 Ant Design UI 框架以及其开箱即用的中台前端/设计解决方案 ANT DESIGN PRO (去年的圣诞风波有点影响,但愿再也不发生相似的事情),框架是一直更新一直迭代,不过里面涉及权限管理的部分的使用场景仍是比较有限,兼容不了须要细化到各模块中的具体动做的场景。授人以鱼不如授人以渔,没有就本身撸一个呗。javascript
虽然是本身撸,但仍是得站在前辈的肩膀上,离开设计的代码都不够优雅。向我司的校长(霸气绰号,具体为何叫校长能够在 https://www.luweitech.cn/ 上找找,可能能找到 (#^.^#)
)学习——写代码要写得像诗同样优雅。前端
找了一圈,最终选了一个设计思想——RBAC,RBAC 以角色为基础的访问控制(英语:Role-based access control,RBAC),简单能够概括为 who、what、how,即,who 对 what进行了 how 的操做,翻译成广东话就系:“有一个靓仔企一个野里面作左滴野”。java
一张简单的图(图是盗来的~)理解:git
即,张3、李四是“销售角色”,而“销售角色拥有查看“客户列表”和“编辑客户”两个动做的权限,天然而然的,张3、李四就拥有查看“客户列表”和“编辑客户”两个动做的权限。github
完整一点就是(图也是盗来的):web
由上能够看出,核心就三步:小程序
我以为核心仍是上面的设计思路,具体的代码实现只是思路的表达,后续封装得更通用再放出完整版出来吧。微信小程序
ps:使用的 ant-design-pro 版本是 2.2.1,有比较多旧系统,还没一会儿升级到最新的,各位能够用最新来撸缓存
定义角色这一步比较简单,就直接跳过了~前端框架
先说第二步,给角色受权权限。先上效果图:
这一步有几个关键步骤:
router.config.js
转化成上图中用于展现数据一部分 router.config.js
,以下
export default [ // user ...节省位置,省略 // app { path: '/', component: '../layouts/BasicLayout', Routes: ['src/pages/Authorized'], routes: [ { path: '/', redirect: '/welcome', }, { name: 'welcome', path: '/welcome', icon: 'smile', component: './Welcome/Welcome', power: ['MENU'], }, { name: 'revenueManagement', path: '/revenueManagement', icon: 'pay-circle', power: ['MENU'], routes: [ { name: 'userDeposit', path: '/revenueManagement/userDeposit', component: './UserDeposit/UserDeposit', power: ['MENU', 'CONTENT', 'EXPORT'], }, { name: 'userConsumptions', path: '/revenueManagement/userConsumptions', component: './UserConsumptions/UserConsumptions', power: ['MENU', 'CONTENT', 'EXPORT'], }, { name: 'staffTuningLogs', path: '/revenueManagement/staffTuningLogs', component: './StaffTuningLogs/StaffTuningLogs', power: ['MENU', 'CONTENT', 'EXPORT'], }, { name: 'userAccount', path: '/revenueManagement/userAccount', component: './UserAccount/UserAccount', power: ['MENU', 'CONTENT', 'EXPORT', 'GIVE_COIN'], }, ] }, ], }, ];
比较关键是准备这几个数据:(聪明的你确定知道 _
是lodash)
/** * 过滤原始的 router 数据,返回有 power 属性的 item * @param {Array} data router.config.js 中关于 app 部分的配置,即:RouterConfig[1].routes,注意,不要直接把 RouterConfig[1].routes 传递进来,这里会改变原来的数据,因此须要深复制后才传进来 * @returns {Array} 格式化后的 RouterConfig[1].routes,过滤掉没有 power 属性的 item */ function filterRouter(data) { return data.filter((item) => { if (item.routes) { item.routes = filterRouter(item.routes); } return item.power; }) } /** * 将 filterRouter且memoizeOneFormatter 出来后的数据的 power 属性改为 [{label: "查看菜单", value: "MENU"}] 的形式,用于在展现是能够出现中文 * @param {Array} data RouterConfig[1].routes执行 filterRouter且memoizeOneFormatter 函数后的数据,一样,该参数须要深复制后才传递进来 * @returns {Array} 修改 power 属性后的数据 */ function setPowerText(data) { return data.map((item) => { if (item.children) { item.children = setPowerText(item.children); } item.power = item.power.map((powerItem) => { return { label: powerName[powerItem], value: powerItem, } }); return item; }); } /** * path 为 key,power 为 value,将 filterRouter且memoizeOneFormatter 后的数据,转成这种 key-value 的对象 * @param {Array} data RouterConfig[1].routes执行 filterRouter且memoizeOneFormatter 函数后的数据,一样,该参数须要深复制后才传递进来 * @returns {Object} * 例如: { '/list': ['MENU'], '/list/basic-list': ['MENU', 'CONTENT', 'ADD', 'UPDATE', 'DELETE'], '/exception': ['MENU'], } */ function getAllPowerKeyValue(data) { let result = {}; const recursion = (data) => { data.forEach((item) => { result[item.path] = item.power; if (item.children) { recursion(item.children); } }); } recursion(data); return result; } const powerOriginData = filterRouter(_.cloneDeep(RouterConfig[1].routes)); // 过滤没有 power 属性的项 const localePowerOriginData = memoizeOneFormatter(powerOriginData, undefined); // 将name 改为相应语言,注意,通过这个函数以后,本来的 routes 就改为 children 了 const powerTextData = setPowerText(_.cloneDeep(localePowerOriginData)); const allPowerKeyValueData = getAllPowerKeyValue(_.cloneDeep(localePowerOriginData));
powerOriginData
是过滤掉没有 power (power 是本身定义的一个属性,用来标明该模块中拥有哪些动做) 属性的项,减小接下来计算中的次数。
powerTextData
纯粹是为了展现用的,把动做的标识换成中文给用户选择时看
allPowerKeyValueData
主要是为了方便接下来的计算,把 router.config.js
中多余的字段都清掉,留下 key(以模块的 path 为 key)和对应的 power。
准备好这些展现数据,后面的交互逻辑和发送给后台就简单了,不啰嗦了~
完成以上三步后,下一个模块就是直接使用了,这里分红两个部分:
登陆后,结合当前用户信息,再向后台的接口请求数据,获取当前用户的全部权限
{/authority: ["MENU"], /authority/role: ["MENU", "CONTENT", "ADD", "UPDATE", "TRIGGER", "RESOURCE_AUTHORIZE"]}
,标志每一个路由(页面)里面匹配当前用户的角色分别有哪些权限,而后存在local storage中,字段命名为:curStaffAuthorized
进入主页面后,加载 src/layouts/BasicLayout.js
组件时会构造侧边栏,在 src/models/menu.js
的 getMenuData
将以上缓存中 curStaffAuthorized
的数据转换成侧边栏的数据,过程以下:(具体能够查看:v2.0 权限控制)
getMenuData
的 payload
参数中有一个 routes
是 config/router.config.js
中的全部路由curStaffAuthorized
的数据就能知道当前用户哪些路由是有权限的,哪些路由没有权限,直接把没有权限的路由从要渲染到侧边栏的数据中删掉后台返回的格式:("/authority"--这个 key 是路由,表明该路由或该页面有哪些权限) { "/authority":[{permission_id: 1, action: "MENU", name: "角色权限管理-角色管理-MENU", description: ""}], "/authority/role":[ {permission_id: 2, action: "MENU", name: "角色权限管理-角色管理-MENU", description: ""}, {permission_id: 3, action: "CONTENT", name: "角色权限管理-角色管理-CONTENT", description: ""}, ] }
这一步就比较简单了(不过很麻烦,在想有没有更好的办法)
在 pages 中,根据 path
和 curStaffAuthorized
检查是否有该权限,而后根据标识控制对应功能的显示与否,好比:
let path = props.match.path; this.contentPower = checkPower(CONTENT, path); this.addPower = checkPower(ADD, path); this.updatePower = checkPower(UPDATE, path); this.deletePower = checkPower(DELETE, path); this.triggerPower = checkPower(TRIGGER, path);
{this.addPower && <Button icon="plus" type="primary" onClick={this.handleAddClick}>新建</Button>}
吴勤发
芦苇科技web前端开发工程师、COO
擅长网站建设、公众号开发、微信小程序开发、小游戏、公众号开发,专一于前端框架、服务端渲染、SEO技术、交互设计、图像绘制、数据分析等研究,有兴趣的小伙伴来撩撩咱们~ web@talkmoney.cn
访问 https://www.luweitech.cn/ 了解更多