广告位 · 页头(后台-模块管理-新建模块后替换此区域)

微信小程序多文件上传:递归上传和Promise.all并发上传,哪种更适合业务场景?

在微信小程序里,文件上传主要依赖 wx.uploadFile 接口,它每次只能接收一个文件路径。当页面需要同时提交多张图片或者多个文档时,开发者通常采用两种策略:一是递归调用,按顺序逐个上传;二是用 Promise.all 把所有上传任务一次性并发发出。两种方案各有优缺点,并且涉及到错误重试、进度展示和并发上限等细节。本文将结合具体代码,对这两种方案展开对比,并给出一种更稳妥的折中实现。

微信小程序多文件上传:递归上传和Promise.all并发上传,哪种更适合业务场景?

递归上传:顺序可靠但耗时累积

递归上传的核心思路是维护一个文件队列,每次只处理一个文件,等待该文件上传完成后再处理下一个。在 JavaScript 中通常使用 async/await 配合 for 循环,将 wx.uploadFile 封装成返回 Promise 的函数,然后逐个等待结果。由于前一个请求结束后才会发起后一个请求,文件提交顺序与数组顺序完全一致。

这种上传方式的优势在于错误处理非常清晰。因为每次只有一个上传任务在执行,可以精确知道是哪个文件失败,并在 catch 块中决定是终止循环、跳过当前文件还是尝试重新上传。对于需要严格保证文件顺序的业务,例如上传一系列需要服务器按顺序拼接的图片,递归上传是更安全的选择。另外,由于不会同时建立多个网络连接,对小程序底层资源和内存的占用也比较小。

但递归上传最明显的缺点是耗时。假设单个文件上传耗时 t,n 个文件总耗时接近 n*t,当文件数量较多时,用户需要等待很长时间。更糟的是,如果某个文件因为网络波动变慢,后续所有文件都会被阻塞,用户体验会急剧下降。因此递归上传更适合文件数量不多或对顺序有强制要求的场景。

// 将 wx.uploadFile 包装为 Promise
function uploadSingle(filePath) {
  return new Promise(function(resolve, reject) {
    wx.uploadFile({
      url: 'https://ipipp.com/upload',
      filePath: filePath,
      name: 'file',
      success: function(res) {
        resolve(JSON.parse(res.data));
      },
      fail: function(err) {
        reject(err);
      }
    });
  });
}
// 递归顺序上传
async function uploadSequentially(fileList) {
  const results = [];
  for (let i = 0; i < fileList.length; i++) {
    try {
      const res = await uploadSingle(fileList[i]);
      results.push({ file: fileList[i], success: true, data: res });
    } catch (err) {
      results.push({ file: fileList[i], success: false, error: err });
    }
  }
  return results;
}

上面代码中用 for 循环而不是 forEach,因为 forEach 无法与 async/await 正确配合。每个文件的结果会被收集到 results 数组里,即使中间某个文件失败,循环仍会继续,最后返回完整的上传状态列表,方便上层业务处理。

Promise.all并发上传:速度快但风险集中

Promise.all 的用法是对一个 Promise 数组进行并行等待,所有 Promise 都 resolve 后整体 resolve,任意一个 reject 则整体 reject。在多文件上传场景中,可以将每个文件的上传函数直接放入数组,由 Promise.all 同时发起所有请求。总耗时不再是每个文件耗时之和,而是接近于耗时最长的那个文件,因此在文件数量较多时能大幅缩短等待时间。

下面是一个基础实现:

function uploadAll(fileList) {
  const tasks = fileList.map(function(filePath) {
    return uploadSingle(filePath);
  });
  return Promise.all(tasks);
}

这个实现虽然简洁,但存在几个隐患。第一,所有请求几乎在同一时刻发起,如果 fileList 长度很大,小程序可能因为并发连接数限制而拒绝部分请求,甚至引起性能问题。第二,Promise.all 只要有一个任务失败,整个 Promise 会立即进入 reject,其他尚未完成的任务状态被忽略,业务层无法得知哪些文件上传成功了。第三,返回的数组顺序与输入顺序一致,但如果某个文件先完成,业务层无法即时感知,只能等所有任务结束。

