刷新时激活更新的服务工作者

Activate updated service worker on refresh

当 service worker 更新时,它并没有以正确的方式控制页面;它进入 "waiting" 状态,等待被激活。

令人惊讶的是,更新后的 service worker 在刷新页面后甚至没有控制选项卡。 Google 说明:

https://developers.google.com/web/fundamentals/instant-and-offline/service-worker/lifecycle

Even if you only have one tab open to the demo, refreshing the page isn't enough to let the new version take over. This is due to how browser navigations work. When you navigate, the current page doesn't go away until the response headers have been received, and even then the current page may stay if the response has a Content-Disposition header. Because of this overlap, the current service worker is always controlling a client during a refresh.

To get the update, close or navigate away from all tabs using the current service worker. Then, when you navigate to the demo again, you should see the horse [updated content].

This pattern is similar to how Chrome updates. Updates to Chrome download in the background, but don't apply until Chrome restarts. In the mean time, you can continue to use the current version without disruption. However, this is a pain during development, but DevTools has ways to make it easier, which I'll cover later in this article.

这种行为在多个选项卡的情况下有意义。我的应用程序是一个需要自动更新的包;我们不能混合和匹配旧捆绑包的一部分和新捆绑包的一部分。 (在本机应用程序中,原子性是自动的并且有保证。)

但在单个选项卡的情况下,我将其称为应用程序的 "last open tab",这种行为不是我想要的。我想刷新上次打开的选项卡以更新我的服务人员。

(很难想象有人真的 想要 旧的 service worker 在最后一个打开的选项卡被刷新时继续 运行。Google 的"navigation overlap" 在我看来,这个论点是一个不幸错误的好借口。)

我的应用程序通常只在单个选项卡中使用。在生产环境中,我希望我的用户只需刷新页面即可使用最新的代码,但这行不通:旧的 service worker 将在刷新过程中保持控制。

我不想告诉用户,"to receive updates, be sure to close or navigate away from my app."我想告诉他们,"just refresh."

如何在用户刷新页面时激活更新后的 Service Worker?

编辑:我知道有一个答案快速、简单但错误:skipWaiting 在 service worker 的安装事件中。 skipWaiting 将使新的 service worker 在更新下载后立即生效,同时打开旧页面选项卡。这使得更新不安全地成为非原子的;这就像在应用程序 运行 时替换本机应用程序包。那对我来说不行。我需要等到用户刷新上次打开的标签页。

从概念上讲有两种更新,从您的问题中我们不清楚我们正在谈论的是哪一种,所以我将同时介绍这两种更新。

更新内容

例如:

  • 博客正文post
  • 活动日程
  • 社交媒体时间轴

这些可能会存储在缓存 API 或 IndexedDB 中。这些商店独立于 service worker 存在——更新 service worker 不应该删除它们,更新它们不应该需要更新 service worker

更新 service worker 等价于发送新的二进制文件,您不需要这样做来(例如)更新文章。

何时更新这些缓存完全取决于您,如果您不更新它们,它们就不会更新。我在 offline cookbook, but this is my favourite one:

中介绍了一些模式
  1. 使用服务工作者从缓存中服务页面 shell、css 和 js。
  2. 用缓存中的内容填充页面。
  3. 尝试从网络获取内容。
  4. 如果内容比缓存中的内容更新,请更新缓存和页面。

就“更新页面”而言,您需要以不会干扰用户的方式进行更新。对于按时间顺序排列的列表,这非常简单,因为您只需将新内容添加到 bottom/top。如果它正在更新一篇文章,那就有点棘手了,因为您不想从用户眼前切换文本。在这种情况下,通常更容易显示某种通知,例如“可用更新 - 显示更新”,点击后会刷新内容。

Trained to thrill(也许是有史以来第一个 service worker 示例)演示了如何更新数据的时间线。

正在更新“应用程序”

在这种情况下,您确实想要更新服务工作者。此模式用于以下情况:

  • 正在更新您的页面shell
  • 正在更新 JS and/or CSS

如果您希望用户以非原子但相当安全的方式获取此更新,也有相应的模式。

  1. 检测到更新的服务工作者“正在等待”
  2. 向用户显示通知“更新可用 - 立即更新
  3. 当用户单击此通知时,post向服务人员发送消息,要求其调用 skipWaiting
  4. 检测到新的 service worker 变得“活跃”
  5. window.location.reload()

实施此模式是我 service worker Udacity course, and here's the diff of the code before/after 的一部分。

你也可以更进一步。例如,您可以使用某种 semver-esque 系统,这样您就可以知道用户当前拥有的 service worker 的版本。如果更新很小,您可以决定调用 skipWaiting() 是完全安全的。

我写了一篇博客 post 解释了如何处理这个问题。 https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68

我在 github 上也有一个工作示例。 https://github.com/dfabulich/service-worker-refresh-sample

