前端大文件分片上传怎么做:切片校验、并发窗口和失败重试
来源: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.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 路并发跑通,再根据真实网络数据调整分片大小与窗口,通常比一开始堆满配置更容易维护。
-
255 收藏
-
130 收藏
-
459 收藏
-
207 收藏
-
282 收藏
-
文章 · 前端 | 1天前 | 前端 · 性能优化 · javascript · 浏览器性能 · PerformanceObserver · JSON解析 PerformanceObserver 浏览器性能 Long Task 主线程卡顿421 收藏
-
文章 · 前端 | 2天前 | 前端 · Cookie · cors · 自动化测试 · playwright · 前端 cookie cors Playwright SameSite 登录态 跨域测试285 收藏
-
339 收藏
-
491 收藏
-
248 收藏
-
241 收藏
-
文章 · 前端 | 1星期前 | 前端 · javascript · css · View Transition API · JavaScript 浏览器兼容 View Transition API document.startViewTransition 前端筛选列表 SPA过渡196 收藏
-
文章 · 前端 | 1星期前 | 前端 · vite · 运维手册 · 白屏排查 · CDN缓存 · 发布回滚 · React 前端 白屏 vite CDN缓存 index.html 发布回滚 JS 404342 收藏
-
文章 · 前端 | 1星期前 | 前端 · 性能优化 · css · Core Web Vitals · 渲染性能 · 前端 渲染性能 CSS性能 CLS content-visibility contain-intrinsic-size Layout430 收藏
-
260 收藏
-
文章 · 前端 | 2星期前 | 前端 · javascript · AbortController · 表单提交 · AbortController 旧响应覆盖 前端重复提交 loading锁 fetch取消 按钮防抖442 收藏
-
文章 · 前端 | 3星期前 | 前端 · 缓存 · Service Worker · 白屏 · 发布故障 · 缓存策略 前端白屏 Service Worker CacheStorage 资源404 发布回滚469 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习