跳转到内容

IndexedDB:面向 PWA 的结构化客户端存储

发布于

一句话: IndexedDB 是浏览器中用于结构化数据的事务型、异步数据库——JavaScript 对象、文件与 blob——按键存储并建立索引以供查找。它是 PWA 离线应用数据的正确存储:比 localStorage 大得多也丰富得多,可在 service worker 内工作,并在受源存储配额约束的前提下 跨会话保留。

  • localStorage 是一个同步的、键与值均为字符串的存储,面向少量数据——存放几个标志位 尚可,存放应用数据则不当,且它不向 service worker 暴露。
  • Cache Storage 为离线资源存储 Request/Response 对——是应用外壳,而非你的记录。
  • IndexedDB 保存结构化记录与二进制 blob,是异步的(永不阻塞主线程),且窗口与 service worker 都可访问——离线草稿、排队的变更、缓存的 API 数据与大型媒体的归宿。
  • 数据库(Database) —— 命名、带版本,限定在单一源内。
  • 对象存储(Object store) —— 类似表;保存以键路径或生成键作为键的记录(任何可结构化 克隆的值)。
  • 索引(Index) —— 对所存记录某个属性的二级查找,让你能用主键以外的东西查询。
  • 事务(Transaction) —— 每次读写都发生在事务内,限定于命名的存储与一种模式 (readonly / readwrite)。一旦其关联请求完成、控制权返回事件循环,事务便自动提交 (正常使用中你不会调用 commit())。
  • 请求(Request) —— 每个操作返回一个 IDBRequest,触发 onsuccess / onerror; 结果到达事件上,而非作为返回值。

原生 API 基于事件,这是它最锋利的边缘。一个常见的坑是:事务只在仍有挂起请求时存活——在 操作之间 await 一个无关的 promise,会让控制权返回事件循环,使事务在你不知情时提交。

const open = indexedDB.open('app', 2);
open.onupgradeneeded = (event) => {
const db = event.target.result;
// schema 变更仅在此处、versionchange 事务内合法。
if (!db.objectStoreNames.contains('drafts')) {
const store = db.createObjectStore('drafts', { keyPath: 'id' });
store.createIndex('byUpdated', 'updatedAt');
}
};
open.onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction('drafts', 'readwrite');
tx.objectStore('drafts').put({ id: 'd1', updatedAt: Date.now(), body: '…' });
tx.oncomplete = () => console.log('saved');
tx.onerror = () => console.error('write failed', tx.error);
};

版本号是 IndexedDB 做迁移的方式。以高于已存储版本的版本号打开会触发 onupgradeneeded, 而该回调是唯一允许创建或删除对象存储与索引的地方。关键规则:

  • schema 变化时提升版本号;按 event.oldVersion 分支,依序应用每个迁移步骤。
  • 当其他标签页仍以旧版本持有数据库时,versionchange 升级会被阻塞——监听 blocked/versionchange 事件并关闭陈旧连接,让升级得以进行。
  • 普通 readwrite 事务中禁止 schema 操作;尝试这样做会抛出异常。

IndexedDB 与 Cache Storage 等一起,共享由 Storage API 管理的单一每源存储配额。默认情况下 存储是尽力而为的,可能在存储压力下被清除,因此对持久性关键的数据应让 IndexedDB 搭配 navigator.storage.persist() 申请以请求持久存储。为写入包裹捕获 QuotaExceededError, 随后清理陈旧数据并重试,而非直接硬失败。(完整的配额与逐出模型见存储持久化参考。)

IndexedDB 在当前主流浏览器上获得支持,且窗口与 service worker 都可访问,使其成为支持离线的 PWA 的基线持久存储。由于原始事件 API 冗长且易误用,许多应用会用一层轻量的基于 promise 的 封装来包裹它——所引 web.dev 指南使用 idb 库,它在保持此处所述底层语义的同时简化了 API。

  • 用 IndexedDB 存放结构化/离线数据;把 localStorage 留给极小的同步标志位。
  • 在 onupgradeneeded 内、依据版本号完成所有 createObjectStore/createIndex 工作。
  • 不要在事务进行中 await 无关 promise——那会让事务提前自动提交。
  • 处理请求与事务上的 onerror;让失败浮现,而非默默丢弃写入。
  • 为你实际运行的查询定义索引,而非扫描每条记录。
  • 捕获 QuotaExceededError、清理并重试;对持久性关键的数据调用 persist()。
  • 考虑用 idb 这类小型的基于 promise 的封装,以免自行重新实现事件 API。