ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

IndexedDB 实战心得:从 onupgradeneeded 到 transaction 作用域,配 TaoToken 统一 Key 调试异步链路

IndexedDB 实战心得:从 onupgradeneeded 到 transaction 作用域,配 TaoToken 统一 Key 调试异步链路 1. 为什么 IndexedDB 的异步链路总在“最后一公里”翻车IndexedDB 是浏览器内置的 NoSQL 数据库能存结构化对象、支持索引和事务适合离线缓存、草稿箱、大体量本地数据这类场景。它最反直觉的地方在于几乎所有操作都是异步的而且事务有严格的生命周期和作用域边界。你写store.get()拿到的不是数据是一个IDBRequest你以为db变量声明完就能用其实它只在onsuccess回调里才有值。我见过太多项目在这三处翻车onupgradeneeded里想再开一个事务结果报A version change transaction is runningput的时候忘了带 keyPath报did not yield a value读取request.result太早报The request has not finished。这些报错的根因都指向同一件事——没搞懂 IndexedDB 的异步回执模型和事务作用域。这篇按“建库升级 → 事务生命周期 → 作用域边界 → 异步回执”的主线走一遍同时把本地调试用的统一 Key 通道配好。调试异步链路时最烦的是请求散落各处、日志对不上号用 TaoToken 把模型对话和 API Key 统一到一个入口配合config.toml骨架能把“哪次请求对应哪段回调”这件事理清楚。下面所有代码都可以直接复制到项目里跑。2. 前置准备用 TaoToken 统一 Key 与调试通道IndexedDB 本身不需要网络但你在调试异步链路时往往要对照模型返回、日志分析、接口联调。如果每个工具各配一套 Key排查问题时光切换就够呛。TaoToken 的做法是把模型对话、Coding Plan、API Keys、接入文档收敛到同一套账号体系下本地调试时只需要维护一份配置。先拿到统一 Key打开 API Keys 管理页https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keys_debugutm_campaignrewrite创建一个 Key 并复制。如果你要对照模型行为来验证异步回调的时序可以直接在模型对话页https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chat_debugutm_campaignrewrite里贴代码问“这段 IndexedDB 回调为什么拿不到 result”比翻文档快。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdoc_indexeddbutm_campaignrewrite里面有完整的参数说明。API 基址是https://taotoken.net/api注意这个地址不带 UTM 参数配置里直接写死即可。长期做前端编码或 Agent 类项目的话Coding Plan 页https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_plan_indexeddbutm_campaignrewrite有额度说明适合把调试链路固定下来。注意TaoToken 在这里的角色是统一 Key 和调试通道不是替代浏览器 DevTools。IndexedDB 的 Application 面板该看还得看。3. 可复制配置config.toml 骨架与建库升级代码3.1 config.toml 骨架把下面这份配置放到项目根目录调试脚本和前端代码共用同一份 Key避免多处硬编码。# config.toml - 本地调试统一配置 [taotoken] api_base https://taotoken.net/api api_key sk-你的统一Key chat_path /v1/chat/completions timeout_ms 30000 [indexeddb] db_name myQuestionaire db_version 3 store_name myQuestionaire key_path key auto_increment true [debug] log_request_id true log_transaction_state truedb_version每次结构变更都要 1这是触发onupgradeneeded的唯一开关。auto_increment true对应建库时的{autoIncrement: true}能避免put时漏传 key 的报错。3.2 onupgradeneeded 里建库与升级关键点onupgradeneeded回调里已经有一个正在运行的 versionchange 事务你只能用event.target.transaction拿它绝对不能再调db.transaction()。// db.js - 建库与升级 const DB_NAME myQuestionaire; const DB_VERSION 3; function openDB() { return new Promise((resolve, reject) { const req indexedDB.open(DB_NAME, DB_VERSION); // 版本不一致时触发建库/升级都在这里 req.onupgradeneeded function (event) { const db event.target.result; // 重要复用 versionchange 事务不要新开 const tx event.target.transaction; let store; if (!db.objectStoreNames.contains(myQuestionaire)) { store db.createObjectStore(myQuestionaire, { keyPath: key, autoIncrement: true }); } else { store tx.objectStore(myQuestionaire); } // 索引也必须在 versionchange 事务内建 if (!store.indexNames.contains(byValue)) { store.createIndex(byValue, value, { unique: false }); } // 升级期间写入数据用同一个事务 const addReq store.add({ key: 11, value: 33 }); addReq.onsuccess () console.log(upgrade add ok); addReq.onerror (e) console.error(upgrade add fail, e.target.error); }; req.onsuccess function (event) { // db 只在这个作用域内有效通过 resolve 传出去 resolve(event.target.result); }; req.onerror function (event) { reject(event.target.error); }; req.onblocked function () { console.warn(数据库被其他标签页占用升级被阻塞); }; }); }onblocked这个回调很多人不写结果多标签页调试时升级一直卡住还以为是代码问题。3.3 transaction 生命周期与作用域事务一旦创建它的生命周期就绑定了当前微任务队列。如果你在事务里await一个非 IndexedDB 的 Promise事务会自动提交后续操作全部报TransactionInactiveError。// 正确所有操作同步挂到同一个事务上 async function saveItem(item) { const db await openDB(); const tx db.transaction(myQuestionaire, readwrite); const store tx.objectStore(myQuestionaire); const req store.put(item); req.onsuccess () console.log(put ok, key , req.result); req.onerror (e) console.error(put fail, e.target.error); // 事务完成后再关连接 tx.oncomplete () { console.log(transaction committed); db.close(); }; tx.onerror (e) console.error(tx error, e.target.error); tx.onabort (e) console.error(tx aborted, e.target.error); }// 错误await 打断了事务导致提前提交 async function saveItemBad(item) { const db await openDB(); const tx db.transaction(myQuestionaire, readwrite); const store tx.objectStore(myQuestionaire); await someOtherAsync(); // 事务在这里已经提交了 store.put(item); // 报 TransactionInactiveError }作用域的核心规则db、store、request都只在创建它们的回调或函数作用域内有效。想跨作用域用就通过 Promise 的resolve往外传而不是声明一个外层变量去接。4. 验证请求跑通一次完整读写并确认结果4.1 写入并读取// verify.js - 验证异步链路 async function verify() { const db await openDB(); // 写入 await new Promise((resolve, reject) { const tx db.transaction(myQuestionaire, readwrite); const store tx.objectStore(myQuestionaire); const req store.put({ value: 66 }); // autoIncrement 自动生成 key req.onsuccess () resolve(req.result); req.onerror (e) reject(e.target.error); }); // 读取 const result await new Promise((resolve, reject) { const tx db.transaction(myQuestionaire, readonly); const store tx.objectStore(myQuestionaire); const req store.get(11); req.onsuccess () resolve(req.result); // 必须在 onsuccess 里读 result req.onerror (e) reject(e.target.error); }); console.log(读取结果:, result); db.close(); } verify().catch(console.error);4.2 用 TaoToken 对照异步时序如果回调没触发、result 读不到把这段代码贴到模型对话页https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chat_verifyutm_campaignrewrite让它帮你逐行标注每个回调的触发时机。实测下来把request.onsuccess、transaction.oncomplete、db.close()三者的顺序画成时间线比盯着报错猜要快得多。成功结果应该看到upgrade add ok→put ok, key 12→transaction committed→读取结果: {key: 11, value: 33}。如果transaction committed出现在put ok之前说明事务被提前提交了回去检查有没有await打断。5. 本篇常见错排查5.1 Failed to read the result property报错原文Uncaught InvalidStateError: Failed to read the result property from IDBRequest: The request has not finished。原因在onsuccess回调之外读request.result。store.get()返回的是请求对象不是数据。修复方式就是把读取逻辑放进request.onsuccess里或者用 Promise 包一层在resolve时才取req.result。5.2 A version change transaction is running报错原文Uncaught InvalidStateError: Failed to execute transaction on IDBDatabase: A version change transaction is running。原因在onupgradeneeded里调用了db.transaction()。此时 versionchange 事务还没结束不允许开第二个。修复用event.target.transaction复用当前事务所有建 store、建索引、写初始数据都挂在它上面。5.3 did not yield a value报错原文failed to execute put on idbobjectstore evaluating the object stores key path did not yield a value。原因建 store 时指定了keyPath: key但put的对象里没有key字段也没开autoIncrement。两种修复要么store.put({key: 11, value: 33})带上 key要么建库时加{keyPath: key, autoIncrement: true}让浏览器自动生成。5.4 db 变量在 onsuccess 外为 null这是最隐蔽的一个。var db null; req.onsuccess function(e){ db e.target.result; }之后在外部用db拿到的还是 null因为赋值只发生在回调作用域内。修复用 Promise 把db传出来或者把所有依赖db的逻辑都写进onsuccess回调。提示善用request.onerror和transaction.onerror把e.target.error打出来比只看控制台第一行报错信息有用得多。6. 把统一 Key 和异步调试固定成习惯IndexedDB 的坑基本都集中在“异步回执”和“事务作用域”这两件事上。建库升级只在onupgradeneeded里做且复用event.target.transaction事务创建后不要用await打断result只在onsuccess里读db通过 Promise 往外传而不是挂外层变量。这四条记住八成报错都能自己定位。调试链路这边把 TaoToken 的 API Key 和config.toml骨架固定到项目里模型对话、接入文档、Coding Plan 都走同一套入口排查异步问题时不用在多个工具间来回切。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdoc_finalutm_campaignrewriteAPI 基址https://taotoken.net/api直接写进配置。下次再遇到TransactionInactiveError先看事务有没有被await打断再看result是不是读早了基本就这两处。
返回列表