某些网站可能需要与服务工件通信,而无需获知结果。下面是一些示例:
- 网页会向 Service Worker 发送要预加载的网址列表,以便在用户点击链接时,文档或网页子资源已在缓存中,从而加快后续导航速度。
- 网页会请求 Service Worker 检索并缓存一组热门文章,以便在离线状态下使用。
将这类非关键任务委托给服务工作器的好处在于,可以释放主线程,以便更好地处理更紧迫的任务,例如响应用户互动。
在本指南中,我们将探索如何使用标准浏览器 API 和 Workbox 库实现从网页到服务工件的单向通信技术。我们将这类用例称为命令式缓存。
生产支持请求
1-800-Flowers.com 通过 postMessage()
使用服务工件实现了强制缓存(预加载),以预加载类别页面中的热门商品,从而加快后续导航到商品详情页面的速度。
它们使用混合方法来确定要预加载哪些项:
- 在页面加载时,它们会请求服务器工作器检索前 9 个商品的 JSON 数据,并将生成的响应对象添加到缓存中。
- 对于其余项,它们会监听
mouseover
事件,以便在用户将光标移到某个项上时,触发对相应资源的“按需”提取。
它们使用 Cache API 存储 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'}
属性并非绝对必需,但它是允许页面向 Service Worker 发送不同类型的指令(即“预提取”与“清除存储空间”)的一种方式。Service Worker 可以根据此标志分支到不同的执行路径。
如果操作成功,用户将能够从中受益,但是如果没有,则不会影响主要的用户流。例如,当 1-800-Flowers.com 尝试预缓存时,该网页无需知道 Service Worker 是否成功。如果是,用户将获得更快的导航体验。如果没有,则页面仍需要导航到新页面。只需要再稍等片刻。
简单的预提取示例
命令式缓存最常见的应用之一是预提取,即在用户转到给定网址之前提取其资源,以加快导航速度。
您可以通过多种方式在网站中实现预提取:
- 在网页中使用链接预提取标记:资源会在浏览器缓存中保留 5 分钟,之后系统会应用资源的常规
Cache-Control
规则。 - 将在 Service Worker 中采用运行时缓存策略作为前一种技术的补充,以延长预加载资源的生命周期。
对于相对简单的预提取场景(例如预提取文档或特定资源 [JS、CSS 等]),这些技术是最佳方法。
如果需要其他逻辑,例如解析预提取资源(JSON 文件或页面)以提取其内部网址,则更合适的做法是将此任务完全委托给 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 进行预加载的妙处在于,即使失败,对页面和主线程的影响也不大。您还可以在预提取内容的后处理中使用更精细的逻辑,使其更灵活,并与其处理的数据解耦。讨论内容海阔天空。
总结
在本文中,我们介绍了页面和服务 worker 之间单向通信的一个常见用例:命令式缓存。所讨论的示例仅用于演示使用此模式的一种方法,同样的方法也可以应用于其他用例,例如按需缓存热门文章以供离线使用、添加书签等。
如需了解更多页面和服务工件通信模式,请参阅: