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

前端大文件分片上传怎么做:切片校验、并发窗口和失败重试

来源:17golang原创

时间:2026-07-20 14:40:02 211浏览 收藏

产品视频一到 800MB,单个 fetch 请求就开始暴露问题:网络抖一下要重传整文件,页面刷新后也不知道已经传到哪一步。更稳的做法是把 File 切成固定大小的分片,让服务端按文件指纹和分片编号接收;前端只维持一个有限并发窗口,失败时回收对应编号,不影响已经成功的部分。

实践要点
  • 分片大小先按网络和服务端限制确定,示例使用 8 MiB,不把并发数当成唯一性能开关。
  • 每个分片都带上 fileHash、chunkIndex、chunkHash 和 totalChunks,重试可以精确到一个分片。
  • 浏览器端只允许固定数量的请求在途,失败分片进入队列,最多重试 3 次并保留错误原因。
  • 上传完成不能只看 HTTP 200,还要核对服务端返回的已收分片数和最终合并状态。

先把一次上传拆成可回查的流水线

分片上传不是把一个请求改成很多请求。它至少包含“识别文件、生成分片、校验、上传、合并”五个状态。服务端若只按到达顺序写临时文件,重试和乱序到达很快就会把文件拼坏;前端若只保存一个进度百分比,刷新后也无法知道缺哪一片。

这套示例约定三个接口:POST /upload/init 返回文件是否已存在和缺失编号,PUT /upload/part 接收单片,POST /upload/complete 在所有编号齐全后触发合并。接口名只是示例,真正重要的是状态字段要稳定。

前端大文件上传的数据生命周期:File 切片、分片校验、上传清单和最终合并状态

用 File.slice 生成带指纹的分片清单

先把文件的基础信息固定下来:文件名只用于展示,真正用于恢复的是文件大小、最后修改时间和内容指纹。生产环境建议在服务端重新计算最终文件哈希,浏览器指纹只负责帮助查询上传会话。

const PART_SIZE = 8 * 1024 * 1024;

async function sha256(blob) {
  const buffer = await blob.arrayBuffer();
  const digest = await crypto.subtle.digest('SHA-256', buffer);
  return [...new Uint8Array(digest)]
    .map(byte => byte.toString(16).padStart(2, '0'))
    .join('');
}

async function buildParts(file) {
  const total = Math.ceil(file.size / PART_SIZE);
  const parts = [];
  for (let index = 0; index 

这里有一个容易忽略的成本:arrayBuffer() 会把当前分片读进内存。不要为了算整个文件哈希再复制一份 800MB 的数组;先按分片建立会话,合并后让服务端做最终校验,页面的内存压力会小很多。

分片清单至少要能回答三个问题

字段用途异常时怎么用
fileHash定位同一个上传会话刷新后查询缺失编号
chunkIndex标记分片位置只重传失败编号
chunkHash核对内容是否被截断服务端拒绝错误分片
totalChunks判断是否可以合并阻止提前合并

触发上传前先做会话和权限核对

/upload/init 不只是“拿一个 uploadId”。它应当校验登录态、文件大小上限、文件类型和目标目录权限,并返回 missingIndexes。如果服务端发现同一 fileHash 已经合并完成,前端可以直接展示完成结果;如果只收到一半,就从缺失列表继续。

const session = await fetch('/upload/init', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    fileHash,
    fileName: file.name,
    fileSize: file.size,
    totalChunks
  })
}).then(response => {
  if (!response.ok) throw new Error(`init failed: ${response.status}`);
  return response.json();
});

const pending = new Set(session.missingIndexes);

权限失败、文件超限和会话过期不要混成“上传失败”。它们需要不同的提示和处理:权限问题回到登录或目录选择,超限问题让用户更换文件,会话过期则重新初始化但保留本地分片清单。

用有限并发窗口推动分片流水线

并发 20 个请求看起来进度很快,实际可能先把浏览器连接、服务端临时磁盘和网关限流一起推满。我更建议从 3 到 6 个并发开始,把每一片的响应时间、重试次数和服务端拒绝原因记录下来,再决定是否调整。

分片上传的有限并发窗口:成功分片前进、失败编号回收、完成后合并
async function uploadPart(session, part) {
  const response = await fetch('/upload/part', {
    method: 'PUT',
    headers: {
      'X-Upload-Id': session.uploadId,
      'X-File-Hash': session.fileHash,
      'X-Chunk-Index': String(part.index),
      'X-Chunk-Hash': part.hash
    },
    body: part.blob
  });
  if (!response.ok) throw new Error(`part ${part.index}: ${response.status}`);
  return response.json();
}

async function runWindow(parts, session, limit = 4) {
  const queue = [...parts];
  const done = [];
  async function worker() {
    while (queue.length) {
      const part = queue.shift();
      if (!part) return;
      const result = await uploadPart(session, part);
      done.push({ index: part.index, etag: result.etag });
    }
  }
  await Promise.all(Array.from({ length: limit }, worker));
  return done;
}

示例里的队列只负责演示窗口。真实项目还要加暂停标记、AbortController 和失败队列。停止上传时不要直接清掉 uploadId,否则用户点击继续时只能从头询问服务端。

失败重试要有边界,不能无限打同一个分片

网络断开、网关 502 和服务端校验失败不是一回事。前两类可以指数退避重试,哈希不匹配则应当重新读取该片,必要时重新生成清单;连续 3 次都失败,就把编号、状态码和最后一次错误交给用户或监控。

async function retryPart(session, part, maxAttempts = 3) {
  let lastError;
  for (let attempt = 1; attempt  setTimeout(resolve, 500 * 2 ** (attempt - 1)));
      }
    }
  }
  throw lastError;
}

不要把所有失败都自动重试。401、403、413 和明确的校验错误,重试只会增加噪声;网络错误、408、429、502、503 才适合放进短暂重试队列。服务端还应使用 uploadId + chunkIndex 做幂等键,避免同一片重复到达时追加两次。

合并前后的质量门禁要落到返回值

前端收到最后一个分片成功,并不代表文件可下载。调用 /upload/complete 前先把本地完成集合与服务端查询结果对齐,再由服务端检查编号连续性、每片哈希和最终文件大小。合并过程最好返回 merging,不要让前端把它误显示成“完成”。

  • 分片数量一致:receivedCount === totalChunks
  • 分片编号完整:没有重复编号,也没有缺失编号。
  • 大小一致:合并文件字节数等于原始 file.size
  • 状态一致:uploading -> merging -> completed,失败时保留 failed 原因。

这个检查点也适合做通知回路:只把 completed 作为业务后续处理的触发条件,缩略图生成、转码或入库任务不要监听“最后一片已上传”这个中间事件。

常见问题:暂停、刷新和小文件怎么处理

小于一个分片的文件也需要走分片接口吗?

不一定。小文件可以走普通上传接口,减少初始化和哈希开销;但如果服务端只有统一的分片协议,也可以把它视为只有 1 个分片,保持状态模型一致。

刷新页面后还能继续上传吗?

可以,前提是用稳定的文件指纹查询 uploadId 和缺失编号。不要只把进度百分比放在 localStorage,百分比不能证明服务端真的保存了哪些分片。

并发数越大上传越快吗?

不是。并发窗口受浏览器连接、用户带宽、网关限流、服务端磁盘和合并能力共同影响。先从 3 到 6 观察成功率和平均耗时,再按网络条件做分档。

为什么最后一片成功了,文件仍然不能下载?

最后一片只代表一个编号到达。还需要完成缺失检查、哈希核对、合并和最终状态切换;任何一步失败,都应该展示明确的合并状态或重试入口。

把上传做成可暂停、可恢复的前端组件

稳定的分片上传组件,核心不是更复杂的进度条,而是把每个阶段都留下可验证的状态:初始化是否通过、哪些编号已保存、哪个分片正在重试、合并是否完成。先用 8 MiB 和 4 路并发跑通,再根据真实网络数据调整分片大小与窗口,通常比一开始堆满配置更容易维护。

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