有四种方法:

  1. skipWaiting() 安装时。 这很危险,因为您的选项卡可能会混合新旧内容。
  2. skipWaiting()安装上,然后在controllerchange 上刷新所有打开选项卡,但是当Tab随机刷新时,您的用户可能会感到惊讶。[= = = = 48=]
  3. 刷新 controllerchange 上所有打开的标签页;安装时,提示用户使用 in-app“刷新”按钮 skipWaiting() 更好,但用户必须使用 in-app“刷新”按钮;浏览器的刷新按钮仍然不起作用。 Google's Udacity course on Service Workers and the Workbox Advanced Guide, as well as the master branch of my sample on Github.
  4. 中对此进行了详细记录
  5. 如果可能,刷新最后一个选项卡。navigate 模式下获取期间,计算打开的选项卡(“客户端”)。如果只有一个打开的选项卡,则立即 skipWaiting() 并通过返回带有 Refresh: 0 header 的空白响应来刷新唯一的选项卡。您可以在我的 sample on Github.
  6. refresh-last-tab 分支中看到一个工作示例

把它们放在一起...

在 Service Worker 中:

addEventListener('message', messageEvent => {
    if (messageEvent.data === 'skipWaiting') return skipWaiting();
});

addEventListener('fetch', event => {
    event.respondWith((async () => {
        if (event.request.mode === "navigate" &&
        event.request.method === "GET" &&
        registration.waiting &&
        (await clients.matchAll()).length < 2
        ) {
            registration.waiting.postMessage('skipWaiting');
            return new Response("", {headers: {"Refresh": "0"}});
        }
        return await caches.match(event.request) ||
        fetch(event.request);
    })());
});

在页面中:

function listenForWaitingServiceWorker(reg, callback) {
    function awaitStateChange() {
        reg.installing.addEventListener('statechange', function () {
            if (this.state === 'installed') callback(reg);
        });
    }
    if (!reg) return;
    if (reg.waiting) return callback(reg);
    if (reg.installing) awaitStateChange();
    reg.addEventListener('updatefound', awaitStateChange);
}

// reload once when the new Service Worker starts activating
var refreshing;
navigator.serviceWorker.addEventListener('controllerchange',
    function () {
        if (refreshing) return;
        refreshing = true;
        window.location.reload();
    }
);
function promptUserToRefresh(reg) {
    // this is just an example
    // don't use window.confirm in real life; it's terrible
    if (window.confirm("New version available! OK to refresh?")) {
        reg.waiting.postMessage('skipWaiting');
    }
}
listenForWaitingServiceWorker(reg, promptUserToRefresh);

除了丹的回答;

1.在 Service Worker 脚本中添加 skipWaiting 事件监听器。

就我而言,基于 CRA 的应用程序我使用 cra-append-sw 将此代码段添加到服务工作者。

Note: if you are using a recent version of react-scripts, the snippet below is automatically added and you won't need to use cra-append-sw.

self.addEventListener('message', event => {
  if (!event.data) {
    return;
  }

  if (event.data === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

2。更新注册服务-worker.js

对我来说,当用户在站点内导航时重新加载页面感觉很自然。在 register-service-worker 脚本中,我添加了以下代码:

if (navigator.serviceWorker.controller) {
  // At this point, the old content will have been purged and
  // the fresh content will have been added to the cache.
  // It's the perfect time to display a "New content is
  // available; please refresh." message in your web app.
  console.log('New content is available; please refresh.');

  const pushState = window.history.pushState;

  window.history.pushState = function () {
    // make sure that the user lands on the "next" page
    pushState.apply(window.history, arguments);

    // makes the new service worker active
    installingWorker.postMessage('SKIP_WAITING');
  };
} else {

本质上,我们挂接到本机 pushState 函数,因此我们知道用户正在导航到新页面。不过,如果您还在 URL 中为产品页面使用过滤器,例如,您可能希望防止重新加载页面并等待更好的机会。

接下来,我还使用 Dan 的代码片段在 Service Worker 控制器更改时重新加载所有选项卡。这也将重新加载活动选项卡。

  .catch(error => {
    console.error('Error during service worker registration:', error);
  });

navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (refreshing) {
    return;
  }

  refreshing = true;
  window.location.reload();
});

尽管 OP 不同意 skipWaiting,但 skipWaiting 实际上是许多应用程序的正确解决方案,尤其是单页应用程序。

在 DevTools 中打开您的预缓存,检查是否有延迟加载的文件。如果没有,即你的预缓存中的所有内容都在页面加载时立即获取,那么调用 skipWaiting 是安全的,它将删除旧的预缓存,替换为新资产(包括新的 index.html),等待在下次刷新时提供。因为所有的旧资产都已经运行旧的index.html了,所以不用担心。

这些 non-lazy-loaded 文件通常只是 html 中的 <script><link> 标记,但也可以在页面加载时由脚本获取,因此最好方法是在页面加载时观察网络流量。

如果您确实有延迟加载的资产,那么它确实可能会导致问题,但这并不是 service worker 造成的新问题。想象一下,您的用户打开了一个选项卡,然后您部署了一个新版本,然后用户单击了触发提取的内容,但该资产不再位于您的服务器上。这是一个老问题。预缓存无意中解决了这个问题,但你不应该依赖它,相反你应该有自己的异常处理逻辑,除非你的应用程序被设计为从一开始就利用预缓存。