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 事务陷阱与最佳实践:TransactionInactiveError 完全解析

前言:一个让人困惑的错误

在开发基于 IndexedDB 的本地存储服务时,我遇到了一个非常诡异的错误:

Failed to save conversation: TransactionInactiveError:
Failed to execute 'put' on 'IDBObjectStore': The transaction has finished.

奇怪的是:

  • ✅ 代码逻辑看起来完全正确
  • ✅ 事务确实创建了
  • ✅ store 对象也获取到了
  • ❌ 但执行 put 操作时就报错

这篇文章将深入分析这个错误的根本原因,以及 IndexedDB 事务的正确使用方式。


一、问题复现

1.1 错误代码

class MessageStorageService {
  /**
   * 保存或更新会话数据
   */
  async saveConversationDB(
    sessionId: number,
    data: Partial<ConversationData>
  ): Promise<boolean> {
    try {
      const database = await this.initDB();

      // 创建读写事务
      const transaction = database.transaction(["conversations"], "readwrite");
      const store = transaction.objectStore("conversations");

      // ❌ 问题:在事务中调用异步方法
      const existingData = await this.getConversationDB(sessionId);

      // 合并数据
      const updatedData = {
        ...existingData,
        ...data,
        updatedAt: Date.now(),
      };

      // ❌ 报错:TransactionInactiveError
      await new Promise<void>((resolve, reject) => {
        const request = store.put(updatedData);
        request.onsuccess = () => resolve();
        request.onerror = () => reject(request.error);
      });

      return true;
    } catch (error) {
      console.error("Failed to save conversation:", error);
      return false;
    }
  }

  /**
   * 获取会话数据
   */
  async getConversationDB(sessionId: number): Promise<ConversationData | null> {
    const database = await this.initDB();
    const transaction = database.transaction(["conversations"], "readonly");
    const store = transaction.objectStore("conversations");

    return new Promise((resolve, reject) => {
      const request = store.get(sessionId);
      request.onsuccess = () => resolve(request.result || null);
      request.onerror = () => reject(request.error);
    });
  }
}

1.2 错误现象

Uncaught (in promise) TransactionInactiveError:
Failed to execute 'put' on 'IDBObjectStore': The transaction has finished.
    at saveConversationDB (MessageStorageService.ts:151)

二、问题根源:IndexedDB 事务的生命周期

2.1 IndexedDB 事务的自动提交机制

核心原理:

IndexedDB 的事务会在当前"微任务队列"清空后自动提交。

// 创建事务
const transaction = db.transaction(["store"], "readwrite");

// 此时事务是 active 的
console.log(transaction.mode); // 'readwrite'

// 执行同步操作
const store = transaction.objectStore("store");
store.put({ id: 1, name: "test" });

// 当前微任务队列清空后,事务自动提交
// 事务变成 'finished' 状态

2.2 async/await 导致的陷阱

async function saveData() {
  const transaction = db.transaction(["store"], "readwrite");
  const store = transaction.objectStore("store");

  // ❌ 问题:await 让出控制权
  const existingData = await getExistingData();
  //    ^^^^^ 这里会让出控制权到事件循环

  // 控制权返回前,微任务队列清空
  // 事务自动提交并变成 inactive

  // ❌ 此时事务已经结束
  store.put(existingData); // TransactionInactiveError!
}

2.3 时序图分析

❌ 错误流程:

┌─────────────────────────────────────────┐
│ 1. 创建 transaction                     │
│    状态: active                         │
└───────────────┬─────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────┐
│ 2. 获取 store                           │
│    状态: active                         │
└───────────────┬─────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────┐
│ 3. await getConversationDB()            │
│    让出控制权 → 事件循环                │
└───────────────┬─────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────┐
│ 4. 微任务队列清空                       │
│    事务自动提交                         │
│    状态: active → finished              │
└───────────────┬─────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────┐
│ 5. await 返回,拿到 existingData        │
└───────────────┬─────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────┐
│ 6. 尝试执行 store.put()                 │
│    状态: finished (inactive)            │
│    ❌ TransactionInactiveError          │
└─────────────────────────────────────────┘

三、解决方案

方案 1:在事务外准备数据(推荐)

async saveConversationDB(sessionId: number, data: Partial<ConversationData>): Promise<boolean> {
  try {
    const database = await this.initDB();

    // ✅ 修复:先在事务外获取现有数据
    const existingData = await this.getConversationDB(sessionId);

    // ✅ 准备好所有数据
    const updatedData: ConversationData = {
      userId: this._currentUserId.value,
      accountId: data.accountId ?? existingData?.accountId ?? 0,
      sessionId,
      messages: data.messages ?? existingData?.messages ?? [],
      tags: data.tags ?? existingData?.tags ?? [],
      notes: data.notes ?? existingData?.notes ?? {},
      customerInfo: data.customerInfo ?? existingData?.customerInfo ?? { id: 0, name: '', avatar: '' },
      unreadCount: data.unreadCount ?? existingData?.unreadCount ?? 0,
      lastMessageTime: data.lastMessageTime ?? existingData?.lastMessageTime ?? Date.now(),
      messageCount: data.messageCount ?? existingData?.messageCount ?? 0,
      updatedAt: Date.now()
    };

    // ✅ 数据准备完成后,再创建事务
    const transaction = database.transaction(['conversations'], 'readwrite');
    const store = transaction.objectStore('conversations');

    // ✅ 立即执行写入,不再有异步等待
    await new Promise<void>((resolve, reject) => {
      const request = store.put(updatedData);
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });

    return true;
  } catch (error) {
    console.error('Failed to save conversation:', error);
    return false;
  }
}

优点:

  • ✅ 简单直接,易于理解
  • ✅ 避免事务超时问题
  • ✅ 代码清晰,逻辑分离

缺点:

  • ⚠️ 读和写分成两个事务,理论上存在并发问题(实际影响很小)

方案 2:在同一个事务中同步操作

async saveConversationDB(sessionId: number, data: Partial<ConversationData>): Promise<boolean> {
  try {
    const database = await this.initDB();

    return new Promise<boolean>((resolve, reject) => {
      // 创建事务
      const transaction = database.transaction(['conversations'], 'readwrite');
      const store = transaction.objectStore('conversations');

      // ✅ 同步获取现有数据
      const getRequest = store.get(sessionId);

      getRequest.onsuccess = () => {
        const existingData = getRequest.result;

        // 合并数据
        const updatedData = {
          userId: this._currentUserId.value,
          accountId: data.accountId ?? existingData?.accountId ?? 0,
          sessionId,
          messages: data.messages ?? existingData?.messages ?? [],
          // ... 其他字段
          updatedAt: Date.now()
        };

        // ✅ 在同一个事务中执行写入
        const putRequest = store.put(updatedData);

        putRequest.onsuccess = () => resolve(true);
        putRequest.onerror = () => reject(putRequest.error);
      };

      getRequest.onerror = () => reject(getRequest.error);
    });
  } catch (error) {
    console.error('Failed to save conversation:', error);
    return false;
  }
}

优点:

  • ✅ 原子性更好,读写在同一个事务
  • ✅ 避免并发问题

缺点:

  • ⚠️ 代码复杂,回调嵌套
  • ⚠️ 不能使用 async/await,可读性差

方案 3:使用 IDB 库(推荐用于大型项目)

import { openDB } from "idb";

class MessageStorageService {
  private dbPromise = openDB("ConversationDB", 5, {
    upgrade(db) {
      const store = db.createObjectStore("conversations", {
        keyPath: "sessionId",
      });
      store.createIndex("userId", "userId");
      store.createIndex("accountId", "accountId");
    },
  });

  async saveConversationDB(
    sessionId: number,
    data: Partial<ConversationData>
  ): Promise<boolean> {
    try {
      const db = await this.dbPromise;

      // ✅ IDB 库自动处理事务
      const existingData = await db.get("conversations", sessionId);

      const updatedData = {
        ...existingData,
        ...data,
        sessionId,
        updatedAt: Date.now(),
      };

      await db.put("conversations", updatedData);

      return true;
    } catch (error) {
      console.error("Failed to save conversation:", error);
      return false;
    }
  }
}

优点:

  • ✅ 支持 async/await
  • ✅ 自动处理事务生命周期
  • ✅ 代码简洁,易于维护

缺点:

  • ⚠️ 需要引入额外依赖

四、IndexedDB 最佳实践

4.1 事务使用原则

✅ DO(推荐做法)

// ✅ 1. 在事务外准备好所有数据
const data = await prepareData();
const transaction = db.transaction(["store"], "readwrite");
const store = transaction.objectStore("store");
store.put(data);

