IndexedDB + 大文件上传:离线续传的最佳实践

前端大文件上传如何通过 IndexedDB 实现离线续传?

在前端进行大文件上传时,可能会遇到网络中断、页面刷新或用户意外关闭页面的情况。使用 IndexedDB 可以存储已上传的文件切片,实现 断点续传,从而避免重新上传整个文件,提高上传效率。以下是实现方案:


1. 计算文件 Hash,确保文件唯一性

  • 先计算 文件的 Hash (如 md5sha-256),用于标识同一个文件。这样即使用户刷新页面,也能从 IndexedDB 读取已上传的切片。

  • 可使用 Web Workers 计算 Hash,避免阻塞主线程。

  • 采用 SparkMD5 库进行 Hash 计算:

    ini 复制代码
    js
    复制编辑
    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 操作:

    javascript 复制代码
    js
    复制编辑
    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

    ini 复制代码
    js
    复制编辑
    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 获取已存储的切片

    javascript 复制代码
    js
    复制编辑
    async function getUploadedChunks(hash) {
        const chunks = await db.chunks.where("hash").equals(hash).toArray();
        return chunks.map((chunk) => chunk.chunkIndex);
    }
  • 上传文件时,跳过已上传的切片

    ini 复制代码
    js
    复制编辑
    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. 服务器合并切片

  • 前端请求合并接口,服务器合并所有切片

    javascript 复制代码
    js
    复制编辑
    async function mergeChunks(hash, totalChunks) {
        await fetch("/api/merge", {
            method: "POST",
            body: JSON.stringify({ hash, totalChunks }),
            headers: { "Content-Type": "application/json" },
        });
    }

5. 页面刷新或断网恢复后自动续传

  • 在页面加载时检查 IndexedDB 中是否有未完成的上传任务

    javascript 复制代码
    js
    复制编辑
    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. 断网恢复 页面刷新后继续上传未完成的部分

相关推荐
kite01212 小时前
浏览器工作原理06 [#]渲染流程(下):HTML、CSS和JavaScript是如何变成页面的
javascript·css·html
крон2 小时前
【Auto.js例程】华为备忘录导出到其他手机
开发语言·javascript·智能手机
coding随想4 小时前
JavaScript ES6 解构:优雅提取数据的艺术
前端·javascript·es6
年老体衰按不动键盘5 小时前
快速部署和启动Vue3项目
java·javascript·vue
小小小小宇5 小时前
一个小小的柯里化函数
前端
灵感__idea5 小时前
JavaScript高级程序设计(第5版):无处不在的集合
前端·javascript·程序员
小小小小宇5 小时前
前端双Token机制无感刷新
前端
小小小小宇5 小时前
重提React闭包陷阱
前端
小小小小宇5 小时前
前端XSS和CSRF以及CSP
前端
UFIT5 小时前
NoSQL之redis哨兵
java·前端·算法