必须用 IndexedDB 替代 localStorage 存储 HTML 编辑器草稿等状态,因其支持事务与异步确认;锁名需绑定文档 ID(如 editor-draft-doc-123),且锁内仅保留“读→改→写→await tx.done”最小闭环,Safari 需 fallback 至服务端幂等校验。

必须用 IndexedDB 替代 localStorage
HTML 编辑器的草稿、光标位置、临时格式状态等,如果还存在 localStorage 里,加任何锁都白搭。因为 localStorage.setItem() 是同步阻塞调用,navigator.locks.request() 的 Promise 根本插不进执行流——多个窗口会在毫秒内先后触发回调并立刻写入,覆盖不可避免。
真正能被锁“串行化”的,只有支持事务和异步完成确认的存储。所以第一步是迁移:
- 把所有草稿数据结构(如
{ id: 'doc-123', content: '<p>...</p>', cursor: 42 })存入 IndexedDB - 每个文档对应一个唯一 objectStore 或用 keyPath 区分,避免全库共用事务
- 弃用所有
localStorage.getItem('draft-')类逻辑,改用db.transaction('drafts', 'readonly').objectStore('drafts').get(id)
锁名要绑定具体文档 ID,不能泛化
写草稿时若用固定锁名如 'editor-draft-write',会导致用户 A 编辑 doc-101 和用户 B 编辑 doc-202 互相阻塞——这不是保护,是人为限流。
立即学习“前端免费学习笔记(深入)”;
正确做法是让锁名携带业务粒度标识:
- ✅ 推荐:
`editor-draft-${docId}`(如'editor-draft-doc-789') - ✅ 若 docId 来自用户输入,先做
encodeURIComponent(docId)确保 URL-safe - ❌ 避免:
'draft-write'、'idb-lock'、'editor-all' - 长度控制在 64 字符内,过长无意义且可能影响内部哈希性能
锁内只包「读 → 改 → 写 → await tx.done」最小闭环
编辑器常见的“读旧草稿 → 合并新内容(如自动保存增量)→ 写回”流程,必须整个包裹在 navigator.locks.request() 回调中,且每一步都 await:
- 从
openDB()开始就进锁,不能在外面开好 DB 再传进去 - 事务必须设为
'readwrite',不能用'readonly'(否则put()报错) -
await store.put(newDraft, docId)后,**必须**await tx.done——tx.commit()不返回 Promise,也不代表落地;不await tx.done,锁会在数据真正写入前释放,其他窗口可能读到旧值再覆盖 - 移出锁外的操作包括:DOM 更新(如刷新编辑器预览)、
fetch()同步服务端、计算 diff、日志上报
Safari 不支持是硬限制,服务端幂等不可跳过
iOS 17.5 / macOS 14.5 的 Safari 仍未实现 navigator.locks。仅靠 if ('locks' in navigator) 跳过逻辑,等于在 Safari 中完全放弃保护。
生产环境必须设计 fallback:
- 前端每次保存都生成唯一
requestId(如cuid()或crypto.randomUUID()),随请求发给后端 - 后端对同一
docId+requestId做幂等校验,拒绝重复提交 - 前端可轻量监听
storage事件,在 Safari 中检测到其他窗口修改了localStorage的草稿时间戳,弹出提示:“其他窗口正在编辑”,而非静默覆盖
最易被忽略的一点:锁本身不解决冲突,它只协调谁有资格跑那段代码;而那段代码是否真能保证原子性,取决于你有没有把 await tx.done 写在最后,以及有没有把无关操作移出锁作用域。



















