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);
}
}八、总结
核心原则
事务会自动提交
- 微任务队列清空后自动提交
- 不要在事务中使用
await
先准备数据,再创建事务
- 在事务外完成异步操作
- 创建事务后立即执行写入
选择合适的事务模式
- 只读操作用
readonly - 写入操作用
readwrite
- 只读操作用
合理使用索引
- 为常用查询字段创建索引
- 避免全表扫描
最佳实践 Checklist
- [ ] 所有异步操作在事务外完成
- [ ] 事务创建后立即使用
- [ ] 批量操作使用单个事务
- [ ] 为常用查询创建索引
- [ ] 添加完善的错误处理
- [ ] 使用事务的
oncomplete确认成功 - [ ] 大数据量使用游标处理
- [ ] 在开发工具中验证数据结构
常见错误速查表
| 错误 | 原因 | 解决方案 |
|---|---|---|
| TransactionInactiveError | 事务中使用 await | 在事务外准备数据 |
| InvalidStateError | 数据库未正确打开 | 在 onsuccess 回调中使用 |
| DataError | 主键类型不匹配 | 保持主键类型一致 |
| ConstraintError | 违反唯一性约束 | 检查唯一索引 |
| DataCloneError | 存储不可克隆对象(Proxy) | JSON 深拷贝或使用 toRaw |
| NotFoundError | ObjectStore 不存在 | 检查 store 名称 |
| ReadOnlyError | 只读事务中写入 | 使用 readwrite 模式 |
参考资料
作者注: IndexedDB 的事务机制初看很简单,但陷阱很多。理解事务的自动提交机制是用好 IndexedDB 的关键。希望这篇文章能帮你避开这些坑!
Happy Coding! 🎉