后台提取简介

Jake Archibald
Jake Archibald

2015 年,我们推出了后台同步,让服务工件可以在用户连接到网络之前推迟工作。这意味着,用户可以输入消息、点击“发送”并离开网站,知道消息会立即发送或在他们有网络连接时发送。

这是一个实用的功能,但需要服务工作器在提取期间保持活跃状态。对于发送消息等短时间的工作,这不是什么问题,但如果任务耗时过长,浏览器会终止服务工件,否则会对用户的隐私和电池造成风险。

那么,如果您需要下载可能需要很长时间的内容(例如电影、播客或游戏关卡),该怎么办?这就是后台提取的用途。

从 Chrome 74 开始,后台提取功能默认可用。

下面是一个时长两分钟的快速演示,展示了传统状态与使用后台提取功能的对比情况:

亲自试用演示版浏览代码

运作方式

后台提取的工作原理如下:

  1. 您可以指示浏览器在后台执行一组提取操作。
  2. 浏览器会提取这些内容,并向用户显示进度。
  3. 提取完成或失败后,浏览器会打开您的 Service Worker 并触发事件,以告知您发生了什么情况。您可以在此处决定如何处理回答(如果有)。

如果用户在第 1 步后关闭了您网站的页面,也不用担心,下载会继续进行。由于提取操作非常明显且易于中止,因此不会出现后台同步任务过长而导致隐私泄露的问题。由于 Service Worker 不会持续运行,因此无需担心它会滥用系统,例如在后台挖掘比特币。

在某些平台(例如 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 与现有的后台提取内容匹配,backgroundFetch.fetch 将拒绝。

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
响应正文的总大小(解压缩后)。

虽然这不是必需的,但我们强烈建议您提供。它用于告知用户下载内容的大小,并提供进度信息。如果您未提供此信息,浏览器会告知用户大小未知,因此用户可能会更有可能中止下载。

如果后台提取下载量超过此处给出的数量,系统会中止后台提取。如果下载量小于 downloadTotal,也完全没问题,因此,如果您不确定下载总量,最好还是多留一些空间。

backgroundFetch.fetch 会返回一个 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

以下项之一:

  • "" - 后台提取正在进行,因此尚无结果。
  • "success" - 后台提取成功。
  • "failure" - 后台提取失败。只有在后台提取完全失败时,此值才会显示,因为浏览器无法重试/恢复。
failureReason

以下项之一:

  • "" - 后台提取未失败。
  • "aborted" - 用户中止了后台提取,或调用了 abort()
  • "bad-status" - 其中一个响应的状态不为“ok”,例如 404。
  • "fetch-error" - 某次提取操作因其他原因而失败,例如 CORS、MIX、部分响应无效,或提取操作出现常规网络故障且无法重试。
  • "quota-exceeded" - 在后台提取期间达到了存储空间配额。
  • "download-total-exceeded" - 超出了提供的“downloadTotal”。
recordsAvailable boolean
能否访问底层请求/响应?

此值变为 false 后,便无法再使用 matchmatchAll

方法
abort() 返回 Promise<boolean>
终止后台提取。

如果提取成功终止,则返回的 promise 会解析为 true。

matchAll(request, opts) 返回 Promise<Array<BackgroundFetchRecord>>
获取请求和响应。

此处的参数与缓存 API 相同。不使用参数进行调用会返回所有记录的 Promise。

详见下文说明。

match(request, opts) 返回 Promise<BackgroundFetchRecord>
与上文相同,但使用第一个匹配项进行解析。
事件
progress uploadeddownloadedresultfailureReason 发生变化时触发。

跟踪进度

这可以通过 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 将被拒绝。

Service Worker 事件

事件
backgroundfetchsuccess 所有内容均已成功提取。
backgroundfetchfailure 一个或多个提取操作失败。
backgroundfetchabort 一个或多个提取操作失败。

只有在您想要清理相关数据时,此方法才非常有用。

backgroundfetchclick 用户点击了下载进度界面。

事件对象具有以下特点:

属性
registration BackgroundFetchRegistration
方法
updateUI({ title, icons }) 用于更改您最初设置的标题/图标。此为可选操作,但您可以根据需要提供更多背景信息。您只能在 backgroundfetchsuccessbackgroundfetchfailure 事件期间执行此操作 *一次*。

对成功/失败做出响应

我们已经看到了 progress 事件,但该事件仅在用户打开指向您网站的网页时才有用。后台提取的主要优势在于,在用户离开网页或甚至关闭浏览器后,相关内容仍会继续运行。

如果后台提取成功完成,您的服务工件将收到 backgroundfetchsuccess 事件,并且 event.registration 将是后台提取注册。

此事件发生后,您将无法再访问提取的请求和响应,因此,如果您想保留这些内容,请将其移至某个位置(例如 cache API)。

与大多数服务工作器事件一样,请使用 event.waitUntil,以便服务工作器知道事件何时完成。

例如,在您的服务工件中:

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

其他资源

更正:本文的先前版本错误地将后台提取称为“网络标准”。此 API 目前不在标准轨道上,规范可在 WICG 中找到,形式为社区群组报告草稿。