案情简介
在业务开发中遇到了一个订单提交的场景, 具体要求如下
- 用户在创建一个订单时需要输入商品的物流信息, 该信息最少有一条, 可能有多条.
- 用户可以添加和删除物流信息.
- 提交时需要校验每个物流信息.
- 订单提交后如果审核不通过仍可以修改, 添加和删除物流信息.
这块功能是之前版本中已经有的, 我需要在该功能基础上进行一次迭代开发. 然后在测试时就出bug了. 由于开发该代码的员工已离职, 导致该bug排查了很久.
bug的具体描述如下:
- 订单提交时需要校验每个物流信息的字段是否按要求填充. 创建订单时校验功能正常, 但在修改订单时出现不校验在修改订单时新增的物流信息的问题.
在两天的debug后发现该问题是由于物流信息输入组件ref的绑定问题导致的. 以下是涉事代码
犯案人验身
<div class="shareholderInfo-list">
<!-- 单个物流信息填写表单 -->
<div
class="shareholderInfo-item"
v-for="(logistics, index) in logisticsListInfo"
:key="index"
>
<yk-form
:ref="logisticsListFormRule[index]"
style="max-width: 1040px"
size="large"
:inline="true"
:formItems="logisticsListFormItems"
:formParams="logistics"
>
......
</yk-form>
<p
v-if="index > 0"
@click="removeLogisticsList(index)"
class="remove-shareholderInfo"
>
<el-icon><Remove /></el-icon>删除
</p>
</div>
<p class="add-shareholderInfo" @click="addLogisticsList">
<el-icon><CirclePlusFilled /></el-icon>添加信息
</p>
</div>
涉及的代码功能可分为三大部分:
- 用于填写的物流信息的yk-form组件
- 一个添加新的物流信息的按钮
- 一个删除物流信息的按钮
乍一看该部分代码还算正常, 而且是线上的代码. 因此其最开始逃脱了被删除的制裁. 但之后debug时一系列的证据都将矛头指向了他. 经调查小组的连夜勘查, 最终确认其犯罪事实无误, 本着依法code的原则, 现呈列物证. 以使嫌疑人心服口服.
- 在提交修改的订单信息时, 同样会进行一次校验. 但在校验函数中打印信息最终只获得了第一个物流信息的数据, 其他物流信息完全没有校验. 毫无疑问这是ref的绑定出了问题.
在获得这份决定性的证据后调查小组立即对和ref绑定的logisticsListFormRule展开了调查. 同时也对其相关回调removeLogisticsList和addLogisticsList进行了上门走访. 在经过了一个小时的实地调查后确认了这是一起团伙作案. 为避免后人重蹈覆辙, 现公布其犯罪细节.
团队成员介绍
嫌疑人1遗照
let logisticsListFormRule: any = reactive({});
logisticsListFormRule = ref<Array<Ref>>([]);
logisticsListFormRule.value.push(ref("logisticsListFormRule0"));
可以看到, 由于保存物流信息的组件是通过v-for动态渲染的, 因此不能使用常规的字符串ref来进行绑定. 嫌疑人1借助自己数组的身份取得了yk-form组件的信任, 负责其ref绑定. 下文使用ref[]来称呼嫌疑人1.
嫌疑人2遗照
const addGoodsListInfo = () => {
goodsListInfo.value.push({ amount:'', goodsName: "", number:'', price:'',unit: "",});
goodsListFormRule.value.push(ref(`goodsListFormRule${goodsListInfo.value.length - 1}`));
};
嫌疑人2和嫌疑人1配合, 当新增一个物流信息时, 就向ref[]中添加一个字符串代表新增的yk-form绑定. 下文使用 addref 称呼嫌疑人2.
嫌疑人3遗照
const removeLogisticsList = (index: number) => {
logisticsListInfo.value.splice(index, 1);
logisticsListFormRule.value.splice(index,1);
};
同addref类似, 嫌疑人3负责在删除一个物流信息时从ref[]中删除和该信息绑定的字符串. 下文使用 delref 称呼嫌疑人3.
可以看到, 虽然使用一个字符串数组来绑定ref确实有点low, 但是其使用index来确保每个物流信息都能和一个唯一的字符串对应.逻辑还算严谨, 那么问题出在哪了呢?
问题在于ref[]的初始化. ref[]的初始化是直接添加一个字符串"logisticsListFormRule0"来和第一个物流信息填写组件进行绑定. 然后在添加新的物流信息时触发addref来添加对应的ref到ref[]中, 删除物流信息时通过delref从ref[]中删除对应的ref.
如果订单只有提交功能的话以上逻辑是可以正常工作的. 但问题在于, 订单的信息是可以被修改的. 当已有2个及以上的物流信息时, ref[]的初始化仍然只添加了第一个物流信息的绑定ref. 直接提交时只校验第一个物流信息. 如果添加一个新的物流信息的话, 会发现坐标和ref的长度对不上.
// 当是修改订单时, index是从logisticsListInfo中取值
// logisticsListFormRule的长度初始只有1,
// 当logisticsListInfo的长度, 即物流信息多于一个时, 就会出现index超过了logisticsListFormRule的长度
// 导致绑定的ref为undefined
<div
class="shareholderInfo-item"
v-for="(logistics, index) in logisticsListInfo"
:key="index"
>
<yk-form
:ref="logisticsListFormRule[index]"
style="max-width: 1040px"
size="large"
:inline="true"
:formItems="logisticsListFormItems"
:formParams="logistics"
>
...
好的, 至此案情已梳理完毕. 接下来就要看一下如果修复这个bug了. 其实也比较简单, 这个问题主要是因为初始化导致的, 可以在onMounted时根据物流信息的长度为ref[]添加字符串即可. 当然其实在面对需要给v-for生成的元素添加ref时, 可以通过使用回调函数添加的方式更加的合理.