装饰器在vue业务代码中的实践

3,145 阅读3分钟

业务代码的痛点分析

高频常见业务代码的抽离

日常业务开发中常见如下需求

  • 异步处理 loading 状态
  • 用户确认 confirm 按钮
  • 函数防抖与节流
  • 埋点等

于是常常思考

  • 这些处理不属于业务代码范畴
  • 这些代码 与 业务代码过分耦合不利于修改
  • 代码重复性高 容易遗漏导致bug

看以下代码

// 对于loading的处理
 async getData () {
  this.loading = true
  try {
  // 此处 $apis.getXXXX 之类为统一封装方法 如果后台返回错误信息 则抛出异常
    this.data = await this.$apis.getXXXXX({ id:this.xxxid })
  } catch (error) {
    console.error(error)
  }finally{
     this.loading = false
 }
}

实际编程中 经常遗漏finally 导致一直loading 状态

那么让暂时忘掉 loading的处理 看看函数应该长成什么样子

// 忽略loading的处理
 async getData () {
 //此处如获取数据失败 或后端返回错误 则抛出异常 不会赋值
 this.detailsData = await this.$apis.getXXXXX({ id:this.xxxid })
}

感觉这才是我心目中完美的函数 请问什么代码不会出错?

答 不写代码 所以当代码行数降低 则会带来更少的bug

那么 为了不让 ui 童鞋砍死我 势必要对loading 做处理 由此引入了装饰器模式

请看装饰器对上面函数的改造

// 此处字符串loading 是在vue data 中注册的 loading
/**
  data () {
    return {
      loading: false,`
    }
  },
*/
@loading('loading')
 async getData () {
 //此处如获取数据失败 或后端返回错误 则抛出异常 不会赋值
 this.detailsData = await this.$apis.getXXXXX({ id:this.xxxid })
}

只需要添加一行 @loading 即可 做到之前 try cache finally 的事情 可读性更强

在项目中使用

  1. 首先 需要通过eslint 校验 将以下集成到eslint 配置中去
parserOptions: {
    parser: 'babel-eslint',
    ecmaFeatures: { // 支持装饰器
      legacyDecorators: true
    }
  }
  1. 为 vscode 做处理
在 .vscode/settings.json 中添加一行
关闭 vetur 对script 的校验  我选择将验证交给 eslint 处理
"vetur.validation.script": false,
  1. 编写装饰器文件
/**
 * loading 开关装饰器
 * @param {String} loading 当前页面控制开关的变量名字
 * @param {Function} errorCb 请求异常的回调 返回error 一般不用写
 * 如果 errorCb 为 function 为你绑定 this  如果是箭头函数 则第二个参数为this
 * @example
 * @loading('pageLoading',function(){that.demo = '123123'})
 * async getTable(){
 *  this.table =  this.$apis.demo()
 * }
 * @example
 * @loading('pageLoading',(error,that)=>{that.demo = '123123'})
 * async getTable(){
 *  this.table =  this.$apis.demo()
 * }
 */
export function loading (loading, errorCb = Function.prototype) {
  return function (target, name, descriptor) {
    const oldFn = descriptor.value
    descriptor.value = async function (...args) {
      try {
        this[loading] = true
        await oldFn.apply(this, args)
      } catch (error) {
        // 这里的globalError 等就是console.error 只是不会被打包摇树
        globalError(`${name}-----start-----${error}`)
        globalLog(error)
        globalError(`${name}-----end-----${error}`)
        errorCb.call(this, error, this)
      } finally {
        this[loading] = false
      }
    }
  }
}

那么通过对 descriptor.value 添加了额外的 try cache finally 就实现了对一个函数的loading 状态处理 那么这里不仅仅可以处理loading 如 disable 等 任何开关变量都可以进行处理 只是选择了最常见的loading 来对其进行命名

那么来看一段实际项目中的代码

可以看到 这个函数需要142号权限(吐槽后端 应该用字符串)

并且对 loadingList 这个变量进行 开关控制

只有一行业务代码 不论是修改 还是可读性都大大提升

装饰器代码分享

除了上述提到的loading 装饰器 我还使用了一些常见的装饰器 分享给大家

import { debounce as _debounce, throttle as _throttle, isString } from 'lodash-es'
import { Modal } from 'ant-design-vue'
/**
 * 提示装饰器
 * @param {String | Object} message 需要提示用户的信息 或者 confirm 的配置
 * @param {Function} errorFn 请求异常的回调 返回this 使用function 则为你绑定
 */
export function confirm (message, errorFn) {
  const defaultConf = {
    // primary ghost dashed danger link
    okType: 'danger',
    maskClosable: false
  }
  return function (target, name, descriptor) {
    const oldFn = descriptor.value
    descriptor.value = function (...args) {
      Modal.confirm(Object.assign(
        defaultConf,
        isString(message) ? { title: message } : message, // if use string then create Object else use Object to assign
        {
          onOk: () => oldFn.apply(this, args),
          onCancel: () => {
            // 无论如何都提示
            globalWarn(`用户点击了取消:${name}`)
            if (errorFn) {
              errorFn.call(this, this)
            }
          }
        }
      ))
    }
  }
}
/**
 *
 * @param {number} wait 需要延迟的毫秒数
 * @param {config} options
 * @typedef config {{
 *  leading:boolean,
 *  maxWait:number,
 *  trailing:boolean
 * }}
 */
export function debounceFn (wait = 500, options = {}) {
  return function (target, name, descriptor) {
    const oldFn = descriptor.value
    descriptor.value = _debounce(oldFn, wait, options)
  }
}

export function debounceFnStart (wait = 500) {
  return function (target, name, descriptor) {
    const oldFn = descriptor.value
    descriptor.value = _debounce(oldFn, wait, { leading: true, trailing: false })
  }
}

export function debounceFnEnd (wait = 500) {
  return function (target, name, descriptor) {
    const oldFn = descriptor.value
    descriptor.value = _debounce(oldFn, wait, { leading: false, trailing: true })
  }
}

/**
 *
 * @param {*} wait
 * @param {config} options
 * @typedef config {{
 *  leading:boolean,
 *  trailing:boolean,
 * }}
 */
export function throttleFn (wait = 500, options = { trailing: true, leading: true }) {
  return function (target, name, descriptor) {
    const oldFn = descriptor.value
    descriptor.value = _throttle(oldFn, wait, options)
  }
}

对于业务中 需要用户确认的 状态开关的 以及防抖节流的 这些常见的函数装饰器

对于vue3 的思考

vue3 中使用了hook 思想 (真香) 但是对于 写在 setup 函数中 则无法使用这些装饰器

对于函数的修改应该更加简单

async function getData(id){
    data.value  = await $api.getXXX({id})
}

const {loading,runFn} = useLoading(getData)

runFn = debounce(runFN)

return {runFn}

是否更简单更直观呢? 还是有更好的解决方案? 暂未可知

结束语

感谢大家的观看 有意见 欢迎留言讨论