命令式缓存指南
某些网站可能需要与 Service Worker 进行通信,而无需获得有关结果的通知。下面是一些示例:
- 页面向 Service Worker 发送要预提取的网址列表,以便在用户点击链接时,缓存中已有文档或页面子资源,从而加快后续导航的速度。
- 该页面要求 Service Worker 检索并缓存一组热门文章,以供离线使用。
将这些类型的非关键任务委托给 Service Worker 的好处是,可以释放主线程来更好地处理更紧迫的任务,例如响应用户互动。
在本指南中,我们将介绍如何使用标准浏览器 API 和 Workbox 库实现从页面到 Service Worker 的单向通信技术。我们将这些类型的用例称为命令式缓存。
生产案例
1-800-Flowers.com 通过 postMessage()
使用 Service Worker 实现了命令式缓存(预提取),以预提取类别页面中的顶部商品,从而加快后续导航到商品详情页面的速度。
它们使用混合方法来确定要预提取的内容:
- 在网页加载时,它们会要求 Servicer Worker 检索前 9 项的 JSON 数据,并将生成的响应对象添加到缓存中。
- 对于其余项,它们会监听
mouseover
事件,以便在用户将光标移到某个项顶部时,可以按需触发对资源的提取。
它们使用 Cache API 存储 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 是否成功执行。如果是,用户将享受到更快的导航体验。如果未显示,则相应页面仍需导航到新页面。这需要再花一点时间。
简单的预提取示例
命令式缓存最常见的应用之一是预提取,即在用户转到给定网址之前提取资源以加快导航速度。
在网站上实现预提取的方法有多种:
- 在网页中使用链接预提取代码:资源会在浏览器缓存中保留 5 分钟,5 分钟过后,系统将应用资源的常规
Cache-Control
规则。 - 使用 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 之间的单向通信的一种常见用例:命令式缓存。我们讨论的示例仅用于演示一种使用此模式的方法,相同的方法也适用于其他用例,例如,按需缓存热门文章以供离线阅读、添加书签,等等。