SSF0SSF0
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
  • HTML

    • html1
    • html2
  • CSS

    • Flex 布局常见问题与解决方案
  • JavaScript

    • 数据类型及引用问题
    • 处理 Blob 类型文件下载问题总结
    • localStorage 与 sessionStorage 区别
    • JavaScript 中的 script 标签问题详解
    • JavaScript 中的 this 指向问题详解
    • SessionStorage踩坑记录
    • 动态加载 JS 脚本方法对比
    • 浏览器页面关闭场景下的数据上报
    • HTTP流式接口详解与封装实践
    • JavaScript 中 this 绑定与解构:为什么 Pinia Store 可以解构,但 Class 实例方法不行?
  • es6

    • Promise
    • es6 模块导出方式全解析
    • reduce
    • forEach 中使用 async/await 的异步陷阱
  • Vue2

    • created VS mounted 发起异步请求
    • vue2-2
  • Vue3

    • Vite + Vue3 修改 element-plus 源码
    • Vue v-if 与 v-show
    • Vue3 ref 获取组件
    • Vue3 路由传参
    • 父子组件与组件里 Hooks 加载顺序
    • 第三方组件传入参数TS提示
    • Vue3 Props 在 Hooks 中的响应性处理
    • Vue Router 的两种历史模式及部署配置详解
    • 在Vue 3项目中顺利集成Tailwind CSS的完整指南
    • Vue Scoped CSS 深度解析
    • Vue 3 Teleport 组件完全指南:从原理到实战
    • Vant List 组件中双向绑定 loading 的注意事项
    • Vue 3 + Vite 中的路径别名与静态资源处理深度解析
    • 获取图片高度
    • 深入理解 Pinia:storeToRefs vs toRefs - 避开 TypeScript 类型推断的坑
  • webSocket

    • WebSocket 鉴权入门(实战版):握手票据 / 首帧鉴权 + 连接级 cid
  • Electron

    • 快速构建 electron + vue3 项目
  • TS

    • TS 泛型
    • 记录模板使用断言的问题
    • type 与 interface
  • WebPack

    • Webpack 介绍
  • Vite

    • Vite CLI 常见命令
    • vite 与 webpack 比较
    • Vite base 配置与 public 静态资源路径问题
  • 客户端数据库

    • IndexedDB 索引陷阱:字段重命名后索引返回旧数据
    • IndexedDB 事务陷阱与最佳实践:TransactionInactiveError 完全解析
  • UniApp

    • uni-app + Monorepo + 微信小程序热更新失效问题排查与结论总结
    • Monorepo + uni-app 开发中监听 / 热更新机制差异与解决方案
    • uni-app 运行与编译原理梳理
    • 为什么 uni-app 在 Monorepo 中必须做「环境区分」,而 Web 项目却不用?
  • 项目工程

    • 前端代码风格
    • Vue3 项目规范
    • npm 镜像问题
    • 包管理工具
    • 使用 engines 限制开发环境依赖
    • 打包与shell交互指定模式
    • Monorepo示例
    • PC 项目兼容 uni-app:基于 pnpm Monorepo 的工程化重构实践
    • Vue3 + Vite + pnpm Monorepo 迁移问题指南
    • 前端工程化:深入理解 npm 依赖管理机制
    • pnpm 修改依赖源码打包报错
    • 代码片段
  • 移动端

    • 浏览器跳转外部应用时请求状态码问题
  • 记录一下小程序
  • 控制浏览器正确保存网站账号密码的技巧
  • 信息泄露
  • 使用Gulp实现前端项目哈希化压缩打包
  • 前端缓存策略与文件哈希化的关联解析
  • 浏览器刷新方式与 Vue 生命周期、缓存行为解析
  • 浏览器消息铃声实现记录

IndexedDB 索引陷阱:字段重命名后索引返回旧数据

问题场景

在使用 IndexedDB 开发会话管理系统时,遇到一个诡异的问题:

现象:

  1. 升级了数据库版本号(从 7 升到更高版本)
  2. 修改了字段结构:lastMessageTime: number → lastMessage: { time, content, type }
  3. 保存新数据成功,浏览器 DevTools 中看到的数据是新结构
  4. 但通过索引查询返回的数据却是旧结构!
// 实际代码:MessageStorageService.ts

export interface ConversationDataDB {
  userId: number;
  accountId: number;
  sessionId: number;
  messages: MessageItem[];
  // ... 其他字段

  // ✅ 新字段定义
  lastMessage: {
    time: number;
    content: string;
    type: "text" | "files" | "images";
  };
}

