Service Worker · API
Background Sync API
发布于 更新于
Background Sync API 让页面在 service worker 注册上登记一个带名字的 sync;当浏览器判定设备恢复连接时,它会在 service worker 中触发 sync 事件,即使发起登记的页面早已关闭。它是 Chromium 独有的能力(Chrome 49、Edge 79、Samsung Internet 5.0),Firefox 与 Safari 均未实现(BCD api.SyncManager),因此所有依赖它的重试都必须准备一条不依赖它的回退路径。
// 页面或 service worker 中(Chromium 要求存在一个 window 客户端)const registration = await navigator.serviceWorker.ready;await registration.sync.register(tag);const tags = await registration.sync.getTags();
// service worker 中self.addEventListener('sync', (event) => { event.waitUntil(doWork(event.tag, event.lastChance));});register() 返回 Promise<void>,在注册写入存储后兑现。对一个仍在等待的 tag 重复注册不会报错,只是无操作,所以页面可以在每次请求失败时直接调用,无需自己记账。getTags() 返回 Promise<string[]>,列出尚未成功触发的 tag。
该方法接受一个字符串参数;事件对象暴露两个只读属性。
| 名称 | 类型 | 说明 |
|---|---|---|
tag |
string |
标识这次注册。每个 tag 同时只存在一条待处理注册;同一个 tag 会以 SyncEvent.tag 送达。Chromium 限制 tag 最长 10,240 个字符。 |
SyncEvent.tag |
string |
浏览器此次触发所对应的 tag。一个 worker 处理多条队列时据此分支。 |
SyncEvent.lastChance |
boolean |
为 true 表示若 waitUntil() 的 Promise 再被拒绝,浏览器不会再重试。此时应把失败告知用户,而不是继续重试。 |
register() 以拒绝的 Promise 报错,不会同步抛出。
InvalidStateError:注册上还没有处于 active 状态的 service worker。先await navigator.serviceWorker.ready再调用register()即可避免(规范 “register(tag)” 第 3 步)。InvalidAccessError:在 Chromium 中,从一个没有任何 window 客户端的 service worker 调用了register(),或 tag 超出长度上限。Chromium 的报错原文是Attempted to register a sync event without a window or registration tag too long.。NotAllowedError:站点的“后台同步”权限被屏蔽。Chromium 在chrome://settings/content/backgroundSync与站点权限面板中暴露该开关;拒绝信息为Permission denied.。TypeError:Firefox 与 Safari 中registration.sync为undefined,registration.sync.register()在产生任何 Promise 之前就会抛出。用'sync' in registration做特性检测。
waitUntil() 的 Promise 被拒绝不会在页面侧表现为异常:浏览器会安排重试(Chromium:最多 3 次尝试,第二次在 5 分钟后,退避系数为 3),并在最后一次把 lastChance 置为 true。
两个示例都假定已在 /sw.js 注册了 scope 覆盖当前页面的 service worker。
把失败的 POST 入队,并在 sync 事件中重放
Section titled “把失败的 POST 入队,并在 sync 事件中重放”页面把待发送的请求写入 IndexedDB(这里假定有一个类似 idb-keyval 的极简封装)并注册 tag;事件触发后由 worker 清空队列。队列必须放在存储里而不是 worker 的变量中:从请求失败到 sync 事件触发之间,浏览器可能已经终止过这个 worker。
async function sendOrQueue(payload) { try { await fetch('/api/messages', { method: 'POST', body: JSON.stringify(payload) }); } catch { await outbox.add(payload); // 基于 IndexedDB 的队列 const registration = await navigator.serviceWorker.ready; await registration.sync.register('outbox'); }}
// sw.jsself.addEventListener('sync', (event) => { if (event.tag !== 'outbox') return; event.waitUntil(drainOutbox(event.lastChance));});
async function drainOutbox(lastChance) { for (const item of await outbox.all()) { const response = await fetch('/api/messages', { method: 'POST', body: JSON.stringify(item.payload) }); if (!response.ok && !lastChance) throw new Error(`replay failed: ${response.status}`); await outbox.remove(item.id); }}在 drainOutbox() 中抛错会让 waitUntil() 的 Promise 被拒绝,这正是向浏览器申请重试的方式。到了 lastChance 这一轮,上面的代码不再抛错并清空队列,让注册结束;生产环境的应用应把这些条目标记为失败,在下次启动时展示给用户。
检测支持并在不支持时改用别的方式重试
Section titled “检测支持并在不支持时改用别的方式重试”Firefox 与 Safari 的注册对象上没有 sync 属性。下面的回退在 online 事件(各浏览器均会触发)时重试一次,否则留到下次页面加载再处理。
async function deferOrRetry(tag, retryNow) { const registration = await navigator.serviceWorker.ready; if ('sync' in registration) { return registration.sync.register(tag); } // 没有 Background Sync:页面一察觉网络恢复就立即重试。 window.addEventListener('online', () => retryNow(), { once: true });}回退只在页面打开期间有效,这正是 Background Sync 所补上的能力缺口;应明确告知用户损失了什么(“下次打开应用时会发送你的消息”),而不是掩盖它。
- Background Synchronization specification: register(tag) method(wicg.github.io)
- Introducing Background Sync(developer.chrome.com)
- Chromium source: sync_manager.cc(chromium.googlesource.com)
- Background Sync 浏览器支持
- Periodic Background Sync API
- Service worker 生命周期
- IndexedDB
规范
| 规范 | 状态 |
|---|---|
| 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。