登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

IndexedDB versionchange 处理旧连接阻塞升级的方法

来源:17golang原创

时间:2026-11-13 19:43:26 363浏览 收藏

我在把一个离线缓存从旧 schema 升到新 schema 时,最容易误判的不是 onupgradeneeded,而是升级请求一直没有进入它。原因通常是另一个标签页还握着旧的 IDBDatabase 连接。解决办法是:所有成功打开的连接都监听 versionchange,收到通知后停止继续发起业务事务并调用 db.close();发起升级的一侧监听 blocked,给用户明确的刷新提示。连接释放后,升级请求才会进入 upgradeneeded。

这套处理只解决“旧连接阻塞结构升级”,不替代数据迁移设计,也不意味着可以在普通 readwrite 事务里创建 object store。

先把阻塞关系看清楚

IndexedDB 的结构变更由更高版本的 indexedDB.open(name, version) 触发。浏览器会先向仍然打开同一数据库的连接发送 versionchange;只要连接没有关闭,新的打开请求就可能停在 blocked。因此不要只在升级页写 onblocked,旧页面也必须具备主动让路的处理。

IndexedDB 旧连接阻塞升级请求的结构关系说明图
图1:IndexedDB 升级阻塞关系说明图,不是浏览器截图或运行证据。

旧连接收到 versionchange 后立即关闭

连接建立成功时就安装监听器。实际项目里我会先设置一个标记,阻止新的业务操作继续使用这条即将失效的连接,然后关闭连接并提示刷新。这样既能减少升级等待,也能避免页面在迁移期间继续提交旧结构假设下的请求。

let db;
let upgrading = false;

function attachDatabase(nextDb) {
  // 保存当前连接,并在其他页面要求升级时主动让路。
  db = nextDb;
  db.onversionchange = () => {
    upgrading = true;
    db.close();
    // 真实项目可替换为 toast;不要继续使用旧连接写数据。
    showUpgradeNotice('数据结构正在更新,请刷新页面');
  };
  db.onclose = () => {
    // 非预期关闭也要让业务层停止提交新请求。
    upgrading = true;
  };
}

function assertDatabaseReady() {
  // 在业务入口拦截已经收到升级通知的旧页面。
  if (upgrading || !db) throw new Error('database connection is not ready');
  return db;
}

这里的 close() 不会删除数据,只是释放连接。未完成的业务事务仍应由业务层按失败路径处理,不能把“收到通知”当成一次成功写入。

升级请求用 blocked 做用户反馈

新页面打开更高版本时,把数据库迁移集中放在 onupgradeneeded,同时用 onblocked 告诉用户还有旧页面没有释放连接。提示语应该说明动作,例如关闭其他标签页或刷新旧页面,而不是只显示“升级失败”。

function openLatestDatabase() {
  // 版本号只在确实需要变更结构时递增。
  const request = indexedDB.open('offline-cache', 4);

  request.onblocked = () => {
    // 这表示仍有旧连接未关闭,不要重复发起 open 请求。
    showUpgradeNotice('请关闭其他标签页后重试');
  };

  request.onupgradeneeded = (event) => {
    // schema 变更只能在 versionchange 事务中完成。
    const nextDb = event.target.result;
    if (!nextDb.objectStoreNames.contains('settings')) {
      nextDb.createObjectStore('settings', { keyPath: 'key' });
    }
    if (!nextDb.objectStoreNames.contains('records')) {
      const store = nextDb.createObjectStore('records', { keyPath: 'id' });
      store.createIndex('updatedAt', 'updatedAt');
    }
  };

  request.onsuccess = (event) => {
    // 成功后统一交给连接管理器,避免漏装 versionchange。
    attachDatabase(event.target.result);
  };

  request.onerror = (event) => {
    // 错误信息用于诊断,不把失败伪装成 blocked。
    console.error('IndexedDB open failed', event.target.error?.name);
  };
}

如果旧页面已经执行了 db.close(),blocked 通常会很快结束;不要用定时器轮询“数据库是否升级”,事件本身就是同步边界。

把关闭、迁移和异常处理放进同一条链

迁移分支要根据 oldVersion 逐段推进,不能只写“当前版本直接建全部表”。这样从 1 升到 4 和从 3 升到 4 都能走同一套逻辑。另一个边界是 VersionError:如果某个旧页面反而用更低版本打开,应该停止它的初始化并刷新,而不是反复重试。

IndexedDB versionchange 关闭迁移和异常恢复的生命周期边界说明图
图2:versionchange 生命周期边界说明图,不是实际运行截图。
function migrate(event) {
  // 按旧版本逐段迁移,让跨多个版本的用户也能到达当前结构。
  const db = event.target.result;
  const oldVersion = event.oldVersion;

  if (oldVersion 

上线前用这张清单复查

  • 每个 onsuccess 得到的连接都安装了 onversionchange,并且确实调用 close()。
  • 升级请求有 blocked、error 和 success 分支,旧页面不会无限重复 open()。
  • object store 与 index 的创建只发生在 upgradeneeded,迁移分支按 oldVersion 可重复执行。
  • 收到关闭通知后,业务入口不再提交新事务;刷新或重新打开是明确的恢复路径。
现象优先检查处理方向
一直 blocked其他标签页的 versionchange关闭旧连接并提示用户刷新
直接 VersionError打开版本低于磁盘版本刷新代码或统一版本常量
迁移重复报错object store/index 是否已存在用 contains/indexNames 做幂等判断

常见疑问

为什么只写 onblocked 仍然升级不了?

onblocked 只能告诉升级请求“还有连接占用”,不能替旧页面关闭连接。真正的让路动作必须写在每个旧连接的 onversionchange 处理里。

收到 versionchange 后一定要立刻刷新吗?

不一定。先关闭连接并停止新事务即可;如果页面必须依赖新 schema 才能继续工作,再提示用户刷新。刷新是产品交互选择,close() 才是释放升级锁的技术动作。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>