前端JS如何处理大文件分片上传的进度计算
大文件上传是前端绕不开的一个场景:视频、设计稿、安装包,动辄几百 MB 甚至几个 GB。直接 `input[type=file]` 一次性 POST 出去,网络稍微抖动就可能前功尽弃。于是大家普遍转向分片上传:把文件切成若干块,并发发送,失败重试,最后让服务端合并。
但分片之后,进度条反而更难做了。很多实现一开始会用“已完成分片数 / 总分片数”来算,结果进度条要么一格一格跳,要么在并发上传时忽高忽低。问题在于,分片上传的进度不只是“传了几个片”,还包括“每个片内部传了多少字节”。
分片上传的进度来自两个层级
分片上传的进度可以拆成两层来看。
第一层是分片级进度:当前这个分片从 0 字节传到 `chunk.size` 字节,走了多少。第二层是文件级进度:所有分片加起来,已经上传的字节数占总文件大小的比例。
单看第一层,浏览器其实已经给了能力。使用 `XMLHttpRequest` 时,`xhr.upload.onprogress` 会持续返回 `event.loaded` 和 `event.total`,前者是当前分片已上传字节,后者通常是分片大小。`fetch` 目前没有稳定的上传进度事件,所以在需要进度条的场景里,XHR 或者基于 XHR 封装的库仍然是更务实的选择。
单分片进度:优先用 XHR 而不是 fetch
一个最简的分片上传监听大致是这样:
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
updateChunkProgress(index, e.loaded, e.total);
}
};
xhr.open('POST', uploadUrl);
xhr.send(chunkBlob);
这里拿到的 `e.loaded` 只代表当前分片的上传进度。如果并发上传 3 个分片,每个分片都有自己的 `onprogress`,你需要把这些局部进度汇总成全局进度。
并发上传时,总进度应该按字节汇总
最稳的做法是维护一张分片状态表。每个分片记录它的总大小、已上传字节、是否已完成。每次任意分片进度更新,就重新计算全局已上传字节。
不要简单地用“完成数 / 总数”,因为并发时,每个分片的实际进度不一样。文件级进度应该是:
总进度 = 已上传字节总和 / 文件总大小
已上传字节总和包括两部分:已经完成的分片按完整大小计入;正在上传的分片按 `loaded` 计入。伪代码可以写成:
const chunkState = new Map();
function calcUploadedBytes() {
let uploaded = 0;
for (const chunk of chunkState.values()) {
uploaded += chunk.done ? chunk.size : chunk.loaded;
}
return uploaded;
}
function updateChunkProgress(index, loaded, total) {
const prev = chunkState.get(index) || { loaded: 0, size: total, done: false };
if (prev.done) return;
chunkState.set(index, {
...prev,
loaded: Math.min(loaded, total),
done: loaded >= total,
});
renderProgress(calcUploadedBytes() / file.size);
}
这样做的好处是,每次都是全量重算,不需要手动维护增量,能避免暂停、重试、并发乱序带来的重复计数问题。分片数量通常几十到几百,按这个量级做 O(n) 汇总,性能完全可接受。
并发控制与进度条节流
分片上传一般不会无限并发,浏览器对同域名请求数也有限制。可以用一个简单的 Promise 池控制并发,比如同时跑 3 到 5 个分片。
另一个容易被忽略的点是 UI 刷新频率。`onprogress` 触发非常密集,如果每次都 `setState` 或者直接操作 DOM,页面会卡。推荐用 `requestAnimationFrame` 合并刷新,或者按 100ms 节流一次。进度条看起来会更顺滑,也不会拖慢上传本身。
暂停、续传和重试时,进度要能恢复
如果支持续传,初始化时应该先向服务端询问已上传分片列表,把完成的分片提前计入 `chunkState`,并标记 `done: true`。这样进度条不会从 0 重新开始。
失败重试时要注意:重试前先把该分片的 `loaded` 重置为 0,再重新发送。否则之前记过的字节会残留,总进度可能虚高。使用“全量重算”策略时,这类问题会少很多,因为最终数值只取决于每个分片当前的状态。
别忘了服务端合并阶段
前端所有分片发送完成,不代表文件已经可用。服务端还需要合并分片,甚至做校验。如果进度条在最后一片传完直接跳到 100%,用户可能会立刻关闭页面。更合理的做法是:分片上传完成后进入“合并中”状态,等服务端返回合并成功,再显示最终完成。
总结一下,大文件分片上传的进度计算,核心不是“数分片”,而是“算字节”。用 XHR 拿到每个分片的上传进度,维护分片状态表,按已上传字节汇总全局进度,再配合并发控制、节流刷新和续传恢复,进度条才能真正反映上传过程,而不是一个装饰性的动画。
转载请注明出处,版权归原作者所有。
管理员
黑卡会员



