存储 · 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 等封装库内部做的就是这件事。
用迁移阶梯打开数据库
Section titled “用迁移阶梯打开数据库”按 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,直到本页关闭。
写入而不丢失事务
Section titled “写入而不丢失事务”一个事务里的请求要么同步发完,要么在前一个请求的 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。
检测支持并选择后备存储
Section titled “检测支持并选择后备存储”「语法」一节列出的所有引擎都有 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。
- Indexed Database API 3.0: open() method(w3.org)
- Working with IndexedDB(web.dev)
- IDBFactory: open() method(developer.mozilla.org)
- StorageManager.persist() 与 persisted()
- StorageManager.estimate()
- 后台同步
规范
| 规范 | 状态 |
|---|---|
| 无。 | |