为了改进错误处理,可以使用 Promise.allSettled,它会在所有任务完成后返回每个任务的状态,不会因为单个失败而中断。代码如下:

function uploadAllSettled(fileList) {
  const tasks = fileList.map(function(filePath) {
    return uploadSingle(filePath)
      .then(function(data) {
        return { file: filePath, status: 'fulfilled', data: data };
      })
      .catch(function(err) {
        return { file: filePath, status: 'rejected', reason: err };
      });
  });
  return Promise.allSettled(tasks);
}

即便如此,并发数量不受控制仍是 Promise.all 方案的硬伤。它适合对速度要求极高、文件数量不算太大且网络环境稳定的场景,同时需要开发者自行处理并发限制或改用其他方案。

折中方案:限制并发数的批量上传

递归上传过于保守,Promise.all 又过于激进。实际业务中更常见的做法是设置一个并发上限,比如同时最多上传 3 个文件,这样既能利用并发带来的速度提升,又能控制资源占用和请求数量。这种方案通常被称为并发池或信号量模式,通过维护一个工作队列和若干个 worker 来实现。

下面是一个带并发上限的实现,核心是使用 index 指针分配任务,每个 worker 不断从队列中取下一个文件直到取完:

async function uploadWithLimit(fileList, limit) {
  const results = new Array(fileList.length);
  let index = 0;
  const workers = [];
  for (let i = 0; i < limit; i++) {
    workers.push((async function() {
      while (index < fileList.length) {
        const current = index;
        index++;
        try {
          const res = await uploadSingle(fileList[current]);
          results[current] = { file: fileList[current], success: true, data: res };
        } catch (err) {
          results[current] = { file: fileList[current], success: false, error: err };
        }
      }
    })());
  }
  await Promise.all(workers);
  return results;
}

代码中通过 while 循环和共享的 index 变量实现任务分配,多个 worker 并发执行但总数不超过 limit。每个任务完成后会把结果写入 results 数组对应位置,这样既保留了顺序信息,又能独立处理每个文件的成功或失败,不会因为一个文件失败而影响其他任务。

这种方案在实际项目中非常实用。你可以根据手机性能、网络类型动态调整 limit 值,例如在 Wi-Fi 环境下设置为 5,在 4G 环境下设置为 3。同时,还可以结合 wx.uploadFile 的进度回调,累加已上传字节数,实现总体进度条。需要注意的是,limit 并不是越大越好,微信小程序对并发 TCP 连接数有内部限制,过高的并发可能导致请求被系统拒绝。

方案选型与对比总结

为了更直观地比较三种方式,下面用一个表格从几个关键维度进行梳理:

方案总耗时顺序控制错误定位资源占用实现复杂度
递归上传线性累加,最慢严格按顺序精准到单个文件低简单
Promise.all接近最慢单个,最快顺序由结果数组保证,但完成顺序不定需额外包装高简单
限制并发介于两者之间结果顺序可控精准到单个文件可控中等

从实际业务角度出发,如果文件数量小于 3 个,或者服务器要求文件必须按顺序到达,递归上传是最直接的选择,代码清晰且不容易出错。如果文件数量很大、用户对等待时间敏感,并且可以接受一定程度的并发,那么直接使用 Promise.all 配合 allSettled 也能满足要求,但需要做好网络异常处理。

大多数场景下,更推荐使用限制并发数的批量上传方案。它结合了递归的顺序可控和 Promise.all 的速度优势,通过调整 limit 参数可以适应不同网络环境。此外,不管采用哪种方案,都应该为上传任务设置超时控制,并支持用户主动取消,避免因为网络问题导致页面长时间无法操作。最终选择哪种方案,取决于你的业务对顺序、速度、稳定性的具体权重。

广告位 · 文章底部(后台-模块管理替换此区域)