前端JS如何处理大文件分片上传的进度计算

阿乐
阿乐 管理员 黑卡会员
发布于 2026-09-12 04:49 ·2 浏览 ·0 回复

大文件上传是前端绕不开的一个场景:视频、设计稿、安装包,动辄几百 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 拿到每个分片的上传进度,维护分片状态表,按已上传字节汇总全局进度,再配合并发控制、节流刷新和续传恢复,进度条才能真正反映上传过程,而不是一个装饰性的动画。

本文转载自 阿乐技术社区,原文地址:https://www.leleweb.cn/thread-248.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~