Angular 服务工作者和 index.html 缓存

Angular service worker and index.html caching

虽然有类似的帖子,但我找不到明确的答案如果 index.html 应该被缓存 使用 Cache-Control header。

如果我错了请纠正我,但现在我正在 returning Cache-Control: no-storeindex.html 以避免哈希不匹配错误,这会迫使 service worker 进入降级模式。

我认为如果 index.htmlCache-Control: max-age=3600 缓存在 CDN 服务器上并且应用程序将在缓存过期之前更新, ngsw.json 将 return 不同的文件与脚本文件相比的哈希值,包含在 index.html 中,并且会发生不好的事情。对吧?

此外,为了清楚起见,我注意到有些人 index.html 添加到 ngsw-config.json 并且这也没有意义,因为 index.html 在 service worker 之前加载。

我不是这方面的专家,但我很确定以下 link 将帮助您解决疑虑。

https://angular.io/guide/service-worker-getting-started#whats-being-cached

What's being cached?

Notice that all of the files the browser needs to render this application are cached. The ngsw-config.json boilerplate configuration is set up to cache the specific resources used by the CLI:

  • index.html.

  • favicon.ico.

  • Build artifacts (JS and CSS bundles).

  • Anything under assets.

  • Images and fonts directly under the configured outputPath (by default ./dist//) or resourcesOutputPath. See ng build for more information about these options.

下面的 link 有关于 Service worker and caching of app resources. from which I would like you to read about App versions, Update checks and Resource integrity 的信息。

https://angular.io/guide/service-worker-devops#service-worker-and-caching-of-app-resources

我也把这三个部分的内容贴在这里,只是为了避免做这个答案"a link only answer"

应用版本

在 Angular service worker 的上下文中,"version" 是代表 Angular 应用程序特定构建的资源集合。每当部署应用程序的新版本时,服务工作线程都会将该版本视为应用程序的新版本。即使只更新了一个文件也是如此。在任何给定时间,service worker 的缓存中可能有多个版本的应用程序,并且它可能同时为它们提供服务。有关详细信息,请参阅下面的应用标签部分。

为了保持应用的完整性,Angular service worker 将所有文件分组到一个版本中。归为一个版本的文件通常包括HTML、JS和CSS文件。这些文件的分组对于完整性至关重要,因为 HTML、JS 和 CSS 文件经常相互引用并依赖于特定内容。例如,index.html 文件可能有一个引用 bundle.js 的标记,它可能会尝试从该脚本中调用函数 startApp()。任何时候提供此版本的 index.html,都必须提供相应的 bundle.js。例如,假设两个文件中的 startApp() 函数都重命名为 runApp()。在这种情况下,提供调用 startApp() 的旧 index.html 以及定义 runApp() 的新包是无效的。

这个文件完整性在延迟加载模块时尤为重要。一个 JS 包可能引用许多惰性块,惰性块的文件名对于应用程序的特定构建是唯一的。如果 运行 版本 X 的应用程序尝试加载延迟块,但服务器已经更新到版本 X + 1,则延迟加载操作将失败。

应用的版本标识由所有资源的内容决定,任何资源发生变化,版本标识也随之变化。实际上,版本由 ngsw.json 文件的内容决定,其中包括所有已知内容的哈希值。如果任何缓存文件发生变化,文件的哈希值将在 ngsw.json 中发生变化,导致 Angular service worker 将活动文件集视为新版本。

通过 Angular service worker 的版本控制行为,应用程序服务器可以确保 Angular 应用程序始终具有一致的文件集。

更新检查

每次用户打开或刷新应用程序时,Angular service worker 通过查找 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 没有更新的资源。

我认为您有必要了解 Angular 应用程序工作流程和 Angular Service Worker 运行时缓存机制。 所以我要在这里写下它们。这将有助于您的理解。

Angulars 开始按照以下步骤工作。

  • Angular starts with main.ts.
  • Angular app is bootstrapped and app.module.ts is passed as an argument.
  • And Angular analyze app component, reading the set up passed there and there is SELECTOR app-root.
  • Now, Angular is enable to handle app-root in the index.html and knows rules for the SELECTOR.
  • SELECTOR should insert the app components and have some HTML code - a template attached to him - html component.

Angular ServiceWorker

Angular CLI has also included in Angular application root module the Service Worker module. The CLI has also added a new configuration file called ngsw-config.json, which configures the Angular Service Worker runtime behavior, and the generated file comes with some intelligent defaults. There is a lot going on here, so let's break it down step-by-step. This file contains the default caching behavior or the Angular Service Worker, which targets the application static asset files: the index.html, the CSS and Javascript bundles.

The Angular Service Worker can cache all sorts of content in the browser Cache Storage. This is a Javascript-based key/value caching mechanism that is not related to the standard browser Cache-Control mechanism, and the two mechanisms can be used separately.

The files under the app section are the application: a single page is made of the combination of its index.html plus its CSS and Js bundles. These files are needed for every single page of the application and cannot be lazy loaded.

In the case of these files, we want to cache them as early and permanently as possible, and this is what the app caching configuration does. The app files are going to be proactively downloaded and installed in the background by the Service Worker, and that is what the install mode prefetch means. The Service worker will not wait for these files to be requested by the application, instead, it will download them ahead of time and cache them so that it can serve them the next time that they are requested. This is a good strategy to adopt for the files that together make the application itself (the index.html, CSS and Javascript bundles) because we already know that we will need them all the time.


index.html depends on index.js which depends on chunk.js which depends on jquery.js. chunk is loaded from the browser cache.

默认包含index.html。如果您不将它包含在清单中,那么它就不会成为散列和检查文件的一部分。如果它不在清单中(随后,ngsw.json),对 index.html 的更改将不会触发服务工作者中的事件。当然,当您下一个 load/refresh 站点时,它会选择新的 index.html.

如果您 index.html 从 CDN 提供服务,那么它可能是您在上次部署时构建的分发的一部分。应该正确计算。如果您的文件与 ngsw.json 中的哈希值不匹配,您在上面突出显示的区域很重要。如果出于某种原因,您正在修改 index.html 而没有更新整个发行版,服务人员将假定该文件已损坏。它会再试一次;由于文件与 ngsw.json 中的哈希不匹配,SW 将假定第二次尝试已损坏并关闭。

就我而言,这是因为应用程序包含在构建期间留下的令牌,这些令牌在发布管道中被替换为 Azure 资源密钥。构建应用程序时,哈希值是正确的。在该版本中,令牌替换为 运行 后,我的主要 *.js 文件不再与它们在 ngsw.json 中的哈希值一致。我选择修复它的方法是添加一个 powershell 步骤并重新计算哈希值。重要的是要注意,虽然实际文件名具有唯一的哈希值?嵌入代码,您无需更正该代码即可让服务工作者工作。 filename/hash key/value 对必须指向有效文件,并且该文件的 SHA1 散列必须与 ngsw.json 中的内容匹配。我编写的用于 post-compile validation/correction 哈希的脚本如下。如果您有一些独立于整个发行版更新 index.html 的进程,请使用此脚本更新 ngsw.json 并将其包含在您的 index.html 推送中。

备注:

  • 脚本接受 3 个参数。如果他们没有通过,它假设:
    • 脚本正在 运行 来自 angular 项目的根
    • 工作目录是"./dist"(要检查的脚本所在的位置)
    • 输入路径为"<working_dir>/ngsw.json"
    • 输出路径为"<working_dir>/ngsw_out.json"
  • 如果要修改文件,请确保指定相同的输入路径和输出路径
  • 如果将其放入 AzDO,则需要选中 "use Powershell Core" 复选框。

Powershell 脚本开始:

param([string]$working_path = "./dist"
  , [string]$input_file_path = "$working_path/ngsw.json"
  , [string]$output_file_path = "$working_path/ngsw_out.json")

"Checking for existence of hash script..."

$fileExists = Test-Path -Path $input_file_path

if ($fileExists) {
  "Service Worker present.  Beginning hash reconciliation."
  ""
  $files_to_calc = @()
  $ngsw_json = (Get-Content $input_file_path -Raw) | ConvertFrom-Json

  "-----------------------------------------"
  "Getting list of javascript files to check"
  "-----------------------------------------"
  $found_count = 0
  for ($idx = 0; $idx -lt $ngsw_json.hashtable.psobject.properties.name.count; $idx++) {
    $current_file = $ngsw_json.hashtable.psobject.properties.name[$idx]
    if ($current_file.Contains(".js")) {
      $files_to_calc += $current_file
      "   File {$idx} $($files_to_calc[-1]) found."
      $found_count++
    }
  }

  "---------------------------------------"
  "$($files_to_calc.count) files to check."
  "---------------------------------------"
  $replaced_count = 0
  $files_to_calc | ForEach-Object {
    $new_hash_value = (Get-FileHash -Algorithm SHA1 "$($working_path)$_").Hash.ToLower()
    $current_hash_value = $ngsw_json.hashTable.$_
    $current_index = [array]::IndexOf($ngsw_json.hashTable.psobject.properties.name, $_)
    $replaced = $false

    if ($ngsw_json.hashTable.$_ -ne $new_hash_value) {
      $ngsw_json.hashTable.$_ = "$new_hash_value"
      $replaced = $true
      $replaced_count++
    }

    "$($replaced ? '** ' : '   '){$current_index}:$_ --- Current Value: " +
    "$($current_hash_value.substring(0, 8))... New Value: " +
    "$($new_hash_value.substring(0, 8))..."

  }
  ""
  "--> Replaced $replaced_count hash values"

  $ngsw_json | ConvertTo-Json -depth 32 | set-content "$output_file_path"
}
else {
  "Service Worker missing.  Skipping."
}