业务代码的痛点分析
高频常见业务代码的抽离
日常业务开发中常见如下需求
- 异步处理 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 的事情 可读性更强
在项目中使用
- 首先 需要通过eslint 校验 将以下集成到eslint 配置中去
parserOptions: {
parser: 'babel-eslint',
ecmaFeatures: { // 支持装饰器
legacyDecorators: true
}
}
- 为 vscode 做处理
在 .vscode/settings.json 中添加一行
关闭 vetur 对script 的校验 我选择将验证交给 eslint 处理
"vetur.validation.script": false,
- 编写装饰器文件
/**
* 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}
是否更简单更直观呢? 还是有更好的解决方案? 暂未可知
结束语
感谢大家的观看 有意见 欢迎留言讨论