angular - 轮询服务器的 PWA 永远不会更新到较新的版本
问题描述
我们的应用程序有一个活动版本,它会立即开始轮询服务器以获取一些基本信息 - 当请求失败时,它会在一秒钟后重试,直到请求成功。
在较新的版本中,我们删除了此轮询:即旧端点不再存在。
现在的问题是,PWA 永远不会更新到新版本:即,当之前已经打开旧版本的用户打开应用程序时,他们将看到不断尝试轮询服务器的旧版本(以及这个请求当然失败了)。但是角度服务工作者永远不会更新到新版本(即刷新不起作用,关闭浏览器选项卡,或者整个浏览器也不起作用)。
注意:该问题似乎与轮询有关
- 当我们有一个成功轮询服务器的应用程序版本(即轮询停止)时,应用程序将按预期更新
- 当我们有一个不轮询服务器的应用程序版本时,应用程序将按预期更新
- 但是在我们的例子中,我们有一个轮询服务器的应用程序并且轮询不断失败(并且每秒都会重试):在这种情况下,应用程序永远不会更新!
唯一的解决方法是按 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)时,OLD 应用程序版本仍将处于活动状态!
- 关闭浏览器窗口并再次打开(启用计时器)将无济于事
- 我们仍然看到旧版本 - 刷新不起作用
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 选项卡已打开
- 每当您在http://127.0.0.1:8080/ngsw/state URL上按刷新时,都会出现一个新客户端并且不会消失
- 似乎任务队列永远不会改变
启动计时器的应用程序组件:
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)"
]
}
}
]
}
解决方案
发布与我在 GitHub 上发布的相同答案以提高知名度。
首先,感谢非常详细的复制,@tmtron ❤️
它确实有助于识别问题。
因此,会发生以下情况:
为了避免干扰应用程序请求,SW 不会立即检查初始化和导航请求的更新。Insted 它将安排一个任务在“空闲”时检查更新。目前,当 SW 5 秒内没有收到任何请求时,则认为 SW 处于空闲状态。
因此,由于您的应用程序不断轮询(即发送请求),SW 永远不会认为自己空闲,因此永远不会执行检查更新任务。
注意:这与请求失败或成功无关。碰巧当请求失败时,您的应用会在 5s 空闲阈值之前发送一个新的。
尽管应用程序在失败时不断发出相同的请求可能不是一个好主意(例如,应该有一些退避机制),但 SW 应该能够更优雅地处理它:grin:
例如,即使 SW 不被视为“空闲” , IdleScheduler也可能有一个最大延迟(比如 30 秒),在此之后它会执行其挂起的任务。
就解决方法而言,可以在客户端上做几件事来避免这个问题:
调用SwUpdate#checkForUpdate()以触发立即检查更新。这不通过
IdleScheduler
,所以它不受问题的影响。对有问题的请求使用ngsw-bypass绕过 SW(这意味着重复请求不会阻止 SW 被视为空闲)。
避免在请求失败时以恒定速率轮询并实施某种指数退避。
当然,如果您无法更新客户端代码,这些都不会帮助您(因为 SW 一直服务于旧版本)
恐怕,您唯一能从服务器做的就是为轮询请求返回一个 2xx 响应(这样应用程序就会停止轮询并且软件可以自行更新。
或者(或另外)您可以将服务器配置为包含已删除端点的Clear-Site-Data标头(带有"storage"
or"*"
指令),以便发出该请求的旧客户端将通过浏览器取消注册其 SW。
推荐阅读
- python-3.x - Python |S1 向量到字符串
- python - 使用 2D numpy 数组的每一行创建 1D numpy 数组的元组列表
- java - 如何使用 log4j2.properties 文件将不同级别的日志发送到不同的附加程序?
- python - 循环列表列表,但 if 语句似乎无法正常工作
- javascript - 推入数组并循环,直到数组长度达到3
- r - 根据其他列的条件选择列中的最大值
- angular - 在参数中访问自定义 RxJS 运算符中的流变量
- c# - 使用 Web SDK 构建交互式控制台应用程序
- python - 在 CPython 中,哪个结构用于实现用户定义的类及其实例
- c# - 如何在 MySql 和 C# 中存储存储过程选择查询的返回值