强制缓存指南

有些网站可能需要与 Service Worker 通信,而无需被告知结果。以下是一些示例:

  • 页面向 Service Worker 发送 URL 列表进行预提取,以便当用户点击链接时,文档或页面子资源已在缓存中可用,从而使后续导航速度更快。
  • 页面要求 Service Worker 检索并缓存一组热门文章,以便离线使用。

将这些类型的非关键任务委托给 Service Worker 的好处是,可以释放主线程,以便更好地处理更紧迫的任务,例如响应用户交互。

Diagram of a page requesting resources to cache to a service worker.

在本指南中,我们将探讨如何使用标准浏览器 API 和 Workbox 库实现从页面到 Service Worker 的单向通信技术。我们将这些类型的用例称为强制缓存

生产案例

1-800-Flowers.com 通过 postMessage() 使用 Service Worker 实现了强制缓存(预提取),以预提取类别页面中的热门商品,从而加快后续导航到商品详情页面的速度。

Logo of 1-800 Flowers.

他们使用混合方法来决定要预提取哪些商品:

  • 在页面加载时,他们要求 Service Worker 检索前 9 个商品的 JSON 数据,并将生成的响应对象添加到缓存中。
  • 对于其余商品,他们监听 mouseover 事件,以便当用户将光标移动到商品上方时,他们可以“按需”触发资源的提取。

他们使用 Cache API 来存储 JSON 响应。

Logo of 1-800 Flowers.
从 1-800Flowers.com 的商品列表页面预提取 JSON 商品数据。

当用户点击商品时,可以从缓存中获取与其关联的 JSON 数据,而无需访问网络,从而加快导航速度。

使用 Workbox

Workbox 通过 workbox-window 包(一组旨在在窗口上下文中运行的模块)提供了一种向 Service Worker 发送消息的简便方法。它们是对在 Service Worker 中运行的其他 Workbox 包的补充。

要使页面与 Service Worker 通信,首先获取对已注册 Service Worker 的 Workbox 对象引用:

const wb = new Workbox('/sw.js');
wb.register();

然后,您可以直接声明式地发送消息,而无需费力获取注册、检查激活或考虑底层通信 API。

wb.messageSW({"type": "PREFETCH", "payload": {"urls": ["/data1.json", "data2.json"]}}); });

Service Worker 实现了一个 message 处理程序来监听这些消息。它可以选择性地返回响应,尽管在这些情况下,这不是必需的。

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PREFETCH') {
    // do something
  }
});

使用浏览器 API

如果 Workbox 库不足以满足您的需求,以下是如何使用浏览器 API 实现窗口到 Service Worker 的通信。

postMessage API 可用于建立从页面到 Service Worker 的单向通信机制。

页面在 Service Worker 接口上调用 postMessage()

navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
  payload: 'some data to perform the task',
});

Service Worker 实现了一个 message 处理程序来监听这些消息。

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === MSG_ID) {
    // do something
  }
});

{type : 'MSG_ID'} 属性不是绝对必需的,但它是一种允许页面向 Service Worker 发送不同类型指令(即“预提取”与“清除存储”)的方法。Service Worker 可以根据此标志分支到不同的执行路径。

如果操作成功,用户将能够从中受益,但如果失败,则不会改变主要用户流程。例如,当 1-800-Flowers.com 尝试预缓存时,页面不需要知道 Service Worker 是否成功。如果成功,则用户将享受更快的导航。如果失败,页面仍然需要导航到新页面。只是会花费更长的时间。

一个简单的预提取示例

强制缓存最常见的应用之一是预提取,这意味着在用户移动到给定 URL 之前提取该 URL 的资源,以加快导航速度。

站点中有不同的预提取实现方式:

对于相对简单的预提取场景,例如预提取文档或特定资产(JS、CSS 等),这些技术是最佳方法。

如果需要额外的逻辑,例如解析预提取资源(JSON 文件或页面)以提取其内部 URL,则更适合将此任务完全委托给 Service Worker。

将这些类型的操作委托给 Service Worker 具有以下优点:

  • 将提取和提取后处理(稍后将介绍)的繁重工作卸载到辅助线程。通过这样做,它可以释放主线程来处理更重要的任务,例如响应用户交互。
  • 允许多个客户端(例如选项卡)重用通用功能,甚至可以同时调用服务而不会阻塞主线程。

预提取商品详情页面

首先,在 Service Worker 接口上使用 postMessage() 并传递要缓存的 URL 数组。

navigator.serviceWorker.controller.postMessage({
  type: 'PREFETCH',
  payload: {
    urls: [
      'www.exmaple.com/apis/data_1.json',
      'www.exmaple.com/apis/data_2.json',
    ],
  },
});

在 Service Worker 中,实现一个 message 处理程序来拦截和处理任何活动选项卡发送的消息。

addEventListener('message', (event) => {
  let data = event.data;
  if (data && data.type === 'PREFETCH') {
    let urls = data.payload.urls;
    for (let i in urls) {
      fetchAsync(urls[i]);
    }
  }
});

在前面的代码中,我们引入了一个名为 fetchAsync() 的小型辅助函数,用于迭代 URL 数组并为每个 URL 发出提取请求。

async function fetchAsync(url) {
  // await response of fetch call
  let prefetched = await fetch(url);
  // (optionally) cache resources in the service worker storage
}

当获得响应时,您可以依赖资源的缓存标头。但在许多情况下,例如在商品详情页面中,资源未缓存(这意味着,它们具有 Cache-control 标头 no-cache)。在这些情况下,您可以覆盖此行为,方法是将提取的资源存储在 Service Worker 缓存中。这还具有在离线场景中提供文件的额外好处。

超越 JSON 数据

从服务器端点提取 JSON 数据后,它通常包含其他也值得预提取的 URL,例如与此一级数据关联的图像或其他端点数据。

假设在我们的示例中,返回的 JSON 数据是杂货店购物网站的信息:

{
  "productName": "banana",
  "productPic": "https://cdn.example.com/product_images/banana.jpeg",
  "unitPrice": "1.99"
 }

修改 fetchAsync() 代码以迭代商品列表并缓存每个商品的主图。

async function fetchAsync(url, postProcess) {
  // await response of fetch call
  let prefetched = await fetch(url);

  //(optionally) cache resource in the service worker cache

  // carry out the post fetch process if supplied
  if (postProcess) {
    await postProcess(prefetched);
  }
}

async function postProcess(prefetched) {
  let productJson = await prefetched.json();
  if (productJson && productJson.product_pic) {
    fetchAsync(productJson.product_pic);
  }
}

您可以围绕此代码添加一些异常处理,以应对 404 等情况。但使用 Service Worker 进行预提取的美妙之处在于,即使失败,对页面和主线程也不会产生太大影响。您还可以在预提取内容的后处理中拥有更精细的逻辑,使其更灵活,并与它处理的数据解耦。天空才是极限。

结论

在本文中,我们介绍了页面和 Service Worker 之间单向通信的常见用例:强制缓存。讨论的示例仅用于演示使用此模式的一种方式,相同的方法也可以应用于其他用例,例如按需缓存热门文章以供离线消费、添加书签等。

有关页面和 Service Worker 通信的更多模式,请查看:

  • 广播更新:从 Service Worker 调用页面以通知重要更新(例如,WebApp 的新版本可用)。
  • 双向通信:将任务委托给 Service Worker(例如,大型下载),并让页面了解进度。