indexeddb存草稿需先确保数据库就绪,用getdb()封装open()并每次调用;主键用组合id或crypto.randomuuid()避免覆盖;节流写入、比对内容防无效提交;卸载前同步保存并兜底sessionstorage;多标签页取最新草稿。

草稿存 IndexedDB 之前先检查数据库是否就绪
IndexedDB 不是直接可用的,得等 indexedDB.open() 成功回调后才能写入。如果页面一加载就急着调 put(),大概率会报 InvalidStateError: Failed to execute 'put' on 'ObjectStore': The transaction has finished.——因为事务已关闭或压根没开。
实操建议:
- 封装一个
getDB()工具函数,内部用Promise包裹indexedDB.open(),只 resolve 成功的db实例,拒绝掉所有升级/打开失败 - 每次存草稿前都调一次
getDB(),不复用旧 db 引用(它可能已 close) - 别在
upgradeneeded里建好 store 就以为万事大吉——versionchange事务自动结束,后续写入必须新开事务
草稿数据结构设计要带时间戳和唯一标识
用户可能同时编辑多个表单(比如两封邮件、三个笔记),只用一个 draft 键存值会互相覆盖。而且没时间戳,就无法判断哪份更新、是否该自动恢复。
实操建议:
- 主键用组合 ID:
`${formType}-${userId}-${timestamp}`或直接用crypto.randomUUID()(注意 IE 不支持,需 fallback) - 值对象至少包含:
{ content: string, updatedAt: number, formId: string, isAutoSaved: boolean } - store 设计为
keyPath: 'id',启用autoIncrement: false,避免索引混乱
监听输入并节流写入,别每敲一个字都触发 put
高频输入下频繁开事务 + put() 会卡 UI,还可能因事务冲突失败(尤其多标签页编辑同一草稿时)。浏览器对 IndexedDB 的并发事务控制很严格。
实操建议:
- 用
setTimeout实现简单节流:输入停止 1.5 秒后再存,期间新输入则 clearTimeout 重置 - 写入前先用
get()拿旧值比对content和updatedAt,内容未变就不提交(防无意义写入) - 事务设为
readwrite,但只在一个事务里做get+put,别混其他操作
页面卸载前强制保存一次,但得防 Safari 的 beforeunload 限制
beforeunload 是最后防线,但 Safari 对它的执行有严格限制:不能异步等待 IndexedDB,否则直接丢弃事务。Chrome 也倾向快速返回,不等 Promise。
实操建议:
- 卸载前不走 Promise 链,直接同步调
getDB().then(db => { ... })内部用db.transaction(...).objectStore(...).put(...) - 加个兜底:把当前草稿先存到
sessionStorage,哪怕 IndexedDB 失败,刷新后也能从 sessionStorage 恢复(再尝试写入 DB) - 别依赖
visibilitychange事件——切到其他 tab 不一定触发,且 iOS Safari 常静默丢弃后台页
最易被忽略的是多标签页场景:两个窗口同时编辑同一草稿,后保存的会覆盖先保存的。真要解决得加乐观锁(比如用 updatedAt 时间戳校验),但多数产品只需“最后保存生效”,那就得在恢复草稿时优先取时间最新的那条记录——别只查 get(id),要用 openCursor() 配合 IDBKeyRange.bound 扫一遍同 formId 的所有草稿。










