命令式缓存指南

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

某些网站可能需要与服务工件通信,而无需获知结果。下面是一些示例:

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

将这类非关键任务委托给服务工件的好处在于,可以释放主线程,以便更好地处理更紧迫的任务,例如响应用户互动。

显示页面请求将资源缓存到 Service Worker 的示意图。

在本指南中,我们将探索如何使用标准浏览器 API 和 Workbox 库实现从网页到服务工件的单向通信技术。我们将这类用例称为命令式缓存

生产支持请求

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

1-800 Flowers 徽标。

它们使用混合方法来确定要预加载哪些项:

  • 在页面加载时,它们会请求服务器工作器检索前 9 个商品的 JSON 数据,并将生成的响应对象添加到缓存中。
  • 对于其余项,它们会监听 mouseover 事件,以便在用户将光标移到某个项上时,触发对相应资源的“按需”提取。

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

1-800 Flowers 徽标。
从 1-800Flowers.com 中的商品详情页面预提取 JSON 商品数据。

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

使用 Workbox

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

如需让页面与服务工作器通信,请先获取对已注册服务工作器的 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 实现窗口与服务 worker 之间的通信。

postMessage API 可用于建立从网页到服务工件的单向通信机制。

该网页会对服务工件接口调用 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'} 属性并非绝对必需,但它是一种允许网页向服务工作器发送不同类型指令(即“预加载”与“清除存储空间”)的方法。服务工件可以根据此标志分支到不同的执行路径。

如果操作成功,用户将能够获享相应好处;如果操作失败,则不会改变主要用户体验流程。例如,当 1-800-Flowers.com 尝试预缓存时,该网页无需知道 Service Worker 是否成功。如果是,用户将获得更快的导航体验。如果没有,则页面仍需要导航到新页面。只需要再稍等片刻。

一个简单的预提取示例

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

您可以通过多种方式在网站中实现预提取:

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

如果需要额外的逻辑(例如,解析预加载资源 [JSON 文件或网页] 以提取其内部网址),则更适合将此任务完全委托给服务工件。

将此类操作委托给服务工件具有以下优势:

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

预提取商品详情页面

首先,在服务工作器接口上使用 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)。在这种情况下,您可以通过将提取的资源存储在服务工件缓存中来替换此行为。这样一来,还可以让文件在离线场景中进行传送。

除了 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 进行预加载的妙处在于,即使失败,对页面和主线程的影响也不大。您还可以在预提取内容的后处理中使用更精细的逻辑,使其更灵活,并与其处理的数据解耦。潜力无限。

总结

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

如需了解更多页面和服务工件通信模式,请参阅:

  • 广播更新:从 Service Worker 调用页面,以便告知重要更新(例如有新版 Web 应用可用)。
  • 双向通信:将任务委托给服务工件(例如大量下载),并让页面及时了解进度。