查看原文
其他

一个指令,释放 1000 行屎山代码

作者:前端手术刀 

https://juejin.cn/post/7274072378606223415

前言

在我们业务项目开发中,会遇到按钮级权限控制的需求

本文更多的是分享和经历,因为这次的业务需求,并不是通用的权限指令

比如我们内部业务中是按页面来获取权限信息~

业务场景复现

彦祖们, 下面就来看下笔者目前遇到的屎山代码的目录结构

优化前目录结构

  • views
    • A.vue
    • B.vue
    • C.vue
    • D.vue
    • ...
  • mixins
    • btnCodeMap.js
    • btnMixins.js

案例介绍

下面 我们以 A页面新增按钮 权限为例,来介绍一下这些文件的作用

  • A.vue 页面文件
<el-button v-if="!hideBtnAdd">新增</el-button>
  • btnCodeMap.js 按钮权限配置文件
// A 页面 button 权限 ,
export const btnCodeMapA = [{
  title'主表新增',
  resourceCode'add-A',
  hideBack(self) => {
    self.hideBtnAdd = true
  }
}, {
  title'主表编辑',
  resourceCode'edit-A',
  hideBack(self) => {
    self.hideBtnEdit = true
  }
}]
// 之后新增页面 就要在这里继续配置...
  • btnMixin.js 按钮权限请求文件
import {btnCodeMapA} from './btnCodeMap.js'
export const btnMixin = {
    data(){
        return {
            hideBtnAdd:false // 每增加一个权限都要新增一个变量...
        }
    },
    created(){
        // 获取后端权限 省略...
        const permissionList = await getPermissionList()
        // 此处获取 A 页面的配置权限映射, 也就是 btnCodeMapA
        const btnCodeMapA = this.getMap() 
        // 然后各种循环遍历 拿到无权限的列表 hideList, 并执行 hideBack 回调
        const hideList = btnCodeMapA.forEach(()=>permissionList.forEach()...)
        hideList.forEach((item)=>item.hideBack(this))
    }
}

也就是说一个按钮权限的变动,需要改动三处文件,并且命名需要一一对应

彦祖们 想必你们看到这里已经快顶不住了吧😭 这谁顶得住啊...要我也是一手关闭

昨天看了下

btnCodeMap.js 已经堆成这样了...

btnMixin.js 已经这样了

image.png

再过几个月 估计就是这样了...

image.png

听到同事最痛心的一句话就是 我的滚轮都滚累了

指令实现

其实笔者一开始想的还是比较简单的,一个简单的权限指令就能解决了

但是因为我们的权限数据是根据页面请求的,所以还是会稍有不同。

理想的使用方式

<span v-permission="audit">审核</span>

最初版本

  • permission.js
const permissionList = getPerimissionList() // 获取后端权限数据
export const permission = {
  inserted(el, binding, vnode, oldVnode) {
      // 此时发现了数据异步问题,数据还没返回
  }
}

实际问题

下面就实际开发遇到的三个难点 分享一下笔者的经历

1.数据异步问题

2.解决多个指令的 重复请求 问题

3.解决表格单列多个 相同 code 节点移除问题

下面只展示了部分核心代码,彦祖们可以直接看完整版本

数据异步问题

其实这个问题 笔者想了好几种方案

1.使用路由守卫来拦截请求,之后 再 next 跳转

这种方式 在请求超时的场景下,会导致页面白屏卡段(虽然几率不大...)

2.直接在指令 inserted 钩子内部请求

export const permission = {
  inserted(el, binding, vnode, oldVnode) {
      await getPerimissionList()
  }
}

最终还是选了 方案2 虽然牺牲了部分通用性, 但是个人认为更符合 高内聚 的原则

多个指令的重复请求问题

想象一下,如果一个页面中存在多个指令

那么就会多次 进入 inserted 钩子

也就是说会调用多次 getPerimissionList

<span v-permission="audit">审核</span>
<span v-permission="unAudit">取消审核</
span>
<span v-permission="confirm">确认</span>

此时 我们就要维护一个 permissionObj 用来存储页面权限

const permissionObj = Object.create(null)
export const permission = { 
    inserted(el, binding, vnode, oldVnode) { 
        const res = await getPerimissionList() 
        const { viewId } = router.history.current.meta //我们内部的业务数据,每个路由唯一
        permissionObj[viewId] = res
     } 
    
}

看到这里 彦祖们是不是还不是很明白 这和解决重复请求有半毛钱关系?

的确一开始 笔者也没想到好的解决方案,但是 permissionObj 既然能存储数据,那不也就能存储 Promise吗?

