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

68

当服务工作者更新时,它不会立即控制页面;而是进入“等待”状态,等待被激活。

令人惊讶的是,在刷新页面后,更新后的服务工作者甚至不会控制标签。Google 解释道:

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

即使您只在演示中打开一个选项卡,刷新该页面也不足以让新版本接管。这是由于浏览器导航的工作方式。当您进行导航时,当前页面只有在接收到响应标头之后才会消失,即使响应具有 Content-Disposition 标头,当前页面也可能会保留。由于这种重叠,当前服务工作者始终在刷新期间控制客户端。

要获取更新,请关闭或导航离开所有使用当前服务工作者的选项卡。然后,当您再次导航到演示时,您应该会看到新内容 [updated content]。

这种模式类似于 Chrome 的更新方式。Chrome 的更新是在后台下载的,但在重新启动 Chrome 之前不会应用。在此期间,您可以继续使用当前版本而不会受到干扰。但是,在开发过程中,这很麻烦,但是 DevTools 有方法使它变得更容易,我将在本文后面介绍。

对于多个选项卡的情况,这种行为是有道理的。我的应用程序是一个需要原子方式更新的捆绑包;我们不能混合和匹配旧的捆绑包和新的捆绑包。 (在本地应用程序中,原子性是自动且保证的。)

但是,在单个选项卡的情况下(我称之为应用程序的“最后打开的选项卡”),此行为并不是我想要的。我希望刷新最后打开的选项卡以更新我的服务工作者。

很难想象有人希望在最后一个打开的标签页刷新时继续运行旧的 service worker。Google 提出的“导航重叠”论点听起来像是不幸 bug 的借口。

我的应用程序通常只在单个标签页中使用。在生产环境中,我希望用户只需通过刷新页面就能使用最新的代码,但这并行不通:旧的 service worker 将在刷新时继续控制页面。

我不想告诉用户,“要接收更新,请务必关闭或导航离开我的应用程序。” 我想告诉他们,“只需刷新”。

如何在用户刷新页面时激活已更新的 service worker?

编辑:我知道有一种快速、简单且错误的解决方法:skipWaiting 在 service worker 的安装事件中使用。 skipWaiting 会使新的 service worker 在更新下载完成后立即生效,而旧的页面标签仍然打开。这会使更新不安全且不可重复;就像在应用程序正在运行时替换本地应用程序包一样。这对我来说是不可接受的。我需要等待用户刷新最后一个打开的标签页。


2
我知道有一种快速、简单但不安全的解决方案:在服务工作者的“install”事件中使用“skipWaiting”。我不想在用户正在工作的时候,随意替换我的服务工作者,因此需要等待用户刷新页面。 - Dan Fabulich
5个回答

73

概念上有两种更新方式,从你的问题中无法确定我们正在讨论哪一种,因此我将涵盖两种。

更新内容

例如:

  • 博客文章的文本
  • 活动的日程安排
  • 社交媒体时间线

这些内容可能存储在缓存API或IndexedDB中。这些存储与服务工作者独立存在——更新服务工作者不应删除它们,更新它们也不需要更新服务工作者。

更新服务工作者相当于出货新的二进制文件,你不需要这样做来更新文章等内容。

你可以自行决定何时更新这些缓存,没有你的更新它们不会自动更新。我在离线手册中介绍了许多模式,但这是我最喜欢的一个

  1. 使用服务工作者从缓存中提供页面外壳、CSS和JS。
  2. 使用缓存填充页面内容。
  3. 尝试从网络获取内容。
  4. 如果内容比缓存中的更新,则更新缓存和页面。

在“更新页面”方面,你需要以不干扰用户的方式进行。对于时间线这种按时间排序的列表来说,这很容易,只需将新内容添加到底部/顶部即可。如果是更新文章,则有些棘手,因为你不想在用户眼前切换文本。在这种情况下,通常最容易的方法是显示一些类似于“可用更新 - 显示更新”的通知,在单击后刷新内容。

Trained to thrill(可能是第一个使用服务工作者的示例)演示了如何更新时间线数据。

更新“应用”

这种情况下,您需要更新服务工作者。当您需要:

  • 更新页面外壳
  • 更新JS和/或CSS

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

  1. 检测到更新的服务工作者处于“等待”状态
  2. 向用户显示通知:“可用更新 - 立即更新
  3. 当用户点击该通知时,将postmessage发送到服务工作者,请求其调用skipWaiting
  4. 检测新服务工作者变为“活动状态”
  5. window.location.reload()

实现此模式是我服务工作者Udacity课程的一部分,这是代码之前/之后的差异

您也可以进行更进一步的操作。例如,您可以采用某种类似于semver的系统,以便了解用户当前拥有的服务工作者版本。如果更新是次要的,则您可能会决定调用skipWaiting()是完全安全的。


