【问题标题】:Activate updated service worker on refresh刷新时激活更新的服务工作者
【发布时间】:2016-10-18 06:21:29
【问题描述】:

当 Service Worker 更新时,它不会以正确的方式控制页面;它进入“等待”状态,等待被激活。

令人惊讶的是,更新后的 Service Worker 甚至在刷新页面后都无法控制选项卡。谷歌解释说:

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

即使你只打开了一个标签页,刷新页面 还不足以让新版本接管。这是由于如何 浏览器导航工作。导航时,当前页面不会 在收到响应头之前离开,即使这样 如果响应有 Content-Disposition,则当前页面可能会保留 标题。由于这种重叠,当前的服务工作者总是 在刷新期间控制客户端。

要获取更新,请使用 当前的服务人员。然后,当您再次导航到演示时,您 应该看到马[更新内容]。

这种模式类似于 Chrome 的更新方式。对 Chrome 的更新 在后台下载,但在 Chrome 重新启动之前不要应用。在 同时,您可以继续使用当前版本,无需 中断。但是,这在开发过程中很痛苦,但是 DevTools 有办法让它变得更容易,我将在本文后面介绍。

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

但对于单个选项卡,我将其称为应用程序的“最后打开的选项卡”,这种行为不是我想要的。我想刷新最后打开的选项卡以更新我的服务人员。

(很难想象有人真的希望在刷新最后一个打开的标签时继续运行旧的服务人员。Google 的“导航重叠”论点在我看来是一个很好的借口错误。)

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

我不想告诉用户“要接收更新,请务必关闭或离开我的应用”。我想告诉他们,“刷新一下。”

当用户刷新页面时,如何激活我更新的 Service Worker?

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

【问题讨论】:

  • 我知道有一个答案是快速、简单且不安全的:skipWaiting 在服务人员的install 事件中。我不想在用户工作的过程中发现更新后就随机更换我的服务人员。我需要等到用户刷新页面。

标签: service-worker


【解决方案1】:

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

更新内容

例如:

  • 博文的正文
  • 活动时间表
  • 社交媒体时间线

这些可能会存储在缓存 API 或 IndexedDB 中。这些存储独立于服务工作者 - 更新服务工作者不应该删除它们,更新它们不应该需要更新服务工作者

更新 Service Worker 相当于发布一个新的二进制文件,您不需要这样做来(例如)更新一篇文章。

何时更新这些缓存完全取决于您,如果不更新它们就不会更新。我在offline cookbook 中介绍了许多模式,但this is my favourite one

  1. 使用 service worker 从缓存中提供页面 shell、css 和 js。
  2. 使用缓存中的内容填充页面。
  3. 尝试从网络中获取内容。
  4. 如果内容比缓存中的内容更新,请更新缓存和页面。

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

Trained to thrill(可能是第一个 Service Worker 示例)演示了如何更新数据的时间线。

更新“应用”

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

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

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

  1. 检测更新的 Service Worker “等待”
  2. 向用户显示通知“更新可用 - 立即更新
  3. 当用户单击此通知时,向 service worker 发送消息,要求它调用skipWaiting
  4. 检测新的 Service Worker 变为“活跃”
  5. window.location.reload()

实现此模式是我的service worker Udacity coursehere's the diff of the code before/after 的一部分。

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

【讨论】:

  • 我确实在开发一个“应用程序”。刷新时真的没有办法skipWaiting吗?在应用程序中实现我自己的刷新按钮似乎是一个 hack。
  • 上面的两个选项不依赖于你正在构建的东西的类型,而是你正在更新的东西。您可能会在同一个应用程序或网站中使用这两种模式。您希望更新什么样的内容?
  • 我正在开发一款游戏。我要更新游戏的代码,包括JS。
  • 我正在尝试实现上述和 Udacity 课程中更新应用程序的方法,但我似乎从未收到“controllerchange”事件。但是,使用控制台日志,我能够看到 skipWaiting 消息发布,并且我可以在 Chrome 的开发工具中查看服务人员,并看到旧的在新的接管时消失了。知道为什么会这样吗?编辑:GH commit.
  • 更换js或者css为什么需要更新ServiceWorker?我知道谷歌工作箱指南就是这样做的,但是为什么服务人员不能只获取更新的文件并在缓存中替换它们。那么 sw 仅在 sw 真正更新时才需要更新。
【解决方案2】:

我写了一篇博文来解释如何处理这个问题。 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. 刷新controllerchange上所有打开的标签;在安装时,使用应用内“刷新”按钮提示用户skipWaiting() 更好,但用户必须使用应用内“刷新”按钮;浏览器的刷新按钮仍然不起作用。这在Google's Udacity course on Service WorkersWorkbox Advanced Guide 以及我的sample on Githubmaster 分支中有详细记录。
  4. 如果可能,刷新最后一个标签。navigate 模式下获取期间,计算打开的标签(“客户端”)。如果只有一个打开的选项卡,则skipWaiting() 立即通过返回带有Refresh: 0 标头的空白响应来刷新唯一的选项卡。您可以在我的sample on Githubrefresh-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);

【讨论】:

  • 我只是不明白你的第二个和第三个解决方案。你是说刷新所有打开的标签。即使有人检查您的样本,您的 cmets 也是错误的。如果我有 5 个选项卡,并且我刷新了其中一个发现有新的服务人员,则该选项卡将显示按钮告诉我是否要刷新。该按钮不会显示在其他选项卡上,如果单击“是”,则只会刷新此选项卡。不是其他的。你为什么说所有打开的标签都会刷新?
  • 第三种方案在Google's Udacity course on Service Workers中给出了详尽的演示。试试看。您会发现:如果客户端配置为在 controllerchange 上重新加载,并且您单击任何选项卡中的应用内刷新按钮,则 controllerchange 事件会在所有选项卡上触发,因此它们都会立即刷新。
  • @DanFabulich 使用方法 3,即使是第一次访问我的网站没有任何服务人员的用户,我也会看到横幅。
  • Dan,注册 Service Worker 的 reg 是什么?
【解决方案3】:

除了丹的回答;

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

在我的例子中,基于 CRA 的应用程序我使用 cra-append-sw 将此 sn-p 添加到 service worker。

注意:如果您使用的是最新版本的react-scripts,则下面的sn-p 是automatically added,您不需要使用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 的 sn-p 在 service worker 控制器更改时重新加载所有选项卡。这也将重新加载活动选项卡。

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-14
    • 2021-08-27
    • 2018-02-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多