演示前端
在年初開發一箇中後臺管理系統,功能涉及到了各個部門(產品、客服、市場等等),在開始的版本中,我和後端配合使用了花褲衩手摸手系列的權限方案,前期很是nice,可是慢慢的隨着功能增多、業務愈來愈複雜,就變得有些吃力了,由於咱們的權限動態性太大了vue
爲了解決上面2個痛點,我將原方案進行了一丟丟改造。ios
後端的配合:git
routerName
字段,用於可視化權限編輯有一些注意點:github
以Restful風格接口爲例ajax
const operations = [
{
url: '/xxx',
type: 'get',
name: '查詢xxx',
routeName: 'route1', // 接口對應的路由
opcode: 'XXX_GET' // 操做碼,不變的
},
{
url: '/xxx',
type: 'post',
name: '新增xxx',
routeName: 'route1',
opcode: 'XXX_POST'
},
// ......
]
複製代碼
在路由的meta
中增長一個配置字段如requireOps
,值可能爲String
或者Array
,這表示當前路由頁面要顯示的必要的操做碼,Array
類型是爲了處理一個路由頁面須要知足同時存在多個操做權限時才顯示的狀況。若值不爲這2種則視爲無權限控制,任何用戶都能訪問axios
因爲最終須要根據過濾後的權限路由動態生成菜單,因此還須要在路由選項中增長几個字段處理顯示問題,其中hidden
優先級大於visible
後端
hidden
,值爲true時,路由包括子路由都不會出如今菜單中visible
,值爲false時,路由不顯示,但顯示子路由const permissionRoutes = [
{
// visible: false,
// hidden: true,
path: '/xxx',
name: 'route1',
meta: {
title: '路由1',
requireOps: 'XXX_GET'
},
// ...
}
]
複製代碼
因爲路由在前端維護,因此以上配置只能寫死,若是後端能贊成維護這一份路由表,那就能夠有不少的發揮空間了,體驗也能作的更好。安全
先將權限路由規範一下,同時保留一個副本,可能在可視化時須要用到函數
const routeMap = (routes, cb) => routes.map(route => {
if (route.children && route.children.length > 0) {
route.children = routeMap(route.children, cb)
}
return cb(route)
})
const hasRequireOps = ops => Array.isArray(ops) || typeof ops === 'string'
const normalizeRequireOps = ops => hasRequireOps(ops)
? [].concat(...[ops])
: null
const normalizeRouteMeta = route => {
const meta = route.meta = {
...(route.meta || {})
}
meta.requireOps = normalizeRequireOps(meta.requireOps)
return route
}
permissionRoutes = routeMap(permissionRoutes, normalizeRouteMeta)
const permissionRoutesCopy = JSON.parse(JSON.stringify(permissionRoutes))
複製代碼
獲取到操做列表後,只須要遍歷權限路由,而後查詢requireOps
表明的操做有沒有在操做列表中。這裏須要處理一下requireOps
未設置的狀況,若是子路由中都是權限路由,須要爲父級路由自動加上requireOps
值,否則當全部子路由都沒有權限時,父級路由就被認爲是無權限控制且可訪問的;而若是子路由中只要有一個路由無權限控制,那就不須要處理父路由。因此這裏能夠用遞歸來解決,先處理子路由再處理父路由
const filterPermissionRoutes = (routes, cb) => {
// 可能父路由沒有設置requireOps 須要根據子路由肯定父路由的requireOps
routes.forEach(route => {
if (route.children) {
route.children = filterPermissionRoutes(route.children, cb)
if (!route.meta.requireOps) {
const hasNoPermission = route.children.some(child => child.meta.requireOps === null)
// 若是子路由中存在不須要權限控制的路由,則跳過
if (!hasNoPermission) {
route.meta.requireOps = [].concat(...route.children.map(child => child.meta.requireOps))
}
}
}
})
return cb(routes)
}
複製代碼
而後根據操做列表對權限路由進行過濾
let operations = null // 從後端獲取後更新它
const hasOp = opcode => operations
? operations.some(op => op.opcode === opcode)
: false
const proutes = filterPermissionRoutes(permissionRoutes, routes => routes.filter(route => {
const requireOps = route.meta.requireOps
if (requireOps) {
return requireOps.some(hasOp)
}
return true
}))
// 動態添加路由
router.addRoutes(proutes)
複製代碼
這個組件實現很簡單,根據傳入的操做碼進行權限判斷,若經過則返回插槽內容,不然返回null。另外,爲了統一風格,支持一下root
屬性,表示組件的根節點
const AccessControl = {
functional: true,
render (h, { data, children }) {
const attrs = data.attrs || {}
// 若是是root,直接透傳
if (attrs.root !== undefined) {
return h(attrs.root || 'div', data, children)
}
if (!attrs.opcode) {
return h('span', {
style: {
color: 'red',
fontSize: '30px'
}
}, '請配置操做碼')
}
const opcodes = attrs.opcode.split(',')
if (opcodes.some(hasOp)) {
return children
}
return null
}
}
複製代碼
以ElementUI爲例,因爲動態渲染須要進行遞歸,若是以文件組件的形式會多一層根組件,因此這裏直接用render function簡單寫一個示例,能夠根據本身的需求改造
// 權限菜單組件
export const PermissionMenuTree = {
name: 'MenuTree',
props: {
routes: {
type: Array,
required: true
},
collapse: Boolean
},
render (h) {
const createMenuTree = (routes, parentPath = '') => routes.map(route => {
// hidden: 爲true時當前菜單和子菜單都不顯示
if (route.hidden === true) {
return null
}
// 子路徑處理
const fullPath = route.path.charAt(0) === '/' ? route.path : `${parentPath}/${route.path}`
// visible: 爲false時不顯示當前菜單,但顯示子菜單
if (route.visible === false) {
return createMenuTree(route.children, fullPath)
}
const title = route.meta.title
const props = {
index: fullPath,
key: route.path
}
if (!route.children || route.children.length === 0) {
return h(
'el-menu-item',
{ props },
[h('span', title)]
)
}
return h(
'el-submenu',
{ props },
[
h('span', { slot: 'title' }, title),
...createMenuTree(route.children, fullPath)
]
)
})
return h(
'el-menu',
{
props: {
collapse: this.collapse,
router: true,
defaultActive: this.$route.path
}
},
createMenuTree(this.routes)
)
}
}
複製代碼
咱們通常用axios,這裏只須要在axios封裝的基礎上加幾行代碼就能夠了,axios封裝花樣多多,這裏簡單示例
const ajax = axios.create(/* config */)
export default {
post (url, data, opcode, config = {}) {
if (opcode && !hasOp(opcode)) {
return Promise.reject(new Error('沒有操做權限'))
}
return ajax.post(url, data, { /* config */ ...config }).then(({ data }) => data)
},
// ...
}
複製代碼
到這裏,這個方案差很少就完成了,權限配置的可視化能夠根據操做列表中的routeName
來作,將操做與權限路由一一對應,在demo中有一個簡單實現