登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

前端上传大文件怎么用 Blob slice 实现可恢复分片

来源:17golang原创

时间:2026-09-07 20:35:47 321浏览 收藏

前端大文件上传不要把整个 File 一次塞进请求。更稳妥的做法是用 file.slice(start, end) 取出一个 Blob,为每个分片携带稳定的 uploadIdindex,上传失败只重试当前分片;页面刷新后再向服务端查询已收到的索引,跳过它们继续传。这样“可恢复”依赖的是服务端保存的分片状态,而不是浏览器里一个容易丢失的循环变量。

要点速览
  • Blob.slice(start, end) 的 end 是排除位置,适合按固定字节范围切片。
  • 恢复标识至少要稳定包含文件特征,服务端再把它映射到唯一 uploadId
  • 上传分片时不要手动设置 multipart/form-data 的 Content-Type,让浏览器生成 boundary。
  • 只有服务端确认所有 index 到齐,前端才请求合并;重复请求必须按 uploadId 与 index 幂等处理。

一、先固定恢复标识和接口契约

先把前后端约定写清楚。示例用文件名、大小和最后修改时间组成 resumeKey;生产环境可换成服务端签发的指纹或内容哈希,但原则不变:同一个文件再次选择时,初始化接口应返回同一个上传任务或明确告诉前端哪些分片已经存在。

接口前端发送服务端返回
POST /api/uploads/initname、size、lastModified、chunkSize、resumeKeyuploadId、chunkSize、received
POST /api/uploads/chunkuploadId、index、total、chunk、filenameindex、stored
POST /api/uploads/completeuploadId、totalmerged、url

received 是服务端已持久化的分片索引数组,例如 [0, 1, 3]。前端不应只保存“上次传到了第几个”,因为第 2 片可能失败,而第 3 片已经成功。

二、用 Blob.slice 切出单个分片

MDN 对 Blob.slice() 的定义是返回原 Blob 某个字节子集的新 Blob。它不会要求前端把整个文件转成一个大数组。下面的函数只负责计算范围并取片,end 不包含在结果中。

前端大文件上传中 File 经过 Blob slice 切成分片并由 FormData 携带 uploadId 与 index 的结构图
图1:浏览器只把当前 Blob 子集装进 FormData,上传请求携带分片索引和恢复标识。
function getChunk(file, index, chunkSize) {
  // end 是排除位置,最后一片不足 chunkSize 也能正常返回。
  const start = index * chunkSize;
  const end = Math.min(start + chunkSize, file.size);
  return file.slice(start, end);
}

function getTotalChunks(file, chunkSize) {
  // 向上取整,保证尾部不足一片时仍有一个合法 index。
  return Math.ceil(file.size / chunkSize);
}

例如 chunkSize 设为 8 MiB,前端内存里同时只需要处理当前分片。这里不绑定某个固定大小,实际项目应结合网关限制、网络质量和服务端磁盘写入能力压测后再定。

三、给单片请求加幂等重试

重试对象应是当前 index,而不是从头重传整个文件。服务端用 uploadId + index 作为幂等键:同一分片重复到达时覆盖或直接返回已保存状态,但不能生成第二份分片。

async function uploadChunk(file, upload, index, maxRetry = 3) {
  const chunk = getChunk(file, index, upload.chunkSize);
  let lastError;

  for (let attempt = 0; attempt  setTimeout(resolve, 500 * 2 ** attempt));
    }
  }
  throw lastError;
}

这段代码没有把 Content-Type 写死,是因为 FormData 的 multipart boundary 需要由浏览器生成。接口返回 4xx 时应先修正参数或任务状态,不要无条件重试;只有超时、断网等可恢复错误适合进入退避循环。

四、刷新后跳过已完成分片

初始化接口需要根据 resumeKey 返回 uploadIdreceived。前端将数组转成 Set,每次循环只提交缺失的索引。真正的断点续传因此由服务端状态驱动。

可恢复分片上传中 uploadId 关联 received 分片集合并在缺失分片齐全后进入 merge 的结构图
图2:恢复的关键不是记住循环位置,而是由 uploadId 对应服务端的 received 分片集合。
async function uploadFile(file) {
  const chunkSize = 8 * 1024 * 1024;
  const total = getTotalChunks(file, chunkSize);
  const resumeKey = [file.name, file.size, file.lastModified].join(":");

  const initResponse = await fetch("/api/uploads/init", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      name: file.name,
      size: file.size,
      lastModified: file.lastModified,
      chunkSize,
      resumeKey,
    }),
  });
  if (!initResponse.ok) throw new Error("无法初始化上传任务");
  const upload = { ...(await initResponse.json()), total, chunkSize };
  const received = new Set(upload.received || []);

  for (let index = 0; index 

示例用串行上传换取简单的顺序和较低的并发压力。若后续要并发,仍要保留同一个 uploadIdindex 幂等约束,并限制并发数;不要把“并发”误当成“可恢复”的实现本身。

五、全部到齐后再请求合并

合并接口必须由服务端再次校验分片数量和索引连续性,不能只相信前端循环已经结束。前端只在所有缺失索引都上传成功后调用它:

async function finishUpload(uploadId, total) {
  // 服务端按 uploadId 检查 0 到 total - 1,再按序合并临时分片。
  const response = await fetch("/api/uploads/complete", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ uploadId, total }),
  });
  if (!response.ok) throw new Error("分片未齐全,暂不合并");
  return response.json();
}

验收时重点看三种场景:中途断网后只补传缺失 index;重复点击继续上传不会产生重复文件;最后一片小于 chunkSize 时也能正常合并。只要这三项成立,Blob.slice、重试、恢复标识和合并链路就形成了闭环。

常见问题

为什么不能只把当前 index 存到 localStorage?

因为分片可能乱序完成,单个数字无法表达缺口。应以服务端的 received 集合为准,必要时再把 uploadId 缓存在本地作为查询入口。

每次重试都要重新调用 slice 吗?

可以重新调用,范围计算是确定的;也可以复用当前 Blob。关键是请求仍携带同一 uploadId 和 index,并让服务端幂等。

能否手动设置 multipart/form-data?

不要这样做。使用 FormData 时让浏览器生成带 boundary 的 Content-Type,否则服务端可能无法正确解析字段和文件。

文件名相同就能恢复吗?

不能。文件名可能相同但内容不同,至少还要结合大小和修改时间;对重要文件可改为服务端指纹或内容哈希,并在初始化接口中重新确认。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>