# Background Fetch API

> BackgroundFetchManager.fetch() 如何把大文件下载交给浏览器显示进度，完成后以哪些事件唤醒 service worker，以及它的异常与失败原因。

Background Fetch API 让页面或 service worker 把一组请求作为一个有名字的下载任务交给浏览器，标签页关闭后任务继续进行；浏览器在自己的界面中显示进度与取消控件，并在任务落定时在 service worker 中触发事件。它自 Chrome 74、Edge 79、Samsung Internet 11 起可用，Firefox 与 Safari 均未实现（BCD `api.BackgroundFetchManager`），因此它只能作为普通 `fetch()` 下载之上的增强，而不是替代。

## 语法

```js
// 页面或 service worker 中；registration 来自 navigator.serviceWorker.ready
const bgFetch = await registration.backgroundFetch.fetch(id, requests, options);
const existing = await registration.backgroundFetch.get(id);     // 没有该 id 的任务时为 undefined
const ids = await registration.backgroundFetch.getIds();         // string[]

// service worker 中
self.addEventListener('backgroundfetchsuccess', (event) => { /* event.registration */ });
self.addEventListener('backgroundfetchfail', (event) => { /* event.registration.failureReason */ });
self.addEventListener('backgroundfetchabort', (event) => { /* 用户或应用取消 */ });
self.addEventListener('backgroundfetchclick', (event) => { /* 用户点击了浏览器界面 */ });
```

`fetch()` 在浏览器接受任务后立即返回 `Promise<BackgroundFetchRegistration>`，远早于任何字节到达。任务落定时，`backgroundfetchsuccess`、`backgroundfetchfail`、`backgroundfetchabort` 三者恰好触发一个；`backgroundfetchclick` 只在用户点击浏览器的下载界面时触发。前两个是 `BackgroundFetchUpdateUIEvent` 实例，其 `updateUI({ title, icons })` 可修改完成后的通知；后两个是普通的 `BackgroundFetchEvent` 实例。

## 参数

`fetch()` 接受两个必填参数和一个选项对象。

| 名称 | 类型 | 说明 |
|---|---|---|
| `id` | `string` | 开发者自定的标识符，之后传给 `get(id)`。每个 id 同时只能有一个进行中的注册；第一个任务未完成时复用同一 id 会被拒绝。 |
| `requests` | `RequestInfo \| RequestInfo[]` | 一个 `Request` 或 URL 字符串，或它们的数组。所有请求作为一个用户可见的任务一起下载。`mode: 'no-cors'` 的请求会被拒绝。 |
| `options.title` | `string` | 浏览器进度界面中显示的标题。 |
| `options.icons` | `ImageResource[]` | 含 `src` 以及可选的 `sizes`、`type`、`label` 的对象，用于进度界面。 |
| `options.downloadTotal` | `number` | 预估总大小（字节）。它驱动进度条，同时也是硬上限：已下载字节一旦超过它，浏览器就会中止任务，并把 `failureReason` 置为 `"download-total-exceeded"`。 |

返回的 `BackgroundFetchRegistration` 暴露 `id`、`downloadTotal`、`downloaded`、`uploadTotal`、`uploaded`、`result`（`""`、`"success"` 或 `"failure"`）、`failureReason`（`""`、`"aborted"`、`"bad-status"`、`"fetch-error"`、`"quota-exceeded"` 或 `"download-total-exceeded"`）、`recordsAvailable`、`abort()`、`match()`、`matchAll()`，以及一个在任一计数器或 `result` 变化时触发的 `progress` 事件。

## 异常

`fetch()` 在以下情况拒绝其 Promise（MDN，`BackgroundFetchManager.fetch()`）。

- `TypeError`：没有给出任何请求、某个请求使用了 `mode: 'no-cors'`、注册上没有 service worker、同一 `id` 的注册已存在，或请求对象无法创建。
- `AbortError`（`DOMException`）：任务在被接受之前就被中止。
- `NotAllowedError`（`DOMException`）：用户代理未授予该源发起后台下载的权限；规范把这项权限检查留给浏览器，因此具体条件由浏览器定义。
- `QuotaExceededError`：存储请求时超出了该源的存储配额。响应在 service worker 读取之前保存在与 Cache Storage 同类的存储中，因此与 `caches` 计入同一份配额。

下载失败不会让任何 Promise 被拒绝：它触发 `backgroundfetchfail`，并设置 `event.registration.failureReason`。`"bad-status"` 表示某个响应的状态码不是 ok；`"fetch-error"` 表示网络请求本身在重试后仍然失败。

