Service Worker 中的概念
后台同步、周期同步与 Web Locks
发布于
Background Sync(后台同步)让 Service Worker 在用户代理判断网络条件合适时重试延后执行的工作。Periodic Background Sync(周期性后台同步)由浏览器选择时间唤醒 Service Worker 更新数据;Web Locks 则协调仍在运行的标签页与 worker,避免它们同时操作同一个应用资源。
回退方案取决于支持范围。BCD api.SyncManager 记录 Chrome 49 起支持后台同步,Edge 镜像该支持,Firefox 和 Safari 未记录支持;api.PeriodicSyncManager 记录 Chrome 80 起支持周期性后台同步,Edge 镜像该支持,Firefox 和 Safari 未记录支持;api.LockManager 记录 Chrome 69 起支持 Web Locks,Edge 镜像该支持,并记录 Firefox 96 和 Safari 15.4 起支持。
工作原理
链接到“工作原理”一节这三套机制处理的调度问题并不相同。后台同步通过 ServiceWorkerRegistration.sync 注册标签,用户代理随后向活动 Service Worker 派发 sync 事件。周期性后台同步通过 ServiceWorkerRegistration.periodicSync 注册标签和 minInterval,规范把实际触发时间交给用户代理决定。Web Locks 调用 navigator.locks.request();同名锁可用后,浏览器运行回调,回调返回的 Promise 结束时释放锁。
| 机制 | 触发条件 | 执行环境 | 可用的回退方案 |
|---|---|---|---|
| Background Sync | 调用 sync.register() 后,用户代理判断网络条件合适 |
Service Worker 的 sync 事件 |
下次页面打开时重试 |
| Periodic Background Sync | 用户代理选择时间,频率不会高于 minInterval 所表达的下限 |
Service Worker 的 periodicsync 事件 |
页面打开时刷新 |
| Web Locks | 调用 navigator.locks.request() 后,同名锁变为可用 |
发起请求的窗口或 worker | 不做跨上下文协调,或使用应用自己的租约 |
后台同步适合处理已经失败或主动延后的操作,例如发送已排队的表单。Background Sync 规范通过 SyncManager 定义一次性注册;另一份 Periodic Background Sync 规范通过 PeriodicSyncManager 定义周期性注册。注册不会替你持久保存请求;应先把请求数据写入存储,再注册标签,Service Worker 完成操作后再删除数据。
周期性后台同步适合更新没有用户动作也会过期的数据。Chrome 的说明指出,开发者无法控制具体同步时间;浏览器还会考虑用户参与度、以前使用过的网络、电量和连接状态。因此,minInterval 是最小间隔,不是定时器。
Web Locks 不会安排 Service Worker,也不等待网络恢复。Web Locks 规范规定同名独占锁逐个授予,同名共享锁可以同时授予多个请求。回调决定持锁时间,所以应只包住不能重叠的异步操作。页面关闭后,Web Lock 不能替代持久化的重试队列。
| 需求 | 选择 | 原因 |
|---|---|---|
| 网络恢复后重试已保存的写操作 | Background Sync | sync 事件面向延后执行的一次性工作 |
| 没有打开页面时更新缓存内容 | Periodic Background Sync | periodicsync 事件面向浏览器安排的周期刷新 |
| 阻止两个活动上下文同时更新同一条记录 | Web Locks | 同名独占锁会串行执行对应回调 |
| 在 Firefox 和 Safari 中使用同一套协调机制 | Web Locks | 这两个引擎记录了 LockManager 支持,没有记录两种 SyncManager 支持 |
| 要求精确的墙上时钟截止时间 | 均不适用 | 同步规范把派发时间交给用户代理,锁只管理争用 |
先保存需要持久化的工作,再单独处理运行时协调。下面两个示例都保留页面加载路径,让没有同步 API 的浏览器也能继续完成工作。
没有后台同步时重试队列请求
链接到“没有后台同步时重试队列请求”一节页面先把操作写入 IndexedDB,再请求一次性后台同步。如果没有 SyncManager,或者注册失败,只要页面仍开着,就直接调用同一个队列处理函数。
async function queueMessage(message) { await outbox.put(message); const registration = await navigator.serviceWorker.ready; if (!('sync' in registration)) { return flushOutbox(); } try { await registration.sync.register('send-outbox'); } catch (error) { await flushOutbox(); reportRetryFallback(error); }}Service Worker 处理同一份持久队列。把 Promise 交给 waitUntil(),用户代理就能按 Background Sync 事件模型,在 flushOutbox() 运行时延长事件生命周期。
self.addEventListener('sync', (event) => { if (event.tag !== 'send-outbox') return; event.waitUntil(flushOutbox());});
self.addEventListener('message', (event) => { if (event.data?.type !== 'flush-outbox') return; event.waitUntil(flushOutbox());});周期同步不可用时立即刷新
链接到“周期同步不可用时立即刷新”一节页面仅在 API 和权限检查通过后注册周期刷新。失败分支立即刷新一次,后续重试交给下一次页面加载。
async function scheduleCatalogRefresh() { const registration = await navigator.serviceWorker.ready; if (!('periodicSync' in registration)) { return refreshCatalogNow(); } const status = await navigator.permissions.query({ name: 'periodic-background-sync', }); if (status.state !== 'granted') return refreshCatalogNow(); try { await registration.periodicSync.register('catalog', { minInterval: 24 * 60 * 60 * 1000, }); } catch (error) { await refreshCatalogNow(); reportRefreshFallback(error); }}若多个活动上下文都可能调用 refreshCatalogNow(),可用 Web Lock 包住它。这样只能消除活动上下文之间的重叠,不会替代持久队列,也不会产生 Service Worker 事件。
async function refreshCatalogOnce() { if (!('locks' in navigator)) return refreshCatalogNow(); return navigator.locks.request('catalog-refresh', { ifAvailable: true, }, async (lock) => { if (!lock) return { status: 'already-running' }; await refreshCatalogNow(); return { status: 'refreshed' }; });}后台同步兼容性数据和 Web Locks 兼容性数据说明了为何两处特性检测都不能省略。
另请参阅
链接到“另请参阅”一节- Background Synchronization(wicg.github.io)
- Web Periodic Background Synchronization(wicg.github.io)
- Web Locks API(w3c.github.io)
- 后台同步
- 周期性后台同步
- Web Locks API
规范
| 规范 | 状态 |
|---|---|
| Background Sync(后台同步) | WICG 草案 |
- 图例
- 支持
- 部分支持
- 需开启标志
- 不支持
- 未知
| 浏览器 / 平台 | 支持 | 版本 | 置信度 | 来源 | 备注 |
|---|---|---|---|---|---|
| Chrome (Desktop)# | 支持 | 49 | 高 | 来源 | — |
| Chrome (Android)# | 支持 | 49 | 高 | 来源 | 1 |
| Edge (Desktop)# | 支持 | 79 | 高 | 来源 | 2 |
| Firefox (Desktop)# | 不支持 | — | 高 | 来源 | 3 |
| Firefox (Android)# | 不支持 | — | 高 | 来源 | 45 |
| Safari (macOS)# | 不支持 | — | 高 | 来源 | 6 |
| Safari (iOS)# | 不支持 | — | 高 | 来源 | 78 |
| Samsung Internet# | 支持 | 5.0 | 高 | 来源 | 9 |
| WebView (Android)# | 不支持 | — | 高 | 来源 | 10 |
- 由 browser-compat-data 镜像自 Chrome 的数据推导。
- 由 browser-compat-data 镜像自 Chrome 的数据推导。
- browser-compat-data 未记录 Firefox 的支持。
- browser-compat-data 未记录 Firefox for Android 的支持。
- 由 browser-compat-data 镜像自 Firefox 的数据推导。
- 实现跟踪:https://webkit.org/b/182565。
- 实现跟踪:https://webkit.org/b/182565。
- 由 browser-compat-data 镜像自 Safari 的数据推导。
- 由 browser-compat-data 镜像自 Chrome Android 的数据推导。
- 实现跟踪:https://crbug.com/40449796。