阅读(1772) (0)

Angular 生产环境下的Service Worker

2022-07-01 14:17:57 更新

生产环境下的 Service Worker

本页讲的是如何使用 Angular Service Worker 发布和支持生产环境下的应用。它解释了 Angular Service Worker 如何满足大规模生产环境的需求、Service Worker 在多种条件下有哪些行为以及有哪些可用的资源和故障保护机制。

Service Worker 与应用资源的缓存

从概念上说,你可以把 Angular Service Worker 想象成一个转发式缓存或装在最终用户浏览器中的 CDN 边缘。Service Worker 的工作是从本地缓存中满足 Angular 应用对资源或数据的请求,而不用等待网络。和所有缓存一样,它有一些规则来决定内容该如何过期或更新。

应用的版本

在 Angular Service Worker 的语境下,“版本”是指用来表示 Angular 应用的某一次构建成果的一组资源。当应用的一个新的构建发布时,Service Worker 就把它看做此应用的一个新版本。就算只修改了一个文件,也同样如此。在任何一个给定的时间,Service Worker 可能会在它的缓存中拥有此应用的多个版本,这几个版本也都能用于提供服务。

要保持应用的整体性,Angular Service Worker 会用所有的文件共同组成一个版本。组成版本的这些文件通常包括 HTML、JS 和 CSS 文件。把这些文件分成一组是至关重要的,因为它们会互相引用,并且依赖于一些特定内容。比如,​index.html​ 文件可能有个引用 ​bundle.js​ 的 ​<script>​ 标签,它可能会试图从这个脚本中调用一个 ​startApp()​ 函数。任何时候,只要这个版本的 ​index.html​ 被提供了,与它对应的 ​bundle.js​ 也必须同时提供。这种情况下,使用调用了 ​startApp()​ 的老的 ​index.html​ 并同时使用定义了 ​runApp()​ 的新 bundle 就是无效的。

当使用惰性加载模块时,文件的整体性就显得格外重要。某个 JS 包可能引用很多惰性块,而这些惰性块的文件名在应用的每次特定的构建中都是唯一的。如果运行应用的 ​X​ 版本视图加载一个惰性块,但该块的服务器已经升级到了 ​X + 1​ 版本,这次惰性加载操作就会失败。

本应用的版本标识符由其所有资源的内容决定,如果它们中的任何一个发生了变化,则版本标识符也随之改变。实际上,版本是由 ​ngsw.json​ 文件的内容决定的,包含了所有已知内容的哈希值。如果任何一个被缓存的文件发生了变化,则该文件的哈希也将在 ​ngsw.json​ 中随之变化,从而导致 Angular Service Worker 将这个活动文件的集合视为一个新版本。

ngsw.json​ 是在构建时基于 ​ngsw-config.json​ 生成的清单文件。

借助 Angular Service Worker 的这种版本控制行为,应用服务器就可以确保这个 Angular 应用中的这组文件始终保持一致。

更新检测

每当用户打开或刷新应用程序时,Angular Service Worker 都会通过查看清单(manifest)文件 “ngsw.json” 的更新来检查该应用程序的更新。如果它找到了更新,就会自动下载并缓存这个版本,并在下次加载应用程序时提供。

资源整体性

长周期缓存的潜在副作用之一就是可能无意中缓存了无效的资源。在普通的 HTTP 缓存中,硬刷新或缓存过期限制了缓存这种无效文件导致的负面影响。而 Service Worker 会忽略这样的约束,事实上会对整个应用程序进行长期缓存。因此,让 Service Worker 获得正确的内容就显得至关重要。

为了确保资源的整体性,Angular Service Worker 会验证所有带哈希的资源的哈希值。通常,对于 ​Angular CLI​ 应用程序,用户的 ​src/ngsw-config.json​ 配置文件中会涵盖 ​dist ​目录下的所有内容。

如果某个特定的文件未能通过验证,Angular Service Worker 就会尝试用 “cache-busting” URL 为参数重新获取内容,以消除浏览器或中间缓存的影响。如果该内容也未能通过验证,则 Service Worker 会认为该应用的整个版本都无效,并停止用它提供服务。如有必要,Service Worker 会进入安全模式,这些请求将退化为直接访问网络。如果服务无效、损坏或内容过期的风险很高,则会选择不使用缓存。
导致哈希值不匹配的原因有很多:

  • 在源服务器和最终用户之间缓存图层可能会提供陈旧的内容。
  • 非原子化的部署可能会导致 Angular Service Worker 看到部分更新后的内容。
  • 构建过程中的错误可能会导致更新了资源,却没有更新 ​ngsw.json​。反之,也可能发生没有更新资源,却更新了 ​ngsw.json​ 的情况。

不带哈希的内容

