近 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/ 瞭解更多