向使用 Service Worker 的页面广播更新

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

  • 在新版本的 Service Worker 安装后通知页面,以便页面可以向用户显示“更新以刷新”按钮,从而立即访问新功能。
  • 通过显示指示,例如:“应用现在可以离线工作”“内容的新版本可用”,让用户了解 Service Worker 端发生的缓存数据更改。
Diagram showing a service worker communicating with the page to send an update.

我们将这些类型的用例(即 Service Worker 无需接收来自页面的消息即可启动通信)称为“广播更新”。在本指南中,我们将回顾通过使用标准浏览器 API 和 Workbox 库实现页面和 Service Worker 之间此类通信的不同方法。

生产案例

Tinder

Tinder PWA 使用 workbox-window 来监听来自页面的重要 Service Worker 生命周期时刻(“installed”、“controlled”和“activated”)。这样,当新的 Service Worker 生效时,它会显示“有可用更新”横幅,以便他们可以刷新 PWA 并访问最新功能

A screenshot of Tinder's webapp 'Update Available' functionality.
在 Tinder PWA 中,Service Worker 告诉页面新版本已准备就绪,页面会向用户显示“有可用更新”横幅。

Squoosh

Squoosh PWA 中,当 Service Worker 缓存了使其能够离线工作的所有必要资源时,它会向页面发送消息以显示“已准备好离线工作”Toast 消息,从而让用户了解此功能

A screenshot of Squoosh webapp 'Ready to work offline' functionality.
在 Squoosh PWA 中,当缓存准备就绪时,Service Worker 会向页面广播更新,并且页面会显示“已准备好离线工作”Toast 消息。

使用 Workbox

监听 Service Worker 生命周期事件

workbox-window 提供了一个直接的接口来监听重要的 Service Worker 生命周期事件。在底层,该库使用客户端 API,例如 updatefoundstatechange,并在 workbox-window 对象中提供更高级别的事件监听器,从而使用户可以更轻松地使用这些事件。

以下页面代码使你可以检测到每次安装新版本的 Service Worker,以便你可以将其传达给用户

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 添加到 Service Worker 端的策略选项中

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

Service Worker 创建一个 BroadcastChannel 对象并开始向其发送消息。任何对接收这些消息感兴趣的上下文(例如页面)都可以实例化一个 BroadcastChannel 对象并实现消息处理程序以接收消息。

要在新 Service Worker 安装时通知页面,请使用以下代码

// 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 与多个客户端通信。

使用以下 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,但并非所有方法都受支持。使用前请检查浏览器支持。

Message Channel

Message Channel 需要一个初始配置步骤,即通过将端口从页面传递到 Service Worker,以建立它们之间的通信通道。页面实例化一个 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 通信的一个特殊案例:“广播更新”。探讨的示例包括监听重要的 Service Worker 生命周期事件,以及向页面传达有关内容或缓存数据更改的信息。你可以考虑更多有趣的用例,其中 Service Worker 主动与页面通信,而无需事先接收任何消息。

有关窗口和 Service Worker 通信的更多模式,请查看

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

其他资源