前端上传大文件怎么用 Blob slice 实现可恢复分片
来源:17golang原创
时间:2026-09-07 20:35:47 321浏览 收藏
前端大文件上传不要把整个 File 一次塞进请求。更稳妥的做法是用 file.slice(start, end) 取出一个 Blob,为每个分片携带稳定的 uploadId 和 index,上传失败只重试当前分片;页面刷新后再向服务端查询已收到的索引,跳过它们继续传。这样“可恢复”依赖的是服务端保存的分片状态,而不是浏览器里一个容易丢失的循环变量。
Blob.slice(start, end)的 end 是排除位置,适合按固定字节范围切片。- 恢复标识至少要稳定包含文件特征,服务端再把它映射到唯一
uploadId。 - 上传分片时不要手动设置
multipart/form-data的 Content-Type,让浏览器生成 boundary。 - 只有服务端确认所有 index 到齐,前端才请求合并;重复请求必须按 uploadId 与 index 幂等处理。
一、先固定恢复标识和接口契约
先把前后端约定写清楚。示例用文件名、大小和最后修改时间组成 resumeKey;生产环境可换成服务端签发的指纹或内容哈希,但原则不变:同一个文件再次选择时,初始化接口应返回同一个上传任务或明确告诉前端哪些分片已经存在。
| 接口 | 前端发送 | 服务端返回 |
|---|---|---|
POST /api/uploads/init | name、size、lastModified、chunkSize、resumeKey | uploadId、chunkSize、received |
POST /api/uploads/chunk | uploadId、index、total、chunk、filename | index、stored |
POST /api/uploads/complete | uploadId、total | merged、url |
received 是服务端已持久化的分片索引数组,例如 [0, 1, 3]。前端不应只保存“上次传到了第几个”,因为第 2 片可能失败,而第 3 片已经成功。
二、用 Blob.slice 切出单个分片
MDN 对 Blob.slice() 的定义是返回原 Blob 某个字节子集的新 Blob。它不会要求前端把整个文件转成一个大数组。下面的函数只负责计算范围并取片,end 不包含在结果中。

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 返回 uploadId 和 received。前端将数组转成 Set,每次循环只提交缺失的索引。真正的断点续传因此由服务端状态驱动。

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
示例用串行上传换取简单的顺序和较低的并发压力。若后续要并发,仍要保留同一个 uploadId、index 幂等约束,并限制并发数;不要把“并发”误当成“可恢复”的实现本身。
五、全部到齐后再请求合并
合并接口必须由服务端再次校验分片数量和索引连续性,不能只相信前端循环已经结束。前端只在所有缺失索引都上传成功后调用它:
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,否则服务端可能无法正确解析字段和文件。
文件名相同就能恢复吗?
不能。文件名可能相同但内容不同,至少还要结合大小和修改时间;对重要文件可改为服务端指纹或内容哈希,并在初始化接口中重新确认。
-
255 收藏
-
130 收藏
-
459 收藏
-
282 收藏
-
381 收藏
-
364 收藏
-
173 收藏
-
文章 · 前端 | 4小时前 | pwa · Service Worker · 前端缓存 · 缓存更新 Service Worker skipWaiting clients.claim Cache Storage251 收藏
-
168 收藏
-
343 收藏
-
464 收藏
-
247 收藏
-
447 收藏
-
133 收藏
-
247 收藏
-
297 收藏
-
379 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习