HTML5本身不支持断点续传,需前端用Blob.slice()切片、服务端记录已传分片并返回uploaded列表,前端据此跳过已传块;关键在uploadId基于文件内容哈希、禁用缓存、缓存File实例。
uploadWithResume API浏览器原生不提供断点续传上传接口,XMLHttpRequest 和 fetch 都只负责单次请求。所谓“断点续传”,本质是前端主动切片 + 服务端配合记录 + 前后端约定校验逻辑。关键不在 HTML5 本身,而在如何用 Blob.slice()、File 对象和 Range 请求头协同工作。
断点续传不是“暂停再继续”,而是把大文件拆成固定大小的 Blob,每次传一个块,并让服务端返回已接收的块列表,前端跳过已传部分。常见错误是直接对整个 File 调用 send(),结果失败就全重传。
file.slice(start, end) 切出每个分片(注意:slice 返回新 Blob,不是字符串截取)uploadId(由首次请求创建)、chunkIndex、totalChunks、chunkHash(可选,防重复)uploadId 下已成功接收的 chunkIndex 集合,响应时返回 { uploaded: [0,2,4] }
chunkIndex 开始传,而非简单 +1fetch 传分片时必须处理 409 Conflict 和 208 Already Reported
很多实现忽略服务端对重复分片的响应码。当网络抖动导致分片重发,服务端应返回 409

208(避免前端误判为失败),前端收到后应直接跳过该块,继续下一个。否则会卡死或覆盖写错位置。
fetch('/upload', {
method: 'POST',
headers: { 'X-Upload-ID': uploadId, 'X-Chunk-Index': '5' },
body: blobSlice
})
.then(res => {
if (res.status === 208 || res.status === 409) return { skip: true };
return res.json();
});
开发中常因以下问题导致“看似续传,实则重头来”:
File.lastModified 或 File.name 变了,服务端认为是新文件,清空历史记录 → 必须用文件内容哈希(如 spark-md5 前 2MB)作为 uploadId 基础fetch 可能复用缓存响应 → 所有上传请求 URL 后加随机参数或禁用缓存:headers: { 'Cache-Control': 'no-cache' }
File 引用,用户切换页面再回来时 file.slice() 报错 → 必须在首次选择文件时缓存 file 实例,不要依赖 input 元素的 files[0] 后续读取