const permissionObj = Object.create(null)
const handleElPermission = (el, permission) => {
  if (permission === false) el.remove()
}
export const permission = { 
    inserted(el, binding, vnode, oldVnode) { 
        const { viewId } = router.history.current.meta //我们内部的业务数据,每个路由唯一
        const {value} = binding
        // 说明当前页面正在请求数据
        if(permissionObj[viewId] instanceof Promisereturn
        
        // 当前页面已经有权限数据 直接操作
        if(permissionObj[viewId]){
          handleElPermission(permissionObj[viewId],el)
          return
        }
        
        const p = getPerimissionList() 
        permissionObj[viewId] = p // 把 Promise 复制给permissionObj[viewId] 表示正在请求
        
        p.then(res=>{
            permissionObj[viewId] = res
            handleElPermission(el,res[value])
        }).catch(e=>{
            permissionObj[viewId] = null
        })
        
     } 
    
}

多个相同 code 节点移除问题

这种场景存在表格中,比如表格的操作列中 每一行存在一个 编辑 按钮

那么 10 行数据就有 10 个编辑按钮

也就是说 等权限接口回来,我们需要操作这 10 个按钮

<el-button v-permission="edit">编辑</el-button>

此时我们遇到的一个难题就是, 用什么方式来存储这么多的 dom 节点

彦祖们 可以停下来 想一下有什么好的方案?

笔者后来用 Map 数据结构 解决了这个问题

const codeElementMap = new Map()
export const permission = { 
    inserted(el, binding, vnode, oldVnode) { 
        const {value:code} = binding
        // 建立 code el 映射,用来存储 el
        if (codeElementMap.has(code)) return codeElementMap.get(code).push(el)
        else codeElementMap.set(code, [el])
        
        p.then(res=>{
            permissionObj[viewId] = res
            
            const els = codeElementMap.get(code)
            els.forEach(el=>{
                handleElPermission(el,res[code])
            })
        }).catch(e=>{
            permissionObj[viewId] = null
        })
     } 
    
}

完整代码


import router from '../router/'
import { getBtnList } from '@/api/user' // 业务接口

const permissionObj = Object.create(null)
const codeElementMap = new Map()

const getCurrentPermission = () => {
  const { viewId } = router.history.current.meta
  return new Promise(resolve => {
    getBtnList(viewId).then(resolve)
  })
}

const handleElPermission = (el, permission) => {
  if (permission === false) el.remove()
}

export const permission = {
  inserted(el, binding, vnode, oldVnode) {
    const { value: code } = binding

    if (!code) return // 无 code 不处理

    const { viewId } = router.history.current.meta

    if (permissionObj[viewId] instanceof Promisereturn // 当前页面正在请求权限数据,防止重复请求

    // 当前页面已经存在权限数据 直接操作
    if (permissionObj[viewId]) {
      const elPermission = permissionObj[viewId][code]
      handleElPermission(el, elPermission)
      return
    }

    // 建立 code el 映射
    if (codeElementMap.has(code)) return codeElementMap.get(code).push(el)
    else codeElementMap.set(code, [el])

    // 设置当前页面各按钮权限
    const request = getCurrentPermission()
    permissionObj[viewId] = request

    request.then((res) => {
      // 设置 viewId 对应的 permission {viewId:{code:true|false}}
      const viewPermission = (res.data ?? []).reduce((prev, { permission, resourceCode }) => {
        prev[resourceCode] = permission
        return prev
      }, {})

      permissionObj[viewId] = viewPermission

      const elPermission = viewPermission[code]
      const elements = codeElementMap.get(code)
      elements.forEach(el => {
        handleElPermission(el, elPermission)
      })
    }).catch(() => {
      permissionObj[viewId] = null
    })
  },
  // 解绑时候, 释放 code 对应的内存
  unbind(el, binding, vnode) {
    const code = binding.value
    codeElementMap.delete(code)
  }
}

写在最后

业务项目中 遇到 屎山代码 是难以避免的,不过总得有人负重前行

幸运的是 我们及时发现并避免了它

但是各位彦祖们 请不要轻易尝试~

因为 改好了没业绩, 改坏了就是你的大锅...

个人能力有限

如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟

推荐阅读  点击标题可跳转

1、某一线前端小组长的 Code Review 分享

2、写给高级前端的 Nginx 知识,一网打尽!!

3、想成为优秀前端,你需要知道这些!(基本素养、代码规范、开发技巧)

继续滑动看下一个
大前端技术之路
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存