立即处理 Service Worker 更新

默认情况下,Service Worker 生命周期要求在找到并安装更新的 Service Worker 时,在更新的 Service Worker 激活并取得控制之前,当前 Service Worker 控制的所有打开的标签页都必须关闭或经历导航。

在许多情况下,允许这种情况在适当时进行可能没什么问题,但在某些情况下,您可能需要提前告知用户存在待处理的 Service Worker 更新,然后自动切换到新 Service Worker。为此,您需要在页面和 Service Worker 中添加一些代码。

要放置在网页中的代码

以下代码使用从 CDN 托管的 workbox-window 版本导入的 JavaScript 模块,在内嵌 <script> 元素中运行。它使用 workbox-window 注册 Service Worker,如果 Service Worker 在等待阶段卡住,将做出响应。当发现等待的 Service Worker 时,此代码会通知用户网站有更新版本可用,并提示他们重新加载。

<!-- This script tag uses JavaScript modules, so the proper `type` attribute value is required -->
<script type="module">
  // This code sample uses features introduced in Workbox v6.
  import {Workbox} from 'https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-window.prod.mjs';

  if ('serviceWorker' in navigator) {
    const wb = new Workbox('/sw.js');
    let registration;

    const showSkipWaitingPrompt = async (event) => {
      // Assuming the user accepted the update, set up a listener
      // that will reload the page as soon as the previously waiting
      // service worker has taken control.
      wb.addEventListener('controlling', () => {
        // At this point, reloading will ensure that the current
        // tab is loaded under the control of the new service worker.
        // Depending on your web app, you may want to auto-save or
        // persist transient state before triggering the reload.
        window.location.reload();
      });

      // When `event.wasWaitingBeforeRegister` is true, a previously
      // updated service worker is still waiting.
      // You may want to customize the UI prompt accordingly.

      // This code assumes your app has a promptForUpdate() method,
      // which returns true if the user wants to update.
      // Implementing this is app-specific; some examples are:
      // https://open-ui.org/components/alert.research or
      // https://open-ui.org/components/toast.research
      const updateAccepted = await promptForUpdate();

      if (updateAccepted) {
        wb.messageSkipWaiting();
      }
    };

    // Add an event listener to detect when the registered
    // service worker has installed but is waiting to activate.
    wb.addEventListener('waiting', (event) => {
      showSkipWaitingPrompt(event);
    });

    wb.register();
  }
</script>

如果它们接受,messageSkipWaiting() 会告知等待的 Service Worker 调用 self.skipWaiting(),这意味着它将开始激活。激活后,新 Service Worker 将控制所有现有客户端,从而触发 workbox-window 中的 controlling 事件。发生这种情况时,当前页面会使用所有预缓存资源的最新版本和在更新的 Service Worker 中找到的所有新路由逻辑重新加载。

要放入 Service Worker 的代码

获得页面中上一部分中的代码后,您需要向 Service Worker 添加一些代码,让其知道何时应跳过等待阶段。如果您使用的是 workbox-build 中的 generateSW,并且将其 skipWaiting 选项设置为 false(默认值),就可以开始使用,因为以下代码将自动包含在您生成的 Service Worker 文件中。

如果您要编写自己的 Service Worker(可能是在 injectManifest 模式下与 Workbox 的某个构建工具结合使用),则需要自行添加以下代码:

addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

这将监听从 workbox-window 发送到 type 值为 SKIP_WAITING 的 Service Worker 的消息,如果发生这种情况,则调用 self.skipWaiting()。前面的代码示例中所示的 workbox-window 中的 messageSkipWaiting() 方法负责发送此消息。

您需要显示提示吗?

这并不是每个部署 Service Worker 的应用都需要遵循的模式。它适用于部分场景,在这些场景中,未能提供在 Service Worker 更新时重新加载页面的机会,可能会导致意外行为。对于是否应该显示重新加载提示,没有硬性规定,但在以下几种情况下,确实有效果:

  • 您大量使用预缓存。对于静态资源来说,如果对导航请求使用网络优先或仅限网络策略,但延迟加载静态资源,则稍后可能会出现问题。这可能会导致以下情形:带版本的资源可能发生更改,并且 Service Worker 尚未预缓存这些资源。在此处提供重新加载按钮可以避免一些意外行为。
  • 如果您提供预缓存的 HTML。在这种情况下,强烈应考虑在 Service Worker 更新时提供重新加载按钮,因为在更新的 Service Worker 获得控制权之前,将无法识别对该 HTML 的更新。
  • 如果您不主要依赖于运行时缓存。在运行时缓存资源时,您无需告知用户它们应该重新加载。随着带版本号的资源发生变化,它们将适时添加到运行时缓存中(假设导航请求使用网络优先或仅限网络)。
  • 使用 stale-while-revalidate 策略时,您可以考虑使用 workbox-broadcast-update 模块向用户通知 Service Worker 的更新。

是否需要通知用户 Service Worker 的更新取决于您的应用及其独特要求。如果您发现用户在推送新的 Service Worker 时遇到异常行为,这可能是您应该通知他们的最佳信号。