【问题标题】:PWA that polls the server never updates to a newer version轮询服务器的 PWA 永远不会更新到较新的版本
【发布时间】:2020-12-17 12:24:52
【问题描述】:

我们的应用程序有一个活动版本,它会立即开始轮询服务器以获取一些基本信息 - 当请求失败时,它会在一秒钟后重试,直到请求成功。

在较新的版本中,我们已删除此轮询:即旧端点不再存在。
现在的问题是,PWA 永远不会更新到新版本:即,当之前已经打开旧版本的用户打开应用程序时,他们会看到不断尝试轮询服务器的旧版本(以及这个请求当然失败了)。但是 Angular Service Worker 永远不会更新到新版本(即刷新不起作用、关闭浏览器选项卡或整个浏览器也不起作用)。

注意:问题似乎与投票有关

  • 当我们有一个成功轮询服务器的应用版本(即轮询停止)时,该应用将按预期更新
  • 当我们有一个不轮询服务器的应用版本时,该应用将按预期更新
  • 但在我们的例子中,我们有一个应用程序轮询服务器并且轮询不断失败(并且会每秒重试一次):在这种情况下,应用程序永远不会更新!

唯一的解决方法是按 CTRL+F5 硬重新加载应用程序。
但这当然不是解决方案,因为普通用户不知道他们可以/应该这样做......

知道如何解决这个问题吗?

  • 我们需要一个可以在服务器上完成的解决方案:即不可能告诉每个用户按 CTRL+F5 或通过开发工具等卸载应用程序。
  • 目前一个令人讨厌的临时解决方法是我们重新实现了端点(旧版本轮询)并返回一些有效的虚拟数据

复制和细节

这是一个重现问题的测试应用程序:https://github.com/tmtron/pwa-test
自述文件包含完整的详细信息。

初始状态:

我们之前启动过应用,浏览器有不断轮询服务器并失败的 PWA 版本。现在我们关闭浏览器标签(显示我们的应用)

  • 下一步:构建并提供新的应用版本
  • 当我们打开浏览器时,它仍会提供以前的应用程序版本 2(如预期的那样)
  • 由于这是一个导航请求,浏览器将检查更新
  • 当我们查看 http-server 的终端时 - 几秒钟后它会显示浏览器请求新版本的 ngsw-worker.js
      [2020-12-17T11:03:23.948Z]  "GET /ngsw-worker.js" ...
    
  • 但它没有得到新的应用程序版本
  • 当我们现在刷新选项卡(按 F5)时,旧应用版本仍将处于活动状态!
  • 关闭浏览器窗口并再次打开(启用计时器)将无济于事
    • 我们仍然看到旧版本 - 刷新不起作用

NGSW 状态

http://127.0.0.1:8080/ngsw/state

NGSW Debug Info:

Driver state: NORMAL ((nominal))
Latest manifest hash: b5a9f50d4efae604dbc6706040c71ecb2029c1cf
Last update check: never

=== Version b5a9f50d4efae604dbc6706040c71ecb2029c1cf ===

Clients: 4cad0ef1-179e-4629-b20f-602c4a4be1f9, 4d82d618-8fac-4e79-938c-605266702fa1, e0813c6a-3b76-4aeb-a14a-9043f0417b24, 9bf1be36-e911-4612-8158-21819eb222dc, 09337bd0-d060-46a1-b9e9-56257b879bdd, 0ce1327b-05bb-44e1-bba7-f3769cbad9b2, c9b796dd-b20c-417b-a504-7f065fd841db

=== Idle Task Queue ===
Last update tick: 1s996u
Last update run: never
Task queue:
 * init post-load (update, cleanup)
 * initialization(b5a9f50d4efae604dbc6706040c71ecb2029c1cf)
 * check-updates-on-navigation

Debug log:
  • 我不知道为什么有这么多客户端 - 该应用程序仅在一个选项卡中打开,并且 ngsw/state 选项卡已打开
  • 似乎任务队列永远不会改变

启动计时器的应用组件:

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'pwa-update-test-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
  private timerId?: number;
  status = 'uninit';

  version = 1;

  timerActive = true;

  constructor(private readonly httpClient: HttpClient) {}

  ngOnInit() {
    this.status = 'waiting';
    const timerUrlParamIsPresent = window.location.href.indexOf('timer') >= 0;
    this.timerActive = timerUrlParamIsPresent
    if (this.timerActive) {
      this.timerId = window.setTimeout(() => this.getAppInfo());
    } else {
      this.status = 'timer deactivated via URL parameter';
    }
  }

  getAppInfo() {
    this.status = 'working';
    this.httpClient
      .get(window.location.origin+'/non-existing-end-point')
      .toPromise()
      .catch((err) => {
        this.status =
          new Date().toLocaleTimeString() + ' failed! (will retry soon)';
        console.log('getAppInfo failed', err);
        if (this.timerActive) {
          this.timerId = window.setTimeout(() => this.getAppInfo(), 2000);
        }
      });
  }

  stopTimer() {
    this.timerActive = false;
    clearTimeout(this.timerId);
  }
}

ngsw 配置

{
  "$schema": "../../node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/manifest.webmanifest",
          "/*.css",
          "/*.js"
        ]
      }
    },
    {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
        ]
      }
    }
  ]
}

【问题讨论】:

标签: angular progressive-web-apps service-worker


【解决方案1】:

发布与on GitHub 相同的答案以提高知名度。


首先,感谢非常详细的复制,@tmtron ❤️ ? ?
它确实有助于确定问题。

所以,这就是发生的事情:
为了避免干扰应用程序请求,SW 不会立即检查初始化和导航请求的更新。 Insted 它将安排一个任务在“空闲”时检查更新。目前,如果 SW 5 秒内没有收到任何请求,则认为该 SW 处于空闲状态。

因此,由于您的应用不断轮询(即发送请求),因此 SW 永远不会认为自己处于空闲状态,因此永远不会执行检查更新任务。

注意:这与请求失败或成功无关。碰巧当请求失败时,你的应用会在 5s 空闲阈值之前发送一个新的。

尽管应用程序在失败时不断发出相同的请求可能不是一个好主意(例如,应该有一些退避机制),但 SW 应该能够更优雅地处理它:grin :

例如,IdleScheduler 可能有一个最大延迟(比如 30 秒),在此之后它会执行其挂起的任务,即使 SW 不被视为“空闲”。

就解决方法而言,可以在客户端上做几件事来避免这个问题:

  1. 致电SwUpdate#checkForUpdate() 触发立即检查更新。这不经过IdleScheduler,所以不受问题的影响。

  2. 在有问题的请求上使用ngsw-bypass 以绕过 SW(这意味着重复请求不会阻止 SW 被视为空闲)。

  3. 避免在请求失败时以恒定速率轮询并实现某种exponential backoff

当然,如果你不能更新你的客户端代码,这些都不会帮助你(因为 SW 一直服务于旧版本)?

恐怕,服务器唯一能做的就是为轮询请求返回一个 2xx 响应(以便应用停止轮询并且 SW 可以自行更新。

或者(或另外)您可以将服务器配置为包含 Clear-Site-Data 标头(带有 "storage""*" 指令)用于已删除的端点,以便发出该请求的旧客户端将拥有他们的 SW被浏览器取消注册。

【讨论】:

    猜你喜欢
    • 2012-02-16
    • 1970-01-01
    • 2015-02-28
    • 2018-11-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多