需求

现在有个需求,需要设置每个用户的按钮级权限。
也就是说需要根据某个用户的角色权限来判断页面上某些按钮能不能点击,或者是否展示这些按钮。
这里选择第二种,也就是当用户没有按钮权限时,移除这个按钮。

实现

假设后端权限接口接口返回是这样的数据:

{
   
  "permissions": [
    "patient:listByPage", 
    "patient:patientInfo:save",
    "patient:patientTransfer:delete",
    "patient:dialysisSchedule:save", 
    "shop:item:update"
  ]
}

现在前端需要根据这些权限来移除页面上的按钮
使用vue的自定义指令来实现

Vue.directive('permission', {
   
  inserted: function (el, binding) {
   
    const args = binding.arg.split(',')
    for (let i = 0; i < args.length; i++) {
   
      if (!hasPermission(args[i])) {
   
        // 官网说inserted不保证插入,若还未插入,remove函数无效
        // 这里使用Vue.nextTick() 函数来保证dom已插入
        Vue.nextTick(() => el.remove())
        break
      }
    }
  }
})
function hasPermission (permissionText) {
   
  const permissions = store.state.user.permissions || []
  for (let j = 0; j < permissions.length; j++) {
   
    if (permissions[j].includes(permissionText)) {
   
      return true
    }
  }
  return false
}

Vue官网说inserted是指被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。这里可以使用Vue.nextTick函数来保证下次渲染完毕再执行节点的删除逻辑。
el.remove()就是删除该节点。

自定义指令使用方式

<button class="info" v-permission:patient:listByPage>xxx</button>

上面的意思是如果该用户没有patient:listByPage这个权限,那么该节点会删除。
如果该用户有这个权限,这个button正常展示。

问题

看上面自定义指令代码,会将从后端接口查询回来的权限数据存入stroe,然后判断节点权限时,从store中取出permission数据进行判断有无权限。问题就是在大部分情况下,dom的渲染会先于从接口查询回来的权限数据,也就是说在Vue自定义指令执行时还没有权限数据,导致权限出错。

解决方法

这里使用订阅发布模式,权限指令的处理订阅,待权限接口数据返回后再执行自定义指令的逻辑。
订阅发布模式一般是多个订阅者同时监听同一个数据对象,当这个数据对象发生变化的时候会执行一个发布事件,通过这个发布事件会通知到所有的订阅者,使它们能够自己改变对数据对象依赖的部分状态。

指令代码
Vue.directive('permission', {
   
  // bind DOM未没挂载 inserted DOM已挂在(官网说不保证插入)
  inserted: function (el, binding) {
   
    // 若store.state.user.permission没值,注册观察者,等有值了执行回调exe函数
    if (store.state.user.permissions) {
   
      exe(el, binding)
    } else {
   
      Observer.register(Observer.permission, () => exe(el, binding))
    }
  }
})
function exe (el, binding) {
   
  const args = binding.arg.split(',')
  for (let i = 0; i < args.length; i++) {
   
    if (!hasPermission(args[i])) {
   
      Vue.nextTick(() => el.remove())
      break
    }
  }
  this && Observer.remove(this)
}
function hasPermission (permissionText) {
   
  const permissions = store.state.user.permissions || []
  for (let j = 0; j < permissions.length; j++) {
   
    if (permissions[j].includes(permissionText)) {
   
      return true
    }
  }
  return false
}
订阅发布中间者代码
let Observer = (function () {
   
  const queue = []
  return {
   
    register: function (fn) {
   
      queue.push(fn)
    },
    notify: function (args) {
   
      for (let i = 0; i < queue.length; i++) {
   
        queue[i](args)
      }
    },
    remove: function (fn) {
   
      for (let i = queue.length - 1; i >= 0; i--) {
   
        queue[i] === fn && queue.splice(i, 1)
      }
    }
  }
})()
查询权限接口数据的相关代码
queryPermission (context) {
   
  return new Promise(resolve => {
   
    ajax.$form('/auth/getRolePermission?userId=' + state.adminId).then(res => {
   
      const permissions = res.permissions
      context.commit('setPermissions', permissions)
      Observer.notify(permissions)
      resolve(permissions)
    })
  })
}

结语

这里在接口未返回时,所有自定义指令会将处理函数订阅到消息管理器上,也就是上面的Observer.register。待接口数据返回,调用Observer.notify,执行所有的订阅函数,完成指令逻辑的处理。