断点续传是指下载或上传中断后能从中断位置继续传输的技术;其核心是通过HTTP Range头(下载)或Content-Range头(上传)指定字节范围,配合服务端持久化已传状态实现续传,前端需绕过FormData直接使用ReadableStream+fetch,并校验服务端实际进度。

不能直接用 FormData 配合 ReadableStream 实现流式断点续传。 FormData 与 ReadableStream 在语义和底层机制上冲突:FormData 是为构造 multipart/form-data 表单体设计的,要求所有字段(包括文件)在发送前就完全确定;而 ReadableStream 的核心价值是“边读边传”,不预加载整个文件。两者强行混合会导致 FormData 内部无法处理流式 body,浏览器会报错或静默失败。
FormData.append() 不接受 ReadableStream 作为 file 参数
当你尝试 formData.append('file', file.stream()),浏览器会抛出 TypeError:Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'。因为 FormData.append() 的第二个参数只接受 Blob、File 或 USVString,不接受 ReadableStream —— 即使 file.stream() 返回的是合法流,它也不符合该 API 的类型契约。
- 想用流式上传,必须绕过 FormData,直接把
ReadableStream作为fetch的body - 想用 FormData,就必须用
file.slice()切片 + 分片上传,再靠服务端协议支持断点续传 - 二者不可混用;所谓“FormData + ReadableStream”是常见误解,源于对 Streams API 和表单编码规范的混淆
真正可行的流式断点续传路径:fetch + stream + Content-Range
要实现内存可控、可中断、可恢复的大文件上传,必须放弃 FormData,改用原始流 + 自定义 HTTP 头。关键在于服务端能按字节范围接收并落盘:
- 调用
file.stream()获取流,用stream.getReader()分块读取Uint8Array - 手动维护已上传字节数
uploadedBytes,每次读取后更新 - 在
fetch请求中设置Content-Range头,例如bytes 0-1048575/1073741824 - 服务端需解析该头,将数据追加写入临时文件对应位置,并记录已接收范围
- 中断后,前端重新发起请求时从
uploadedBytes开始,服务端校验是否已存在该段
示例片段(简化):
const stream = file.stream();
const reader = stream.getReader();
let uploaded = 0;
const total = file.size;
<p>while (uploaded < total) {
const { done, value } = await reader.read();
if (done) break;</p><p>await fetch('/upload/resumable', {
method: 'PUT',
headers: {
'Content-Range': <code>bytes ${uploaded}-${uploaded + value.length - 1}/${total}</code>,
'Content-Type': 'application/octet-stream'
},
body: value
});</p><p>uploaded += value.length;
}为什么分片上传(非流式)反而更实用
虽然流式上传理论更优雅,但实际落地门槛高:服务端需完整实现 Content-Range 解析、随机写入、范围校验、合并触发等逻辑;且 Chrome/Edge 以外的浏览器对 fetch + 流式 body 的 PUT 支持不稳定。相比之下,基于 file.slice() 的分片上传更健壮:
- 每片仍是独立
Blob,可安全传给FormData - 每片带
index、hash、totalChunks等元数据,服务端易识别缺失片 - 前端可用
localStorage存已传index列表,恢复时只重传未完成片 - 失败重试粒度小,不依赖服务端的字节级状态管理
真正容易被忽略的点是:断点续传的“断点”不在前端内存里,而在服务端持久化状态中。前端存的 uploadedBytes 或 uploadedIndices 只是缓存,最终必须和服务端 /upload/status?hash=xxx 接口对齐 —— 否则多端同步、页面刷新、隐私模式都会导致状态丢失。


















