2015 年,我们推出了后台同步,让服务工作线程能够将工作推迟到用户有网络连接时再执行。这意味着,用户可以输入消息、点击“发送”,然后离开网站,知道消息会在当前或连接网络后发送。
这是一项实用功能,但它要求服务工作线程在提取请求的整个过程中保持活跃状态。对于发送消息等短时间完成的任务,这不会造成问题,但如果任务耗时过长,浏览器会终止服务工作线程,否则会给用户的隐私和电池带来风险。
那么,如果您需要下载可能需要很长时间的内容(例如电影、播客或游戏关卡),该怎么办呢?这就是后台提取的用途。
自 Chrome 74 起,后台提取功能默认可用。
下面是一个简短的 2 分钟演示,展示了传统状态与使用后台提取之间的区别:
运作方式
后台提取的工作方式如下:
- 您告知浏览器在后台执行一组提取操作。
- 浏览器会提取这些内容,并向用户显示进度。
- 提取完成后或失败后,浏览器会打开您的服务工作线程并触发一个事件,告知您发生了什么情况。您可以在此处决定如何处理回答(如果有)。
如果用户在第 1 步之后关闭了您网站的页面,没关系,下载会继续进行。由于提取操作非常显眼且可轻松中止,因此不会出现后台同步任务过长而导致隐私泄露的问题。由于服务工作线程不会持续运行,因此不必担心它会滥用系统,例如在后台挖掘比特币。
在某些平台(例如 Android)上,浏览器可能会在第 1 步之后关闭,因为浏览器可以将提取操作交给操作系统。
如果用户在离线时开始下载,或在下载期间离线,后台提取将暂停,并在稍后恢复。
API
功能检测
与任何新功能一样,您需要检测浏览器是否支持该功能。对于后台提取,只需执行以下简单操作:
if ('BackgroundFetchManager' in self) {
  // This browser supports Background Fetch!
}
开始后台提取
主 API 依赖于 service worker 注册,因此请确保您已先注册了 service worker。然后,执行以下操作:
navigator.serviceWorker.ready.then(async (swReg) => {
  const bgFetch = await swReg.backgroundFetch.fetch('my-fetch', ['/ep-5.mp3', 'ep-5-artwork.jpg'], {
    title: 'Episode 5: Interesting things.',
    icons: [{
      sizes: '300x300',
      src: '/ep-5-icon.png',
      type: 'image/png',
    }],
    downloadTotal: 60 * 1024 * 1024,
  });
});
backgroundFetch.fetch 接受三个实参:
| 参数 | |
|---|---|
| id | string唯一标识此后台提取。 如果 ID 与现有的后台提取相匹配, | 
| requests | Array<Request|string>要提取的内容。字符串将被视为网址,并通过 new Request(theString)转换为Request。只要资源通过 CORS 允许,您就可以从其他来源提取内容。 注意:Chrome 目前不支持需要进行 CORS 预检的请求。 | 
| options | 一个对象,可能包含以下内容: | 
| options.title | string供浏览器显示的标题,与进度一起显示。 | 
| options.icons | Array<IconDefinition>包含 `src`、`size` 和 `type` 的对象数组。 | 
| options.downloadTotal | number响应正文的总大小(解压缩后)。 虽然这是可选操作,但我们强烈建议您提供此信息。它用于告知用户下载的大小,并提供进度信息。如果您不提供此信息,浏览器会告知用户大小未知,导致用户更可能中止下载。 如果后台提取下载次数超过此处给定的数量,系统将中止下载。如果下载量小于  | 
backgroundFetch.fetch 会返回一个 promise,该 promise 会解析为 BackgroundFetchRegistration。稍后我会详细介绍这一点。如果用户已选择停用下载功能,或者提供的某个参数无效,则相应 promise 会被拒绝。
为单个后台提取提供多个请求,可让您将用户在逻辑上视为单个事物的内容组合在一起。例如,一部电影可能会拆分为数千个资源(MPEG-DASH 的典型做法),并附带图片等其他资源。游戏的某个关卡可能分布在许多 JavaScript、图片和音频资源中。但对于用户而言,它只是“电影”或“关卡”。
获取现有后台提取
您可以通过如下方式获取现有的后台提取:
navigator.serviceWorker.ready.then(async (swReg) => {
  const bgFetch = await swReg.backgroundFetch.get('my-fetch');
});
…通过传递所需的后台提取的 id 来实现。如果不存在具有相应 ID 的有效后台提取,get 会返回 undefined。
从注册后台提取的那一刻起,直到后台提取成功、失败或中止,后台提取都被视为“活跃”。
您可以使用 getIds 获取所有活跃的后台提取的列表:
navigator.serviceWorker.ready.then(async (swReg) => {
  const ids = await swReg.backgroundFetch.getIds();
});
后台提取注册
BackgroundFetchRegistration(在上述示例中为 bgFetch)具有以下特征:
| 属性 | |
|---|---|
| id | string后台提取的 ID。 | 
| uploadTotal | number要发送到服务器的字节数。 | 
| uploaded | number成功发送的字节数。 | 
| downloadTotal | number注册后台提取时提供的值,或零。 | 
| downloaded | number成功接收的字节数。 此值可能会减少。例如,如果连接断开且无法继续下载,浏览器会从头开始重新提取相应资源。 | 
| result | 以下项之一: 
 | 
