演示前端
在年初开发一个中后台管理系统,功能涉及到了各个部门(产品、客服、市场等等),在开始的版本中,我和后端配合使用了花裤衩手摸手系列的权限方案,前期很是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中有一个简单实现