Vue实现按钮级权限方案


Posted in Javascript onNovember 21, 2019

演示

在年初开发一个中后台管理系统,功能涉及到了各个部门(产品、客服、市场等等),在开始的版本中,我和后端配合使用了花裤衩手摸手系列的权限方案,前期非常nice,但是慢慢的随着功能增多、业务越来越复杂,就变得有些吃力了,因为我们的权限动态性太大了

  • 手摸手系列权限方案是有比较清晰的权限划分的,而我们公司部门的岗位职责有时比较模糊。
  • 后端采用RBAC权限方案,为了达到第1点要求,将角色划分的很细,并且角色有时频繁变动,导致每一次前端都需要手动维护
  • 为了解决上面2个痛点,我将原方案进行了一丢丢改造。
  • 前端不再以角色来控制权限,而是以更小粒度的操作(接口)来控制,也就是前端不关心角色
  • 路由还是由前端维护(我们的后端很排斥维护和他们不相干的东西:joy:),但改为通过操作列表对权限路由进行过滤
  • 使用单一的方式(方便维护)控制页面的局部权限,不再使用自定义指令方式,而是通过函数式组件,原因是使用自定义指令有多余的开销(插入再移除)

后端的配合:

routerName

有一些注意点:

  • 比如一个有权限的列表页面A,同时这个列表接口被权限页面B使用,现在你配置权限让某一个用户没有A页面权限,但可以使用B页面,如果你的本意是可以使用B页面的所有功能,这时就会有问题,所以尽量不要将权限接口跨页面使用,需要分清哪些数据需要通过字典接口获取还是通过权限接口获取
  • 有些人可能会纠结,前端维护权限安全吗?肯定是不安全的,安全性主要还在后端这边把控,后端做好数据和接口方面的权限控制,前端做权限控制我认为主要还是为了交互体验等。没有权限你为什么要让我看到那一坨?
  • 在使用这种方式之前,要明确当前场景是否确实需要这么做,毕竟在项目比较大且接口很多的情况下,你跟操作码之间有一场持久战

实现

操作列表示例

以Restful风格接口为例

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种则视为无权限控制,任何用户都能访问

由于最终需要根据过滤后的权限路由动态生成菜单,所以还需要在路由选项中增加几个字段处理显示问题,其中 hidden 优先级大于 visible

hidden
visible
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 中有一个简单实现

总结

以上所述是小编给大家介绍的Vue实现按钮级权限方案,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
javascript题目,重写函数让其无限相加
Feb 15 Javascript
JavaScript创建对象的写法
Aug 29 Javascript
jQuery调取jSon数据并展示的方法
Jan 29 Javascript
简单介绍JavaScript的变量和数据类型
Jun 03 Javascript
js实现商城星星评分的效果
Dec 29 Javascript
Google 地图API资料整理及详细介绍
Aug 06 Javascript
AngularJs 延时器、计时器实例代码
Sep 16 Javascript
Grunt针对静态文件的压缩,版本控制打包的实例讲解
Sep 29 Javascript
JS散列表碰撞处理、开链法、HashTable散列示例
Feb 08 Javascript
详解javascript replace高级用法
Feb 17 Javascript
详解JavaScript中的函数、对象
Apr 01 Javascript
vue 使用鼠标滚动加载数据的例子
Oct 31 Javascript
微信小程序实现星级评价
Nov 20 #Javascript
微信小程序音乐播放器开发
Nov 20 #Javascript
微信小程序实现音乐播放器
Nov 20 #Javascript
vue移动端模态框(可传参)的实现
Nov 20 #Javascript
微信小程序实现上拉加载功能
Nov 20 #Javascript
微信小程序实现锚点功能
Nov 20 #Javascript
vue实现element表格里表头信息提示功能(推荐)
Nov 20 #Javascript
You might like
PHP 函数call_user_func和call_user_func_array用法详解
2014/03/02 PHP
PHP中__FILE__、dirname与basename用法实例分析
2014/12/01 PHP
java模拟PHP的pack和unpack类
2016/04/13 PHP
js实现右下角可关闭最小化div(可用于展示推荐内容)
2013/06/24 Javascript
JavaScript 实现完美兼容多浏览器的复制功能代码
2015/04/28 Javascript
JavaScript操作Cookie方法实例分析
2015/05/27 Javascript
纯javascript移动优先的幻灯片效果
2015/11/02 Javascript
值得分享的轻量级Bootstrap Table表格插件
2016/05/30 Javascript
基于JavaScript代码实现自动生成表格
2016/06/15 Javascript
js实现延迟加载的几种方法
2017/04/24 Javascript
清空元素html("") innerHTML="" 与 empty()的区别和应用(推荐)
2017/08/14 Javascript
mpvue中配置vuex并持久化到本地Storage图文教程解析
2018/03/15 Javascript
electron实现qq快捷登录的方法示例
2018/10/22 Javascript
详解关于Vuex的action传入多个参数的问题
2019/02/22 Javascript
记录vue项目中遇到的一点小问题
2019/05/14 Javascript
深入分析JavaScript 事件循环(Event Loop)
2020/06/19 Javascript
详解js中的几种常用设计模式
2020/07/16 Javascript
python检查字符串是否是正确ISBN的方法
2015/07/11 Python
python GUI实现小球满屏乱跑效果
2019/05/09 Python
python3.6根据m3u8下载mp4视频
2019/06/17 Python
解决Tensorflow 使用时cpu编译不支持警告的问题
2020/02/03 Python
CSS3实现内凹圆角的实例代码
2017/05/04 HTML / CSS
html5+css3气泡组件的实现
2014/11/21 HTML / CSS
欧铁通票官方在线销售网站:Eurail.com
2017/10/14 全球购物
新加坡网上化妆品店:Best Buy World
2018/05/18 全球购物
模具设计与制造专业应届生求职信
2013/10/18 职场文书
创建省级文明单位实施方案
2014/02/27 职场文书
电子商务助理求职自荐信
2014/04/10 职场文书
新颖的化妆品活动方案
2014/08/21 职场文书
股东出资证明书范例
2014/10/04 职场文书
出租车拒载检讨书
2015/01/28 职场文书
学习雷锋主题班会
2015/08/14 职场文书
化工厂员工工作总结
2015/10/15 职场文书
2016年“七一建党节”广播稿
2015/12/18 职场文书
TV动画《史上最强大魔王转生为村民A》番宣CM公布
2022/04/01 日漫
基于Python实现对比Exce的工具
2022/04/07 Python