轮询服务器的 PWA 永远不会更新到较新的版本
PWA that polls the server never updates to a newer version
我们的应用程序有一个活动版本,它会立即开始轮询服务器以获取一些基本信息 - 当请求失败时,它会在一秒钟后重试,直到请求成功。
在较新的版本中,我们删除了此轮询:即旧端点不再存在。
现在的问题是,PWA 永远不会更新到新版本:即,当之前已经打开旧版本的用户打开应用程序时,他们将看到不断尝试轮询服务器的旧版本(以及此请求当然失败了)。但是 angular service worker 永远不会更新到新版本(即刷新不起作用,关闭浏览器选项卡,或者整个浏览器也不起作用)。
注意: 问题似乎与轮询有关
- 当我们有一个成功轮询服务器的应用程序版本(即轮询停止)时,该应用程序将按预期更新
- 当我们有一个不轮询服务器的应用程序版本时,应用程序将按预期更新
- 但在我们的例子中,我们有一个轮询服务器并且轮询不断失败(并且将每秒重试)的应用程序:在这种情况下,该应用程序永远不会更新!
唯一的解决方法是按 CTRL+F5 硬重新加载应用程序。
但这当然不是解决方案,因为普通用户不知道他们 can/should 这样做...
知道如何解决这个问题吗?
- 我们需要一个可以在服务器上完成的解决方案:即不可能告诉每个用户按 CTRL+F5 或通过开发工具等卸载应用程序。
- 目前一个临时的讨厌的解决方法是我们重新实现端点(旧版本轮询)和return一些有效的虚拟数据
复制和细节
这是一个重现问题的测试应用程序:https://github.com/tmtron/pwa-test
自述文件包含完整详细信息。
初始状态:
我们之前启动过应用程序,浏览器有不断轮询服务器并失败的 PWA 版本。现在我们关闭浏览器选项卡(显示我们的应用程序)
- 下一步:构建并提供新的应用程序版本
- 当我们打开浏览器时,它仍会提供以前的应用程序版本 2(如预期的那样)
- 由于这是一个导航请求,浏览器将检查更新
- 当我们查看 http 服务器的终端时 - 几秒钟后它会显示浏览器请求新版本的 ngsw-worker.js
[2020-12-17T11:03:23.948Z] "GET /ngsw-worker.js" ...
- 但是没有获取到新的应用程序版本
- 当我们现在刷新选项卡(按 F5)时,旧应用版本仍将处于活动状态!
- 关闭浏览器 window 并再次打开(启用定时器)将无济于事
- 我们仍然看到旧版本 - 刷新不起作用
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)"
]
}
}
]
}
发布与我发布的相同答案 on GitHub 以提高知名度。
首先,感谢非常详细的复制,@tmtron ❤️
它确实有助于确定问题。
所以,事情是这样的:
为了避免干扰应用程序请求,软件不会立即检查初始化和导航请求的更新。 Insted 它将安排一个任务来检查“空闲”时的更新。当前,当 5 秒内未收到任何请求时,SW 将被视为空闲。
因此,由于您的应用程序不断轮询(即发送请求),软件永远不会认为自己空闲,因此永远不会执行 check-for-updates 任务。
注意:这与请求失败或成功无关。恰好当请求失败时,你的应用会在 5s 空闲阈值之前发送一个新的。
尽管应用程序在失败时不断发出相同的请求可能不是一个好主意(例如,应该有一些退避机制),软件应该能够更优雅地处理它:咧嘴笑:
例如,IdleScheduler 可以有一个最大延迟(比如 30 秒),在此之后它会执行其未决任务,即使 SW 不被视为“空闲”。
就 work-arounds 而言,可以在客户端上做几件事来避免这个问题:
调用 SwUpdate#checkForUpdate() 触发立即检查更新。这个没有经过IdleScheduler
,所以不属于这个问题。
在有问题的请求上使用 ngsw-bypass 绕过 SW(这意味着重复请求不会阻止 SW 被视为空闲)。
避免在请求失败时以恒定速率进行轮询,并实施某种exponential backoff。
当然,none 如果您无法更新客户端代码(因为 SW 一直为旧版本提供服务),其中的 none 会对您有所帮助
恐怕,您唯一可以从服务器做的事情是 return 对轮询请求的 2xx 响应(以便应用停止轮询并且 SW 可以自行更新。
或者(或另外)您可以将服务器配置为包含已删除端点的 Clear-Site-Data header(使用 "storage"
或 "*"
指令),这样发出该请求的老客户将通过浏览器取消注册他们的 SW。
我们的应用程序有一个活动版本,它会立即开始轮询服务器以获取一些基本信息 - 当请求失败时,它会在一秒钟后重试,直到请求成功。
在较新的版本中,我们删除了此轮询:即旧端点不再存在。
现在的问题是,PWA 永远不会更新到新版本:即,当之前已经打开旧版本的用户打开应用程序时,他们将看到不断尝试轮询服务器的旧版本(以及此请求当然失败了)。但是 angular service worker 永远不会更新到新版本(即刷新不起作用,关闭浏览器选项卡,或者整个浏览器也不起作用)。
注意: 问题似乎与轮询有关
- 当我们有一个成功轮询服务器的应用程序版本(即轮询停止)时,该应用程序将按预期更新
- 当我们有一个不轮询服务器的应用程序版本时,应用程序将按预期更新
- 但在我们的例子中,我们有一个轮询服务器并且轮询不断失败(并且将每秒重试)的应用程序:在这种情况下,该应用程序永远不会更新!
唯一的解决方法是按 CTRL+F5 硬重新加载应用程序。
但这当然不是解决方案,因为普通用户不知道他们 can/should 这样做...
知道如何解决这个问题吗?
- 我们需要一个可以在服务器上完成的解决方案:即不可能告诉每个用户按 CTRL+F5 或通过开发工具等卸载应用程序。
- 目前一个临时的讨厌的解决方法是我们重新实现端点(旧版本轮询)和return一些有效的虚拟数据
复制和细节
这是一个重现问题的测试应用程序:https://github.com/tmtron/pwa-test
自述文件包含完整详细信息。
初始状态:
我们之前启动过应用程序,浏览器有不断轮询服务器并失败的 PWA 版本。现在我们关闭浏览器选项卡(显示我们的应用程序)
- 下一步:构建并提供新的应用程序版本
- 当我们打开浏览器时,它仍会提供以前的应用程序版本 2(如预期的那样)
- 由于这是一个导航请求,浏览器将检查更新
- 当我们查看 http 服务器的终端时 - 几秒钟后它会显示浏览器请求新版本的 ngsw-worker.js
[2020-12-17T11:03:23.948Z] "GET /ngsw-worker.js" ...
- 但是没有获取到新的应用程序版本
- 当我们现在刷新选项卡(按 F5)时,旧应用版本仍将处于活动状态!
- 关闭浏览器 window 并再次打开(启用定时器)将无济于事
- 我们仍然看到旧版本 - 刷新不起作用
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)"
]
}
}
]
}
发布与我发布的相同答案 on GitHub 以提高知名度。
首先,感谢非常详细的复制,@tmtron ❤️
它确实有助于确定问题。
所以,事情是这样的:
为了避免干扰应用程序请求,软件不会立即检查初始化和导航请求的更新。 Insted 它将安排一个任务来检查“空闲”时的更新。当前,当 5 秒内未收到任何请求时,SW 将被视为空闲。
因此,由于您的应用程序不断轮询(即发送请求),软件永远不会认为自己空闲,因此永远不会执行 check-for-updates 任务。
注意:这与请求失败或成功无关。恰好当请求失败时,你的应用会在 5s 空闲阈值之前发送一个新的。
尽管应用程序在失败时不断发出相同的请求可能不是一个好主意(例如,应该有一些退避机制),软件应该能够更优雅地处理它:咧嘴笑:
例如,IdleScheduler 可以有一个最大延迟(比如 30 秒),在此之后它会执行其未决任务,即使 SW 不被视为“空闲”。
就 work-arounds 而言,可以在客户端上做几件事来避免这个问题:
调用 SwUpdate#checkForUpdate() 触发立即检查更新。这个没有经过
IdleScheduler
,所以不属于这个问题。在有问题的请求上使用 ngsw-bypass 绕过 SW(这意味着重复请求不会阻止 SW 被视为空闲)。
避免在请求失败时以恒定速率进行轮询,并实施某种exponential backoff。
当然,none 如果您无法更新客户端代码(因为 SW 一直为旧版本提供服务),其中的 none 会对您有所帮助
恐怕,您唯一可以从服务器做的事情是 return 对轮询请求的 2xx 响应(以便应用停止轮询并且 SW 可以自行更新。
或者(或另外)您可以将服务器配置为包含已删除端点的 Clear-Site-Data header(使用 "storage"
或 "*"
指令),这样发出该请求的老客户将通过浏览器取消注册他们的 SW。