如何通过 navigator.locks 避免多窗口本地缓存数据写入冲突协调?
- 内容介绍
- 相关推荐
本文共计859个文字,预计阅读时间需要4分钟。
navigator.locks不操作数据,也不涉及预缓存写入逻辑,它仅提供跨窗口(同源)的轻量级协调信号。要实现多窗口对本地缓存的并发读写冲突,关键不在于加锁这个动作,而是通过读取当前缓存+计算新值+写回缓存这一完整逻辑,在锁的异步回调中确保所有窗口都遵守同一套规则。
必须搭配支持事务的存储方案
localStorage 不行——它是同步、无事务、无原子性的 API,锁根本插不进执行流。一旦调用 setItem,写入立刻发生,锁只是事后签到。
- ✅ 正确选择:IndexedDB。它原生支持 readwrite 事务,能保证单次 put/delete 操作的原子性;配合 navigator.locks,可实现“串行化读-改-写”
- ⚠️ 警惕陷阱:不要只锁 store.put() 这一行。必须从 openDB() 开始,到 await tx.done 结束,全程 await,否则事务可能在锁释放后才提交,失去保护意义
- ❌ 避免替代方案:用 localStorage + storage 事件模拟锁,仅适合提示用户“别人正在编辑”,无法防止竞态覆盖,不是强一致方案
锁名设计决定协调粒度
锁名不是标签,而是作用域标识符。相同名字的锁,在整个 origin 下互斥;名字不同,就形同虚设。
- 全局共享状态(如主题偏好):用固定名,例如 'ui-preferences'
- 用户级隔离(如购物车):必须带上下文,例如 `cart-${userId}`,避免张三编辑时李四的窗口被阻塞
- 资源级精细控制(如某条草稿):推荐 `draft-${docId}`,让并发只发生在真正冲突的数据单元上
写操作必须完整包裹并显式等待
锁不会自动感知你做了什么。它只在你调用 request() 并 await 回调完成时,才真正生效。
- 必须写成 await navigator.locks.request('key', async () => { ... }),不能省略 await,也不能只调用不等待
- 回调内所有 IndexedDB 操作都要 await:openDB()、transaction()、store.put()、tx.done
- 别在锁里做耗时操作(如上传、复杂计算),否则其他窗口会长时间排队;可先在锁外准备数据,只在锁内做“读→算→写”核心三步
兼容性与降级不可跳过
Safari 完全不支持 navigator.locks(截至 iOS 17.5 / macOS 14.5)。仅靠 feature detection 就跳过逻辑,等于在 Safari 里裸奔。
- 服务端必须兜底:所有写请求附带客户端生成的 requestId 或版本戳(如 _rev),后端做幂等校验或乐观锁比对
- 前端可降级为 localStorage + 时间戳标记:写前存 lock:cart-123 = Date.now(),监听 storage 事件发现冲突则弹窗提示,不阻断但提升体验
- 高交互场景建议加超时:用 { ifAvailable: true },锁不可用时快速失败,走降级路径,避免 UI 卡死
本文共计859个文字,预计阅读时间需要4分钟。
navigator.locks不操作数据,也不涉及预缓存写入逻辑,它仅提供跨窗口(同源)的轻量级协调信号。要实现多窗口对本地缓存的并发读写冲突,关键不在于加锁这个动作,而是通过读取当前缓存+计算新值+写回缓存这一完整逻辑,在锁的异步回调中确保所有窗口都遵守同一套规则。
必须搭配支持事务的存储方案
localStorage 不行——它是同步、无事务、无原子性的 API,锁根本插不进执行流。一旦调用 setItem,写入立刻发生,锁只是事后签到。
- ✅ 正确选择:IndexedDB。它原生支持 readwrite 事务,能保证单次 put/delete 操作的原子性;配合 navigator.locks,可实现“串行化读-改-写”
- ⚠️ 警惕陷阱:不要只锁 store.put() 这一行。必须从 openDB() 开始,到 await tx.done 结束,全程 await,否则事务可能在锁释放后才提交,失去保护意义
- ❌ 避免替代方案:用 localStorage + storage 事件模拟锁,仅适合提示用户“别人正在编辑”,无法防止竞态覆盖,不是强一致方案
锁名设计决定协调粒度
锁名不是标签,而是作用域标识符。相同名字的锁,在整个 origin 下互斥;名字不同,就形同虚设。
- 全局共享状态(如主题偏好):用固定名,例如 'ui-preferences'
- 用户级隔离(如购物车):必须带上下文,例如 `cart-${userId}`,避免张三编辑时李四的窗口被阻塞
- 资源级精细控制(如某条草稿):推荐 `draft-${docId}`,让并发只发生在真正冲突的数据单元上
写操作必须完整包裹并显式等待
锁不会自动感知你做了什么。它只在你调用 request() 并 await 回调完成时,才真正生效。
- 必须写成 await navigator.locks.request('key', async () => { ... }),不能省略 await,也不能只调用不等待
- 回调内所有 IndexedDB 操作都要 await:openDB()、transaction()、store.put()、tx.done
- 别在锁里做耗时操作(如上传、复杂计算),否则其他窗口会长时间排队;可先在锁外准备数据,只在锁内做“读→算→写”核心三步
兼容性与降级不可跳过
Safari 完全不支持 navigator.locks(截至 iOS 17.5 / macOS 14.5)。仅靠 feature detection 就跳过逻辑,等于在 Safari 里裸奔。
- 服务端必须兜底:所有写请求附带客户端生成的 requestId 或版本戳(如 _rev),后端做幂等校验或乐观锁比对
- 前端可降级为 localStorage + 时间戳标记:写前存 lock:cart-123 = Date.now(),监听 storage 事件发现冲突则弹窗提示,不阻断但提升体验
- 高交互场景建议加超时:用 { ifAvailable: true },锁不可用时快速失败,走降级路径,避免 UI 卡死