ngsw.json​ 清单中唯一带哈希值的资源就是构建清单时 ​dist ​目录中的资源。而其它资源,特别是从 CDN 加载的资源,其内容在构建时是未知的,或者会比应用程序部署得更频繁。

如果 Angular Service Worker 没有哈希可以验证给定的资源,它仍然会缓存它的内容,但会使用 “重新验证时失效” 的策略来承认 HTTP 缓存头。也就是说,当被缓存资源的 HTTP 缓存头指出该资源已过期时,Angular Service Worker 将继续提供内容,并尝试在后台刷新资源。这样,那些被破坏的非哈希资源留在缓存中的时间就不会超出为它配置的生命周期。

App 选项卡

如果应用程序的资源版本突然发生了变化或没有给出警告,就可能会有问题。

Angular Service Worker 会保证:正在运行的应用程序会继续运行和当前应用相同的版本。而如果在新的 Web 浏览器选项卡中打开了该应用的另一个实例,则会提供该应用的最新版本。因此,这个新标签可以和原始标签同时运行不同版本的应用。

重要:
这种担保比普通的 Web 部署模型提供的担保还要更强一点。 如果没有 Service Worker,则不能保证稍后在这个正在运行的应用中惰性加载的代码 和其初始代码的版本是一样的。

Angular Service Worker 为什么可能会更改运行中的应用的版本有几个有限的原因。 其中一些是因为出错了:

  • 由于哈希验证失败,当前版本变成了无效的
  • 某个无关的错误导致 Service Worker 进入了安全模式,或者说,它被暂时禁用了

Angular Service Worker 能知道在任何指定的时刻正在使用哪些版本,并清除那些没有被任何选项卡使用的版本。

另一些可能导致 Angular Service Worker 在运行期间改变版本的因素是一些正常事件:

  • 页面被重新加载/刷新。
  • 该页面通过 ​SwUpdate ​服务请求立即激活这个更新。

Service Worker 更新

Angular Service Worker 是一个运行在 Web 浏览器中的小脚本。有时,这个 Service Worker 也可能会需要更新,以修复错误和增强特性。

首次打开应用时或在一段非活动时间之后再访问应用程序时,就会下载 Angular Service Worker。如果 Service Worker 发生了变化,Service Worker 就会在后台进行更新。

Angular Service Worker 的大部分更新对应用程序来说都是透明的 - 旧缓存仍然有效,其内容仍然能正常使用。但是,在 Angular Service Worker 中可能偶尔会有错误修复或新功能,需要让旧的缓存失效。这时,应用程序就从会网络上透明地进行刷新。

绕过 Service Worker

某些情况下,你可能想要完全绕过 Service Worker,转而让浏览器处理请求。比如当你要用到某些 Service Worker 尚不支持的特性时(比如报告文件上传的进度)。

要想绕过 Service Worker,你可以设置一个名叫 ​ngsw-bypass​ 的请求头或查询参数。(这个请求头或查询参数的值会被忽略,可以把它设为空字符串或略去。)

调试 Angular Service Worker

偶尔,可能会需要检查运行中的 Angular Service Worker,以调查问题或确保它在按设计运行。浏览器提供了用于调试 Service Worker 的内置工具,而且 Angular Service Worker 本身也包含了一些有用的调试功能。

定位并分析调试信息

Angular Service Worker 会在虚拟目录 ​ngsw/​ 下暴露出调试信息。目前,它暴露的唯一的 URL 是 ​ngsw/state​。下面是这个调试页面中的一段范例内容:

NGSW Debug Info:

Driver version: 13.3.7
Driver state: NORMAL ((nominal))
Latest manifest hash: eea7f5f464f90789b621170af5a569d6be077e5c
Last update check: never

=== Version eea7f5f464f90789b621170af5a569d6be077e5c ===

Clients: 7b79a015-69af-4d3d-9ae6-95ba90c79486, 5bc08295-aaf2-42f3-a4cc-9e4ef9100f65

=== Idle Task Queue ===
Last update tick: 1s496u
Last update run: never
Task queue:
 * init post-load (update, cleanup)

Debug log:

驱动程序的状态

第一行表示驱动程序的状态:

Driver state: NORMAL ((nominal))

NORMAL ​表示这个 Service Worker 正在正常运行,并且没有处于降级运行的状态。

有两种可能的降级状态:

降级状态

详情

EXISTING_CLIENTS_ONLY

这个 Service Worker 没有该应用的最新已知版本的干净副本。较旧的缓存版本可以被安全的使用,所以现有的选项卡将继续使用较旧的版本运行本应用,但新的应用将从网络上加载。

SAFE_MODE

Service Worker 不能保证使用缓存数据的安全性。发生了意外错误或所有缓存版本都无效。这时所有的流量都将从网络提供,尽量少运行 Service Worker 中的代码。

在这两种情况下,后面的括号注解中都会提供导致 Service Worker 进入降级状态的错误信息。

