IndexedDB 怎样迁移对象仓库结构
来源:17golang原创
时间:2026-11-08 05:06:09 303浏览 收藏
IndexedDB 的对象仓库结构不能像普通 JavaScript 对象那样随时改写。需要新增仓库、删除仓库或调整索引时,应提升数据库版本,让浏览器触发 upgradeneeded,再在这个版本变更事务中完成结构迁移。
官方参考:https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
把每次结构变化当成一个递增版本:新增结构直接创建,索引变化在升级事务中调整,keyPath变化则先把旧数据读出,再重建仓库并写回;同时处理旧页面连接导致的blocked。
先把结构变化拆成可迁移的版本
IndexedDB 用数据库版本号判断是否需要升级。首次打开数据库,或用比当前版本更大的版本号调用 indexedDB.open() 时,打开请求会触发 upgradeneeded。对象仓库和索引的创建、删除只能放在这个版本变更事务里,不能在普通读写事务中临时修改结构。
因此,不要把“当前应该有哪些仓库”写成每次启动都执行的无条件脚本,而要把变化拆成 oldVersion 到目标版本的几个区间。这样用户即使从很老的版本直接升级,也能按顺序补齐中间结构。

// 用递增版本号承载结构变化,避免每次启动都重复创建仓库。
const request = indexedDB.open("notes-db", 3);
request.onupgradeneeded = (event) => {
// oldVersion 表示升级前版本,newVersion 是本次目标版本。
const db = event.target.result;
const oldVersion = event.oldVersion;
if (oldVersion {
// 打开成功后才把连接交给业务层,普通读写从这里开始。
const db = event.target.result;
console.log("database opened", db.version);
};
这里的判断重点是“升级前版本小于某个版本”,而不是只判断当前版本是否等于某个值。用户可能从 1 直接跳到 3,版本 2 的索引也必须被补上。对新仓库和新索引使用名称检查,还能让同一升级逻辑在已完成部分结构的数据库上更稳妥。
在 onupgradeneeded 中按版本执行变更
新增对象仓库最简单:提高版本号,在 onupgradeneeded 中调用 createObjectStore()。删除仓库则使用 deleteObjectStore()。已有仓库的索引同样应在升级事务中创建或删除,业务读写事务只负责数据,不负责改变模式。
可以把升级逻辑写成一个小型迁移表,每个版本只关心自己引入的变化。不要在每次打开时无条件调用 createObjectStore(),因为名称已经存在时会抛出异常,使升级事务失败。
// 每个版本块只声明新增的结构,已存在的名称不重复创建。
function migrateSchema(event) {
const db = event.target.result;
const tx = event.target.transaction;
const oldVersion = event.oldVersion;
if (oldVersion {
// 升级事务失败时不要假装迁移完成,把错误交给上层记录。
console.error("IndexedDB schema upgrade failed", request.error);
};
删除仓库是破坏性动作,只有在确认数据已不再需要,或数据已经被搬到新仓库后才使用。索引变化通常不会改变仓库中的记录,但创建唯一索引时,如果旧数据存在重复值,升级事务仍可能失败,所以唯一性规则要先与旧数据兼容。
遇到 keyPath 变化时重建并搬运数据
keyPath 是对象仓库的结构属性,不能通过一个“修改选项”的调用直接替换。需要改变主键路径时,常见做法是在升级事务中读取旧仓库记录,创建带新 keyPath 的临时仓库,把转换后的记录写入临时仓库,再删除旧仓库并把临时仓库改成最终名称。
迁移数据时要注意:对象仓库的删除会一并删除其中的数据;迁移事务中任何一个请求失败,都可能让整个版本升级回滚。因此转换函数要明确处理缺失字段,并让异常自然进入事务失败路径。
// 将 notes 的主键从自增 id 迁移到稳定的 noteKey。
function rebuildNotesStore(event) {
const db = event.target.result;
const tx = event.target.transaction;
const oldStore = tx.objectStore("notes");
const cursorRequest = oldStore.openCursor();
// 临时仓库使用新 keyPath,避免直接覆盖旧数据。
const nextStore = db.createObjectStore("notes-v2", { keyPath: "noteKey" });
nextStore.createIndex("by-updated-at", "updatedAt", { unique: false });
cursorRequest.onsuccess = (cursorEvent) => {
const cursor = cursorEvent.target.result;
if (!cursor) {
// 游标耗尽后再删除旧仓库,保证此前记录已经逐条写入临时仓库。
db.deleteObjectStore("notes");
return;
}
const oldRecord = cursor.value;
if (typeof oldRecord.noteKey !== "string" || oldRecord.noteKey === "") {
// 缺少新主键时抛错,让升级事务回滚,而不是静默丢弃记录。
throw new Error("noteKey is required during migration");
}
nextStore.put({ ...oldRecord, noteKey: oldRecord.noteKey });
cursor.continue();
};
}
const request = indexedDB.open("notes-db", 6);
request.onupgradeneeded = (event) => {
// 只有从 5 以下升级时才执行一次 keyPath 重建。
if (event.oldVersion
这里是迁移模型而不是可直接套用的业务代码:临时仓库的名字、字段转换规则和索引都要按项目数据设计。更稳妥的做法是先确认所有旧记录都能生成新主键,并把失败原因记录出来;不要为了“让升级通过”而跳过无法转换的记录。
处理旧页面连接和失败回退
当一个页面已经打开旧版本数据库,另一个页面尝试升级时,旧连接可能让升级请求进入 blocked。新页面应监听 blocked,旧页面应监听数据库连接的 versionchange 并及时关闭连接,否则升级没有机会取得版本变更锁。

// 让旧页面在收到版本变更通知后关闭连接,避免新版本长期 blocked。
function openNotesDatabase(version) {
const request = indexedDB.open("notes-db", version);
request.onblocked = () => {
// blocked 说明仍有旧连接占用版本变更锁,应提示用户关闭旧页面。
console.warn("Please close an older tab before upgrading the database");
};
request.onsuccess = (event) => {
const db = event.target.result;
db.onversionchange = () => {
// 旧连接主动关闭,避免阻塞其他页面的升级请求。
db.close();
};
db.onerror = () => {
// 连接级错误只做记录,具体事务错误仍由事务自身处理。
console.error("IndexedDB connection error", db.error);
};
};
request.onerror = () => {
// open 失败时不要替换本地数据,交给上层决定重试或降级。
console.error("IndexedDB open failed", request.error);
};
return request;
}
升级失败时不要把数据库版本写入应用自己的配置表来“标记成功”。浏览器会根据升级事务是否完成来维护版本;迁移请求报错或事务被 abort 时,应用应停止使用未完成的目标结构,并根据错误类型提示重试、关闭旧页面或清理明确可重建的缓存。
迁移时最容易忽略的四个边界
| 边界 | 应该怎么做 | 不要怎么做 |
|---|---|---|
| 版本跳跃 | 用 oldVersion 依次补齐每个版本 | 只处理恰好从 n-1 升级的情况 |
| 名称重复 | 创建前检查对象仓库或索引名称 | 每次打开都无条件 create |
| 主键变化 | 临时仓库搬运数据后再替换旧仓库 | 直接删除旧仓库再期待数据恢复 |
| 旧连接阻塞 | 监听 versionchange、blocked,及时关闭旧连接 | 只监听 onsuccess,忽略 blocked |
可以把一次迁移的发布检查压缩成三问:目标版本是否递增?每个旧版本是否都有明确路径?迁移失败时旧数据是否仍可回退?这三问比单纯检查“打开请求成功”更能发现结构升级风险。
常见问题
新增索引也需要提升数据库版本吗?
需要。索引属于对象仓库结构,应该通过更高版本触发 upgradeneeded,在版本变更事务中创建或删除。
能不能在普通 readwrite 事务中创建对象仓库?
不能。对象仓库和索引的结构调整属于版本变更事务,普通读写事务只适合读写已有结构中的记录。
迁移时直接 deleteObjectStore 会怎样?
仓库中的记录会被删除。如果旧数据仍有价值,应先在同一个升级事务中读取并写入新仓库,确认搬运完成后再删除旧仓库。
为什么新版本一直没有触发升级?
先确认传给 indexedDB.open() 的版本号确实高于当前数据库版本;如果版本没有增加,浏览器不会把普通打开当作结构升级。
小结
IndexedDB 迁移的核心不是在启动时反复“修库”,而是把每次结构变化写成递增版本。新增仓库和索引放进 onupgradeneeded,主键路径变化采用临时仓库搬运,旧页面通过 versionchange 关闭连接,并为 blocked、error 和 abort 留下清晰的失败处理。
-
306 收藏
-
194 收藏
-
427 收藏
-
480 收藏
-
486 收藏
-
110 收藏
-
320 收藏
-
文章 · 前端 | 39分钟前 | javascript · css · View Transition API view-transition-name Element.startViewTransition409 收藏
-
293 收藏
-
199 收藏
-
127 收藏
-
406 收藏
-
265 收藏
-
207 收藏
-
305 收藏
-
332 收藏
-
313 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习