如何利用FormData API和流式上传实现超大文件分片上传及断点续传功能?
- 内容介绍
- 相关推荐
本文共计885个文字,预计阅读时间需要4分钟。
FormData 本身不支持流式上传,也不具备分片或断点续传的能力。所谓基于 FormData API 配合流式上传,实际上是指使用 FormData 封装每一片(而非整个文件),再配合前端控制逻辑与服务端协同,实现分片上传和断点续传。关键不在于 FormData 的强大,而在于如何巧妙地设计切片、标记、查询和重试机制。
合理切片:用 file.slice(),别塞整块进内存
浏览器原生 FormData 会把 append 的 Blob 加载进内存构造请求体。若单片太大(如 20MB),容易触发 V8 内存溢出或卡死。实测 Firefox 对 10MB/片已明显延迟,Chrome 在 50MB/片可能直接报 RangeError: Maximum call stack size exceeded。
- 推荐切片大小:2–5MB/片,兼顾传输效率与内存安全
- 必须用
file.slice(start, end)(File.prototype.slice),不是读成 ArrayBuffer 再转 Blob——后者会双倍占用内存 - 避免
URL.createObjectURL(file),它不自动释放,长期持有大文件引用会持续吃内存
唯一标识与上下文携带:每片带 uploadId + chunkIndex + totalChunks
服务端靠这三个字段识别归属、排序、校验完整性。uploadId 必须全局唯一且前端可控,不能依赖服务端返回后再发片(否则首片失败就断链)。
- 前端生成 uploadId:优先用
crypto.randomUUID();兼容旧环境可用Date.now() + Math.random().toString(36).substr(2, 9) - chunkIndex 从 0 开始,totalChunks 提前算好:
Math.ceil(file.size / chunkSize) - 每个 FormData 实例都 append 这三字段,例如:
formData.append('uploadId', id); formData.append('chunkIndex', i); formData.append('totalChunks', total)
断点续传:上传前先查,跳过已成功分片
断点能力不靠前端“记日志”,而靠服务端提供状态接口。前端每次上传前,必须主动查询哪些片已落盘成功,再决定发哪些。
- 调用
GET /upload/status?uploadId=xxx,服务端应返回已成功写入的 chunkIndex 数组,如[0,1,3,4] - 前端比对本地分片索引,跳过已存在的项,只发起缺失分片的请求
- 注意:上传单片成功后,建议再查一次状态,确认服务端确实写入(防止 Nginx 缓存 200 但后端失败)
服务端必须支持的四个能力,缺一不可
前端做得再细,服务端没对齐,上传就只是“看起来在分片”,实际无法合并或校验。
- 任意顺序接收:不强制按 0→1→2…上传,能接受乱序到达的分片
- 幂等写入:同一 uploadId + 同一 chunkIndex 重复提交,结果一致(如检查临时文件存在则跳过)
-
断点状态可查:提供
/upload/status接口,返回已收分片列表 -
可靠合并校验:所有分片收齐后,按 chunkIndex 升序拼接,并做 md5/sha256 校验,失败需明确报错(如
{"code":4001,"msg":"chunk 5 missing"})
本文共计885个文字,预计阅读时间需要4分钟。
FormData 本身不支持流式上传,也不具备分片或断点续传的能力。所谓基于 FormData API 配合流式上传,实际上是指使用 FormData 封装每一片(而非整个文件),再配合前端控制逻辑与服务端协同,实现分片上传和断点续传。关键不在于 FormData 的强大,而在于如何巧妙地设计切片、标记、查询和重试机制。
合理切片:用 file.slice(),别塞整块进内存
浏览器原生 FormData 会把 append 的 Blob 加载进内存构造请求体。若单片太大(如 20MB),容易触发 V8 内存溢出或卡死。实测 Firefox 对 10MB/片已明显延迟,Chrome 在 50MB/片可能直接报 RangeError: Maximum call stack size exceeded。
- 推荐切片大小:2–5MB/片,兼顾传输效率与内存安全
- 必须用
file.slice(start, end)(File.prototype.slice),不是读成 ArrayBuffer 再转 Blob——后者会双倍占用内存 - 避免
URL.createObjectURL(file),它不自动释放,长期持有大文件引用会持续吃内存
唯一标识与上下文携带:每片带 uploadId + chunkIndex + totalChunks
服务端靠这三个字段识别归属、排序、校验完整性。uploadId 必须全局唯一且前端可控,不能依赖服务端返回后再发片(否则首片失败就断链)。
- 前端生成 uploadId:优先用
crypto.randomUUID();兼容旧环境可用Date.now() + Math.random().toString(36).substr(2, 9) - chunkIndex 从 0 开始,totalChunks 提前算好:
Math.ceil(file.size / chunkSize) - 每个 FormData 实例都 append 这三字段,例如:
formData.append('uploadId', id); formData.append('chunkIndex', i); formData.append('totalChunks', total)
断点续传:上传前先查,跳过已成功分片
断点能力不靠前端“记日志”,而靠服务端提供状态接口。前端每次上传前,必须主动查询哪些片已落盘成功,再决定发哪些。
- 调用
GET /upload/status?uploadId=xxx,服务端应返回已成功写入的 chunkIndex 数组,如[0,1,3,4] - 前端比对本地分片索引,跳过已存在的项,只发起缺失分片的请求
- 注意:上传单片成功后,建议再查一次状态,确认服务端确实写入(防止 Nginx 缓存 200 但后端失败)
服务端必须支持的四个能力,缺一不可
前端做得再细,服务端没对齐,上传就只是“看起来在分片”,实际无法合并或校验。
- 任意顺序接收:不强制按 0→1→2…上传,能接受乱序到达的分片
- 幂等写入:同一 uploadId + 同一 chunkIndex 重复提交,结果一致(如检查临时文件存在则跳过)
-
断点状态可查:提供
/upload/status接口,返回已收分片列表 -
可靠合并校验:所有分片收齐后,按 chunkIndex 升序拼接,并做 md5/sha256 校验,失败需明确报错(如
{"code":4001,"msg":"chunk 5 missing"})

