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

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

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


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

  • 先计算 文件的 Hash (如 md5 或 sha-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. 断网恢复 页面刷新后继续上传未完成的部分

相关推荐
李纲明15 小时前
微信小程序 getSystemInfo 提示已废弃、屏幕高度算错胶囊还对不齐?getWindowInfo 和 getDeviceInfo 怎么接才稳
javascript·微信小程序·小程序·小程序开发·devdiy
deli00715 小时前
从一句反馈到一次升级:Mystudy 的自进化实践
前端
FungLeo15 小时前
成为全栈·Vue 管理后台篇·用 fetch 建一层可控的请求内核
前端·javascript·vue.js·http·fetch·formdata·接口封装
用户842981424181016 小时前
ByteJS,让重要的 JavaScript 以字节码运行。
javascript
FungLeo17 小时前
成为全栈·Vue 管理后台篇·Vue Router如何守住多角色后台边界
前端·javascript·vue.js·rbac·vue router·路由守卫·前端权限
FungLeo17 小时前
成为全栈·Vue 管理后台篇·分页只显示第一页:从接口total 定位到前端修复
前端·vue.js·状态模式·分页·naive ui·服务端分页·前端问题排查
时间的拾荒人17 小时前
Redis 集合类型详解:Set 与 Zset
redis·缓存·面试
ZhengEnCi18 小时前
苹果滑动为什么有「肉感」:橡皮筋越界阻尼与弹簧动画的物理原理
前端
Moment18 小时前
不会后期的我,发现了一个开源胶片神器😍😍😍
前端·后端·开源
L1mbo18 小时前
Lite模式 缓解微信开发者工具卡顿,附 uni-app 实践方案
前端·微信小程序