| failureReason | 以下项之一: 
 | 
| recordsAvailable | boolean是否可以访问底层请求/响应? 一旦此值为 false,便无法使用  | 
| 方法 | |
| abort() | 返回 Promise<boolean>中止后台提取。 如果提取请求已成功中止,则返回的 promise 会解析为 true。 | 
| matchAll(request, opts) | 返回 Promise<Array<BackgroundFetchRecord>>获取请求和响应。 此处的实参与缓存 API 相同。如果不使用任何实参进行调用,则会返回一个包含所有记录的 promise。 详见下文说明。 | 
| match(request, opts) | 返回 Promise<BackgroundFetchRecord>与上述相同,但会解析为第一个匹配项。 | 
| 事件 | |
| progress | 当 uploaded、downloaded、result或failureReason中的任何一个发生变化时触发。 | 
跟踪进度
这可以通过 progress 事件实现。请注意,downloadTotal 是您提供的任何值,如果您未提供值,则为 0。
bgFetch.addEventListener('progress', () => {
  // If we didn't provide a total, we can't provide a %.
  if (!bgFetch.downloadTotal) return;
  const percent = Math.round(bgFetch.downloaded / bgFetch.downloadTotal * 100);
  console.log(`Download progress: ${percent}%`);
});
获取请求和响应
bgFetch.match('/ep-5.mp3').then(async (record) => {
  if (!record) {
    console.log('No record found');
    return;
  }
  console.log(`Here's the request`, record.request);
  const response = await record.responseReady;
  console.log(`And here's the response`, response);
});
record 是一个 BackgroundFetchRecord,如下所示:
| 属性 | |
|---|---|
| request | Request所提供的请求。 | 
| responseReady | Promise<Response>提取的响应。 由于可能尚未收到响应,因此响应位于 promise 之后。如果提取失败,promise 将拒绝。 | 
Service Worker 事件
| 事件 | |
|---|---|
| backgroundfetchsuccess | 所有内容均已成功提取。 | 
| backgroundfetchfailure | 一次或多次提取失败。 | 
| backgroundfetchabort | 一次或多次提取失败。 如果您想清理相关数据,此方法才真正有用。 | 
| backgroundfetchclick | 用户点击了下载进度界面。 | 
活动对象具有以下属性:
| 属性 | |
|---|---|
| registration | BackgroundFetchRegistration | 
| 方法 | |
| updateUI({ title, icons }) | 可让您更改最初设置的标题/图标。此为可选操作,但可让您在必要时提供更多背景信息。您只能在 backgroundfetchsuccess和backgroundfetchfailure活动期间执行此操作 *一次*。 | 
对成功/失败做出反应
我们已经了解了 progress 事件,但只有在用户打开您网站的网页时,该事件才有用。后台提取的主要优势在于,即使在用户离开网页或关闭浏览器后,相关操作仍可继续进行。
如果后台提取成功完成,您的服务工作线程将收到 backgroundfetchsuccess 事件,而 event.registration 将是后台提取注册。
在此事件之后,您将无法再访问已提取的请求和响应,因此如果您想保留它们,请将它们移到某个位置,例如 Cache API。
与大多数 Service Worker 事件一样,请使用 event.waitUntil,以便 Service Worker 知道事件何时完成。
例如,在您的服务工作线程中:
addEventListener('backgroundfetchsuccess', (event) => {
  const bgFetch = event.registration;
  event.waitUntil(async function() {
    // Create/open a cache.
    const cache = await caches.open('downloads');
    // Get all the records.
    const records = await bgFetch.matchAll();
    // Copy each request/response across.
    const promises = records.map(async (record) => {
      const response = await record.responseReady;
      await cache.put(record.request, response);
    });
    // Wait for the copying to complete.
    await Promise.all(promises);
    // Update the progress notification.
    event.updateUI({ title: 'Episode 5 ready to listen!' });
  }());
});
失败可能只是因为一个 404 错误,而这个错误对您来说可能并不重要,因此仍然值得像上面那样将一些响应复制到缓存中。
对点击做出反应
显示下载进度和结果的界面可供点击。借助服务工作线程中的 backgroundfetchclick 事件,您可以对此做出反应。如上所述,event.registration 将是后台提取注册。
此事件的常见操作是打开一个窗口:
addEventListener('backgroundfetchclick', (event) => {
  const bgFetch = event.registration;
  if (bgFetch.result === 'success') {
    clients.openWindow('/latest-podcasts');
  } else {
    clients.openWindow('/download-progress');
  }
});
其他资源
更正:本文的先前版本错误地将后台提取称为“Web 标准”。该 API 目前不在标准轨道上,您可以在 WICG 中找到作为社区组报告草稿的规范。