// 保存数据
await messageStorage.saveConversationDB(1, {
  accountId: 100,
  lastMessage: {
    time: Date.now(),
    content: "你好",
    type: "text",
  },
});
// ✅ 保存成功

// Application → IndexedDB → ConversationDB → conversations
// ✅ 浏览器中看到:
// { sessionId: 1, accountId: 100, lastMessage: { time: 1234567890, content: '你好', type: 'text' } }

// 通过索引查询
const conversations = await messageStorage.getConversationsByAccountDB(100);
console.log(conversations[0]);
// ❌ 输出:{ sessionId: 1, accountId: 100, lastMessageTime: 0 }
// 字段是旧的 lastMessageTime,而不是新的 lastMessage!

问题对比

直接查询 vs 索引查询

// 方式 1:直接通过主键查询 - ✅ 正常
const conversation = await messageStorage.getConversationDB(1);
console.log(conversation.lastMessage);
// 输出:{ time: 1234567890, content: '你好', type: 'text' }

// 方式 2:通过 accountId 索引查询 - ❌ 返回旧数据
const conversations = await messageStorage.getConversationsByAccountDB(100);
console.log(conversations[0].lastMessage);
// 输出:undefined

console.log(conversations[0].lastMessageTime);
// 输出:0
// 旧字段!
查询方式结果
浏览器 DevTools 查看✅ lastMessage: { time, content, type }
store.get(sessionId)✅ lastMessage: { time, content, type }
index.getAll(accountId)❌ lastMessageTime: 0

问题原因

IndexedDB 索引机制

IndexedDB 的索引是一个独立的数据结构,类似于传统数据库的索引:

┌─────────────────────────────────────────────────────────────┐
│                ObjectStore (实际存储的数据)                   │
├─────────────────────────────────────────────────────────────┤
│ sessionId: 1, accountId: 100, lastMessage: {...}            │  ← 最新数据
│ sessionId: 2, accountId: 100, lastMessage: {...}            │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                 Index: accountId (索引表)                    │
├─────────────────────────────────────────────────────────────┤
│ accountId: 100 → [旧数据快照 with lastMessageTime: 0]      │  ← 缓存的旧数据
└─────────────────────────────────────────────────────────────┘

为什么会出现这种情况?

根本原因:只修改了字段定义和保存逻辑,但没有触发索引重建

// 你的代码:MessageStorageService.ts

const DB_VERSION = 1; // ❌ 问题:版本号没变或升级逻辑不完整

request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
  const database = (event.target as IDBOpenDBRequest).result;

  if (!database.objectStoreNames.contains(CONVERSATION_STORE)) {
    const conversationStore = database.createObjectStore(CONVERSATION_STORE, {
      keyPath: "sessionId",
    });

    conversationStore.createIndex("userId", "userId", { unique: false });
    conversationStore.createIndex("accountId", "accountId", { unique: false });
    conversationStore.createIndex("unreadCount", "unreadCount", {
      unique: false,
    });
    // ...
  } else {
    // ❌ 问题:这部分代码被注释掉了!
    // const transaction = (event.target as IDBOpenDBRequest).transaction;
    // if (transaction) {
    //   const conversationStore = transaction.objectStore(CONVERSATION_STORE);
    //   if (!conversationStore.indexNames.contains("userId")) {
    //     conversationStore.createIndex("userId", "userId", { unique: false });
    //   }
    // }
  }
};

问题分析:

  1. 数据库在版本 1 时创建了 accountId 索引
  2. 索引扫描了当时的所有数据(旧字段 lastMessageTime)
  3. 后来修改了接口定义,字段改为 lastMessage
  4. 但没有升级版本号或重建索引
  5. 索引仍然缓存着旧数据结构
  6. 新数据保存到 ObjectStore 成功,但索引没有更新

解决方案

方案 1:升级版本号 + 删除并重建索引(推荐)

// MessageStorageService.ts

const DB_NAME = "ConversationDB";
const DB_VERSION = 2;  // ✅ 升级版本号:1 → 2
const CONVERSATION_STORE = "conversations";

export interface ConversationDataDB {
  userId: number;
  accountId: number;
  sessionId: number;
  messages: MessageItem[];
  tags: Array<{ name: string; color: string; }>;
  notes: Record<string, any>;
  customerInfo: { id: number; name: string; avatar: string; };
  updatedAt: number;
  unreadCount: number;
  messageCount: number;

  lastMessage: {  // 新字段
    time: number;
    content: string;
    type: "text" | "files" | "images";
  };
}

// ... 省略其他代码

