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

> IndexedDB 如何在浏览器中存储结构化数据与大型 blob——对象存储、索引、事务、异步请求模型、带版本的 schema 升级，以及为什么多数应用会采用封装库。

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

## 何时该用 IndexedDB

- **`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，会让控制权返回事件循环，使事务在你不知情时提交。

```js
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);
};
```

## 带版本的 schema 升级

版本号是 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。