这两种状态都是暂时的;它们仅在 ServiceWorker 实例 的生命周期内保存。 浏览器有时会终止空闲的 Service Worker,以节省内存和处理能力,并创建一个新的 Service Worker 实例来响应网络事件。 无论先前实例的状态如何,新实例均以 ​NORMAL ​模式启动。

最新清单的哈希

Latest manifest hash: eea7f5f464f90789b621170af5a569d6be077e5c

这是 Service Worker 所知道的应用最新版本的 SHA1 哈希值。

最后一次更新检查

Last update check: never

这表示 Service Worker 最后一次检查应用程序的新版本或更新的时间。“never” 表示 Service Worker 从未检查过更新。

在这个调试文件范例中,这次更新检查目前是已排期的,如下一节所述。

版本

=== Version eea7f5f464f90789b621170af5a569d6be077e5c ===

Clients: 7b79a015-69af-4d3d-9ae6-95ba90c79486, 5bc08295-aaf2-42f3-a4cc-9e4ef9100f65

在这个例子中,Service Worker 拥有一个版本的应用程序缓存并用它服务于两个不同的选项卡。

注意:
这个版本哈希值是上面列出的“最新清单的哈希”。 它的两个客户运行的都是最新版本。每个客户都用浏览器中 ​Clients ​API 的 ID 列了出来。

空闲任务队列

=== Idle Task Queue ===
Last update tick: 1s496u
Last update run: never
Task queue:
 * init post-load (update, cleanup)

空闲任务队列是 Service Worker 中所有在后台发生的未决任务的队列。如果这个队列中存在任何任务,则列出它们的描述。在这个例子中,Service Worker 安排的任务是一个用于更新检查和清除过期缓存的后期初始化操作。

最后的 tick/run 计数器给出了与特定事件发生有关的空闲队列中的时间。“Last update run” 计数器显示的是上次执行空闲任务的时间。“Last update tick” 显示的是自上次事件以来可能要处理的队列的时间。

调试日志

Debug log:

在 Service Worker 中出现的任何错误都会记录在这里。

开发者工具

Chrome 等浏览器提供了能与 Service Worker 交互的开发者工具。这些工具在使用得当时非常强大,但也要牢记一些事情。

  • 使用开发人员工具时,Service Worker 将继续在后台运行,并且不会重新启动。这可能会导致开着 Dev Tools 时的行为与用户实际遇到的行为不一样。
  • 如果你查看缓存存储器的查看器,缓存就会经常过期。右键单击缓存存储器的标题并刷新缓存。
  • 在 Service Worker 页停止并重新启动这个 Service Worker 将会触发一次更新检查。

Service Worker 的安全性

像任何复杂的系统一样,错误或损坏的配置可能会导致 Angular Service Worker 以不可预知的方式工作。虽然它在设计时就尝试将此类问题的影响降至最低,但是,如果管理员需要快速停用 Service Worker,Angular Service Worker 也包含多种故障保护机制。

故障保护机制

要停用 Service Worker,请删除或重命名 ​ngsw.json​ 文件。当 Service Worker 对 ​ngsw.json​ 的请求返回 ​404 ​时,Service Worker 就会删除它的所有缓存并注销自己,本质上就是自毁。

安全工作者

@angular/service-worker​ NPM 包中还包含一个小脚本 ​safety-worker.js​,当它被加载时就会把它自己从浏览器中注销,并移除此 Service Worker 的缓存。这个脚本可以作为终极武器来摆脱那些已经安装在客户端页面上的不想要的 Service Worker。

重要:
你不能直接注册这个 Safety Worker,因为具有已缓存状态的旧客户端可能无法看到一个新的、用来安装 另一个 worker 脚本的 ​index.html​。

你必须在想要注销的 Service Worker 脚本的 URL 中提供 ​safety-worker.js​ 的内容, 而且必须持续这样做,直到确定所有用户都已成功注销了原有的 Worker。 对大多数网站而言,这意味着你应该永远为旧的 Service Worker URL 提供 这个 Safety Worker。 这个脚本可以用来停用 ​@angular/service-worker​(并移除相应的缓存)以及任何其它曾在你的站点上提供过的 Service Worker。

更改应用的位置

重要:
Service Worker 无法在重定向后工作。你可能已经遇到过这种错误:​The script resource is behind a redirect, which is disallowed​。

如果你不得不更改应用的位置,就可能会出现问题。如果你设置了从旧位置(比如 ​example.com​)到新位置(比如 ​www.example.com​)的重定向,则 Service Worker 将停止工作。同样,对于完全从 Service Worker 加载该网站的用户,甚至都不会触发重定向。老的 Worker(注册在 ​example.com​)会尝试更新并将请求发送到原来的位置 ​example.com​,该位置重定向到新位置 ​www.example.com​ 就会导致错误 ​The script resource is behind a redirect, which is disallowed​。