3
我确实正在开发一个“应用程序”。在刷新时,是否真的没有办法跳过skipWaiting?实现自己的应用内刷新按钮似乎是一种折衷方法。 - Dan Fabulich
以上两个选项不取决于您正在构建的事物类型,而是您正在更新的事物类型。您可能会在同一应用程序或网站中使用这两种模式。您要更新哪种类型的内容? - JaffaTheCake
我正在开发一款游戏。我想更新游戏的代码,包括JS。 - Dan Fabulich
我正在尝试按照上述和Udacity课程中描述的方法实现应用程序更新的方法,但似乎从未收到“controllerchange”事件。然而,使用控制台日志,我能够看到skipWaiting消息发布,并且我可以在Chrome的开发工具中查看服务工作者,并看到旧的服务工作者消失,新的服务工作者接管。有什么想法吗?编辑:GH提交 - David Millar
如果我正在使用PWA Starter Kit,如果我尝试遵循这些Service Worker的精彩配方,是否会遇到太多混乱?实际上,我甚至不知道从哪里开始:如果我正在使用PWA Starter Kit,我的(心爱的)Service Worker在哪里?非常感谢您的帮助。 - Vladimir Brasil
显示剩余3条评论

51
我写了一篇博客文章,详细说明如何处理这个问题。链接为: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时刷新所有打开的选项卡。更好一些,但用户可能会感到惊讶,因为选项卡随机刷新。 3. 在安装时,提示用户使用应用内“刷新”按钮来使用skipWaiting(),并在controllerchange时刷新所有打开的选项卡。甚至更好,但用户必须使用应用内“刷新”按钮;浏览器的刷新按钮仍然无法工作。这在谷歌Udacity课程上的Service Workers章节Workbox高级指南以及我在Github上的示例中的主要分支中详细记录。 4. 如果可能,刷新最后一个选项卡。在navigate模式下进行获取时,计算打开的选项卡数(“客户端”)。如果只有一个打开的选项卡,则立即使用skipWaiting()并返回一个带有Refresh:0头的空响应来刷新唯一的选项卡。您可以在我的Github示例的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
我不理解你的第二个和第三个解决方案。你是说要刷新所有打开的选项卡。即使有人检查了你的样本,你的评论也是错的。如果我有5个选项卡,并且我刷新了其中一个,发现有新的服务工作者,那么该选项卡将显示按钮告诉我是否要刷新。其他选项卡上不会显示该按钮,如果单击“是”,只会刷新此选项卡,而不是其他选项卡。为什么你说所有打开的选项卡都要刷新呢? - Nika Kurashvili
第三种解决方案在Google的Udacity服务工作者课程中得到了全面的演示。试一试吧。你会发现:如果客户端配置为在控制器更改时重新加载,并且您在任何选项卡中单击应用内刷新按钮,则所有选项卡上都会触发控制器更改事件,因此它们立即刷新。 - Dan Fabulich
使用第三种方法,我发现即使是第一次访问我的网站且没有任何服务工作者的用户也能看到横幅。 - Puneet Sabharwal
丹,对于你来说,reg是什么?是已注册的服务工作者吗? - Willie
@DanFabulich 这个解决方案真的很有帮助,特别是你的博客链接非常出色,讲解得非常清楚。谢谢 :) - Rakesh Kumar
@DanFabulich 感谢你的精彩文章,真的很有帮助。最近有没有关于服务工作者方面的更新,使得这个过程更加简单? - Trystan Lapinig-Wilcock

7

除了Dan的回答之外;

1. 在service worker脚本中添加skipWaiting事件监听器。

在我的情况下,我使用基于CRA的应用程序cra-append-sw来添加此代码段到service worker中。

注意: 如果你正在使用最近版本的react-scripts,下面的代码段会自动添加,您不需要使用cra-append-sw

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

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

2. 更新register-service-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的片段,在服务工作程序控制器更改时重新加载所有选项卡。这也将重新加载活动选项卡。
  .catch(error => {
    console.error('Error during service worker registration:', error);
  });

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

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

2

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

在DevTools中打开您的预缓存,检查是否有任何懒加载文件。如果没有,即所有预缓存都会在页面加载时立即获取,则可以安全地调用skipWaiting,它将删除旧的预缓存,替换为新的资产(包括新的index.html),等待在下次刷新时提供服务。由于所有旧资产已经在内存中,运行着旧的index.html,所以没有什么可担心的。

如果您在预缓存中看到一个文件,但不确定它是否在页面加载时加载,则可以重新加载并自己观察DevTools中的网络流量。

如果您确实具有延迟加载的资产,则可能会导致问题,但这不是服务工作者创建的新问题。想象一下,您的用户打开了一个选项卡,然后您部署了一个新版本,然后用户单击某个东西触发获取,但该资产不再在您的服务器上。这是一个旧问题。预缓存无意中解决了这个问题,但您不应该依赖它,而应该有自己的异常处理逻辑,除非您的应用程序从一开始就设计为利用预缓存。


当懒加载资源具有唯一的名称(例如,像Webpack一样包含哈希值),并且永远不会被覆盖和永远不会被删除时,它们不会成为问题。 - undefined

0

我的工作内容:

在ServiceWorker中:

self.addEventListener("activate", event => {
    event.waitUntil(
        caches.delete(cacheName)
    );
});

在index.html中:
navigator.serviceWorker.addEventListener('controllerchange', function() {
    window.location.reload(true);
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接