private initDB(): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    if (this.db.value && this._isInitialized.value) {
      resolve(this.db.value);
      return;
    }

    const request = indexedDB.open(DB_NAME, DB_VERSION);

    request.onerror = () => {
      reject(new Error("Failed to open IndexedDB"));
    };

    request.onsuccess = () => {
      this.db.value = request.result;
      this._isInitialized.value = true;
      resolve(request.result);
    };

    request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
      const database = (event.target as IDBOpenDBRequest).result;
      const oldVersion = event.oldVersion;

      // 首次创建数据库
      if (!database.objectStoreNames.contains(CONVERSATION_STORE)) {
        const conversationStore = database.createObjectStore(CONVERSATION_STORE, {
          keyPath: "sessionId",
        });

        conversationStore.createIndex("userId", "userId", { unique: false });
        conversationStore.createIndex("accountId", "accountId", { unique: false });
        conversationStore.createIndex("customerName", "customerInfo.name", { unique: false });
        conversationStore.createIndex("unreadCount", "unreadCount", { unique: false });
        conversationStore.createIndex("tags", "tags.name", { multiEntry: true });

        console.log("数据库首次创建完成");
      } else {
        // ✅ 版本升级逻辑
        const transaction = (event.target as IDBOpenDBRequest).transaction!;
        const conversationStore = transaction.objectStore(CONVERSATION_STORE);

        // 版本 1 → 2:重建所有索引(因为数据结构改变了)
        if (oldVersion < 2) {
          console.log(`开始升级数据库:版本 ${oldVersion} → 2`);

          // 1️⃣ 删除所有索引(强制重建)
          const allIndexes = Array.from(conversationStore.indexNames);
          allIndexes.forEach(indexName => {
            conversationStore.deleteIndex(indexName);
            console.log(`已删除索引: ${indexName}`);
          });

          // 2️⃣ 重新创建索引(会自动扫描所有数据,使用新的数据结构)
          conversationStore.createIndex("userId", "userId", { unique: false });
          conversationStore.createIndex("accountId", "accountId", { unique: false });
          conversationStore.createIndex("customerName", "customerInfo.name", { unique: false });
          conversationStore.createIndex("unreadCount", "unreadCount", { unique: false });
          conversationStore.createIndex("tags", "tags.name", { multiEntry: true });

          console.log("索引重建完成");
        }
      }
    };
  });
}

关键步骤:

  1. 升级版本号:DB_VERSION = 2(从 1 升到 2)
  2. 删除所有索引:conversationStore.deleteIndex(indexName)
  3. 重新创建索引:IndexedDB 会自动扫描 ObjectStore 中的所有数据,使用当前的数据结构重建索引

为什么这样能解决问题?

  • 删除索引会清空索引缓存
  • 重新创建索引时,IndexedDB 会扫描 ObjectStore 中的实际数据
  • 此时数据已经是新结构(lastMessage),索引也会基于新结构建立

方案 2:删除数据库重新初始化(开发环境最快)

适用场景:

  • ✅ 开发/测试阶段
  • ✅ 本地数据可以丢失
  • ✅ 数据可以从服务器重新拉取
// 方式 1:浏览器控制台执行
indexedDB.deleteDatabase("ConversationDB");
location.reload(); // 刷新页面,数据库会自动重新创建

// 方式 2:代码中临时添加(记得之后删除)
if (import.meta.env.DEV) {
  await indexedDB.deleteDatabase("ConversationDB");
  console.log("开发环境:数据库已删除,即将刷新");
  location.reload();
}

优点:

  • ✅ 最快的解决方式
  • ✅ 无需编写升级逻辑
  • ✅ 数据和索引都是全新的

缺点:

  • ❌ 会丢失所有本地数据
  • ❌ 不适用于生产环境

验证修复是否成功

1. 浏览器控制台验证

// 打开浏览器控制台执行
const request = indexedDB.open("ConversationDB");
request.onsuccess = (e) => {
  const db = e.target.result;
  console.log("数据库版本:", db.version); // 应该是 2

  const transaction = db.transaction(["conversations"], "readonly");
  const store = transaction.objectStore("conversations");

  // 检查索引列表
  console.log("索引列表:", Array.from(store.indexNames));

  // 对比两种查询方式
  const sessionId = 1;

  // 直接查询
  store.get(sessionId).onsuccess = (e) => {
    console.log("直接查询:", e.target.result);
    console.log(
      "字段检查:",
      "lastMessage" in e.target.result ? "✅ 新字段" : "❌ 旧字段"
    );
  };

  // 索引查询
  const index = store.index("accountId");
  index.getAll(100).onsuccess = (e) => {
    console.log("索引查询:", e.target.result);
    if (e.target.result.length > 0) {
      console.log(
        "字段检查:",
        "lastMessage" in e.target.result[0] ? "✅ 新字段" : "❌ 旧字段"
      );
    }
  };
};

