在前端开发中,经常会有大批量文件上传的需求。在面对“数千张高清图片批量上传”的需求时,传统的前端上传方案往往会出现各种各样的问题:
一次性全部上传 :极易触发服务器 413 Payload Too Large 报错,且巨大的 HTTP 包体容易导致连接超时。
暴力循环 :瞬间发起数千个 HTTP 请求,不仅会挤爆浏览器的并发限制,导致大量请求排队挂起,还可能瞬间压垮后端服务。
针对这些问题,就需要前端分批上传,下面介绍一下一种支持分组、控制并发、且具备自动重试机制 的大批量文件上传策略
1. 核心设计思路
分批 : 将 大批量文件切分成若干个小批次(比如每批 50-100 个),
并发控制 : 一次性全部并发(太卡)。我们维护一个并发池(例如限制同时 3 个请求)。当池子满了,必须等其中一个请求结束,才能塞入下一个。这最大化利用了带宽,同时保护了浏览器和服务器。
失败重试 : 网络是不稳定的。如果某一批次失败,不应该直接报错停止,而是应该将失败的批次收集起来,在当前轮次结束后自动重试,直到达到最大重试次数。
2. 代码实战 下面是实现该逻辑的完整代码封装。我们使用了 axios 发送请求,利用 Promise.race 实现并发控制。
const handleBatchUpload = async (Files ) => { const UploadFiles = Array .from (Files ) 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 ) { 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 (); batchItem.files .forEach (file => { 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); } } } 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