能力 · API
Web Locks API
发布于 更新于
navigator.locks.request() 向浏览器的锁管理器申请一把命名锁,在持锁期间运行回调,回调的 Promise 完结后释放。同一源下共享同一个存储桶的标签页、iframe 和 worker 在相同名字上排队,因此 PWA 可以把对 IndexedDB 的写入串行化、选出一个标签页独占 WebSocket,或防止两个 Service Worker 客户端同时跑同一份迁移。
支持面很宽:Chrome 69、Edge 79、Firefox 96、macOS 与 iOS 上的 Safari 15.4、Samsung Internet 10.0 以及 Android WebView 69 都实现了带 request() 与 query() 的 LockManager(BCD api.LockManager)。接口标注为 [SecureContext, Exposed=(Window,Worker)],所以在专用、共享与 Service Worker 中都可用,但在 localhost 以外的 http:// 源上不存在。
navigator.locks.request(name, callback)navigator.locks.request(name, options, callback)
navigator.locks.query()request() 返回 Promise<any>,在锁释放之后以 callback 的返回值兑现或以其抛出的值拒绝。同步回调会被包成一个立即兑现的 Promise,所以锁只在同步执行期间被持有。query() 返回 Promise<LockManagerSnapshot>,描述快照那一刻该源锁管理器里每一把已持有和排队中的锁。
request() 接受资源名、可选的 LockOptions 字典和一个回调;回调始终是最后一个参数。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
DOMString |
是 | 由应用自行命名的资源名。以 - 开头的名字保留给浏览器,会被拒绝。 |
options |
LockOptions |
否 | 模式与等待行为,见下表。 |
callback |
LockGrantedCallback |
是 | (lock) => Promise<any> | any,授予时以 Lock(name、mode)调用;ifAvailable 为 true 且无法立即授予时以 null 调用。 |
LockOptions 有四个成员,全部可选。
| 成员 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mode |
LockMode("exclusive" 或 "shared") |
"exclusive" |
多个上下文可同时持有同名的 "shared" 锁;"exclusive" 持有者会阻塞该名字上的所有其他请求直到释放。 |
ifAvailable |
boolean |
false |
只在无需等待时授予,否则以 null 调用 callback 而不排队。仍是异步的,因为检查通常要跨进程。 |
steal |
boolean |
false |
释放该名字的当前持有者(它们的 request() Promise 以 AbortError 拒绝),跳过队列并授予本次请求。只能与 "exclusive" 模式搭配。 |
signal |
AbortSignal |
无 | 在请求仍在排队时中止它;锁一旦授予,信号即被忽略。 |
query() 不接受参数。其快照包含 held 与 pending 两个 LockInfo 数组,每项带 name、mode 以及所在 frame 或 worker 的 clientId(与 Service Worker 里 Client.id 的值相同)。
出现下列情况之一时,request() 在任何锁入队之前即拒绝,顺序与规范的方法步骤一致。
| 异常 | 条件 |
|---|---|
InvalidStateError |
调用方文档不是 fully active(例如已脱离文档树的 iframe)。 |
SecurityError |
无法为该环境获取锁管理器:不透明源(如没有 allow-same-origin 的沙箱 iframe),或浏览器排除在 Locks API 之外的上下文。 |
NotSupportedError |
name 以 U+002D HYPHEN-MINUS(-)开头;steal 与 ifAvailable 同为 true;steal 为 true 但 mode 不是 "exclusive";或同时传了 signal 与 steal 或 ifAvailable。 |
AbortError(或信号的 abort reason) |
调用 request() 时 signal 已经中止,或请求仍在排队时被中止。 |
AbortError |
另一个带 steal: true 的请求夺走了本持有者的锁。 |
query() 只会以同样的 InvalidStateError 与 SecurityError 条件拒绝。两个方法都不同步抛出;回调抛出的任何值都会在锁释放后成为 request() Promise 的拒绝原因。
每个示例都先检测 navigator.locks,并给出缺失时执行的分支;在上文列出的浏览器里,回退分支只会在非安全源上运行。
跨标签页串行化 IndexedDB 迁移
Section titled “跨标签页串行化 IndexedDB 迁移”打开同一个数据库的两个标签页可能同时判断需要升级 schema。把升级包进一把独占锁,第二个标签页就会先等待,然后发现活已经干完。没有锁支持时函数直接执行升级,这也正是引入锁之前代码的行为。
async function withLock(name, work) { if (!('locks' in navigator)) return work(); return navigator.locks.request(name, work);}
await withLock('db-migration', async () => { const version = await readSchemaVersion(); if (version < 3) await migrateTo3();});回调的返回值会成为外层 Promise 的值,所以 withLock() 可以把持锁期间读到的数据直接返回,不必再跑一个来回。
用 AbortSignal 在超时后放弃
Section titled “用 AbortSignal 在超时后放弃”排在一个卡在长回调里的标签页后面的请求,否则会一直等下去。传入 AbortSignal 并由定时器中止它;拒绝原因就是传给 abort() 的值,没传时为 AbortError。
async function requestWithTimeout(name, ms, work) { if (!('locks' in navigator)) return work(); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(new DOMException('Lock wait exceeded', 'TimeoutError')), ms); try { return await navigator.locks.request(name, { signal: controller.signal }, work); } catch (err) { if (err.name === 'TimeoutError') return null; throw err; } finally { clearTimeout(timer); }}锁一旦授予,信号就被忽略,之后再中止也不会打断 work()。
让读者共享缓存、由一个写者刷新
Section titled “让读者共享缓存、由一个写者刷新”共享锁允许任意数量的读者同时进行,而写者要等它们全部结束,然后阻塞新的读者。ifAvailable: true 让刷新步骤在已有写者持锁时直接跳过,而不是排队再做一次重复刷新。
async function readCatalog() { if (!('locks' in navigator)) return loadCatalogFromCache(); return navigator.locks.request('catalog', { mode: 'shared' }, () => loadCatalogFromCache());}
async function refreshCatalog() { if (!('locks' in navigator)) return fetchAndStoreCatalog(); return navigator.locks.request('catalog', { ifAvailable: true }, async (lock) => { if (!lock) return 'skipped'; await fetchAndStoreCatalog(); return 'refreshed'; });}刷新总是返回 'skipped' 时,navigator.locks.query() 能显示是哪个 clientId 以什么模式持有 catalog。
- Storage Buckets API,决定哪些上下文共享同一个锁管理器的边界
- IndexedDB,大多数锁名最终保护的存储
- Clients API,
query()快照中clientId的来源 - Web Locks API: request() method(w3.org)
- Web Locks API: LockOptions(w3.org)
- Web Locks API (MDN)(developer.mozilla.org)
规范
| 规范 | 状态 |
|---|---|
| Web Locks API(网页锁) | W3C 草案 |
| Web Locks API: request() method | W3C 草案 |
| Web Locks API: query() method | W3C 草案 |
- 图例
- 支持
- 部分支持
- 需开启标志
- 不支持
- 未知
| 浏览器 / 平台 | 支持 | 版本 | 置信度 | 来源 | 备注 |
|---|---|---|---|---|---|
| Chrome (Desktop) | 支持 | 69 | 高 | 来源 | — |
| Chrome (Android) | 支持 | 69 | 高 | 来源 | 1 |
| Edge (Desktop) | 支持 | 79 | 高 | 来源 | 2 |
| Firefox (Desktop) | 支持 | 96 | 高 | 来源 | — |
| Firefox (Android) | 支持 | 96 | 高 | 来源 | 3 |
| Safari (macOS) | 支持 | 15.4 | 高 | 来源 | — |
| Safari (iOS) | 支持 | 15.4 | 高 | 来源 | 4 |
| Samsung Internet | 支持 | 10.0 | 高 | 来源 | 5 |
| WebView (Android) | 支持 | 69 | 高 | 来源 | 6 |
- 由 browser-compat-data 镜像自 Chrome 的数据推导。
- 由 browser-compat-data 镜像自 Chrome 的数据推导。
- 由 browser-compat-data 镜像自 Firefox 的数据推导。
- 由 browser-compat-data 镜像自 Safari 的数据推导。
- 由 browser-compat-data 镜像自 Chrome Android 的数据推导。
- 由 browser-compat-data 镜像自 Chrome Android 的数据推导。