一、定位:它解决什么问题
IndexedDB 是浏览器内置的、唯一面向大规模结构化数据 、支持事务 与索引查询的客户端存储方案。把它和其他方案并列,定位立刻清晰:
| 维度 | Cookie | localStorage | IndexedDB |
|---|---|---|---|
| 容量 | ~4 KB | ~5--10 MB | 磁盘配额的数百 MB ~ 数 GB |
| 数据类型 | 字符串 | 字符串 | 结构化克隆支持的几乎任意对象 |
| 读写方式 | 同步 | 同步(阻塞主线程) | 异步(基于请求/事件) |
| 索引查询 | 无 | 无 | 多索引、范围查询、游标 |
| 事务 | 无 | 无 | ACID 语义 |
| Worker 可用 | 否 | 否 | 是(含 Service Worker) |
三个本质差异:异步 (不阻塞 UI 线程)、事务 (一组操作的原子性)、索引(无需全表扫描即可按字段查)。localStorage 同步写几 MB 会卡死渲染;IndexedDB 所有操作都通过异步请求完成。
二、对象模型:七个核心抽象
#mermaid-svg-p20PCm2guKeBeYYE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-p20PCm2guKeBeYYE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-p20PCm2guKeBeYYE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-p20PCm2guKeBeYYE .error-icon{fill:#552222;}#mermaid-svg-p20PCm2guKeBeYYE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-p20PCm2guKeBeYYE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-p20PCm2guKeBeYYE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-p20PCm2guKeBeYYE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-p20PCm2guKeBeYYE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-p20PCm2guKeBeYYE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-p20PCm2guKeBeYYE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-p20PCm2guKeBeYYE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-p20PCm2guKeBeYYE .marker.cross{stroke:#333333;}#mermaid-svg-p20PCm2guKeBeYYE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-p20PCm2guKeBeYYE p{margin:0;}#mermaid-svg-p20PCm2guKeBeYYE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-p20PCm2guKeBeYYE .cluster-label text{fill:#333;}#mermaid-svg-p20PCm2guKeBeYYE .cluster-label span{color:#333;}#mermaid-svg-p20PCm2guKeBeYYE .cluster-label span p{background-color:transparent;}#mermaid-svg-p20PCm2guKeBeYYE .label text,#mermaid-svg-p20PCm2guKeBeYYE span{fill:#333;color:#333;}#mermaid-svg-p20PCm2guKeBeYYE .node rect,#mermaid-svg-p20PCm2guKeBeYYE .node circle,#mermaid-svg-p20PCm2guKeBeYYE .node ellipse,#mermaid-svg-p20PCm2guKeBeYYE .node polygon,#mermaid-svg-p20PCm2guKeBeYYE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-p20PCm2guKeBeYYE .rough-node .label text,#mermaid-svg-p20PCm2guKeBeYYE .node .label text,#mermaid-svg-p20PCm2guKeBeYYE .image-shape .label,#mermaid-svg-p20PCm2guKeBeYYE .icon-shape .label{text-anchor:middle;}#mermaid-svg-p20PCm2guKeBeYYE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-p20PCm2guKeBeYYE .rough-node .label,#mermaid-svg-p20PCm2guKeBeYYE .node .label,#mermaid-svg-p20PCm2guKeBeYYE .image-shape .label,#mermaid-svg-p20PCm2guKeBeYYE .icon-shape .label{text-align:center;}#mermaid-svg-p20PCm2guKeBeYYE .node.clickable{cursor:pointer;}#mermaid-svg-p20PCm2guKeBeYYE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-p20PCm2guKeBeYYE .arrowheadPath{fill:#333333;}#mermaid-svg-p20PCm2guKeBeYYE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-p20PCm2guKeBeYYE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-p20PCm2guKeBeYYE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p20PCm2guKeBeYYE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-p20PCm2guKeBeYYE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p20PCm2guKeBeYYE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-p20PCm2guKeBeYYE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-p20PCm2guKeBeYYE .cluster text{fill:#333;}#mermaid-svg-p20PCm2guKeBeYYE .cluster span{color:#333;}#mermaid-svg-p20PCm2guKeBeYYE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-p20PCm2guKeBeYYE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-p20PCm2guKeBeYYE rect.text{fill:none;stroke-width:0;}#mermaid-svg-p20PCm2guKeBeYYE .icon-shape,#mermaid-svg-p20PCm2guKeBeYYE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p20PCm2guKeBeYYE .icon-shape p,#mermaid-svg-p20PCm2guKeBeYYE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-p20PCm2guKeBeYYE .icon-shape .label rect,#mermaid-svg-p20PCm2guKeBeYYE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p20PCm2guKeBeYYE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-p20PCm2guKeBeYYE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-p20PCm2guKeBeYYE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} open
transaction
objectStore
index
openCursor
openCursor
add/put/get/delete
查询参数
查询参数
IDBFactory
(window.indexedDB)
IDBDatabase
(数据库连接)
IDBTransaction
(事务:作用域 + 模式)
IDBObjectStore
(对象仓库 ≈ 表)
IDBIndex
(索引)
IDBCursor
(游标)
IDBRequest
(异步请求)
IDBKeyRange
(键范围)
- IDBFactory :入口
window.indexedDB,只有open/deleteDatabase/databases/cmp。 - IDBDatabase:一个打开的连接,持有 schema,是创建事务的唯一来源。
- IDBObjectStore :对象仓库,本质是按键排序的键值对集合,无需预定义字段。
- IDBIndex:索引;想按主键以外的字段查询,必须先建。
- IDBTransaction :事务,所有读写都必须在事务中进行。
- IDBRequest :异步操作的句柄,结果在
onsuccess时挂到result;游标场景下会反复触发 success,这是它无法直接等价于 Promise 的根本原因。 - IDBKeyRange:描述一个键的连续区间。
三、异步事件模型
原生 API 诞生于 Promise 普及之前,采用请求 + 事件回调:
javascript
const request = indexedDB.open('myDB', 1);
request.onerror = () => console.error(request.error);
request.onsuccess = () => { const db = request.result; };
request.onupgradeneeded = (e) => { /* 仅版本升高/首次创建时触发 */ };
实践中几乎都会用一层 Promise 封装(或直接用 idb 库),但封装必须避开后文的"事务自动提交陷阱"。
四、数据库的打开与版本管理
4.1 版本号是什么、存在哪
版本号就是一个整数计数器 ,代表"schema 的第几版"。它的唯一作用是让浏览器判断磁盘上的结构是否落后于代码期望的结构。
#mermaid-svg-HYMKxnIJkEGiWa1L{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HYMKxnIJkEGiWa1L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HYMKxnIJkEGiWa1L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HYMKxnIJkEGiWa1L .error-icon{fill:#552222;}#mermaid-svg-HYMKxnIJkEGiWa1L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HYMKxnIJkEGiWa1L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HYMKxnIJkEGiWa1L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HYMKxnIJkEGiWa1L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HYMKxnIJkEGiWa1L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HYMKxnIJkEGiWa1L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HYMKxnIJkEGiWa1L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HYMKxnIJkEGiWa1L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HYMKxnIJkEGiWa1L .marker.cross{stroke:#333333;}#mermaid-svg-HYMKxnIJkEGiWa1L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HYMKxnIJkEGiWa1L p{margin:0;}#mermaid-svg-HYMKxnIJkEGiWa1L .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HYMKxnIJkEGiWa1L .cluster-label text{fill:#333;}#mermaid-svg-HYMKxnIJkEGiWa1L .cluster-label span{color:#333;}#mermaid-svg-HYMKxnIJkEGiWa1L .cluster-label span p{background-color:transparent;}#mermaid-svg-HYMKxnIJkEGiWa1L .label text,#mermaid-svg-HYMKxnIJkEGiWa1L span{fill:#333;color:#333;}#mermaid-svg-HYMKxnIJkEGiWa1L .node rect,#mermaid-svg-HYMKxnIJkEGiWa1L .node circle,#mermaid-svg-HYMKxnIJkEGiWa1L .node ellipse,#mermaid-svg-HYMKxnIJkEGiWa1L .node polygon,#mermaid-svg-HYMKxnIJkEGiWa1L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HYMKxnIJkEGiWa1L .rough-node .label text,#mermaid-svg-HYMKxnIJkEGiWa1L .node .label text,#mermaid-svg-HYMKxnIJkEGiWa1L .image-shape .label,#mermaid-svg-HYMKxnIJkEGiWa1L .icon-shape .label{text-anchor:middle;}#mermaid-svg-HYMKxnIJkEGiWa1L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HYMKxnIJkEGiWa1L .rough-node .label,#mermaid-svg-HYMKxnIJkEGiWa1L .node .label,#mermaid-svg-HYMKxnIJkEGiWa1L .image-shape .label,#mermaid-svg-HYMKxnIJkEGiWa1L .icon-shape .label{text-align:center;}#mermaid-svg-HYMKxnIJkEGiWa1L .node.clickable{cursor:pointer;}#mermaid-svg-HYMKxnIJkEGiWa1L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HYMKxnIJkEGiWa1L .arrowheadPath{fill:#333333;}#mermaid-svg-HYMKxnIJkEGiWa1L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HYMKxnIJkEGiWa1L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HYMKxnIJkEGiWa1L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HYMKxnIJkEGiWa1L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HYMKxnIJkEGiWa1L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HYMKxnIJkEGiWa1L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HYMKxnIJkEGiWa1L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HYMKxnIJkEGiWa1L .cluster text{fill:#333;}#mermaid-svg-HYMKxnIJkEGiWa1L .cluster span{color:#333;}#mermaid-svg-HYMKxnIJkEGiWa1L div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HYMKxnIJkEGiWa1L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HYMKxnIJkEGiWa1L rect.text{fill:none;stroke-width:0;}#mermaid-svg-HYMKxnIJkEGiWa1L .icon-shape,#mermaid-svg-HYMKxnIJkEGiWa1L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HYMKxnIJkEGiWa1L .icon-shape p,#mermaid-svg-HYMKxnIJkEGiWa1L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HYMKxnIJkEGiWa1L .icon-shape .label rect,#mermaid-svg-HYMKxnIJkEGiWa1L .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HYMKxnIJkEGiWa1L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HYMKxnIJkEGiWa1L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HYMKxnIJkEGiWa1L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 0(不存在)
1 或 2
3
4+
open('myDB', 3)
磁盘当前版本?
触发 upgradeneeded
(自动开启 versionchange 事务)
结构最新 → 直接 success
代码比磁盘旧 → VersionError
在事件内建表/建索引/迁移数据
事务提交 → success
版本号是数据库自身的元数据,由底层引擎持久化在磁盘上,按源(origin)隔离------Chromium 存在 LevelDB,Firefox/Safari 存在 SQLite 元数据表。你的代码只是声明期望版本,真相永远在磁盘上。可主动查询:
javascript
await indexedDB.databases(); // → [{ name: 'myDB', version: 3 }]
open 的版本参数可省略:
| 调用 | DB 已存在 | DB 不存在 |
|---|---|---|
open(name) |
用当前版本打开,不升级 | 创建,版本默认 1,触发一次 upgradeneeded |
open(name, N) |
按上图对比版本 | 创建并升到 N |
注意:版本不能传 0 或小数(抛 TypeError);不写版本时无法主动触发升级。
4.2 schema 只能在 upgradeneeded 里改
创建/删除对象仓库与索引,只能在 versionchange 事务中进行 ,而该事务无法手动创建 ,只在 upgradeneeded 触发时由浏览器自动开启。这强制把"结构变更"与"版本递增"绑定,保证 schema 演进可追溯。
4.3 升级代码必须能从任意旧版本升上来
用户的库可能停在任何 旧版本。正确写法是用 fall-through 的 switch,把每一版的增量变更分别写出:
javascript
request.onupgradeneeded = (event) => {
const db = request.result;
const tx = request.transaction; // 自动开启的 versionchange 事务
switch (event.oldVersion) { // 故意不写 break,逐版贯穿
case 0:
db.createObjectStore('users', { keyPath: 'id' });
case 1:
tx.objectStore('users').createIndex('by_email', 'email', { unique: true });
db.createObjectStore('logs', { autoIncrement: true });
case 2:
tx.objectStore('users').createIndex('by_age', 'age');
}
};
| 用户当前版本 | 入口 | 执行 |
|---|---|---|
| 0(新用户) | case 0 | 全跑 |
| 1 | case 1 | 加 by_email → 建 logs → 加 by_age |
| 2 | case 2 | 只加 by_age |
没有 break,从哪版进来就补齐其后所有增量,最终所有用户到达统一结构。绝不能用"删表重建"偷懒 ,那会丢光存量数据。需要迁移数据时,在同一升级事务里用游标 cursor.update() 完成。
五、多标签页:连接阻塞与升级协调
5.1 同源标签页共享同一个库
同源的多个标签页,物理上访问的就是同一个数据库 (磁盘只有一份),且磁盘任意时刻只有一个版本号 。如果两个标签页跑完全相同的代码 ,它们要么都在升级前、要么都在升级后,不会版本冲突。
版本错位只发生在代码版本漂移(version skew)时------典型场景:你发布了新版前端(open 用 v2),用户某个标签页还停在老页面(open 用 v1)。线上一定会有这种用户,所以必须兜底。
5.2 升级需要独占,因此会阻塞
升级是破坏性的结构操作:只要还有任何旧连接没关,升级就只能挂起等待。
Tab B(新代码,要 v2) 磁盘 DB Tab A(旧代码,持有 v1) Tab B(新代码,要 v2) 磁盘 DB Tab A(旧代码,持有 v1) #mermaid-svg-ZC8CYDDaKAwlKKZt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZC8CYDDaKAwlKKZt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZC8CYDDaKAwlKKZt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZC8CYDDaKAwlKKZt .error-icon{fill:#552222;}#mermaid-svg-ZC8CYDDaKAwlKKZt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZC8CYDDaKAwlKKZt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZC8CYDDaKAwlKKZt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZC8CYDDaKAwlKKZt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZC8CYDDaKAwlKKZt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZC8CYDDaKAwlKKZt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZC8CYDDaKAwlKKZt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZC8CYDDaKAwlKKZt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZC8CYDDaKAwlKKZt .marker.cross{stroke:#333333;}#mermaid-svg-ZC8CYDDaKAwlKKZt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZC8CYDDaKAwlKKZt p{margin:0;}#mermaid-svg-ZC8CYDDaKAwlKKZt .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZC8CYDDaKAwlKKZt text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ZC8CYDDaKAwlKKZt .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ZC8CYDDaKAwlKKZt .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ZC8CYDDaKAwlKKZt .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ZC8CYDDaKAwlKKZt .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ZC8CYDDaKAwlKKZt #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ZC8CYDDaKAwlKKZt .sequenceNumber{fill:white;}#mermaid-svg-ZC8CYDDaKAwlKKZt #sequencenumber{fill:#333;}#mermaid-svg-ZC8CYDDaKAwlKKZt #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ZC8CYDDaKAwlKKZt .messageText{fill:#333;stroke:none;}#mermaid-svg-ZC8CYDDaKAwlKKZt .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZC8CYDDaKAwlKKZt .labelText,#mermaid-svg-ZC8CYDDaKAwlKKZt .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ZC8CYDDaKAwlKKZt .loopText,#mermaid-svg-ZC8CYDDaKAwlKKZt .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ZC8CYDDaKAwlKKZt .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ZC8CYDDaKAwlKKZt .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ZC8CYDDaKAwlKKZt .noteText,#mermaid-svg-ZC8CYDDaKAwlKKZt .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ZC8CYDDaKAwlKKZt .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZC8CYDDaKAwlKKZt .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZC8CYDDaKAwlKKZt .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZC8CYDDaKAwlKKZt .actorPopupMenu{position:absolute;}#mermaid-svg-ZC8CYDDaKAwlKKZt .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ZC8CYDDaKAwlKKZt .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZC8CYDDaKAwlKKZt .actor-man circle,#mermaid-svg-ZC8CYDDaKAwlKKZt line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ZC8CYDDaKAwlKKZt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} open('myDB', 2)versionchange(请让路)blocked(A 还占着)db.close()升级到 v2upgradeneeded → success
| 事件 | 派发给谁 | 含义 | 应对 |
|---|---|---|---|
versionchange |
持旧连接的 A | "有人要升级,请让路" | db.close() |
blocked |
发起升级的 B | "你被旧连接挡住了" | 提示用户关其他标签页 |
5.3 Tab A 如何重连
A 收到 versionchange 并 close 后,默认不会被推送"升级完成"通知。两条路:
- 直接重试 open() :升级期间 A 的 open 会排队,等升级结束再出队。但 A 用的是自己代码里的版本号 ------若 A 是旧代码
open(name, 1),而磁盘已是 v2,A 请求版本更低 → 抛VersionError,无解。 - 刷新页面(推荐):最省心,顺带解决代码漂移。
javascript
db.onversionchange = () => {
db.close();
location.reload(); // 强制加载最新代码 + 最新版本重连
};
需要更精细的协调时,B 升级成功后用 BroadcastChannel 广播,A 收到后重连(前提是 A 的代码也认识新版本)。
六、对象仓库:键的来源
主键来源有两类,互斥:
| 方式 | 创建配置 | add/put 调用 | 键来自 |
|---|---|---|---|
| 内联键(in-line) | { keyPath: 'id' } |
store.add(value) |
value[keyPath] 自动提取 |
| 外联键(out-of-line) | {}(无 keyPath) |
store.add(value, key) |
第二参数显式给出 |
javascript
const users = db.createObjectStore('users', { keyPath: 'id' });
users.add({ id: 1, name: 'A' }); // ✅ 键自动 = 1
users.add({ id: 1, name: 'A' }, 99); // 💥 DataError:内联键不能再传 key
const logs = db.createObjectStore('logs'); // 无 keyPath
logs.add({ msg: 'hi' }, Date.now()); // ✅ 必须显式给键
logs.add({ msg: 'hi' }); // 💥 DataError:外联键必须给 key
有 keyPath 就别传 key,没 keyPath 才手动给------二选一。 自增的组合:{ keyPath:'id', autoIncrement:true } 会自动生成键并写回对象;仅 { autoIncrement:true } 自动生成键但不写进值。键生成器的当前值不因 delete 或 clear 回退,只有删表重建才重置。
七、索引:multiEntry 与复合键
javascript
store.createIndex('by_author', 'author'); // 普通
store.createIndex('by_isbn', 'isbn', { unique: true }); // 唯一
store.createIndex('by_au_yr', ['author', 'year']); // 复合(数组 keyPath)
store.createIndex('by_tag', 'tags', { multiEntry: true }); // 数组展开
multiEntry 的语义:当 tags = ['js','db']------
false(默认):整个数组作为一个索引键,只有完全相等的数组才命中。true:为'js'和'db'各建一条索引项 ,二者都指向该记录,于是index.get('js')可命中。适合标签/分类检索。
八、事务:自动提交是最大的陷阱
8.1 心智模型
记住一句话:事务不是"我创建、我关闭",而是"它在某段代码执行期间活着,代码一交还控制权它就自己提交"。
事务有一个内部活跃标志(active flag):
- 创建时 → true;当前同步代码执行期间 → 一直 true。
- 某请求的
onsuccess/onerror派发期间 → 重新 true。 - 控制权回到事件循环 且 无挂起请求 → false → 立即提交。
#mermaid-svg-gALruYRCRufV6GKO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gALruYRCRufV6GKO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gALruYRCRufV6GKO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gALruYRCRufV6GKO .error-icon{fill:#552222;}#mermaid-svg-gALruYRCRufV6GKO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gALruYRCRufV6GKO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gALruYRCRufV6GKO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gALruYRCRufV6GKO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gALruYRCRufV6GKO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gALruYRCRufV6GKO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gALruYRCRufV6GKO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gALruYRCRufV6GKO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gALruYRCRufV6GKO .marker.cross{stroke:#333333;}#mermaid-svg-gALruYRCRufV6GKO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gALruYRCRufV6GKO p{margin:0;}#mermaid-svg-gALruYRCRufV6GKO defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-gALruYRCRufV6GKO g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-gALruYRCRufV6GKO g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-gALruYRCRufV6GKO g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-gALruYRCRufV6GKO g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-gALruYRCRufV6GKO g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-gALruYRCRufV6GKO .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-gALruYRCRufV6GKO .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-gALruYRCRufV6GKO .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-gALruYRCRufV6GKO .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-gALruYRCRufV6GKO .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-gALruYRCRufV6GKO .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-gALruYRCRufV6GKO .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-gALruYRCRufV6GKO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gALruYRCRufV6GKO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gALruYRCRufV6GKO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gALruYRCRufV6GKO .edgeLabel .label text{fill:#333;}#mermaid-svg-gALruYRCRufV6GKO .label div .edgeLabel{color:#333;}#mermaid-svg-gALruYRCRufV6GKO .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-gALruYRCRufV6GKO .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-gALruYRCRufV6GKO .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-gALruYRCRufV6GKO .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-gALruYRCRufV6GKO .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-gALruYRCRufV6GKO .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gALruYRCRufV6GKO .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gALruYRCRufV6GKO #statediagram-barbEnd{fill:#333333;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gALruYRCRufV6GKO .cluster-label,#mermaid-svg-gALruYRCRufV6GKO .nodeLabel{color:#131300;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-gALruYRCRufV6GKO .note-edge{stroke-dasharray:5;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-note text{fill:black;}#mermaid-svg-gALruYRCRufV6GKO .statediagram-note .nodeLabel{color:black;}#mermaid-svg-gALruYRCRufV6GKO .statediagram .edgeLabel{color:red;}#mermaid-svg-gALruYRCRufV6GKO #dependencyStart,#mermaid-svg-gALruYRCRufV6GKO #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-gALruYRCRufV6GKO .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gALruYRCRufV6GKO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 创建(当前任务内活跃)
同步发新请求(保持活跃)
控制权交还事件循环 + 无挂起请求
某请求 success/error 派发期间
无更多请求
持久化完成(触发 complete)
abort() / 异常 / 约束冲突
回滚(触发 abort)
Active
Inactive
Committing
Finished
Aborted
8.2 onsuccess 是宏任务,不是微任务
关键澄清:onsuccess/onerror 通过"排入一个任务"派发,是宏任务(task),与 Promise.then(微任务)不同。
那为什么"在 onsuccess 里发请求"能保证事务还活着?因为挂起的请求会阻止提交 。提交条件是"控制权回事件循环 且无挂起请求 "。你一发起 store.get(1),请求就挂在事务上,在它完成、success 任务执行完之前,事务绝不提交------它会一直等到那个宏任务跑完。所以"宏任务调度时机不确定"不构成问题:不是抢在提交前发请求,而是挂起的请求把提交拦住了。
而"派发期间活跃标志重新置 true"是 IndexedDB 自己的钩子,不依赖回调是宏任务还是微任务。
边缘行为:规范里活跃标志在"派发后的微任务检查点之后"才清除,所以 onsuccess 里
await Promise.resolve()有时 也能存活,但浏览器实现不一致,不可移植,不要依赖。安全心智:任何让出到事件循环的操作都视为终结事务。
8.3 await fetch 的完整提交过程
javascript
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
const user = await idbGet(store, 1); // 真实 IDB 请求 → 事务存活
await fetch('/api'); // 不是 IDB 请求 → 事务被提交
store.put(user); // 💥 TransactionInactiveError
fetch 事务 async 函数 fetch 事务 async 函数 #mermaid-svg-yV7CRFVot4zHMq4C{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yV7CRFVot4zHMq4C .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yV7CRFVot4zHMq4C .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yV7CRFVot4zHMq4C .error-icon{fill:#552222;}#mermaid-svg-yV7CRFVot4zHMq4C .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yV7CRFVot4zHMq4C .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yV7CRFVot4zHMq4C .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yV7CRFVot4zHMq4C .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yV7CRFVot4zHMq4C .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yV7CRFVot4zHMq4C .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yV7CRFVot4zHMq4C .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yV7CRFVot4zHMq4C .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yV7CRFVot4zHMq4C .marker.cross{stroke:#333333;}#mermaid-svg-yV7CRFVot4zHMq4C svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yV7CRFVot4zHMq4C p{margin:0;}#mermaid-svg-yV7CRFVot4zHMq4C .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yV7CRFVot4zHMq4C text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-yV7CRFVot4zHMq4C .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-yV7CRFVot4zHMq4C .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-yV7CRFVot4zHMq4C .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-yV7CRFVot4zHMq4C .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-yV7CRFVot4zHMq4C #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-yV7CRFVot4zHMq4C .sequenceNumber{fill:white;}#mermaid-svg-yV7CRFVot4zHMq4C #sequencenumber{fill:#333;}#mermaid-svg-yV7CRFVot4zHMq4C #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-yV7CRFVot4zHMq4C .messageText{fill:#333;stroke:none;}#mermaid-svg-yV7CRFVot4zHMq4C .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yV7CRFVot4zHMq4C .labelText,#mermaid-svg-yV7CRFVot4zHMq4C .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-yV7CRFVot4zHMq4C .loopText,#mermaid-svg-yV7CRFVot4zHMq4C .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-yV7CRFVot4zHMq4C .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-yV7CRFVot4zHMq4C .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-yV7CRFVot4zHMq4C .noteText,#mermaid-svg-yV7CRFVot4zHMq4C .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-yV7CRFVot4zHMq4C .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yV7CRFVot4zHMq4C .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yV7CRFVot4zHMq4C .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yV7CRFVot4zHMq4C .actorPopupMenu{position:absolute;}#mermaid-svg-yV7CRFVot4zHMq4C .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-yV7CRFVot4zHMq4C .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yV7CRFVot4zHMq4C .actor-man circle,#mermaid-svg-yV7CRFVot4zHMq4C line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-yV7CRFVot4zHMq4C :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 有挂起请求 → 不提交 挂起请求归零 → active=false → 自动提交 → finished 创建(active=true)idbGet → get(1),挂起请求+1await idbGet 让出控制权get 完成,派发 success,恢复函数,user 赋值await fetch 让出控制权fetch 完成(几百 ms 后),恢复函数store.put(user)💥 事务已 finished → TransactionInactiveError
差别就在于 await 的对象是否会在事务上挂一个 IDB 请求 :idbGet 会(存活),fetch 不会(提交)。
为什么这样设计? 事务持有锁。若允许它等待任意 Promise(如挂死的 fetch),事务会无限期持锁,阻塞其他事务甚至死锁。所以规范刻意规定:JS 一旦不再喂 IDB 请求,就立即提交、释放锁。
8.4 实践纪律
javascript
// ✅ 外部异步在事务之外,事务内只有纯 IDB 操作
const extra = await fetch('/api').then(r => r.json()); // 先取好外部数据
const tx = db.transaction('users', 'readwrite'); // 再开事务
const store = tx.objectStore('users');
const u = await idbGet(store, 1);
store.put({ ...u, extra });
await txComplete(tx);
铁律:开了事务,就别在事务期间碰任何非 IndexedDB 的异步。 较新规范还提供 tx.commit()(显式提交)和 { durability: 'relaxed' | 'strict' | 'default' }(落盘策略,relaxed 批量写更快但极端断电可能丢最后一批)。
九、键的类型与排序
9.1 合法与非法的键
合法:number、string、Date、ArrayBuffer(及视图)、以及由这些构成的 Array。 非法(抛 DataError):boolean、null、undefined、NaN、普通对象、函数。
记忆法:只有能比较大小、能排序的东西才能当键。
9.2 为什么需要跨类型的全序
因为对象仓库底层是按键排序的有序结构 (B 树 / LSM 树),其根基是"任意两个键都必须可比较"。若仓库混存数字键和字符串键,引擎放入有序结构、执行游标 next/prev、范围查询时,必然 要回答 5 和 'apple' 谁大。不定义跨类型顺序,有序结构就崩了。所以规范强制给出全序:
number < Date < string < binary < array
具体顺序有点任意,但必须确定 且跨浏览器一致。这套顺序也暴露为公开 API:
javascript
indexedDB.cmp(5, 'apple'); // -1
indexedDB.cmp(['a'], ['a','b']); // -1(前缀更短者在前)
9.3 两个陷阱
- 字符串按 UTF-16 码元比较,非字典序 :
'Z' < 'a'(0x5A < 0x61),大写全排小写前;Emoji 用代理对,排序可能反直觉。需自然语言排序时,额外存一个归一化排序字段并对它建索引。 - 数组键逐元素比较 + 前缀规则 :短数组若是长数组前缀,短者在前。
['Alice'] < ['Alice', 2020] < ['Bob', 2018]。
十、键范围:IDBKeyRange
javascript
IDBKeyRange.only(5) // = 5
IDBKeyRange.lowerBound(5, true) // > 5(true 表示开区间)
IDBKeyRange.upperBound(10) // <= 10
IDBKeyRange.bound(5, 10, true, true) // 5 < x < 10
lowerBound(5) : [5═══════════► 含 5
lowerBound(5, true) : (5───────────► 不含 5
bound(5, 10) : [5═════════10] 两端含
可传给 get/getAll/getAllKeys/count/openCursor。
复合键的前缀范围查询 (最实用的模式):索引 keyPath 为 [author, year],查"Alice 的所有书"。利用前缀规则与跨类型顺序(空数组 [] 是 array 类型,大于任何 number/string 的 year):
javascript
const index = store.index('by_au_yr');
const range = IDBKeyRange.bound(['Alice'], ['Alice', []]);
index.getAll(range); // 命中所有 ['Alice', *],自动按 year 排序
['Alice'] ← 下界(含)
['Alice', 1990] ✓
['Alice', 2099] ✓
['Alice', []] ← 上界(含;array 排在所有同前缀键之后)
['Bob', 2018] ✗
这让一个复合索引同时支持"按作者精确查"和"按作者+年份范围查",无需建多个索引。
十一、底层:结构化克隆
IndexedDB 存值时用结构化克隆算法(Structured Clone Algorithm)做深拷贝,而非存引用。
可克隆:基本类型、Array、普通对象、Map、Set、Date、RegExp、ArrayBuffer/TypedArray、Blob、File,以及循环引用 。 不可克隆(抛 DataError):函数、Symbol、DOM 节点。会丢失:原型链、getter/setter、不可枚举属性。
最常踩的是原型丢失:
javascript
class User { greet() { return 'hi'; } }
store.add(new User(), 1);
const got = await idbGet(store, 1);
got.greet(); // 💥 TypeError:取出的是普通对象
got instanceof User; // false
存领域对象后通常需手动重建实例。
十二、底层引擎与配额
| 浏览器 | 引擎 |
|---|---|
| Chrome / Edge | LevelDB(LSM-Tree,天然契合"按键有序、范围扫描") |
| Firefox / Safari | SQLite |
数据按源隔离,遵循同源策略。容量受 Storage Standard 约束:
javascript
const { usage, quota } = await navigator.storage.estimate();
const persisted = await navigator.storage.persist(); // 申请持久化
- best-effort (默认):磁盘紧张时可能被自动驱逐(LRU)。
- persistent (需授权):不会被自动驱逐,但用户仍可手动清除。
它是缓存吗?
关键认知:客户端存储是可能消失的 ------用户随时可清除,best-effort 下浏览器也可驱逐。而版本升级本身不丢数据 (只有显式 deleteObjectStore 才删),会丢数据的是用户删除 ,且 IndexedDB 无任何恢复机制。
因此:永远不要把客户端存储当作关键数据的唯一真相来源。
| 数据性质 | 策略 |
|---|---|
| 服务端数据的本地副本 | 当缓存,丢了重新拉 |
| 纯本地、可容忍丢失 | 接受风险 |
| 重要纯本地数据 | persist() 防自动驱逐(但用户手删仍会丢),并设计同步/备份 |
准确定位:IndexedDB 是**"较持久、但非绝对永久"的本地存储**。"既存浏览器、又保证永不丢失"的方案不存在,这是客户端存储的固有约束。
十三、工程实践清单
- 事务内只做纯 IDB 操作 ,所有
fetch/setTimeout/外部 Promise 放到事务外------这是避免TransactionInactiveError的根本纪律。 - 批量写入用一个事务,同步链式发起多个请求,兼具高性能与原子性。
- 能用索引就别全表扫描 ;大数据集用游标,小数据集用
getAll(后者一次性克隆进内存)。 - 升级代码用 fall-through switch,逐版增量,保护存量数据;需迁移时在升级事务内用游标。
- 多标签页必处理
versionchange和blocked,最省心的方案是location.reload(),顺带解决代码漂移。 - 存领域对象后重建原型,因为结构化克隆会剥离方法。
- 生产用 Promise 封装层 (如
idb库),但封装必须正确处理事务自动提交语义。 - 重要数据调
persist(),并对QuotaExceededError做降级。