# IDBFactory.open() 与 IndexedDB 请求模型

> indexedDB.open() 如何创建并升级带版本号的数据库、schema 变更为何只能在 upgradeneeded 内进行，以及 PWA 必须处理的 VersionError 与配额错误。

`indexedDB.open(name, version)` 打开一个具名、带版本号、按源隔离的数据库，返回 `IDBOpenDBRequest`，连接通过它的事件送达；随后每次读写都在限定了对象仓库的 `IDBTransaction` 内进行。IndexedDB 是浏览器中存放结构化记录和 blob 的存储，窗口、worker 和 Service Worker 都能访问，也是受配额管理的存储里唯一支持索引和范围查询的一种。

## 语法

```js
indexedDB.open(name)
indexedDB.open(name, version)
```

调用同步返回 `IDBOpenDBRequest`。连接在请求的 `success` 事件中以 `request.result` 到达；版本号升高时先触发 `upgradeneeded`，另一个连接仍以旧版本打开时触发 `blocked`。Chrome 23、Firefox 10、Safari 8 起支持；Safari 14 曾有首次 `open()` 永久挂起的缺陷，自 Safari 15 起稳定（BCD `api.IDBFactory.open`，WebKit bug 226547）。

## 参数

| 参数 | 类型 | 含义 |
|---|---|---|
| `name` | string | 数据库名，区分大小写，作用域为源。任何字符串都合法，包括空串。 |
| `version` | unsigned long long，可选 | 要打开的 schema 版本。省略时沿用已存版本，新库取 `1`。高于已存版本：触发 `upgradeneeded`，携带 `event.oldVersion` 与 `event.newVersion`。低于已存版本：请求以 `VersionError` 失败。 |

schema 变更（`createObjectStore()`、`deleteObjectStore()`、`createIndex()`、`deleteIndex()`）只在 `upgradeneeded` 期间运行的 `versionchange` 事务上合法，在其他地方调用会抛出 `InvalidStateError`。

## 异常

`open()` 本身只在一种情况下抛出，其余错误都出现在请求或事务上。

| 错误 | 出现位置 | 触发条件 |
|---|---|---|
| `TypeError` | 由 `open()` 抛出 | `version` 为 `0`、负数、`NaN`，或无法表示为无符号 64 位整数。 |
| `VersionError` | `request.error` | 请求的版本低于已存版本。不传版本即可按已存版本打开。 |
| `AbortError` | `request.error` | `upgradeneeded` 处理函数抛出或调用了 `transaction.abort()`，升级回滚，连接未打开。 |
| `QuotaExceededError` | 写入请求的 `request.error`；事务中止 | 该源配额已用尽。整个事务回滚，而不只是失败的那个请求。 |
| `TransactionInactiveError` | 由 `objectStore.put()` 等抛出 | 事务已经提交：控制权在没有待处理请求的情况下回到了事件循环，典型原因是中途 `await` 了一个无关的 Promise。Chromium 的报错文本为 `The transaction has finished.`（[idb_database.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/indexeddb/idb_database.cc)，chromium.googlesource.com）。 |
| `InvalidStateError` | 由 `createObjectStore()`、`createIndex()` 抛出 | 在 `versionchange` 事务之外调用。Chromium 的报错文本为 `The database is not running a version change transaction.`。 |
| `ConstraintError` | `request.error` | `add()` 使用了已存在的键，或 `unique: true` 的索引值已存在。 |
| `DataError` | 由 `put()`、`add()`、`get()` 抛出 | 键不是合法键（对象、`undefined`），或向带 `keyPath` 的仓库额外传了键。 |

事务会自动提交。事务在有待处理请求期间保持活跃；最后一个请求的事件处理函数返回且没有发起新请求时即提交。`transaction.commit()`（Chrome 76、Firefox 74、Safari 15）只是把这一步显式化并跳过对事件循环的等待。

## 示例

每个示例都把事件 API 包成 Promise，`idb` 等封装库内部做的就是这件事。

