跳转到内容

存储 · API

IDBFactory.open() 与 IndexedDB 请求模型

发布于

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

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,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()。

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。

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。在启动时决定一次用哪个存储。

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。

规范

规范状态
无。