前端大文件上传如何通过 IndexedDB 实现离线续传?
在前端进行大文件上传时,可能会遇到网络中断、页面刷新或用户意外关闭页面的情况。使用 IndexedDB 可以存储已上传的文件切片,实现 断点续传,从而避免重新上传整个文件,提高上传效率。以下是实现方案:
1. 计算文件 Hash,确保文件唯一性
-
先计算 文件的 Hash (如
md5
或sha-256
),用于标识同一个文件。这样即使用户刷新页面,也能从 IndexedDB 读取已上传的切片。 -
可使用 Web Workers 计算 Hash,避免阻塞主线程。
-
采用 SparkMD5 库进行 Hash 计算:
inijs 复制编辑 import SparkMD5 from "spark-md5"; async function calculateFileHash(file) { return new Promise((resolve) => { const chunkSize = 10 * 1024 * 1024; // 每次读取 10MB const chunks = Math.ceil(file.size / chunkSize); const spark = new SparkMD5.ArrayBuffer(); let currentChunk = 0; const reader = new FileReader(); reader.onload = (event) => { spark.append(event.target.result); currentChunk++; if (currentChunk < chunks) { loadNext(); } else { resolve(spark.end()); // 返回文件 Hash } }; function loadNext() { const start = currentChunk * chunkSize; const end = start + chunkSize >= file.size ? file.size : start + chunkSize; reader.readAsArrayBuffer(file.slice(start, end)); } loadNext(); }); }
2. 切片文件并存入 IndexedDB
-
使用 IndexedDB 存储已上传的文件切片,避免页面刷新后数据丢失。
-
Dexie.js 库提供了更友好的 API 进行 IndexedDB 操作:
javascriptjs 复制编辑 import Dexie from "dexie"; const db = new Dexie("UploadDB"); db.version(1).stores({ chunks: "hash,chunkIndex,data", // 以文件 Hash + 分片索引作为唯一标识 }); async function storeChunk(hash, chunkIndex, chunkData) { await db.chunks.put({ hash, chunkIndex, data: chunkData }); }
-
切片并存入 IndexedDB:
inijs 复制编辑 async function splitFileAndStore(file, hash, chunkSize = 10 * 1024 * 1024) { const totalChunks = Math.ceil(file.size / chunkSize); for (let i = 0; i < totalChunks; i++) { const start = i * chunkSize; const end = start + chunkSize >= file.size ? file.size : start + chunkSize; const chunk = file.slice(start, end); await storeChunk(hash, i, chunk); } }
3. 检测已上传的切片,实现断点续传
-
查询 IndexedDB 获取已存储的切片:
javascriptjs 复制编辑 async function getUploadedChunks(hash) { const chunks = await db.chunks.where("hash").equals(hash).toArray(); return chunks.map((chunk) => chunk.chunkIndex); }
-
上传文件时,跳过已上传的切片:
inijs 复制编辑 async function uploadFile(file) { const hash = await calculateFileHash(file); const uploadedChunks = await getUploadedChunks(hash); const chunkSize = 10 * 1024 * 1024; const totalChunks = Math.ceil(file.size / chunkSize); for (let i = 0; i < totalChunks; i++) { if (uploadedChunks.includes(i)) { console.log(`Chunk ${i} 已上传,跳过`); continue; } const start = i * chunkSize; const end = start + chunkSize >= file.size ? file.size : start + chunkSize; const chunk = file.slice(start, end); await uploadChunkToServer(hash, i, chunk); // 上传到服务器 await storeChunk(hash, i, chunk); // 继续存入 IndexedDB,避免再次上传 } }
4. 服务器合并切片
-
前端请求合并接口,服务器合并所有切片:
javascriptjs 复制编辑 async function mergeChunks(hash, totalChunks) { await fetch("/api/merge", { method: "POST", body: JSON.stringify({ hash, totalChunks }), headers: { "Content-Type": "application/json" }, }); }
5. 页面刷新或断网恢复后自动续传
-
在页面加载时检查 IndexedDB 中是否有未完成的上传任务:
javascriptjs 复制编辑 async function resumeUpload(file) { const hash = await calculateFileHash(file); const uploadedChunks = await getUploadedChunks(hash); console.log(`恢复上传,已上传的切片:`, uploadedChunks); await uploadFile(file); // 继续上传 }
IndexedDB + 断点续传的优势
支持离线存储 :IndexedDB 允许存储大文件数据,页面刷新不丢失进度。
减少网络开销 :跳过已上传的切片,提高上传效率。
支持断网续传 :网络恢复后自动续传,用户体验更好。
高效存储:IndexedDB 在浏览器本地存储数据,适合大文件分片存储。
进阶优化
- Web Workers 计算 Hash:避免主线程阻塞,提高性能。
- Service Worker 结合 PWA:可支持离线存储和自动恢复上传。
- WebAssembly 计算 Hash:优化性能,减少 JS 计算开销。
IndexedDB + 断点续传 是前端大文件上传的最佳实践之一,可以有效减少带宽消耗,提高用户体验!
总结
步骤 | 说明 |
---|---|
1. 计算 Hash | 计算文件 Hash,确保文件唯一性 |
2. 切片存储 | 切片并存入 IndexedDB,避免重新上传 |
3. 断点续传 | 查询 IndexedDB 记录已上传的部分,跳过已上传的切片 |
4. 服务器合并 | 前端通知服务器合并已上传的切片 |
5. 断网恢复 | 页面刷新后继续上传未完成的部分 |