大批量文件上传:前端分组并发与断点重试
在前端开发中,经常会有大批量文件上传的需求。在面对“数千张高清图片批量上传”的需求时,传统的前端上传方案往往会出现各种各样的问题:
- 一次性全部上传:极易触发服务器
413 Payload Too Large报错,且巨大的 HTTP 包体容易导致连接超时。 - 暴力循环:瞬间发起数千个 HTTP 请求,不仅会挤爆浏览器的并发限制,导致大量请求排队挂起,还可能瞬间压垮后端服务。
针对这些问题,就需要前端分批上传,下面介绍一下一种支持分组、控制并发、且具备自动重试机制的大批量文件上传策略
1. 核心设计思路
- 分批: 将 大批量文件切分成若干个小批次(比如每批 50-100 个),
- 并发控制: 一次性全部并发(太卡)。我们维护一个并发池(例如限制同时 3 个请求)。当池子满了,必须等其中一个请求结束,才能塞入下一个。这最大化利用了带宽,同时保护了浏览器和服务器。
- 失败重试: 网络是不稳定的。如果某一批次失败,不应该直接报错停止,而是应该将失败的批次收集起来,在当前轮次结束后自动重试,直到达到最大重试次数。
2. 代码实战
下面是实现该逻辑的完整代码封装。我们使用了 axios 发送请求,利用 Promise.race 实现并发控制。
const handleBatchUpload = async (Files) => {
const UploadFiles = Array.from(Files) //将<input>接收到是filesList转为数组
if (UploadFiles.length === 0) return;
const MAX_RETRY_ROUNDS = 3;// 控制失败重试次数
const BATCH_SIZE = 50; // 每一组包含多少张图片
let batchList = [];
for (let i = 0; i < UploadFiles.length; i += BATCH_SIZE) {
// slice 切割数组,生成一组
let chunkFiles = UploadFiles.slice(i, i + BATCH_SIZE);
batchList.push({
index: i / BATCH_SIZE, // 批次序号
files: chunkFiles // 这一批包含的真实文件数组
});
}
// --- 核心函数:控制并发、分组上传、错误重试 ---
const uploadBatches = async function (list, retryCount) {
// 递归结束条件:列表为空
if (list.length === 0) {
console.log('所有批次上传完成');
// 如果有需要,可以在这里调用合并接口,或者直接提示成功
return;
}
if (retryCount > MAX_RETRY_ROUNDS) {
console.error(`重试 ${MAX_RETRY_ROUNDS} 次后仍有文件失败,停止上传。失败文件:`, list);
return;
}
let pool = []; // 并发池
let max = 3; // 最大并发量
let finish = 0; // 本轮完成的数量
let failList = []; // 失败的批次列表
console.log(`开始处理列表,剩余批次: ${list.length}`);
for (let i = 0; i < list.length; i++) {
let batchItem = list[i];
let formData = new FormData();
// 核心差异:这里要循环把这一组的所有文件都 append 进去
batchItem.files.forEach(file => {
// 'files' 必须与后端接口接收的字段名一致
formData.append('files', file);
});
// 可以附带批次信息
formData.append('batchIndex', batchItem.index);
// 发起请求
let task = axios({
method: 'post',
url: 'http://*****/upload-batch',
data: formData
});
task.then((data) => {
console.log(`批次 ${batchItem.index} 上传成功`);
// 成功后,从并发池移除
let idx = pool.findIndex(t => t === task);
if (idx !== -1) pool.splice(idx, 1);
}).catch((err) => {
console.error(`批次 ${batchItem.index} 上传失败`, err);
// 失败了,将整个批次加入失败列表
failList.push(batchItem);
// 即使失败也要从池中移除
let idx = pool.findIndex(t => t === task);
if (idx !== -1) pool.splice(idx, 1);
}).finally(() => {
finish++;
// 当本轮所有请求都结束(无论成功失败)
if (finish === list.length) {
// 递归调用:如果有失败的,只传失败的
console.log(`本轮结束,有 ${failList.length} 个批次失败,准备重试...`);
uploadBatches(failList, retryCount + 1);
}
});
pool.push(task);
// 并发控制:池子满了就等待最快的一个结束
if (pool.length === max) {
await Promise.race(pool);
}
}
}
// --- 3. 启动上传 ---
uploadBatches(batchList, 0);
}
3. 关键技术点解析
A. 使用 Promise.race 控制并发
在 for 循环中,我们不断将任务推入 pool。
pool.length === max时,意味着并发满了。await Promise.race(pool)会在池子中任意一个请求完成(resolve 或 reject)时立即解除阻塞。- 解除阻塞后,循环继续,下一个任务入池。
B. 使用递归重试
- 定义
failList收集本轮失败的批次。 - 在
finally中判断finish === list.length(本轮是否跑完)。 - 如果跑完了且
failList有数据,直接调用uploadBatches(failList, retryCount + 1)。
4.性能与稳定性分析
直觉认为:“一次性把所有文件扔给服务器最快,因为建立连接(TCP Handshake)的次数最少”。虽然分批上传增加了少量的 HTTP 握手开销(Overhead),但它有着极高的稳定性和用户体验的流畅度。并且合理的分批策略和并发控制反而比一次性上传更快。
在真实公网的环境下,我对1000张图片进行测试,进行分批上次和一次性上传时间测试,结果如下:
(1)分批上传:用时12.75s

(2)一次性上传:用时13.07s