// ✅ 2. 创建事务后立即执行操作
const transaction = db.transaction(["store"], "readwrite");
const store = transaction.objectStore("store");
const request = store.put(data); // 立即执行

// ✅ 3. 使用回调处理事务内的多个操作
const transaction = db.transaction(["store"], "readwrite");
const store = transaction.objectStore("store");
const getRequest = store.get(1);
getRequest.onsuccess = () => {
  const data = getRequest.result;
  store.put({ ...data, updated: true });
};

// ✅ 4. 使用事务的 complete 事件确认提交
transaction.oncomplete = () => {
  console.log("Transaction completed successfully");
};

❌ DON'T(避免做法)

// ❌ 1. 不要在事务中使用 await
const transaction = db.transaction(["store"], "readwrite");
const store = transaction.objectStore("store");
const data = await fetchData(); // ❌ 事务会在这里结束
store.put(data); // TransactionInactiveError

// ❌ 2. 不要在事务中调用异步函数
const transaction = db.transaction(["store"], "readwrite");
const store = transaction.objectStore("store");
await processData(); // ❌
store.put(processedData);

// ❌ 3. 不要在事务中使用 setTimeout/setInterval
const transaction = db.transaction(["store"], "readwrite");
const store = transaction.objectStore("store");
setTimeout(() => {
  store.put(data); // ❌ 事务早已结束
}, 100);

// ❌ 4. 不要跨事件循环使用事务
const transaction = db.transaction(["store"], "readwrite");
button.addEventListener("click", () => {
  const store = transaction.objectStore("store"); // ❌
  store.put(data);
});

4.2 事务模式选择

// 只读事务(readonly)- 默认模式
const transaction = db.transaction(["store"], "readonly");
const store = transaction.objectStore("store");
store.get(1); // ✅ 可以
store.put(data); // ❌ 报错

// 读写事务(readwrite)
const transaction = db.transaction(["store"], "readwrite");
const store = transaction.objectStore("store");
store.get(1); // ✅ 可以
store.put(data); // ✅ 可以

// 版本变更事务(versionchange)- 只在 onupgradeneeded 中
db.onupgradeneeded = (event) => {
  const db = event.target.result;
  // 只有在这里可以修改数据库结构
  db.createObjectStore("newStore");
};

4.3 错误处理

async function saveData(data) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(["store"], "readwrite");
    const store = transaction.objectStore("store");

    // 请求级别的错误处理
    const request = store.put(data);
    request.onerror = () => {
      console.error("Put request failed:", request.error);
      reject(request.error);
    };

    // 事务级别的错误处理
    transaction.onerror = () => {
      console.error("Transaction failed:", transaction.error);
      reject(transaction.error);
    };

    // 事务成功完成
    transaction.oncomplete = () => {
      console.log("Transaction completed");
      resolve(true);
    };

    // 事务被中止
    transaction.onabort = () => {
      console.error("Transaction aborted");
      reject(new Error("Transaction aborted"));
    };
  });
}

4.4 索引使用优化

// 创建索引
db.onupgradeneeded = (event) => {
  const db = event.target.result;
  const store = db.createObjectStore("conversations", { keyPath: "sessionId" });

  // ✅ 为常用查询字段创建索引
  store.createIndex("userId", "userId", { unique: false });
  store.createIndex("accountId", "accountId", { unique: false });
  store.createIndex("updatedAt", "updatedAt", { unique: false });

  // ✅ 嵌套属性索引
  store.createIndex("customerName", "customerInfo.name", { unique: false });

  // ✅ 复合索引(数组)
  store.createIndex("tags", "tags.name", { multiEntry: true });
};

