IndexedDB 实战:浏览器端大文件存储与离线架构
为什么需要 IndexedDB?
浏览器存储方案各有局限:
| 方案 | 容量 | 数据类型 | 同步 API | 适用场景 |
|---|---|---|---|---|
| Cookie | ~4KB | 字符串 | ✅ | 会话标识 |
| localStorage | ~5MB | 字符串 | ✅ | 简单配置 |
| sessionStorage | ~5MB | 字符串 | ✅ | 临时状态 |
| IndexedDB | 数百 MB~GB | 任意结构化数据 | ❌ 异步 | 大文件、离线应用 |
对于需要在浏览器中缓存 PDF 处理中间结果、WASM 模块、或用户上传历史记录的工具站,IndexedDB 是唯一可行的选择。
核心概念
数据库 → 对象存储 → 记录
Database: "toolsku-cache"
├── ObjectStore: "wasm-modules" (key: moduleName)
├── ObjectStore: "file-cache" (key: fileHash, index: timestamp)
└── ObjectStore: "user-preferences" (key: settingKey)
事务与并发
IndexedDB 的事务是自动提交的,且同一时刻每个 ObjectStore 只能有一个读写事务:
const tx = db.transaction(['file-cache'], 'readwrite');
const store = tx.objectStore('file-cache');
await store.put({ hash: 'abc123', data: arrayBuffer, timestamp: Date.now() });
await tx.done; // 等待事务完成
工具库中的 IndexedDB 应用
WASM 模块缓存
ffmpeg.wasm 约 30MB,每次加载耗时 3-5 秒。缓存到 IndexedDB 后,二次访问几乎零延迟:
async function getWasmModule(name: string): Promise<ArrayBuffer> {
const cached = await idb.get('wasm-modules', name);
if (cached) return cached;
const response = await fetch(`/wasm/${name}.wasm`);
const buffer = await response.arrayBuffer();
await idb.put('wasm-modules', buffer, name);
return buffer;
}
大文件分块存储
超过 50MB 的文件不适合一次性存入 IndexedDB。分块策略:
文件 (200MB) → 切分为 4MB chunks → 分别存入 IndexedDB
key: "file:{hash}:chunk:{index}"
读取时按序拼接 → 还原完整文件
版本迁移
数据库 schema 变更时需要 onupgradeneeded 处理迁移:
const request = indexedDB.open('toolsku-cache', 2); // 版本号 2
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (event.oldVersion < 2) {
db.createObjectStore('processing-history', { keyPath: 'id' });
}
};
最佳实践:只增不减 ObjectStore,通过版本号递增触发迁移,避免删除用户数据。
IndexedDB vs OPFS
| 特性 | IndexedDB | OPFS (Origin Private File System) |
|---|---|---|
| API 成熟度 | 广泛支持 | Chrome 86+ / Safari 15.2+ |
| 读写模式 | 键值存储 | 真正的文件系统 |
| 性能 | 中等 | 更高(尤其同步访问) |
| 适用 | 结构化缓存 | 大文件读写 |
工具库目前以 IndexedDB 为主,OPFS 作为 Chrome 上的性能优化备选。
游标操作与索引优化
IndexedDB 支持通过游标(Cursor)遍历存储记录,按索引排序访问:
// 按时间戳倒序遍历文件缓存
const index = store.index('timestamp');
const range = IDBKeyRange.lowerBound(Date.now() - 7 * 24 * 3600 * 1000);
let count = 0;
index.openCursor(range, 'prev').onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (count++ >= 10) return; // 只取前10条
console.log(cursor.key, cursor.value);
cursor.continue();
}
};
索引最佳实践:
- 按访问频率建立复合索引
[name, timestamp],避免全表扫描 - 对频繁范围查询的字段(如日期、文件大小)单独建索引
- 使用
IDBKeyRange精确控制查询范围,减少遍历开销
Blob 大文件存储模式
IndexedDB 可以直接存储 Blob 和 ArrayBuffer,无需 base64 编码(编码浪费 33% 空间):
// ✅ 推荐:直接存储 Blob
const blob = new Blob([chunkData], { type: 'application/octet-stream' });
await store.put(blob, `chunk:${fileHash}:${index}`);
// ❌ 不推荐:先转 base64
const base64 = arrayBufferToBase64(chunkData);
await store.put({ data: base64 }, ...);
存储估算:Chromium 的 IndexedDB 默认最大存储量约为磁盘可用空间的 60%,单条记录大小理论上没有硬限制,但建议控制在 100MB 以内以保证交互流畅。
LRU 淘汰与存储管理
当存储空间不足时,需要自动清理旧数据:
async function evictOldest(maxSize: number): Promise<void> {
const tx = db.transaction('file-cache', 'readwrite');
const store = tx.objectStore('file-cache');
const index = store.index('timestamp');
let totalSize = await estimateSize();
const cursorReq = index.openCursor(null, 'next'); // 从最旧的开始
cursorReq.onsuccess = async (event) => {
const cursor = event.target.result;
if (cursor && totalSize > maxSize) {
totalSize -= cursor.value.size;
cursor.delete();
cursor.continue();
}
};
}
工具库采用 LRU + 文件大小限制 的双重策略:缓存最多保留 500MB,同时单文件上限 100MB,超出自动淘汰最旧的条目。
迁移测试与回滚
IndexedDB 版本迁移在生产中是最容易出错的环节。推荐模式:
const migrations: Record<number, (db: IDBDatabase) => void> = {
1: (db) => { /* 初始 schema */ },
2: (db) => { db.createObjectStore('tasks', { keyPath: 'id' }); },
3: (db) => { db.deleteObjectStore('deprecated_store'); },
};
function applyMigrations(db: IDBDatabase, oldVersion: number, newVersion: number) {
for (let v = oldVersion + 1; v <= newVersion; v++) {
if (migrations[v]) migrations[v](db);
}
}
测试检查清单:
- 模拟从每个旧版本逐步升级到最新版本
- 确保迁移过程中不会丢失用户已有的缓存数据
- 在浏览器 DevTools → Application → IndexedDB 中手动验证 schema 变更
总结
IndexedDB 是构建离线优先浏览器工具的关键基础设施。合理设计 ObjectStore、利用 WASM 缓存、实施分块存储策略,配合游标索引、Blob 直接存储和 LRU 淘汰机制,可以让工具库在弱网环境下仍提供流畅体验。
本站提供浏览器本地工具,免注册即可试用 →