使用 Service Worker 向页面广播更新

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

在某些情况下,服务工件可能需要主动与其控制的任何活跃标签页通信,以便通知特定事件。例如:

  • 在安装新版本的服务工作线程时通知页面,以便页面向用户显示“更新以刷新”按钮,以便用户立即使用新功能。
  • 通过显示指示(例如“应用现在可以离线运行”或“有新版本的内容可用”)告知用户服务工件端缓存数据发生的更改。
显示服务工件与页面通信以发送更新的示意图。

我们将服务工作器无需从网页接收消息即可发起通信的此类用例称为“广播更新”。在本指南中,我们将介绍使用标准浏览器 API 和 Workbox 库在网页和服务工件之间实现此类通信的不同方法。

生产案例

Tinder

Tinder PWA 使用 workbox-window 监听页面上的重要 Service Worker 生命周期时刻(“已安装”“受控”和“已激活”)。这样,当有新的服务工作器投入使用时,系统会显示“有更新可用”横幅,以便用户刷新 PWA 并使用最新功能:

Tinder 网站应用“有更新可用”功能的屏幕截图。
在 Tinder PWA 中,Service Worker 会告知页面新版本已准备就绪,然后页面会向用户显示“有可用更新”横幅。

Squoosh

Squoosh PWA 中,当服务工作器缓存了所有必要的资源以实现离线工作时,它会向网页发送一条消息,以显示“准备好离线工作”消息框,让用户知道该功能:

Squoosh Web 应用的“准备离线工作”功能的屏幕截图。
在 Squoosh PWA 中,当缓存准备就绪时,Service Worker 会广播对网页的更新,并且网页会显示“可以离线工作”消息框。

使用 Workbox

监听服务工作线程生命周期事件

workbox-window 提供了一个简单的接口,用于监听重要的 Service Worker 生命周期事件。在后台,该库使用 updatefoundstatechange 等客户端 API,并在 workbox-window 对象中提供更高级别的事件监听器,以便用户更轻松地使用这些事件。

通过以下页面代码,您可以检测每次安装新版本的服务工作器,以便将其告知用户:

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

wb.addEventListener('installed', (event) => {
  if (event.isUpdate) {
    // Show "Update App" banner
  }
});

wb.register();

告知页面缓存数据发生了变化

Workbox 软件包 workbox-broadcast-update 提供了一种标准方式,用于通知窗口客户端缓存的响应已更新。此策略最常与 StaleWhileRevalidate 策略搭配使用。

如需广播更新,请在服务工件端的策略选项中添加 broadcastUpdate.BroadcastUpdatePlugin

import {registerRoute} from 'workbox-routing';
import {StaleWhileRevalidate} from 'workbox-strategies';
import {BroadcastUpdatePlugin} from 'workbox-broadcast-update';

registerRoute(
  ({url}) => url.pathname.startsWith('/api/'),
  new StaleWhileRevalidate({
    plugins: [
      new BroadcastUpdatePlugin(),
    ],
  })
);

在 Web 应用中,您可以按如下方式监听这些事件:

navigator.serviceWorker.addEventListener('message', async (event) => {
  // Optional: ensure the message came from workbox-broadcast-update
  if (event.data.meta === 'workbox-broadcast-update') {
    const {cacheName, updatedUrl} = event.data.payload;

    // Do something with cacheName and updatedUrl.
    // For example, get the cached content and update
    // the content on the page.
    const cache = await caches.open(cacheName);
    const updatedResponse = await cache.match(updatedUrl);
    const updatedText = await updatedResponse.text();
  }
});

使用浏览器 API

如果 Workbox 提供的功能不足以满足您的需求,请使用以下浏览器 API 实现“广播更新”

Broadcast Channel API

服务工件会创建 BroadcastChannel 对象,并开始向其发送消息。任何有兴趣接收这些消息的上下文(例如页面)都可以实例化 BroadcastChannel 对象并实现消息处理脚本以接收消息。

如需在安装新的服务工作器时通知页面,请使用以下代码:

// Create Broadcast Channel to send messages to the page
const broadcast = new BroadcastChannel('sw-update-channel');

self.addEventListener('install', function (event) {
  // Inform the page every time a new service worker is installed
  broadcast.postMessage({type: 'CRITICAL_SW_UPDATE'});
});

该网页通过订阅 sw-update-channel 来监听这些事件:

// Create Broadcast Channel and listen to messages sent to it
const broadcast = new BroadcastChannel('sw-update-channel');

broadcast.onmessage = (event) => {
  if (event.data && event.data.type === 'CRITICAL_SW_UPDATE') {
    // Show "update to refresh" banner to the user.
  }
};

这是一种简单的技术,但其限制在于浏览器支持:在撰写本文时,Safari 不支持此 API

Client API

Client API 提供了一种简单的方法,可通过迭代 Client 对象数组,从服务工作器与多个客户端进行通信。

使用以下 Service Worker 代码向上次聚焦的标签页发送消息:

// Obtain an array of Window client objects
self.clients.matchAll(options).then(function (clients) {
  if (clients && clients.length) {
    // Respond to last focused tab
    clients[0].postMessage({type: 'MSG_ID'});
  }
});

该网页会实现消息处理脚本来拦截这些消息:

// Listen to messages
navigator.serviceWorker.onmessage = (event) => {
     if (event.data && event.data.type === 'MSG_ID') {
         // Process response
   }
};

对于向多个活动标签页广播信息等用例,Client API 非常适用。所有主流浏览器都支持该 API,但并非其所有方法都受支持。请先检查浏览器支持情况,然后再使用。

消息渠道

消息通道需要执行初始配置步骤,即将端口从网页传递给服务工作器,以便在两者之间建立通信通道。该网页会实例化 MessageChannel 对象,并通过 postMessage() 接口将端口传递给 Service Worker:

const messageChannel = new MessageChannel();

// Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
  messageChannel.port2,
]);

该页面通过在该端口上实现“onmessage”处理脚本来监听消息:

// Listen to messages
messageChannel.port1.onmessage = (event) => {
  // Process message
};

Service Worker 会接收端口并保存对它的引用:

// Initialize
let communicationPort;

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PORT_INITIALIZATION') {
    communicationPort = event.ports[0];
  }
});

从那时起,它可以通过在对接口的引用中调用 postMessage() 向页面发送消息:

// Communicate
communicationPort.postMessage({type: 'MSG_ID' });

由于需要初始化端口,MessageChannel 的实现可能更复杂,但所有主要浏览器都支持它。

后续步骤

在本指南中,我们探讨了窗口与服务工件通信的一个特殊情况:“广播更新”。探索的示例包括监听重要的服务工件生命周期事件,以及与网页通信以了解内容或缓存数据的更改。您可以考虑一些更有趣的用例,其中服务工件会主动与页面通信,而无需先接收任何消息。

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

  • 命令式缓存指南:从网页调用 Service Worker 以提前缓存资源(例如在预加载场景中)。
  • 双向通信:将任务委托给服务工件(例如大量下载),并让页面及时了解进度。

其他资源