// 使用索引查询
async function getConversationsByAccount(accountId) {
  const transaction = db.transaction(["conversations"], "readonly");
  const store = transaction.objectStore("conversations");
  const index = store.index("accountId");

  return new Promise((resolve, reject) => {
    // ✅ 通过索引查询(O(log n))
    const request = index.getAll(accountId);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

五、常见错误与解决方案

错误 1:TransactionInactiveError

// ❌ 错误
async function save() {
  const tx = db.transaction(["store"], "readwrite");
  const data = await fetchData(); // 让出控制权
  tx.objectStore("store").put(data); // TransactionInactiveError
}

// ✅ 修复
async function save() {
  const data = await fetchData(); // 先准备数据
  const tx = db.transaction(["store"], "readwrite");
  tx.objectStore("store").put(data);
}

错误 2:InvalidStateError

// ❌ 错误:数据库未正确打开
const request = indexedDB.open("myDB");
const db = request.result; // ❌ db 可能是 undefined
const tx = db.transaction(["store"]);

// ✅ 修复
const request = indexedDB.open("myDB");
request.onsuccess = () => {
  const db = request.result;
  const tx = db.transaction(["store"]);
};

错误 3:DataError

// ❌ 错误:主键类型不匹配
const store = db.createObjectStore("store", { keyPath: "id" });
store.put({ id: "123", name: "test" }); // id 应该是数字
store.put({ id: 456, name: "test" }); // 类型不一致

// ✅ 修复:保持主键类型一致
store.put({ id: 123, name: "test" });
store.put({ id: 456, name: "test" });

错误 4:ConstraintError

// ❌ 错误:违反唯一性约束
const store = db.createObjectStore("store", { keyPath: "id" });
store.createIndex("email", "email", { unique: true });

store.put({ id: 1, email: "[email protected]" });
store.put({ id: 2, email: "[email protected]" }); // ConstraintError

// ✅ 修复:确保唯一性
store.put({ id: 1, email: "[email protected]" });
store.put({ id: 2, email: "[email protected]" });

错误 5:DataCloneError(不可克隆对象)

问题场景

在 Vue 3 项目中使用 IndexedDB 时,尝试存储响应式数据会报错:

// ❌ 错误:尝试存储 Vue 3 响应式对象(Proxy)
const tags = ref([{ name: "标签1", color: "#ff0000" }]);

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

// ❌ 报错:DataCloneError
store.put({
  id: 1,
  tags: tags.value, // Vue 的 Proxy 对象无法克隆
});

错误信息

Failed to execute 'put' on 'IDBObjectStore': #<Object> could not be cloned.
DataCloneError: Failed to execute 'put' on 'IDBObjectStore'

原因分析

IndexedDB 使用结构化克隆算法(Structured Clone Algorithm)存储数据,以下类型无法被克隆:

类型是否可克隆说明
基本类型✅ 可以string, number, boolean, null, undefined
Date✅ 可以日期对象
Array✅ 可以数组
Object✅ 可以普通对象字面量
Map/Set✅ 可以ES6 集合
ArrayBuffer✅ 可以二进制数据
Function❌ 不可以函数
Symbol❌ 不可以Symbol 类型
Proxy❌ 不可以Vue 3 响应式对象
DOM 节点❌ 不可以HTMLElement 等
循环引用❌ 不可以自引用对象

解决方案 1:统一在 Service 层深拷贝(推荐)

// MessageStorageService.ts
async saveConversationDB(sessionId: number, data: Partial<ConversationDataDB>): Promise<boolean> {
  try {
    const database = await this.initDB();
    const existingData = await this.getConversationDB(sessionId);

    const transaction = database.transaction(['conversations'], 'readwrite');
    const store = transaction.objectStore('conversations');

    // ✅ 深拷贝函数:移除 Proxy、函数等不可克隆对象
    const sanitizeData = <T>(value: T): T => {
      return JSON.parse(JSON.stringify(value));
    };

    const updatedData: ConversationDataDB = {
      userId: this._currentUserId.value,
      accountId: data.accountId ?? existingData?.accountId ?? 0,
      sessionId,
      // ✅ 对复杂对象进行深拷贝
      messages: data.messages ? sanitizeData(data.messages) : existingData?.messages ?? [],
      tags: data.tags ? sanitizeData(data.tags) : existingData?.tags ?? [],
      notes: data.notes ? sanitizeData(data.notes) : existingData?.notes ?? {},
      customerInfo: data.customerInfo
        ? sanitizeData(data.customerInfo)
        : existingData?.customerInfo ?? { id: 0, name: '', avatar: '' },
      unreadCount: data.unreadCount ?? existingData?.unreadCount ?? 0,
      lastMessage: data.lastMessage
        ? sanitizeData(data.lastMessage)
        : existingData?.lastMessage ?? { time: Date.now(), content: '', type: 'text' },
      messageCount: data.messageCount ?? existingData?.messageCount ?? 0,
      updatedAt: Date.now()
    };

    await new Promise<void>((resolve, reject) => {
      const request = store.put(updatedData);
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });

    return true;
  } catch (error) {
    console.error('Failed to save conversation:', error);
    return false;
  }
}

解决方案 2:手动创建纯对象

<script setup lang="ts">
import { ref } from "vue";
import { messageStorage } from "./services/MessageStorageService";

const tagList = ref([
  { id: 1, name: "标签1", color: "#ff0000" },
  { id: 2, name: "标签2", color: "#00ff00" },
]);

// ❌ 错误做法
const addTagWrong = async () => {
  const item = tagList.value.find((t) => t.id === 1);
  const tags = [item]; // item 是 Proxy 对象

  await messageStorage.saveConversationTagsDB(sessionId, tags);
  // DataCloneError!
};

// ✅ 正确做法:手动创建纯对象
const addTagCorrect = async () => {
  const item = tagList.value.find((t) => t.id === 1);

  // 创建纯对象,只提取需要的字段
  const newTag = {
    id: item.id,
    name: item.name,
    color: item.color,
  };

  const tags = [newTag];
  await messageStorage.saveConversationTagsDB(sessionId, tags);
  // ✅ 成功
};
</script>

解决方案 3:使用 toRaw(Vue 3)

import { toRaw } from "vue";

// ✅ 使用 Vue 的 toRaw 移除响应式
const rawTags = toRaw(tags.value);
store.put({ id: 1, tags: rawTags });

实际案例:添加标签功能

<script setup lang="ts">
// BasicInfo.vue
import { ref } from "vue";
import { messageStorage } from "../../services/MessageStorageService";

const tagList = ref<Array<{ id: number; name: string; color: string }>>([]);
const checkedTag = ref("");
const activeSessionId = ref(1);
const clientInfo = ref({ tags: [] });

// 添加标签
const addCheckedTag = async () => {
  const item = tagList.value.find((item) => item.name === checkedTag.value);

  if (!item) {
    ElMessage.error("标签不存在");
    return;
  }

  // ✅ 方式 1:Service 层统一处理(推荐)
  // 直接传入响应式数据,Service 层会自动深拷贝
  const tags = [...(clientInfo.value?.tags || []), item];
  await messageStorage.saveConversationTagsDB(activeSessionId.value, tags);

  // ✅ 方式 2:手动创建纯对象
  const newTag = { id: item.id, name: item.name, color: item.color };
  const tags2 = [...(clientInfo.value?.tags || []), newTag];
  await messageStorage.saveConversationTagsDB(activeSessionId.value, tags2);

  // ✅ 方式 3:JSON 深拷贝
  const tags3 = [...(clientInfo.value?.tags || []), item];
  const pureTags = JSON.parse(JSON.stringify(tags3));
  await messageStorage.saveConversationTagsDB(activeSessionId.value, pureTags);
};
</script>

注意事项

JSON.parse(JSON.stringify()) 的限制:

// ⚠️ 会丢失的数据类型
const data = {
  date: new Date(), // ❌ 变成字符串 "2024-01-01T00:00:00.000Z"
  func: () => {}, // ❌ 会丢失
  symbol: Symbol("test"), // ❌ 会丢失
  undefined: undefined, // ❌ 会丢失
  regex: /test/, // ❌ 变成空对象 {}
  map: new Map(), // ❌ 变成空对象 {}
};

const result = JSON.parse(JSON.stringify(data));
console.log(result);
// { date: "2024-01-01T00:00:00.000Z" }

如果需要保留特殊类型,使用自定义深拷贝:

function deepClone(obj) {
  if (obj === null || typeof obj !== "object") return obj;

  // 处理 Date
  if (obj instanceof Date) return new Date(obj);

  // 处理 Array
  if (Array.isArray(obj)) return obj.map((item) => deepClone(item));

  // 处理普通对象
  const cloned = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloned[key] = deepClone(obj[key]);
    }
  }
  return cloned;
}

六、性能优化建议

6.1 批量操作

// ❌ 低效:多次创建事务
async function saveMultiple(items) {
  for (const item of items) {
    const tx = db.transaction(["store"], "readwrite");
    await new Promise((resolve) => {
      tx.objectStore("store").put(item);
      tx.oncomplete = resolve;
    });
  }
}

// ✅ 高效:一个事务完成所有操作
async function saveMultiple(items) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(["store"], "readwrite");
    const store = tx.objectStore("store");

    items.forEach((item) => store.put(item));

    tx.oncomplete = () => resolve(true);
    tx.onerror = () => reject(tx.error);
  });
}

6.2 游标优化

// ❌ 低效:getAll 加载所有数据到内存
async function processAllRecords() {
  const tx = db.transaction(["store"], "readonly");
  const store = tx.objectStore("store");
  const allRecords = await store.getAll(); // 可能占用大量内存

  allRecords.forEach((record) => process(record));
}

// ✅ 高效:使用游标逐条处理
async function processAllRecords() {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(["store"], "readonly");
    const store = tx.objectStore("store");
    const cursorRequest = store.openCursor();

    cursorRequest.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        process(cursor.value); // 处理当前记录
        cursor.continue(); // 移动到下一条
      } else {
        resolve(); // 处理完成
      }
    };

    cursorRequest.onerror = () => reject(cursorRequest.error);
  });
}

6.3 索引选择

// ❌ 低效:无索引全表扫描
async function findByAccountId(accountId) {
  const tx = db.transaction(["conversations"], "readonly");
  const store = tx.objectStore("conversations");
  const allRecords = await store.getAll();

  // O(n) 内存过滤
  return allRecords.filter((r) => r.accountId === accountId);
}

// ✅ 高效:使用索引查询
async function findByAccountId(accountId) {
  const tx = db.transaction(["conversations"], "readonly");
  const store = tx.objectStore("conversations");
  const index = store.index("accountId");

  // O(log n) 索引查询
  return new Promise((resolve, reject) => {
    const request = index.getAll(accountId);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

七、调试技巧

7.1 开发者工具

// Chrome DevTools: Application -> Storage -> IndexedDB
// 可以查看:
// 1. 数据库列表
// 2. ObjectStore 结构
// 3. 索引信息
// 4. 存储的数据

// 代码中添加日志
const transaction = db.transaction(["store"], "readwrite");

transaction.oncomplete = () => {
  console.log("✅ Transaction completed");
};

transaction.onerror = () => {
  console.error("❌ Transaction error:", transaction.error);
};

transaction.onabort = () => {
  console.warn("⚠️ Transaction aborted");
};

7.2 事务状态检查

function checkTransactionState(transaction) {
  console.log("Transaction mode:", transaction.mode);
  console.log("Transaction db:", transaction.db);
  console.log("Transaction error:", transaction.error);

  // 检查事务是否仍然活跃
  try {
    transaction.objectStore("store");
    console.log("✅ Transaction is active");
  } catch (e) {
    console.error("❌ Transaction is inactive:", e);
  }
}

八、总结

核心原则

  1. 事务会自动提交

    • 微任务队列清空后自动提交
    • 不要在事务中使用 await
  2. 先准备数据,再创建事务

    • 在事务外完成异步操作
    • 创建事务后立即执行写入
  3. 选择合适的事务模式

    • 只读操作用 readonly
    • 写入操作用 readwrite
  4. 合理使用索引

    • 为常用查询字段创建索引
    • 避免全表扫描

最佳实践 Checklist

  • [ ] 所有异步操作在事务外完成
  • [ ] 事务创建后立即使用
  • [ ] 批量操作使用单个事务
  • [ ] 为常用查询创建索引
  • [ ] 添加完善的错误处理
  • [ ] 使用事务的 oncomplete 确认成功
  • [ ] 大数据量使用游标处理
  • [ ] 在开发工具中验证数据结构

常见错误速查表

错误原因解决方案
TransactionInactiveError事务中使用 await在事务外准备数据
InvalidStateError数据库未正确打开在 onsuccess 回调中使用
DataError主键类型不匹配保持主键类型一致
ConstraintError违反唯一性约束检查唯一索引
DataCloneError存储不可克隆对象(Proxy)JSON 深拷贝或使用 toRaw
NotFoundErrorObjectStore 不存在检查 store 名称
ReadOnlyError只读事务中写入使用 readwrite 模式

参考资料

  • MDN: IndexedDB API
  • MDN: IDBTransaction
  • Working with IndexedDB
  • IDB Library
  • IndexedDB Best Practices

作者注: IndexedDB 的事务机制初看很简单,但陷阱很多。理解事务的自动提交机制是用好 IndexedDB 的关键。希望这篇文章能帮你避开这些坑!

Happy Coding! 🎉

最后更新时间:
贡献者: 何风顺
上一页
IndexedDB 索引陷阱:字段重命名后索引返回旧数据