# Periodic Background Sync API

> PeriodicSyncManager.register() 如何按 minInterval 在已安装 PWA 中触发 periodicsync，Chrome 的安装与参与度门槛，以及异常与检测。

import Figure from '@components/Figure.astro';
import periodicSyncDiagram from '@assets/diagrams/periodic-background-sync.svg';

Periodic Background Sync API 让已安装的 web 应用注册一个带名字的任务，由浏览器按自己选择的时机在 service worker 中运行，频率不高于应用要求的 `minInterval`，这样用户下次打开应用时内容已经是新的。它自 Chrome 80、Edge 80、Samsung Internet 13 起可用，Firefox 与 Safari 均未实现（BCD `api.PeriodicSyncManager`），而且 Chrome 只把它授予已安装、且站点参与度分数大于零的应用。

<Figure src={periodicSyncDiagram} alt="周期性后台同步流程图：已安装且有足够参与度的 PWA 检查 periodic-background-sync 权限，并以 tag 与 minInterval 调用 periodicSync.register()；浏览器按不高于 minInterval 的频率排期，到时且设备在线时在 service worker 中触发 periodicsync 事件，worker 在 event.waitUntil() 内拉取并缓存新数据；循环持续到应用被卸载、参与度衰减或调用 periodicSync.unregister()。" caption="周期性后台同步：从权限与注册到反复触发的 periodicsync 事件。" />

## 语法

```js
// 页面或 service worker 中
const registration = await navigator.serviceWorker.ready;
await registration.periodicSync.register(tag, { minInterval });
const tags = await registration.periodicSync.getTags();
await registration.periodicSync.unregister(tag);

// service worker 中
self.addEventListener('periodicsync', (event) => {
  event.waitUntil(refresh(event.tag));
});
```

`register()` 在注册写入存储后返回 `Promise<undefined>`；对已存在的 tag 再次注册会替换它的 `minInterval`。`getTags()` 兑现为所有仍在注册中的 tag，`unregister()` 无论 tag 是否存在都兑现为 `undefined`。`periodicsync` 事件只携带 `tag`；与一次性后台同步不同，它没有 `lastChance`，因为一次失败的运行之后只是等待下一次排期。

## 参数

该方法接受一个 tag 与一个选项对象。

| 名称 | 类型 | 说明 |
|---|---|---|
| `tag` | `string` | 标识这次注册，并以 `PeriodicSyncEvent.tag` 送达。每个 tag 一条注册；多个 tag 可以按不同节奏运行。 |
| `options.minInterval` | `number` | 两次运行之间的最小毫秒数。浏览器把它当作下限而不是时间表：Chrome 让实际频率与应用的使用频率对齐，同时考虑设备的电量与网络状态，参与度分数为零时则完全跳过。 |

Chrome 还要求存在一个此前用过的网络才会触发事件，因此设备连上从未见过的网络时，用户在该网络上打开一次应用之前不会同步。

## 异常

`register()` 在三种情况下拒绝（MDN，`PeriodicSyncManager.register()`）。

- `InvalidStateError`（`DOMException`）：注册上没有 active 状态的 service worker。先 `await navigator.serviceWorker.ready`。
- `NotAllowedError`（`DOMException`）：`periodic-background-sync` 权限未授予。在 Chrome 中，任何普通浏览器标签页都处于这个状态，因为该权限只授予已安装的应用；同样的代码在应用从自己的窗口启动后就能成功。
- `InvalidAccessError`（`DOMException`）：调用方窗口不是顶层或辅助浏览上下文，例如 iframe。
- `TypeError`：Firefox 与 Safari 中 `registration.periodicSync` 为 `undefined`，访问其成员本身就会抛出。用 `'periodicSync' in registration` 做特性检测。

:::observed
在 Chrome DevTools（英文界面）中，Application 面板的 Service workers 窗格在 **Sync** 与 **Push** 输入框旁有一个标为 **Periodic Sync** 的文本框；输入 tag 并点击旁边的按钮，就会不经浏览器调度器直接向选中的 worker 派发带该 tag 的 `periodicsync` 事件，在 Console 的上下文下拉框中选中 worker 脚本 URL 后能看到它的 `console.log()` 输出。Background services 下另有一个 **Periodic Background Sync** 分区，点击 **Start recording** 后记录真实派发，并持续录制最多三天（developer.chrome.com，"Periodic Background Sync" 与 "Debug Progressive Web Apps"）。
:::

## 示例

示例共用位于 `/sw.js` 的一个 service worker，以及名为 `news` 的新闻缓存。

### 检查权限后注册每日刷新

页面在调用 `register()` 之前先查询权限，未安装的应用因此不会撞上 `NotAllowedError`，而是改为向用户显示"打开时刷新"的提示。

```js
async function enableDailyRefresh() {
  const status = await navigator.permissions.query({ name: 'periodic-background-sync' });
  if (status.state !== 'granted') {
    showNotice('安装应用后，头条会在后台自动刷新。');
    return;
  }
  const registration = await navigator.serviceWorker.ready;
  await registration.periodicSync.register('news-refresh', {
    minInterval: 24 * 60 * 60 * 1000,
  });
}
```

Firefox 与 Safari 对未知权限名的 `permissions.query()` 会抛出 `TypeError`，跨浏览器的调用方应把查询包在 `try`/`catch` 中，并把失败视为"未授予"。

### 在 service worker 中处理事件

worker 拉取并存储新闻；抛出的错误会让 `waitUntil()` 的 Promise 被拒绝，浏览器记录后忽略，直到下一次排期。

```js
self.addEventListener('periodicsync', (event) => {
  if (event.tag !== 'news-refresh') return;
  event.waitUntil((async () => {
    const response = await fetch('/api/headlines', { cache: 'no-store' });
    if (!response.ok) throw new Error(`headlines: ${response.status}`);
    const cache = await caches.open('news');
    await cache.put('/api/headlines', response);
  })());
});
```

工作量要短：Chrome 在自己的执行时限到达后结束事件，仍在运行的 worker 会被终止，缓存写入也随之中断。

### 检测支持并改为返回页面时刷新

`periodicSync` 缺失或权限未授予时，回退方案在页面重新可见时刷新，对普通标签页或未安装的应用同样达成"打开即新"的目标，代价是一次可见的网络等待。

```js
async function setUpRefresh(refreshNow) {
  const registration = await navigator.serviceWorker.ready;
  if ('periodicSync' in registration) {
    try {
      await registration.periodicSync.register('news-refresh', { minInterval: 24 * 60 * 60 * 1000 });
      return;
    } catch {
      // 普通标签页中会被拒绝：落入前台回退。
    }
  }
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') refreshNow();
  });
  refreshNow();
}
```

回退只在页面存在期间运行，无法为已关闭的应用刷新内容，这正是该 API 所填补的空缺。

## 另请参阅

- [Web Periodic Background Synchronization: register() method](https://wicg.github.io/periodic-background-sync/#dom-periodicsyncmanager-register)（wicg.github.io）
- [Richer offline experiences with the Periodic Background Sync API](https://developer.chrome.com/docs/capabilities/periodic-background-sync)（developer.chrome.com）
- [PeriodicSyncManager: register() method](https://developer.mozilla.org/en-US/docs/Web/API/PeriodicSyncManager/register)（developer.mozilla.org）
- [Periodic Background Sync 浏览器支持](/zh/compatibility/periodic-background-sync/)
- [Background Sync API](/zh/reference/service-worker/background-sync/)
- [用于内容更新的周期性后台同步](/zh/reference/notifications/periodic-background-sync/)
- [Service worker 生命周期](/zh/reference/service-worker/lifecycle/)