2. 代码中添加日志

// useConversation.ts 或其他调用处

async function testIndexQuery() {
  // 保存测试数据
  await messageStorage.saveConversationDB(999, {
    accountId: 999,
    lastMessage: {
      time: Date.now(),
      content: "测试索引",
      type: "text",
    },
    messages: [],
    tags: [],
    notes: {},
    customerInfo: { id: 999, name: "测试用户", avatar: "" },
    unreadCount: 0,
    messageCount: 0,
  });

  // 通过索引查询
  const result = await messageStorage.getConversationsByAccountDB(999);
  console.log("索引查询结果:", result[0]);

  // 检查字段
  if (result[0].lastMessage) {
    console.log("✅ 索引返回新字段 lastMessage");
    console.log("内容:", result[0].lastMessage);
  } else if ((result[0] as any).lastMessageTime !== undefined) {
    console.log("❌ 索引返回旧字段 lastMessageTime");
  }
}

// 调用测试
testIndexQuery();

为什么会出现这个坑?

常见错误场景

错误 1:只改接口,不升级版本

// ❌ 错误做法
export interface ConversationDataDB {
  // lastMessageTime: number;  // 删除旧字段
  lastMessage: { time: number; content: string; type: string }; // 添加新字段
}

const DB_VERSION = 1; // ❌ 版本号没变

后果:

  • onupgradeneeded 不会触发
  • 索引不会重建
  • 查询返回旧数据

错误 2:升级版本但没有重建索引

// ❌ 错误做法
const DB_VERSION = 2; // ✅ 版本号升级了

request.onupgradeneeded = (event) => {
  // ❌ 但没有处理索引
  const database = (event.target as IDBOpenDBRequest).result;

  if (!database.objectStoreNames.contains(CONVERSATION_STORE)) {
    // 创建表...
  } else {
    // ❌ 什么都没做!索引不会重建
  }
};

后果:

  • 虽然版本号升级了,但索引仍是旧的
  • 查询返回旧数据

错误 3:注释掉升级逻辑

// ❌ 你的代码中的情况
request.onupgradeneeded = (event) => {
  if (!database.objectStoreNames.contains(CONVERSATION_STORE)) {
    // 创建表和索引...
  } else {
    // ❌ 升级逻辑被注释掉了
    // const transaction = (event.target as IDBOpenDBRequest).transaction;
    // if (transaction) {
    //   const conversationStore = transaction.objectStore(CONVERSATION_STORE);
    //   // ...
    // }
  }
};

最佳实践

字段重命名的完整流程

  1. 升级版本号

    const DB_VERSION = 2; // 1 → 2
  2. 更新 TypeScript 接口

    export interface ConversationDataDB {
      lastMessage: { time: number; content: string; type: string };
    }
  3. 编写升级逻辑

    if (oldVersion < 2) {
      // 删除所有索引
      const allIndexes = Array.from(conversationStore.indexNames);
      allIndexes.forEach((indexName) => {
        conversationStore.deleteIndex(indexName);
      });
    
      // 重新创建索引
      conversationStore.createIndex("accountId", "accountId", { unique: false });
      // ...
    }
  4. 更新保存逻辑

    const updatedData: ConversationDataDB = {
      // ...
      lastMessage: data.lastMessage ??
        existingData?.lastMessage ?? {
          time: Date.now(),
          content: "",
          type: "text",
        },
    };
  5. 测试验证

    • 删除旧数据库测试
    • 升级路径测试(版本 1 → 2)
    • 索引查询验证

总结

问题原因解决方案
索引返回旧数据索引未重建,缓存旧结构✅ 升级版本 + 删除索引 + 重建索引
浏览器显示新数据但查询是旧的索引和数据不同步✅ 强制重建索引
开发环境快速修复需要全新数据库✅ 删除数据库 + 刷新页面

核心原则:

  1. ✅ 字段结构改变时必须升级版本号
  2. ✅ 升级时必须删除并重建相关索引
  3. ✅ 不要注释掉升级逻辑代码
  4. ✅ 开发环境可以删库重来,生产环境必须写升级逻辑

记住这个坑:IndexedDB 的索引是独立的数据结构,不会自动跟随数据更新!字段重命名时务必重建索引!

最后更新时间:
贡献者: 何风顺
下一页
IndexedDB 事务陷阱与最佳实践:TransactionInactiveError 完全解析