iview-admin的菜單問題
-
菜單樹無法顯示的缺陷
作者研究的iview-admin是當前最新版本2.0.0,發(fā)現(xiàn)如果一個菜單的兄弟菜單當前用戶無權訪問,可能導致整顆side-menu菜單樹無法顯示。
例如,可將如下配置粘貼到src/router/routers.js的路由配置中進行測試:
{ path: '/multilevel', name: 'multilevel', meta: { icon: 'md-menu', title: '多級菜單' }, component: Main, children: [ { path: 'level_2_3', name: 'level_2_3', meta: { icon: 'md-funnel', title: '二級-3' }, component: () => import('@/view/multilevel/level-2-3.vue') } ] }, { path: '/common', name: 'common', meta: { hideInBread: true }, component: Main, children: [ { path: 'default', name: '一級功能', meta: { access: ['admin999'], icon: 'ios-book', title: '一級功能' }, component: () => import('@/view/common/default.vue') } ] },"多級菜單"的整個菜單是任何用戶都有權訪問的,但是common下的"一級菜單"配置了當前用戶無法訪問的access,隨即整個菜單樹無法顯示。
-
菜單權限無法動態(tài)控制
通常情況下,一個基于角色的權限控制系統(tǒng)會在服務端維護一個角色列表,并為每個角色分配菜單權限。當一個用戶訪問系統(tǒng)時,其能訪問的菜單由該用戶的角色而定。
Iview-admin也許只是出于演示目的,其菜單權限是通過菜單的access屬性靜態(tài)配置,我們可以把這個稱之為允許訪問的角色列表,如示例中的"一級功能",其限制了只有admin999這個角色可以訪問。然而,對一個可維護的系統(tǒng)而言,一個菜單分配給了哪些角色是服務端后臺動態(tài)配置。顯然,這種靜態(tài)配置無法支撐一個正常的生產系統(tǒng)。
iview-admin的路由和菜單
-
路由
Iview-admin的路由其實也是vue的路由,其在src/router/routers.js進行路由配置;
src/router/index.js引入路由配置并創(chuàng)建路由對象,該路由對象會在每次路由訪問前根據當前用戶的access標識(角色列表)和路由配置中的access進行匹配,達到基于角色控制菜單訪問權限的目的;
Iview-admin的啟動入口即src/main.js引入src/router/index.js創(chuàng)建的路由對象,將其傳給vue,從而初始化整個前端。
-
菜單
Iview-admin的菜單由src/view/components/main/components/side-menu組件負責顯示,其顯示內容由menu-list屬性指定;
src/view/components/main/main.vue引用該組件,通過store 的getter將定義在src/store/module/app.js中的menuList返回值指定為side-menu組件的menu-list;
menuList這個getter也是根據當前用戶的角色以及src/router/routers.js的路由配置計算出用戶的最終菜單。
定制思路
-
思路一:不做任何定制
開發(fā)期指定每個菜單允許訪問的角色列表,在返回用戶信息時返回用戶角色,由iview-admin現(xiàn)有方式控制,但是會面臨"iview-admin的菜單問題"中的兩個問題,不僅有顯示缺陷而且角色無法擴展,基本不可用。
-
思路二:基于后臺的角色權限,初始化完整的菜單
從服務端獲取所有角色,以及每個角色能訪問的菜單,根據這些信息動態(tài)修改routers里菜單的access配置,使菜單的access等于完整的運行訪問的角色列表。但是由于"iview-admin的菜單問題"的第一個問題的存在,導致該方法不可行。
-
思路三:基于后臺的角色權限,初始化必要的菜單
如果菜單里是當前用戶都能訪問的菜單,那么可以稱之為必要的菜單,經研究,這樣的菜單可以繞開缺陷一。這樣的菜單其access屬性不需要配置,因為菜單的整個范圍已經是當前用戶可訪問的范圍。
當然,如果有時間和能力直接解決其無法展現(xiàn)菜單樹的缺陷可以考慮思路二,但是,前端生成一顆經過權限過濾后的必要菜單將更干凈純粹。
定制方法
? 作者采用思路三。首先將路由配置一拆為二,分為系統(tǒng)默認路由配置和菜單路由配置;向后臺獲取當前用戶信息時,獲取用戶所能訪問的菜單,結合菜單路由配置生成一顆經過權限過濾后的菜單樹。
-
拆分路由配置
從“iveiw-admin的路由和菜單”可知,src/router/routers.js文件里的配置肩負兩個職責:一是vue路由配置,二是菜單配置,現(xiàn)在我們要將菜單部分獨立出去。
將src/router/routers.js拆分成routers.js和menus.js兩個文件,routers.js保留系統(tǒng)默認路由,menus.js為菜單路由。routers.js只負責login、home、error_401、error_500、error_404這幾個路由,如下所示:
import Main from '@/components/main' export default [ { path: '/login', name: 'login', meta: { title: 'Login - 登錄', hideInMenu: true }, component: () => import('@/view/login/login.vue') }, { path: '/', name: '_home', redirect: '/home', component: Main, meta: { hideInMenu: true, notCache: true }, children: [ { path: '/home', name: 'home', meta: { hideInMenu: true, title: '首頁', notCache: true, icon: 'md-home' }, component: () => import('@/view/single-page/home') } ] }, { path: '/401', name: 'error_401', meta: { hideInMenu: true }, component: () => import('@/view/error-page/401.vue') }, { path: '/500', name: 'error_500', meta: { hideInMenu: true }, component: () => import('@/view/error-page/500.vue') }, { path: '*', name: 'error_404', meta: { hideInMenu: true }, component: () => import('@/view/error-page/404.vue') } ]menus.js負責菜單相關的路由配置,此出的菜單不要配置任何access:如下所示:假如我們有系統(tǒng)功能,以及子功能菜單一覽和角色管理
import Main from '@/components/main' export default [ { path: '/system', name: '系統(tǒng)管理', meta: { showAlways: false, icon: 'md-menu', title: '系統(tǒng)功能' }, component: Main, children: [ { path: 'menumanage', name: '菜單一覽', meta: { icon: 'md-funnel', title: '菜單一覽' }, component: () => import('@/view/system/menumng.vue') } ] } ]
-
路由對象接管所有路由
經過拆分后,路由配置就分成里兩部分,所以還要改造路由對象使其管理所有路由。
src/router/index.js里引入menus.js,在new Router的地方向Router傳入routers.js和menus.js的合并值,如:
import routes from './routers' import menus from './menus' let allMenus = routes.concat(menus) const router = new Router({ routes: allMenus, mode: 'history' })
-
基于狀態(tài)更新菜單樹
src/store/module/app.js里,在store里聲明permission,該屬性為當前用戶所能訪問的路由配置,并將其初始為系統(tǒng)默認路由配置;
從"iview-admin的路由和菜單"一節(jié)我們說過,side-menu菜單樹內容是由app.js里的menuList這個getter來控制,所以調整menuList這個getter,使其基于permission生成菜單列表,如下所示:
state: { permission: routers }, getters: { menuList: (state, getters, rootState) => getMenuByRouter(state.permission, rootState.user.access) },到此,我們做到了菜單樹內容和permission狀態(tài)綁定
-
根據權限動態(tài)更新菜單樹
經過以上的工作,菜單樹內容和表示用戶權限范圍的permission狀態(tài)進行了綁定,我們還需要在用戶登錄時去動態(tài)更新該permission,使菜單內容跟著當前登錄用戶變化而變化。
首先,在src/store/module/app.js里添加修改permission狀態(tài)的mutation
mutation如下,注意此處默認super_admin具有所有權限,可以根據自己業(yè)務修改:
setPermission (state, {name,permission}) { if(name=='super_admin'){ state.permission = routers.concat(menus) }else{ let newMenus = cloneMenus(menus) let filteredMenus = filterMenus(newMenus,permission) state.permission = routers.concat(filteredMenus) } }其中setPermission這個mutation接收的參數(shù)permission為當前用戶所能訪問的菜單列表,如['菜單一覽'],其邏輯為首先從menus.js克隆一份菜單路由配置,根據permission進行權限過濾,得到一個當前用戶所能訪問必要的菜單路由配置。
cloneMenus實現(xiàn)了對菜單路由配置的克隆,其邏輯如下僅供參考:
const cloneMenu = function (newMenus, {path, name, meta, component, children}) { let obj = {path,name,meta,component} newMenus.push(obj) if(children&&children.forEach){ obj.children = [] children.forEach(function (child) { cloneMenu(obj.children,child) }) } } const cloneMenus = function (menus) { let newMenus = [] menus.forEach(function (menu) { cloneMenu(newMenus,menu) }) return newMenus }filterMenus對克隆出的菜單路由配置進行權限過濾,將不能訪問的菜單進行刪除,只保留最小集合:
const filterMenu = function (menu,targets) { if(menu.children){ for(let i=0;i<menu.children.length;i++){ let remain = filterMenu(menu.children[i],targets) if(remain===false){ menu.children.splice(i,1) i-- } } if(menu.children.length===0){ return false } }else if(!targets||targets.indexOf(menu.name)==-1){ return false } } const filterMenus = function (menus,targets) { for(let i=0;i<menus.length;i++){ let remain = filterMenu(menus[i],targets) if(remain===false){ menus.splice(i,1) i-- } } return menus }其次,在獲取用戶信息時觸發(fā)permission狀態(tài)更新
在src/store/module/user.js里,在getUserInfo這個action實現(xiàn)的地方添加觸發(fā)setPermission的邏輯,使菜單內容根據用戶變化而變化,這里需要特別注意的是getUserInfo的服務端邏輯在user對象里一定要傳permission,即用戶能訪問的菜單name組成的列表,如['菜單一覽','角色管理']:
getUserInfo ({ state, commit }) { return new Promise((resolve, reject) => { try { getUserInfo(state.token).then(res => { const data = res.data commit('setAvatar', avatar) commit('setUserName', data.name) commit('setUserId', data.user_id) commit('setAccess', data.access||[]) commit('setHasGetInfo', true) commit('setPermission', data) resolve(data) }).catch(err => { reject(err) }) } catch (error) { reject(error) } }) } -
調整路由權限過濾
路由對象在路由訪問時都要先進行權限判斷,這個判斷在src/router/index.js的turnTo方法中:
const turnTo = (to, access, next) => { if (canTurnTo(to.name, access, routes)) next() // 有權限,可訪問 else next({ replace: true, name: 'error_401' }) // 無權限,重定向到401頁面 }canTurnTo方法基于routes進行判斷,此時應該改成狀態(tài)管理里的permission即用戶有權訪問的所有路由,如下所示:
const turnTo = (to, access, next) => { if (canTurnTo(to.name, access, store.state.app.permission)) next() // 有權限,可訪問 else next({ replace: true, name: 'error_401' }) // 無權限,重定向到401頁面 } -
其他
經過以上步驟前端改造就此完成。
需要注意是:
getUserInfo的服務端實現(xiàn)一定要根據用戶的角色,在user對象里返回permission,即用戶能訪問的菜單name組成的列表;
本方法是根據用戶權限動態(tài)生成權限范圍內的菜單樹,已經不是通過前端路由的access配置來完成的權限過濾,所以menus.js里的菜單不需要再配置access;
參考代碼
? 沒想到這么多人希望要源碼
? 源碼參考https://github.com/tracelessman/iview-admin-menumanage-baseonroles,如果有價值請給個star。
? 注意,代碼中啟用了前端的mock,這樣我就不用把后端的代碼也上傳了,src/mock/login.js 給admin這個用戶添加了permission:['菜單一覽'],使其具有菜單一覽這個菜單權限。