# 后台同步、周期同步与 Web Locks

> 按触发条件、生命周期、浏览器支持和失败方式，选择一次性后台同步、周期性后台同步或 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 支持 |
| 要求精确的墙上时钟截止时间 | 均不适用 | 同步规范把派发时间交给用户代理，锁只管理争用 |

:::caution
Periodic Background Sync 规范把 `minInterval` 定义成最小间隔，并允许用户代理延迟事件。Chrome 的 [Periodic Background Sync 说明](https://developer.chrome.com/docs/capabilities/periodic-background-sync)还列出用户参与度、电量、连接状态和以前使用过的网络等考虑因素。不要把 `minInterval` 标成下一次执行时间。

同一规范要求：Service Worker 注册没有活动 worker 时，`PeriodicSyncManager.register()` 以 `InvalidStateError` 拒绝；没有周期性后台同步权限时，以 `NotAllowedError` 拒绝。遇到这两种具名失败，应转到页面加载时刷新路径。
:::

## 示例

先保存需要持久化的工作，再单独处理运行时协调。下面两个示例都保留页面加载路径，让没有同步 API 的浏览器也能继续完成工作。

### 没有后台同步时重试队列请求

页面先把操作写入 IndexedDB，再请求一次性后台同步。如果没有 `SyncManager`，或者注册失败，只要页面仍开着，就直接调用同一个队列处理函数。

```js
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()` 运行时延长事件生命周期。

```js
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 和权限检查通过后注册周期刷新。失败分支立即刷新一次，后续重试交给下一次页面加载。

```js
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 事件。

```js
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' };
  });
}
```

[后台同步兼容性数据](/zh/compatibility/background-sync/)和 [Web Locks 兼容性数据](/zh/reference/capabilities/web-locks/)说明了为何两处特性检测都不能省略。

## 另请参阅

- [Background Synchronization](https://wicg.github.io/background-sync/spec/)（wicg.github.io）
- [Web Periodic Background Synchronization](https://wicg.github.io/periodic-background-sync/)（wicg.github.io）
- [Web Locks API](https://w3c.github.io/web-locks/)（w3c.github.io）
- [后台同步](/zh/reference/service-worker/background-sync/)
- [周期性后台同步](/zh/reference/service-worker/periodic-background-sync/)
- [Web Locks API](/zh/reference/capabilities/web-locks/)