分享-前端业务场景的一些代码优化
前端开发基本每天都在写业务代码,有些场景我们可以用更好更优雅的方式去处理。今天给大家分享下我在实际业务场景下对代码的一些思考与优化,和大家探讨下。主要是两个方面:
1. 利用对象来简化if else或者switch case
先简单举例说个业务场景:
运单可能有待发车、运输中、待收货、待回单、已收货…等状态,我们需要根据不同状态来显示不同的文案和图标颜色。如果我们采用vue的计算属性实现
第一种写法利用switch case或者if else:
switch (platformOrderStatus) {
case 'transporing':
return {
name: '运输中',
color: '#faad14'
}
case 'loaded':
return {
name: '已装货',
color: '#faad14',
}
...
...
default:
return {
name: '未创建',
color: '#ACBAC3',
}
}
第二种写法:
const maps = {
'transporing': {
text: '运输中',
color: '#faad14'
},
'loaded': {
text: '已装货',
color: '#faad14'
},
'arrived': {
text: '已收货',
color: '#72C650'
},
...
...
}
return maps[this.platformOrderStatus]
对比下两种写法,第一种本质还是在写if else判断条件,不同分支进入不同逻辑。而第二种是提前定好规则,利用对象的key获取到对应规则对应的返回结果。大部分开发都已经在使用第二种方式,这种方式写法相对优雅并且逻辑更加清晰。
上面举的例子中有两个特点,第一是判断条件简单,只有一个订单状态枚举值,第二是处理逻辑相对简单。是如果有类似场景且条件分支大于三个以上都可以使用第二种方式处理。
如果判断条件简单,但是处理逻辑复杂我们可以将处理逻辑封装起来,通过不同的函数调用去处理。想想vue2中我们在options中可以传入data、props、name、mixin、methods、computed、watch…等等这些很多不同的参数,这些参数vue内部都要去做不同的处理。
如果用if else 或者 Switch case至少需要写十几个条件分支。
vue采用策略模式,针对不同的参数先定义好不同的策略:
...
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat // strats对象上有包括data、methods...等不同的处
理函数
options[key] = strat(parent[key], child[key], vm, key)
}
return options
...
其中strats对象上定义了不同的key处理方法,具体strats上定义了哪些,可以阅读下vue/src/core/util/options.js 文件。
2.利用对象map来减少循环,减小时间复杂度
之前接手过一个需求,场景是:挑单夹右侧的数据是以任务单据维度展示,需要将列表数组聚合到上方显示,聚合条件是相同司机手机号、车牌号、客户付款方式等多个条件。
需求可以抽象成一类问题:如何将一个已有数组中某些条件相同的项聚合
eg:将下面的数组中按照车牌号+手机号+付款方式的维度来聚合金额
const originData = [
{ driverName: '张三', truckNo: 'A123', payType: '1', phone: '15555555555', applyAmount: 100 },
{ driverName: '李四', truckNo: 'B456', payType: '2', phone: '17777777777', applyAmount: 200 },
{ driverName: '王五', truckNo: 'C123', payType: '1', phone: '18888888888', applyAmount: 100 },
{ driverName: '张三', truckNo: 'A123', payType: '1', phone: '15555555555', applyAmount: 350 },
{ driverName: '王五', truckNo: 'C123', payType: '1', phone: '18888888888', applyAmount: 500 },
]
let key = null
const mapObj = {}
originData.forEach(item => {
const { driverName, truckNo, phone, payType, applyAmount } = item
key = truckNo + phone + payType
if (!mapObj[key]) {
mapObj[key] = {
...item
}
return
}
mapObj[key].applyAmount = Number(mapObj[key].applyAmount) + (Number(applyAmount))
})
const res = Object.values(mapObj)
console.log(res)
结果:
[
{"driverName": "张三","truckNo": "A123","payType": "1","phone": "15555555555","applyAmount": 450},
{"driverName": "李四","truckNo": "B456","payType": "2","phone": "17777777777","applyAmount": 200},
{"driverName": "王五","truckNo": "C123","payType": "1","phone": "18888888888","applyAmount": 600}
]
因为聚合条件可以对应一个唯一的key,利用这边可以轻松达到实现需求的目的。试想一下,如果不使用key,采用嵌套循环的方式去,复杂度会增加多少倍。
需求场景:提供公共审批按钮给业务调用。
其中一个场景就是在业务列表的操作列中需要渲染审批流按钮组件。一开始封装的时候没有考虑在列表中渲染的场景,后来发现有个比较麻烦的问题:业务方使用组件时只传入业务的id,需要通过业务id去审批流后端查询当前业务单据支持哪些审批流按钮显示,涉及到组件内部发http请求。
这个场景需要组件自己内部维护按钮的显示集合而不是业务方传入。正常单个使用场景没有问题,在列表中选择的时候会出现组件发送太多相同的http请求。
一开始调用截图:
解决方式:后端接口改成支持批量调用,前端等组件全部加载完成后统一发一次http请求
这种场景可以抽象一下就是:当一个维护了http请求的公共组件同时被大量加载时,如何优化合并请求
解决思路:组件可以在生命周期mounted中注入回调逻辑放入队列,等到所有组件都渲染完成后,达到条件,将队列中的回调依次执行。
为了维护队列和对应的业务参数,使用单例模式暴露一个对象
import {
queryValidFLowButtons
} from '@/api/flow'
class MergeFlowButReqs {
constructor() {
this.ids = []
this.businessButtons = []
this.callbacks = []
this.timer = null
}
pushBusinessId(idObj, ele, callback) {
this.ids.push(idObj)
this.callbacks.push({
businessId: idObj.businessId,
businessNo: idObj.businessNo,
ele,
callback
})
this.queryshowBtns()
}
// 清空缓存队列
flushCallbacks() {
this.callbacks.forEach(item => {
if (!item || typeof item.callback !== 'function') return
item.callback(this.businessButtons.find(btn => btn.businessId === item.businessId && btn.businessNo === item.businessNo))
// 标记已完成请求的组件实例
item.ele.removeAttribute('class')
})
this.clear()
}
clear() {
this.ids = []
this.businessButtons = []
this.callbacks = []
}
queryshowBtns() {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
const uniqIds = []
const map = {}
// 去重
this.ids.forEach(item => {
const {
businessNo,
businessId
} = item
const key = businessId + '_' + businessNo
if (!map[key]) {
map[key] = 1
uniqIds.push({
businessId,
businessNo
})
}
})
queryValidFLowButtons({
businessIds: uniqIds
}).then(res => {
if (res.data && res.data.data && res.data.code === '10000') {
this.businessButtons = this.businessButtons.concat(res.data.data)
this.flushCallbacks()
} else {
this.businessButtons = this.businessButtons.concat([])
this.flushCallbacks()
throw new Error('未查询到流程id对应的按钮状态')
}
}, res => {
this.businessButtons = this.businessButtons.concat([])
this.flushCallbacks()
throw new Error('未查询到流程id对应的按钮状态')
})
}, 150)
}
}
export default new MergeFlowButReqs
pushBusinessId方法在每个组件实例中调用,接收三个参数:业务参数、组件el元素、回调函数。
组件代码:
mounted () {
this.initQueryButton()
},
methods: {
// 放入队列查询
initQueryButton () {
mergeInitRequest.pushBusinessId({
businessId: this.businessId,
businessNo: this.businessNo
}, this.$refs.flowbtns, res => {
console.log(res)
if (!res || !res.buttonDTO) {
this.showBtnList = this.disabledBtns.map(item => item.code)
return
}
const arr = res.buttonDTO.filter(item => item.enable && ['tracking', 'approval', 'revoke'].includes(item.code)).concat(this.disabledBtns).map(item => item.code)
this.showBtnList = [... new Set(arr)]
})
},
},
优化后调用截图:
放入队列比较简单,在组件的mounted里面就行。这里如何判断该去清空队列调用flushCallbacks是一个难点,试过几个方案反复测试,最终采用了现在timeout实现。虽然这部分的按钮加载会慢于其他按钮,但是在可接受时间内(如果大家有好的方案可以联系我,共同讨论下)
因为之前了解过vue的nextTick实现,所以当时碰见这个问题的时候立马想到这种解决思路,Vue的nextTick也是维护了自己的队列,将清空队列并执行回调的时间点放在特点阶段。如果有小伙伴还不了解Vue的nextTick是怎么实现的,强烈推荐大家去看下,帮助大家重温下宏任务/微任务和事件循环。
总结
分享的内容点不多,这两个大方面能解决的不仅仅是我列举出来的场景,还有很多都可以参考同样的思路实现。希望与大家共勉,也欢迎大家多给出意见,一起学习!