前言
书接上回使用dom-to-image-more生成页面截图,在实现了编辑器保存时截图并上传的功能后,连一刻都没有为需求的完成而摸鱼,立刻落到头上的工作是——修数。
虽说修数这种活一般情况下是后端负责的,但这次的业务场景比较特殊,需要前端渲染流程图后,再对页面进行截图。所以针对这次的修数工作,后端只能查询出需要修复的数据,再由前端在页面渲染流程图,并调用上传截图的接口。其中便涉及到在循环体中执行异步操作。
初次尝试
通过getServiceList接口查询出需要修正的数据treeInfoList 后,forEach遍历treeInfoList查询每一张流程图的详情信息,并在页面渲染后进行截图。
// 修正数据的方法
async reviseData() {
// 遍历业务域信息列表
this.businessDomainList.forEach(async (item) => {
console.log("step1");
// treeInfoList是需要修正的流程图数据,类型为数组
const { treeInfoList } = await this.rpc.transactionDesign.getServiceList({
turnPageShowNum: "9999", // 大力出奇迹,9999查全部
beltLine: item.bizDomainNo,
});
console.log("step2");
// 遍历数组,调接口查流程图详情→页面渲染→截图上传
treeInfoList.forEach(async (data) => {
console.log("step2.1");
const res = await this.rpc.transactionDesign.getDecisionDetail({
apiId: data.apiId,
});
console.log("step2.2");
// 给相关的数据赋值,让页面渲染流程图
this.dataObj = res;
// 调用截图上传的方法
await this.svg2Image();
console.log("step2.3");
})
})
},
截图上传的方法,从上一篇文章copy,前情提要——使用dom-to-image-more生成页面截图
async svg2Image() {
const node = document.querySelector(".dom_class");
return domtoimage.toPng(node).then(async (dataUrl) => {
const uploadFile = base64toFile(dataUrl, "坤图")
// 通过formData保存文件
const formData = new FormData();
formData.append('uploadFile', uploadFile);
// 设置其他附带参数
formData.append('sing', this.sing);
formData.append('dance', this.dance);
formData.append('rap', this.rap);
formData.append('basketball', this.basketball);
const res = await this.rpc.babybaby.uploadAssetImage(formData);
return res
})
.catch(function (error) {
console.error('crush on you!', error);
});
}
按照预想的逻辑进行,控制台打印的顺序应该是shep1→shep2→(shep2.1→shep2.2→shep2.3)* (treeInfoList.length - 1)→shep1→shep2→……,自信运行起来~
伴随着页面的一通变化,控制台的输出和预想的不能说一模一样,只能说毫无关系。稍作分析,不难察觉forEach循环体内对异步操作的处理有问题。通过面向度娘编程的方式查阅资料后,发现其中原因很简单,因为map/forEach内部使用了while和callback方式来执行函数。
我们可以参考下 Polyfill 版本的 forEach,简化以后类似就是这样的伪代码
while(index < arr.length) {
callback(item, index)
}
从上述代码中我们可以发现,forEach 只是简单地执行了下回调函数而已,并不会去处理异步的情况。而且在 callback 中即便使用 break 也并不能结束遍历。
稍作修改
使用传统的for循环来代替forEach。
// 修正数据的方法
async reviseData() {
// 遍历业务域信息列表
for (let i = 0; i < this.businessDomainList.length; i++) {
console.log("step1");
// treeInfoList是需要修正的流程图数据,类型为数组
const { treeInfoList } = await this.rpc.transactionDesign.getServiceList({
turnPageShowNum: "9999", // 大力出奇迹,9999查全部
beltLine: this.businessDomainList[i].bizDomainNo,
});
console.log("step2");
// 遍历数组,调接口查流程图详情→页面渲染→截图上传
for (let j = 0; j < treeInfoList.length; j++) {
console.log("step2.1");
const res = await this.rpc.transactionDesign.getDecisionDetail({
apiId: treeInfoList[j].apiId,
});
console.log("step2.2");
// 给相关的数据赋值,让页面渲染流程图
this.dataObj = res;
await this.svg2Image();
console.log("step2.3");
}
}
}
自信运行2.0~
控制台的打印顺序是舒服了,但此时又出现了另一个问题——因为数据量较大,接口报错时很难定位到相应的ID。
加入错误处理
通过加入try/catch的逻辑,在接口报错的时候维护好相应的错误信息。
async reviseData() {
// 遍历业务域信息列表
for (let i = 0; i < this.businessDomainList.length; i++) {
// treeInfoList是需要修正的流程图数据,类型为数组
const { treeInfoList } = await this.rpc.transactionDesign.getServiceList({
turnPageShowNum: "9999", // 大力出奇迹,9999查全部
beltLine: this.businessDomainList[i].bizDomainNo,
});
// 遍历数组,调接口查流程图详情→页面渲染→截图上传
for (let j = 0; j < treeInfoList.length; j++) {
try {
const res = await this.rpc.transactionDesign.getDecisionDetail({
apiId: treeInfoList[j].apiId,
});
// 给相关的数据赋值,让页面渲染流程图
this.dataObj = res;
await this.svg2Image();
} catch (error) {
// 记录错误信息
this.id2ErrorMessage[treeInfoList[j].apiId] = error;
continue;
}
}
}
console.log("id2ErrorMessage", this.id2ErrorMessage)
}
总结
forEach 整个函数是同步执行的,它的实现是基于内部的回调函数执行,当进入循环之后,函数内部会去调用传进来的回调函数。当回调函数中存在异步操作时,就会被放入事件循环的异步队列中,但forEach 内部会继续去执行同步代码,也就是继续循环。