### 用迁移阶梯打开数据库

按 `event.oldVersion` 分支，让每一步 schema 变更恰好执行一次，无论用户上次停在哪个版本。创建已存在的仓库会抛出 `ConstraintError`，所以守卫是阶梯而不是 `objectStoreNames.contains()`。

```js
function openDb() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('notes', 3);
    request.onupgradeneeded = (event) => {
      const db = request.result;
      if (event.oldVersion < 1) {
        db.createObjectStore('drafts', { keyPath: 'id' });
      }
      if (event.oldVersion < 2) {
        request.transaction.objectStore('drafts').createIndex('byUpdated', 'updatedAt');
      }
      if (event.oldVersion < 3) {
        db.createObjectStore('outbox', { autoIncrement: true });
      }
    };
    request.onblocked = () => console.warn('关闭其他标签页以完成升级');
    request.onsuccess = () => {
      request.result.onversionchange = () => request.result.close();
      resolve(request.result);
    };
    request.onerror = () => reject(request.error);
  });
}
```

`onversionchange` 处理函数在另一个标签页打开更高版本时关闭本连接；缺少它，另一个标签页会一直停在 `blocked`，直到本页关闭。

### 写入而不丢失事务

一个事务里的请求要么同步发完，要么在前一个请求的 `success` 处理函数里链式发起。两个 `put()` 之间 `await fetch()` 会让事务提交，第二个 `put()` 随即抛出 `TransactionInactiveError`。

```js
async function saveDraft(db, draft) {
  const body = await render(draft); // 异步工作放在打开事务之前
  return new Promise((resolve, reject) => {
    const tx = db.transaction('drafts', 'readwrite');
    tx.objectStore('drafts').put({ ...draft, body, updatedAt: Date.now() });
    tx.oncomplete = () => resolve();
    tx.onabort = () => reject(tx.error); // 配额失败落在这里
  });
}
```

监听 `tx.onabort` 而不只是 `request.onerror`：配额失败会中止整个事务，`tx.error` 携带着说明原因的 `QuotaExceededError`。

### 检测支持并选择后备存储

「语法」一节列出的所有引擎都有 `indexedDB`，但在禁用了存储的部分内嵌 WebView 里它是 `undefined`，从不透明源访问则抛出 `SecurityError`。在启动时决定一次用哪个存储。

```js
function pickStore() {
  if (!('indexedDB' in self)) {
    return memoryStore(); // 没有数据库 API：内存存储，刷新即丢
  }
  try {
    return idbStore(indexedDB);
  } catch (err) {
    if (err.name === 'SecurityError') return memoryStore(); // 不透明源或存储被拦截
    throw err;
  }
}
```

改为退回 `localStorage` 的页面应把值控制在 Firefox 每源 5 MiB 的限制之下，见 [localStorage 与 sessionStorage](/zh/reference/storage/localstorage/)。

:::observed
Chrome DevTools 的 Application > Storage > IndexedDB 以 `<名称> - <源>` 列出每个数据库及其版本和对象仓库；被 `await` 搁置的事务会在 Console 打出 `Uncaught DOMException: Failed to execute 'put' on 'IDBObjectStore': The transaction has finished.`，该字符串即 Chromium [idb_database.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/indexeddb/idb_database.cc)（chromium.googlesource.com）中定义的 `kTransactionFinishedErrorMessage`。
:::

## 另请参阅

- [Indexed Database API 3.0: open() method](https://www.w3.org/TR/IndexedDB/#dom-idbfactory-open)（w3.org）
- [Working with IndexedDB](https://web.dev/articles/indexeddb)（web.dev）
- [IDBFactory: open() method](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/open)（developer.mozilla.org）
- [StorageManager.persist() 与 persisted()](/zh/reference/storage/persistence/)
- [StorageManager.estimate()](/zh/reference/storage/quota-estimate/)
- [后台同步](/zh/reference/service-worker/background-sync/)