1

故事开始了,小程序canvas图片合成 真机测试时,会报错:getImageInfo failed 。
也就是说,我这边异步请求50张图片,每张图片都是通过getImageInfo下载到本地并且绘制到canvas画布上,但是在处理的过程中,getImageInfo会出现获取本地图片错误的情况,也就是说请求50张,最后绘制出来的可能只有45张或者40张,非常明显的数据丢失,要比数据包丢包严重N倍。

经过一系列排查后,发现可能原因有2个:
1.图片请求的域名必须是小程序经过小程序验证的(大佬们通过换域名跳转解决了这个风险)
2.小程序的请求并发数超过小程序的最大并发限制, request、uploadFile、downloadFile 的最大并发限制是 10 个

开发者经验:
"代码问题,小程序同时只能发送10个网络请求,超过后就会报错。图片信息获取也占用这10个网络请求。要把图片信息获取序列化。控制在同一个时间内段内不超10个请求,我是做了队列处理,一次只发送一个请求。完成后在走下一个"

CNODE社区经验
原生promise、async怎么实现控制并发数量
1. bluebird可以做到控制并发数量。
2.用 for 遍历,然后一次拿2个,或者多个出来 再 Promise.all 之后 再 await
3.promise.map包
const promise = pmap(arr, async item => {
       //
}, 10) // 最后是 concurrency
4. 不想用库,那就把 https://github.com/sindresorh... 对应的方法,CTRL+C 过来呗

bluebird并发控制数量...并没有接触过,但是感觉底层都是async和await。

需要去温习下async和await了

async是什么?
async函数声明定义了一个异步函数,这个函数返回一个AsyncFunction对象。

async function name([param,[param[,...param]]]){

     statements

}

函数都有return的东西,async return的是什么,它返回的是AsyncFunction对象,表示执行包含在函数中的代码的异步函数,与new一个Object或者自己写的类一样,new AsyncFunction([arg1[, arg2[, ...argN]],] functionBody)

await是什么?

await操作符被用来等待一个Promise对象。它仅仅在async函数中使用。
[rv] = await expression;

说得形象些,async许下承诺,await接收承诺。
async许下了什么,许下一个承诺函数,也就是一个Promise。
await在等待什么,它在等待一个承诺,也就是一个Promise。
function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}
async function asyncCall() {
  console.log('calling');
  var result = await resolveAfter2Seconds();
  console.log(result);
  // expected output: "resolved"
}
asyncCall();

 =>"calling"//瞬间打印
 =>"resolved"//两秒后打印

分下下上面代码的工作原理:

asyncCall()执行
打印"calling"
resolveAfter2Seconds() 执行,Promise对象执行中
与此同时,await做好了接收Promise对象的准备
2秒后,await接收到Promise,赋值给result
打印"resolved"

温习结束,开始尝试异步控制的办法。

网友的代码只言片语,实在看不懂,到官网Promise.map来看下。

Promise.map(
`    Iterable<any>|Promise<Iterable<any>> input,
    function(any item, int index, int length) mapper,
    [Object {concurrency: int=Infinity} options]`
) -> Promise
`var promises = [];
for (var i = 0; i < fileNames.length; ++i) {
    promises.push(fs.readFileAsync(fileNames[i]));
}
Promise.all(promises).then(function() {
    console.log("don`e");
`});
// Using Promise.map:
Promise.map(fileNames, function(fileName) {
    // Promise.map awaits for returned promises as well.
    return fs.readFileAsync(fileName);
}).then(function() {
    console.log("done");`
});
Map Option: concurrency
You may optionally specify a concurrency limit:
...map(..., {concurrency: 3});
The concurrency limit applies to Promises returned by the mapper function and it basically limits the number of Promises created. For example, if concurrency is 3 and the mapper callback has been called enough so that there are three returned Promises currently pending, no further callbacks are called until one of the pending Promises resolves. So the mapper function will be called three times and it will be called again only after at least one of the Promises resolves.

懵逼,看完并不会用。

但是经过一番折腾,终于会用了,和Array.prototype.map有点类似的,只不过Promise.map传入的callback是一个AsyncFunction,也就是返回一个Promise的函数。

做到了并发数的控制,但是控制并发数以后图片数据还是获取不完整,10个并发,9个并发,5个并发,2个并发,都试过了,都是不行。必须改异步为同步,也就是限制每次只能请求1个,也就是并发数设置为1即可,但是用户体验上非常不好,用户只能看到一张一张图片,假设有60张,需要一张一张去请求,最后再合成一张完整的图片。

但是为了保证所有图片都能下载下来,能够正常显示所有的图片,这是目前唯一的办法。

Promise.map(photoData, function (photo) {
  return getImageInfoPromisified({
    src: photo.url
  }).then(function (res) {
    imagesArr.push(res.path);
    picx = (photo.left / 2) * ratio;
    picy = ((photo.top - 360) / 2) * ratio;
    picwidth = ((photo.width / 2) - 4) * ratio;
    picheight = ((photo.height / 2) - 4) * ratio;
    ctx.drawImage(res.path, picx, picy, picwidth, picheight);
    ctx.draw(true);

  }).catch(function () {
    console.error("get location failed")
  });
}, { concurrency: 1}).then(function () {
  wx.canvasToTempFilePath({
    canvasId: 'pic',
    success: function (res) {
      console.log(res.tempFilePath)
      that.setData({
        onlineImage: res.tempFilePath,
        canvasDisplay: "none"
      })
    }
  })
});

所有这个问题最后还是使用bluebird的promise.map方法控制并发请求数实现的,也就是 { concurrency: 1}这里,但是没有达到预期小于10个并发的处理预期,因为只要是并发,都会造成数据丢失。

所以只能将异步并发改为同步阻塞式渲染。

这个问题的出现与我们图片合成功能的设计架构有很大的问题,其实最理想的情况是,服务端直接返回一张完整的大图片,前端仅需一次网络请求即可,不存在同步异步这种令人头疼的问题。

虽然最后通过异步并发渲染转换为同步阻塞渲染,获取到了全部的图片资源,但是这并不是好的解决方案,所以这篇文章的主要是讲了一个异步并发数量控制的方法,也穿插着加入了一些异步并发渲染与同步阻塞渲染对比的内容,还要就是我对图片合成这个功能设计架构上的一些想法。

That's it !

期待和大家交流,共同进步,欢迎大家加入我创建的与前端开发密切相关的技术讨论小组:

努力成为优秀前端工程师!


趁你还年轻
4.1k 声望4.1k 粉丝