indexeddb迁移需分结构升级与数据迁移:结构变更须在onupgradeneeded中按递增整数版本号执行,建表建索引不可错放;数据迁移应于普通事务中分页渐进处理并记录进度;读写时需兼容旧数据格式,并在多标签页场景下监听onversionchange主动关闭旧连接。

IndexedDB 数据库迁移不是改个版本号就自动完成的事,它需要把结构升级和数据迁移拆开处理,既要保证 schema 变更安全,又要让旧数据能被新逻辑正确读取。
结构升级必须在 onupgradeneeded 中完成
每次新增对象存储、修改 keyPath、加删索引,都得提升版本号,并且所有建表建索引操作只能写在 onupgradeneeded 回调里。放错位置(比如写在 onsuccess 里)会直接报 InvalidStateError。
- 版本号必须是整数,且严格递增(1 → 2 → 3),不能复用或跳回
-
oldVersion是触发前的版本,首次打开时为 0,所以判断要用oldVersion 而不是 <code>=== 1,避免用户从 v1 直接升到 v3 时漏掉 v2 的逻辑 - 创建索引前先检查是否存在:
if (!store.indexNames.contains("email")) store.createIndex("email", "email"),防止重复建导致ConstraintError
数据迁移不能塞进 onupgradeneeded
这个回调只适合做轻量级结构变更。一旦要遍历成千上万条记录做字段转换、补默认值、拆分字段等操作,就不能在里面同步执行——会阻塞主线程,甚至被浏览器中断。
- 结构升级完成后,在普通事务中渐进执行数据迁移,比如分页读取 + 批量写入
- 用元数据表(如
metastore)记录迁移进度,每次只处理 100 条,完成后更新标记,避免刷新后重跑 - 迁移过程中若失败,靠事务自动回滚;成功后才更新版本标记,确保幂等性
兼容旧数据的关键细节
新版本代码上线后,用户可能还带着老格式的数据进来。这时候不能假设所有字段都存在,得做防御性读取。
- 读取时用可选链或默认值:
user.fullName ?? `${user.firstName} ${user.lastName}` - 写入新结构前做一次字段映射转换,比如把
name拆成firstName和lastName - 如果字段类型变了(如字符串转数字),迁移脚本里要显式转换:
age: Number(oldRecord.age) || 0
多标签页场景下的升级保障
只要有一个旧标签页还连着老版本数据库,新页面的 onupgradeneeded 就不会触发,而且不报错——这是最隐蔽的坑。
- 在
onsuccess里立刻监听onversionchange,检测到冲突时主动db.close() - 配合 UI 提示用户“请关闭其他页面再继续”,而不是静默等待
- 不要依赖“等旧标签关闭”,升级逻辑必须按“旧连接随时存在”来设计











