命令式缓存指南

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

某些网站可能需要与 Service Worker 进行通信,而无需获得有关结果的通知。下面是一些示例:

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

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

请求资源以缓存到 Service Worker 的示意图。

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

生产案例

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

1-800 Flowers 的徽标。

它们使用混合方法来确定要预提取的内容:

  • 在网页加载时,它们会要求 Servicer Worker 检索前 9 项的 JSON 数据,并将生成的响应对象添加到缓存中。
  • 对于其余项,它们会监听 mouseover 事件,以便在用户将光标移到某个项顶部时,可以按需触发对资源的提取。

它们使用 Cache API 存储 JSON 响应:

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 是否成功执行。如果是,用户将享受到更快的导航体验。如果未显示,则相应页面仍需导航到新页面。这需要再花一点时间。

简单的预提取示例

命令式缓存最常见的应用之一是预提取,即在用户转到给定网址之前提取资源以加快导航速度。

在网站上实现预提取的方法有多种:

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

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

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

  • 将提取和提取后处理(稍后会介绍)繁重的工作分流到辅助线程。这样做可以释放主线程来处理更重要的任务,例如响应用户互动。
  • 允许多个客户端(例如标签页)重复使用通用功能,甚至在不阻塞主线程的情况下同时调用服务。

预提取商品详情页面

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

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() 的小型辅助函数,用于迭代网址数组并为每个网址发出提取请求:

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 数据后,它通常包含同样值得预提取的其他网址,例如与此第一级数据关联的图像或其他端点数据。

假设在我们的示例中,返回的 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 调用页面以通知重要更新(例如,有新版本的 Web 应用可用)。
  • 双向通信:将任务委托给 Service Worker(例如下载大量内容),并让网页随时了解进度。