:::observed
在 Chrome（英文界面）中，DevTools › Application › Background services › Background fetch 面板在按下录制按钮（提示文字为 "Start recording events"）之前为空；之后注册的每一次状态变化都会成为表格中的一行，列依次为 Timestamp、Event、Origin、Service Worker Scope、Instance ID，选中某行后下方面板显示该注册的 `id`。DevTools 关闭后录制仍可持续最多三天，因此在标签页关闭之后才完成的下载也能事后查看（developer.chrome.com，"Debug background services"）。
:::

## 示例

两个示例都假定已在 `/sw.js` 注册了 service worker，且页面处于安全源。

### 把一集音频与封面图作为一个任务下载

页面以同一个 id 请求音频文件及其封面，浏览器因此只显示一条进度通知；`downloadTotal` 是此前从服务端 API 取得的两个 `Content-Length` 之和。

```js
async function downloadEpisode(episode) {
  const registration = await navigator.serviceWorker.ready;
  const bgFetch = await registration.backgroundFetch.fetch(
    `episode-${episode.id}`,
    [episode.audioUrl, episode.artworkUrl],
    {
      title: episode.title,
      icons: [{ src: '/icons/download-192.png', sizes: '192x192', type: 'image/png', label: 'Episode download' }],
      downloadTotal: episode.audioBytes + episode.artworkBytes,
    },
  );
  bgFetch.addEventListener('progress', () => {
    const percent = Math.round((bgFetch.downloaded / bgFetch.downloadTotal) * 100);
    document.querySelector('#progress').value = percent;
  });
}
```

`progress` 监听器只在这个页面打开期间运行；标签页关闭后，浏览器自己的界面就是唯一的进度指示，这正是该 API 的意义所在。

### 任务落定后保存响应

service worker 在 `backgroundfetchsuccess` 中把每个响应复制到一个命名缓存，然后修改通知文字；失败时记录原因并保持通知原样，让用户看到浏览器的失败状态。

```js
self.addEventListener('backgroundfetchsuccess', (event) => {
  event.waitUntil((async () => {
    const cache = await caches.open('episodes');
    const records = await event.registration.matchAll();
    await Promise.all(records.map(async (record) => {
      const response = await record.responseReady;
      await cache.put(record.request, response);
    }));
    await event.updateUI({ title: `${event.registration.id} is ready to play` });
  })());
});

self.addEventListener('backgroundfetchfail', (event) => {
  console.warn('background fetch failed:', event.registration.failureReason);
});

self.addEventListener('backgroundfetchclick', () => {
  clients.openWindow('/downloads/');
});
```

`record.responseReady` 是一个 Promise，因为部分失败的任务触发事件时，某条记录的响应可能仍在传输；在 `waitUntil()` 内 await 它可以让 worker 存活到复制完成。

### 检测支持并改为前台下载

Firefox 与 Safari 的注册对象上没有 `backgroundFetch` 属性。回退方案执行普通 `fetch()`，并通过临时的 `<a download>` 触发浏览器的保存对话框；它只在页面保持打开时有效，界面应明确告知这一点。

```js
async function downloadWithFallback(id, urls, title) {
  const registration = await navigator.serviceWorker.ready;
  if ('backgroundFetch' in registration) {
    return registration.backgroundFetch.fetch(id, urls, { title });
  }
  for (const url of urls) {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`${url}: ${response.status}`);
    const link = document.createElement('a');
    link.href = URL.createObjectURL(await response.blob());
    link.download = new URL(url, location.href).pathname.split('/').pop();
    link.click();
    URL.revokeObjectURL(link.href);
  }
}
```

前台路径会在保存前把整个文件缓冲在内存中，因此超大文件应逐个下载，并提醒用户不要离开页面。

## 另请参阅

- [Background Fetch specification: fetch() method](https://wicg.github.io/background-fetch/#background-fetch-manager-fetch)（wicg.github.io）
- [Introducing Background Fetch](https://developer.chrome.com/blog/background-fetch)（developer.chrome.com）
- [Debug background services](https://developer.chrome.com/docs/devtools/javascript/background-services)（developer.chrome.com）
- [Background Fetch 浏览器支持](/zh/compatibility/background-fetch/)
- [Background Sync API](/zh/reference/service-worker/background-sync/)
- [Cache API](/zh/reference/service-worker/cache-api/)
- [配额与 StorageManager.estimate()](/zh/reference/storage/quota-estimate/)