断点续传核心是「校验→切片→并发上传→合并」四步,每步存取localStorage进度;用fileId唯一标识文件,uploadAllChunks跳过已传切片,并发控制+重试,mergeChunks前校验完整性。

用 async/await 实现多步骤文件切片上传的断点续传,核心是把上传拆成「校验 → 切片 → 并发上传 → 合并」四步,并在每步中保存/读取进度,失败时能从上次中断处继续。
1. 前置:生成唯一文件标识 + 本地记录上传状态
断点续传的前提是能识别“同一个文件”。通常用文件名 + 文件大小 + 文件最后修改时间(或更稳妥的:前端计算文件 hash)生成唯一 key。建议用 spark-md5 或 Web Crypto API 在浏览器端算 chunk 级或 whole-file 级 hash。
上传状态(如已成功上传的切片序号、总切片数、合并是否完成)可存在 localStorage 或 IndexedDB 中。例如:
// 示例 key:fileId = `${fileName}_${fileSize}_${lastModified}`
const uploadState = {
fileId: 'report.pdf_12485732_1712345678900',
uploadedChunks: [0, 1, 3, 4], // 已成功上传的切片索引
totalChunks: 10,
merged: false
};
localStorage.setItem('upload_state_' + fileId, JSON.stringify(uploadState));
2. 切片与并发上传(带重试 + 进度恢复)
使用 async/await 控制异步流程,配合 Promise.allSettled() 容错并发上传,跳过已传过的切片:
立即学习“Java免费学习笔记(深入)”;
说明:
- 先从 localStorage 读取已上传的切片列表,过滤掉已成功的切片
- 对剩余切片调用
uploadChunk()(返回 Promise),每个请求带切片序号、总切片数、文件唯一 ID - 上传失败的切片单独重试(最多 3 次),失败后不阻塞其他切片
- 每次成功后更新
uploadedChunks并存回 localStorage
async function uploadAllChunks(file, fileId, chunkSize = 1024 * 1024) {
const chunks = [];
const totalChunks = Math.ceil(file.size / chunkSize);
<p>// 读取已有进度
const saved = JSON.parse(localStorage.getItem(<code>upload_state_${fileId}</code>) || '{}');
const uploadedChunks = new Set(saved.uploadedChunks || []);</p><p>// 构建待上传切片
for (let i = 0; i < totalChunks; i++) {
if (uploadedChunks.has(i)) continue;
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
chunks.push({ index: i, blob: file.slice(start, end) });
}</p><p>// 并发上传(最多 3 个同时进行)
const concurrency = 3;
for (let i = 0; i < chunks.length; i += concurrency) {
const batch = chunks.slice(i, i + concurrency);
const results = await Promise.allSettled(
batch.map(chunk => uploadChunk(fileId, chunk.index, chunk.blob, totalChunks))
);</p><pre class="brush:php;toolbar:false;">results.forEach((res, idx) => {
if (res.status === 'fulfilled') {
uploadedChunks.add(batch[idx].index);
localStorage.setItem(
`upload_state_${fileId}`,
JSON.stringify({ fileId, uploadedChunks: Array.from(uploadedChunks), totalChunks })
);
}
});} }
3. 单切片上传函数(含服务端校验与幂等)
uploadChunk() 要支持服务端校验:接收切片时检查该切片是否已存在(比如通过 fileId + chunkIndex 查询),若已存在直接返回 success,避免重复上传。前端需在请求头或 body 中带上这些元数据:
-
X-File-ID:文件唯一标识 -
X-Chunk-Index:当前切片序号(从 0 开始) -
X-Total-Chunks:总切片数 -
X-Chunk-Hash(可选):该切片的 md5,用于服务端内容校验
示例请求逻辑(含简单重试):
async function uploadChunk(fileId, index, blob, total) {
const formData = new FormData();
formData.append('chunk', blob, `chunk_${index}`);
formData.append('index', index);
<p>let retry = 0;
while (retry < 3) {
try {
const res = await fetch('/api/upload/chunk', {
method: 'POST',
headers: {
'X-File-ID': fileId,
'X-Chunk-Index': String(index),
'X-Total-Chunks': String(total)
},
body: formData
});</p><pre class="brush:php;toolbar:false;"> if (res.ok) return await res.json();
throw new Error(`HTTP ${res.status}`);
} catch (err) {
retry++;
if (retry === 3) throw err;
await new Promise(r => setTimeout(r, 1000 * retry)); // 指数退避
}} }
4. 合并切片(仅当所有切片上传完成)
所有切片上传成功后,调用合并接口。为防重复合并,服务端应幂等处理(比如检查 merged 状态或文件是否存在)。前端在合并前再次校验本地状态:
async function mergeChunks(fileId, fileName) {
const saved = JSON.parse(localStorage.getItem(`upload_state_${fileId}`) || '{}');
if (!saved.uploadedChunks || saved.uploadedChunks.length !== saved.totalChunks) {
throw new Error('还有切片未上传完成');
}
<p>const res = await fetch('/api/upload/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileId,
fileName,
totalChunks: saved.totalChunks
})
});</p><p>if (res.ok) {
// 清理本地状态
localStorage.removeItem(<code>upload_state_${fileId}</code>);
return await res.json();
}
throw new Error('合并失败');
}
完整流程可封装为一个可中断/恢复的上传任务:
async function resumeUpload(file) {
const fileId = generateFileId(file); // 自定义生成逻辑
await uploadAllChunks(file, fileId);
await mergeChunks(fileId, file.name);
console.log('上传完成 ✅');
}
用户刷新页面后,调用 resumeUpload(file) 会自动读取 localStorage 中的进度,跳过已传切片,继续后续流程。


















