构建-Angular-离线应用-一-

126 阅读1小时+

构建 Angular 离线应用(一)

原文:Building Offline Applications with Angular

协议:CC BY-NC-SA 4.0

一、构建现代 Web 应用

欢迎光临!恭喜你选择了这本书来学习如何用 Angular 构建离线应用。这一介绍性的章节为这本书设定了期望和框架。它简要介绍了传统的 web 应用开发,以及为什么仅仅创建另一个传统的 web 应用是不够的。

奠定基础

这本书提供了一些观点,并详细阐述了服务工作器和索引数据库等前沿功能。它提供了创建 Angular 应用的分步说明,并逐步添加特性来构建复杂的 web 应用。这本书使用了一个虚构的在线游戏系统 Web Arcade 来说明这些技术。它充当构建现代 web 应用的用例,该应用对网络连接性下降和速度变化具有弹性。

让我们用一点历史来建立背景。您可能知道,web 应用已经流行了二十多年。已经构建了大量的应用。事实上,许多 web 应用对于业务运营至关重要。

在 21 世纪初,这些应用取代了安装在设备、台式机或笔记本电脑上的胖客户端。胖客户端带来了挑战,因为应用必须针对操作系统来构建。大多数应用不能在苹果 macOS 和微软 Windows 之间互操作;然而,也有例外。一些组织和开发人员使用基于 Java 的技术来构建胖客户端,这些客户端运行在 Java 虚拟机(JVM)上。在这种解决方案流行起来之前,web 应用占据了主导地位。很大程度上,胖客户端在不同的平台和操作系统之间是不兼容的。

网络应用帮助解决了这个问题。web 应用在浏览器上运行。浏览器解释并执行超文本标记语言(HTML)、JavaScript 和级联样式表(CSS)。这些特征由欧洲计算机制造商协会(ECMA)和技术委员会 39 (TC39)标准化。

随着在移动设备上安装和运行的移动应用的出现,这种情况发生了变化。这可能是一部手机或平板设备,如 iPad。始于 2008 年的苹果应用商店在组织和开发者向应用的转变中发挥了重要作用。如今,苹果的 iOS 和谷歌的 Android 是两大移动平台。iOS 使用 App Store,Android 使用 Google Play(也叫 Play Store)分发各自平台的应用。

原始问题

在移动设备上,应用又带来了最初的问题:你需要为各自的平台开发应用。你只需为 iOS 构建一次原生应用,然后为 Android 重复构建。当然,还有其他选择,包括混合和跨平台技术的变通方法。它们适合主要的用例,但是总有一些工作流,这样的解决方案没有帮助。此外,对原生用户体验有所妥协;如果用户界面最初是为 Android 开发的,那么 iOS 用户可能会觉得与平台不匹配。许多工作流和应用需要大屏幕和桌面级操作系统提供的灵活性。将 iOS 和 Android 应用带到桌面上的动力很小。在大多数情况下,用户体验是有限的。

Web 应用解决方案的注意事项

组织和开发人员在 iOS 和 Android 以及 macOS 和 Microsoft Windows 上构建 web 应用,以满足所有主要平台和浏览器的需求。请记住,组织最初在远离胖客户端时采用了这种解决方案。当时,这些设备大多固定在桌子上,不能移动。他们通过电缆或无线网络连接。连接是稳定的。在构建应用时,连接不是一个考虑因素。

移动平台改变了这一场景。用户高度移动,进出网络。应用需要考虑连接性因素。假设用户暂时失去连接。当她试图启动移动 web 应用时,传统的 web 应用显示类似“无法显示页面”的消息,并且应用无法启动。用户无法继续。如果用户正在进行交易,问题可能会更严重。数据丢失,用户可能不得不重试整个工作流。

应用提供了现成的解决方案。记住,应用是安装在设备上的。用户可以启动应用,并与过去的数据或消息进行交互,即使用户断开连接。如果用户提交了交易或表格,应用可以临时缓存并在在线时同步。

以 Twitter 这样的社交应用为例。离线时,它允许你启动应用,查看缓存的推文,甚至撰写新推文并保存为草稿。

现代 web 应用支持这种高级缓存特性。这本书详细介绍了如何构建一个现代化的 web 应用,让用户在脱机时也能正常工作。

用例

这本书使用 Web Arcade,一个基于 Web 的在线游戏系统作为用例。您将使用 Angular 和 TypeScript 构建应用。这本书提供了如何创建应用、各种组件、服务等的分步说明。

在本书的整个过程中,您将学习如何做到以下几点:

  • 安装和升级 web 应用。

  • 缓存应用,以便在脱机时可以访问。

  • 缓存检索到的数据。

  • 使应用能够在脱机时运行。借助 Web Arcade 用例,我们将详细介绍如何向系统添加数据。离线时,数据缓存在设备上。一旦重新上线,该应用将提供与服务器同步的机会。

在桌面和移动设备上,谷歌 Chrome、微软 Edge、Safari(在 Mac 和 iOS 上)和 Firefox 等现代浏览器允许用户安装该应用。该书提供了一步一步的指导,使他们能够安装 web 应用并为该应用创建快捷方式。该快捷方式提供了对 web 应用的轻松访问,并在它自己的窗口中启动它(不像典型的 web 应用,它总是在浏览器中启动)。图 1-1 显示了安装在 macOS 上的 Web Arcade 应用。请注意 Dock 中的 Web Arcade 应用图标。您将在 Windows 任务栏中看到类似的图标。应用在它自己单独的窗口中,而不是在浏览器标签中。

img/515699_1_En_1_Fig1_HTML.png

图 1-1

安装在 macOS 上的 Web Arcade 应用

这本书详细介绍了如何使用服务工作器(带 Angular)来缓存应用。它提供了设置开发环境和测试缓存特性的分步说明。在这个阶段,即使网络不可用,应用也会加载。例如,即使在应用脱机时,您也可以使用该功能来掷骰子。可以想象,掷骰子不需要服务器端连接。它是在 1 和 6 之间生成的随机数。应用可视化滚动骰子(图 1-2 )。

img/515699_1_En_1_Fig2_HTML.png

图 1-2

在 Web Arcade 应用中掷骰子

您将从缓存应用数据并在离线时(或在慢速网络上)使用它开始。您将看到当真正的服务器端服务不可用时,如何利用服务工作器缓存。

这本书还详细介绍了如何创建数据;具体来说,Web Arcade 应用将允许用户在离线时添加评论。使用 IndexedDB(一个本地数据库)缓存评论。一旦连接建立,应用就进行识别。它将缓存的离线评论与服务器端服务和数据库同步。

稍后,您需要创建应用和数据库的新版本。这本书涵盖了提示用户可用升级的特性和实现。它详细介绍了如何无缝过渡到新数据库并升级其结构。

代码示例

本节解释如何下载和运行代码示例。从这个 GitHub 位置: https://git-scm.com/downloads 克隆 Web Arcade 库。打开终端/命令提示符并使用下面的命令,这需要在您的机器上安装 Git。

git clone https://github.com/kvkirthy/web-arcade.git

Note

Git 是一个流行的分布式源代码管理(SCM)工具。它占地面积小,在机器上使用最少的资源和磁盘空间。它也是免费和开源的。

默认情况下,您已经克隆了master分支。查看名为book-samples的分支,获取书中示例的样本。首先把目录改成web-arcade。接下来,检查一下book-samples 分店。

cd web-arcade
git checkout book-samples

我们预计会有改进,并将相应地将反馈纳入master分支。然而,分支book-samples致力于精确匹配书中的代码样本。

接下来,运行以下命令来安装代码示例所需的所有包。在整本书中,我们提供了使用节点包管理器(NPM)和 Yarn 的说明。虽然 NPM 是 Node.js 的默认包管理器,但 Yarn 是一个由脸书支持的开源项目。由于其在性能和安全性方面的优势,它在开发人员社区中受到了广泛关注。我们建议你挑一个,坚持到书结束。

npm install
(or)
yarn install

Note

这个命令需要在你的机器上安装 Node.js、NPM 或者 Yarn。如果他们不在,暂时跟着读。下一章提供了详细的说明。

接下来,运行以下命令启动 Web Arcade 示例应用:

npm run start-pwa
(or)
yarn start-pwa

前面的命令启动一个运行在开发人员级 web 服务器上的成熟应用。在阅读和理解代码的同时运行应用是很有用的。但是,如果您正在进行更改和更新代码,这些更改需要在应用中显示出来。通常,页面会重新加载,应用会根据更改进行更新。使用前面的命令很难做到这一点。每次进行更改时,您可能都必须结束该过程并重新启动。因此,考虑在更新代码时使用下面的命令。它会立即用更改更新应用。

npm start
(or)
yarn start

让应用在后台运行是一个很好的做法。在整本书中,您将不断地创建和更新代码,并运行示例。前面代码运行的脚本保持应用正常运行。除非得到指示,否则不要终止此脚本。

Note

在撰写本文时,该命令不支持在应用离线时缓存和加载应用。这是本书中解释的示例应用和概念的一个重要特性。要使用服务工作器测试缓存特性,请使用start-pwa命令。

摘要

这一章提到了对新实现的需求,比如 service workers 和 IndexedDB,它们是大多数现代浏览器所固有支持的。本书的其余部分将详细介绍如何在 web 应用中实现和集成这些技术。我们还介绍了一个名为 Web Arcade 的用例,这是一个基于 Web 的在线游戏系统,将在本书的其余部分使用。

二、入门指南

本章提供了如何开始使用 Angular 应用的说明。这是所有即将到来的章节的基础。按照本章中详细介绍的步骤来设置您的开发环境。接下来的章节将使用本指南中详细介绍的软件、库和软件包。

具体来说,本章提供了创建一个示例应用(即 Web Arcade)的步骤。样例应用将为本书中的所有概念及其解释提供用例及示例。在本章中,您将从创建 Web Arcade Angular 应用开始。

您还将向 Web Arcade Angular 应用添加离线功能。您将看到如何在没有网络连接的情况下访问 Angular 应用的介绍性细节。

先决条件

要创建、运行和测试 Angular 应用,您需要在计算机上安装和设置软件列表。幸运的是,本书中列出和描述的所有软件和库都是开源的,可以免费使用,至少对个人开发者来说是这样。本节列出了开始创建 Angular 应用所需的最低软件要求。

Node.js 和 NPM

Node.js 是一个跨平台的 JavaScript 运行时,在名为 V8 的 JavaScript 引擎上运行。它主要用于在服务器端和后端运行 JavaScript。

在本书中,您将在很大程度上使用 Node.js 安装附带的节点包管理器(NPM)。顾名思义,它是一个包管理器,帮助您安装和管理库和包。它跟踪一个包的整个依赖树。通过简单的命令,它可以帮助下载和管理库及其依赖项。

例如,Lodash 是一个非常有用的 JavaScript 实用程序和函数库。只需一个命令,您就可以将软件包作为依赖项安装并添加到项目中。其他人下载您的项目不需要执行额外的步骤。

从 Node.js 官方网站, https://nodejs.org 下载安装 Node.js。单击网站上的下载链接。它列出了长期支持(LTS)和最新版本的安装程序。最好选择 LTS。接下来,根据您的操作系统和平台选择一个选项。它将下载安装程序。

它安装 Node.js 和 NPM。在撰写本书时,Node.js 版本是 14.17.0,NPM 是 6.14.13。

下载完成后,打开安装程序,按照步骤完成安装。介绍画面和版本信息见图 2-1 。

img/515699_1_En_2_Fig1_HTML.jpg

图 2-1

Node.js installer(节点. js 安装程序)

故事

虽然 NPM 是 Node.js 的默认包管理器,但 Yarn 是一个由脸书支持的开源项目。由于其在性能和安全性方面的优势,它在开发人员社区中受到了广泛关注。书中的例子包括纱线和 NPM 命令。如果你是 Angular 发展的新手,选择一个并在所有的例子和练习中坚持使用它。包括纱线为读者提供了一种选择。有时,团队和组织在选择他们的工具集时会很挑剔(出于各种原因,包括像安全性这样的重要考虑)。因此,它有助于学习 NPM 和纱线。

要安装 Yarn,请运行以下命令:

npm install -g yarn

Note

注意选项-g,它代表“全局”这个包可以在所有的项目和目录中使用。因此,它可能需要提升权限才能运行和安装。

在 Windows 计算机上,以管理员身份运行此命令。

在 macOS 上,以超级用户身份运行命令。考虑下面的片段。该命令将提示输入 root 用户密码。

 sudo npm install -g yarn

如果您不使用-g选项,您仍然可以在目录级别使用yarn(或者任何其他没有安装-g的工具)。您可能需要为每个新目录或项目重新安装它。如果您喜欢将资源保存在项目或目录的本地,这是一个不错的解决方案。

要验证安装是否成功,请运行yarn --version。确保yarn命令被识别并返回版本信息。

% yarn --version
1.22.10

Angular CLI

在使用 Angular 应用时,Angular CLI 是一个非常有用的命令行工具。您将使用该工具完成所有与 angle 相关的任务,包括创建项目、添加新的 angle 组件、使用 angle 服务、运行 angle 应用、执行与构建相关的任务等。

使用以下命令安装 Angular CLI:

 npm install -g @angular/cli
# (or)
 yarn global add @angular/cli

要验证安装是否成功,请运行ng --version。见清单 2-1 。确保 Angular CLI 命令被识别并返回版本信息。

% ng --version

Listing 2-1Verify Angular CLI Installation

img/515699_1_En_2_Figa_HTML.jpg

Angular CLI: 12.0.1
Node: 14.16.1
Package Manager: npm 6.14.12
OS: darwin x64
Angular: undefined

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.1200.1 (cli-only)
@angular-devkit/core         12.0.1 (cli-only)
@angular-devkit/schematics   12.0.1 (cli-only)
@schematics/angular          12.0.1 (cli-only)

Note

注意在清单 2-1 中,Angular 是未定义的;但是,Angular CLI 的版本是 12.0.1。安装成功。一旦您使用 CLI 创建项目,Angular 将显示一个版本。

Visual Studio Code

严格地说,您可以使用任何简单的文本编辑器来编写 Angular 代码,并使用终端或命令提示符来编译、构建和运行应用。对于大多数编程语言来说,这可能是真的。但是,为了提高生产率和简化开发,请使用集成开发环境(IDE ),如 Visual Studio Code。该软件是由微软开发的,它是免费的,占用空间小,易于下载和安装。它的功能在处理 Angular 应用时非常有用。不过,这是个人喜好。你可以选择任何你觉得合适的 IDE 来创建和运行本书中描述的 Angular 应用。

从其网站 https://code.visualstudio.com 下载 Visual Studio Code。使用页面上的下载链接。在撰写本内容时,网站会自动识别您的操作系统,并提供相应的下载链接。

有关 Visual Studio Code 的快照,请参见图 2-2 。

img/515699_1_En_2_Fig2_HTML.png

图 2-2

Visual Studio Code 快照

以下是一些其他的选择:

  • 这是一个共享软件,也是一个有用的文本编辑器。它非常适合 JavaScript 开发,因为它占用空间小、响应速度快、易于使用。

  • 这是一个由 JetBrains 为 JavaScript 开发构建的复杂的 IDE。它为包括 Angular 和 Node.js 在内的许多流行框架提供了定制功能。但是,它是需要购买的专有软件。

  • Atom:这是一个开源的自由文本编辑器。它是一个用 HTML 和 JavaScript 构建的跨平台应用。

http-服务器

Http-Server 是为静态文件运行基于 Node.js 的 web 服务器的一种快速有效的方式。在开发过程中,您将使用这个 NPM 包来处理缓存的应用。

运行以下命令在您的计算机上全局安装 Http-Server:

 npm install -g http-server
# (or)
 yarn global add http-server

要验证安装是否成功,请运行http-server --version *。*确保http-server命令被识别并返回版本信息。

% http-server --version
v0.12.3

Note

如果您在使用yarn global add时遇到问题,并且在全局安装后仍未找到包,请参考本章后面的“使用 yarn 全局添加”一节。

创建 Angular 应用

既然所有的先决条件都已具备,您就可以创建一个新的 Angular 应用了。您将使用刚刚安装的 Angular CLI(@angular/cli)。Angular CLI 的可执行文件命名为ng。换句话说,您将使用一个ng命令来运行该工具。它使用您通过ng命令提供的选项来执行任务。

按照以下说明创建新的 Angular 应用:

ng new web-arcade

ng new是创建新应用的 Angular CLI 命令。创建新应用时,CLI 通常会提示您选择是否要使用 Angular 路由和样式表格式。参见清单 2-2 。

对于示例应用,选择实现路由。随着示例应用的发展,您将创建多个页面。这些页面之间的导航需要 Angular 路由。

Note

路由是单页应用(SPAs)的一个重要特性,因为大多数 web 应用都有不止一个页面。用户在页面之间导航。每个页面将有一个唯一的网址。

在 SPA 中,当用户在页面之间导航时,不会重新加载整个页面。在路由实现的帮助下(在这种情况下是角路由),SPA 只更新页面中在两个 URL 之间变化的部分。

对于关于选择样式表格式的第二个提示,如果您希望匹配书中的示例,请选择 Sass。但是,如果您喜欢其他样式表格式,可以随意选择其他应用。

 % ng new web-arcade
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? Sass   [ https://sass-lang.com/documentation/syntax#the-indented-synt
ax ]

Listing 2-2Prompts While Creating a New Angular Application

现成的 Angular CLI 提供了以下样式表格式的选择:

  • 层叠样式表(CSS) :这是样式表开发的传统方法。它在处理小代码单元时工作良好。

  • 语法上令人敬畏的样式表(SCSS -时髦的 CSS) :与 CSS 相比,它提供了更好的编程类型的特性。这些特性包括变量、函数、混合和嵌套规则。它是 CSS 的超集。

样式表被写入扩展名为.scss的文件中。它被预处理成 CSS。它完全兼容 CSS。因此,所有有效的 CSS 语句也在一个.scss文件中工作。

SCSS 语法包括大括号来表示块的开始和结束,以及分号来表示样式表语句的结束。

  • 语法上令人敬畏的样式表(SASS) :这类似于 SCSS,除了样式表语句是缩进的,而不是使用花括号和分号。为了明确指出文件格式,SASS 代码被写入.sass文件。

  • 更精简的样式表(LESS) :这是 CSS 的另一个超集,允许你使用变量、函数、混合等等。

使用 Angular CLI 的一个优点是,它将构建过程构建为包括样式表的预处理。运行或构建应用时,不需要额外的工作来创建脚本或运行预处理程序。

接下来,Angular CLI 复制文件并安装应用(清单 2-3 )。

CREATE web-arcade/README.md (1055 bytes)
CREATE web-arcade/.editorconfig (274 bytes)
CREATE web-arcade/.gitignore (604 bytes)
CREATE web-arcade/angular.json (3231 bytes)
CREATE web-arcade/package.json (1072 bytes)
CREATE web-arcade/tsconfig.json (783 bytes)
CREATE web-arcade/.browserslistrc (703 bytes)
CREATE web-arcade/karma.conf.js (1427 bytes)
CREATE web-arcade/tsconfig.app.json (287 bytes)
CREATE web-arcade/tsconfig.spec.json (333 bytes)
CREATE web-arcade/src/favicon.ico (948 bytes)
CREATE web-arcade/src/index.html (295 bytes)
CREATE web-arcade/src/main.ts (372 bytes)
CREATE web-arcade/src/polyfills.ts (2820 bytes)
CREATE web-arcade/src/styles.sass (80 bytes)
CREATE web-arcade/src/test.ts (743 bytes)
CREATE web-arcade/src/assets/.gitkeep (0 bytes)
CREATE web-arcade/src/environments/environment.prod.ts (51 bytes)
CREATE web-arcade/src/environments/environment.ts (658 bytes)
CREATE web-arcade/src/app/app-routing.module.ts (245 bytes)
CREATE web-arcade/src/app/app.module.ts (393 bytes)
CREATE web-arcade/src/app/app.component.sass (0 bytes)
CREATE web-arcade/src/app/app.component.html (23809 bytes)
CREATE web-arcade/src/app/app.component.spec.ts (1069 bytes)
CREATE web-arcade/src/app/app.component.ts (215 bytes)
⠼ Installing packages (npm)..

Listing 2-3Angular CLI: Create and Install Web Arcade

添加服务工作器

要向 Angular 应用添加离线特性,运行清单 2-4 中的命令。

ng add @angular/pwa

# This command install @angular/pwa on default project
# If you are running the above command on an existing Angular # solution that has multiple projects, use
# ng add @angular/pwa --project projectname

Listing 2-4Add Progressive Web App Features for Offline Access

服务工作器是渐进式 web 应用(PWAs)的功能之一。它们在浏览器的后台运行,使您能够缓存应用,包括脚本、资产、远程服务响应等。

传统上,web 应用易于部署和管理。为了添加新功能,开发人员或工程团队在一台或多台 web 服务器上部署新版本。用户下次打开网站时,可以访问新的应用和新的功能。但是,移动应用和安装的客户端应用(Windows 或 Mac)需要定期更新。安装需要在数以千计的客户端设备上进行(甚至更多,这取决于应用)。

但是,移动应用和客户端应用有一个优势,即使在网络不可用的情况下也可以访问。例如,社交媒体空间(Twitter 或脸书)中的移动应用可能会显示帖子,即使在没有网络可用的情况下。当网络不可用时,传统的 web 应用会显示一条消息,大意是“找不到页面”。用户完全无法访问应用。他们不能查看缓存的数据或与之交互。

进步的网络应用,特别是服务工作器,弥补了这一差距。您可以继续利用 web 应用的轻松部署和管理。它允许您安装应用、缓存脚本和资产等。

清单 2-4 中的命令允许您缓存脚本、资产和数据。它添加所需的配置,并在 Angular 应用模块中注册服务工作器。

运行 Angular 应用

到目前为止,您已经建立了一个使用 Angular 应用的环境,创建了一个名为 Web Arcade 的新应用,并安装了 PWA 特性。接下来,运行这个基本的应用来验证一切都工作正常。要运行 Web Arcade 应用,请将目录更改为应用文件夹的根目录,并执行以下命令:

npm run build
#(or)
yarn build

前面的命令在package.json文件中运行一个 NPM 脚本(即build)。这是 Angular CLI 在创建新项目时创建的文件之一。在 Visual Studio Code 或您选择的 IDE 中打开该文件。您将在清单 2-5 中看到脚本。

 "scripts": {
   "ng": "ng",
   "start": "ng serve",
   "build": "ng build",
   "watch": "ng build --watch --configuration development",
   "test": "ng test"
 },

Listing 2-5Scripts in the package.json File

Note

在清单 2-5 之前提供的构建脚本运行ng build。记住,Angular CLI 的可执行文件是ng。你可以直接在控制台或终端上运行ng build。两者都会导致同样的结果。

先前的构建命令输出到dist/web-arcade。接下来,在这个目录中运行 Http-Server。它启动一个基于 Node.js 的 web 服务器来呈现 Angular 应用。web 服务器在默认端口 8080 上运行。参见清单 2-6 。

% http-server dist/web-arcade

Starting up http-server, serving dist/web-arcade
Available on:
  http://127.0.0.1:8080
  http://192.168.0.181:8080

Listing 2-6Run Http-Server on Web Arcade Build Output

现在可以在端口号为 8080 的本地主机上访问该应用。在任何现代浏览器上打开链接http://localhost:8080。见图 2-3 。这张图片是在谷歌浏览器上拍摄的。

img/515699_1_En_2_Fig3_HTML.jpg

图 2-3

在 Http 服务器上运行的新应用

Note

您可以使用选项-p <new port number>在不同的端口上启动 Http-Server,例如http-server dist/web-arcade -p 4100

请注意,这是由 Angular CLI 搭建的默认内容。它提供了有用的链接和文档,可以继续用 Angular 构建应用。在本书接下来的章节和代码片段中,我们将增强代码示例,并创建新的组件和服务来创建 Web Arcade 应用。

接下来,打开开发人员工具并重新加载应用。在谷歌浏览器上,你可以在“更多工具”菜单下找到开发者工具。导航到网络选项卡并查找ngsw.json。这是服务工作器的配置文件。这允许您在浏览器中注册 Web Arcade 服务工作器。见图 2-4 。

img/515699_1_En_2_Fig4_HTML.png

图 2-4

Web Arcade 的 Chrome 开发工具

在节流选项中(在开发工具的网络选项卡上),选择选项离线*。*现在,如果您在此浏览器中导航到任何在线网站,它都不会加载页面。但是,localhost:8080 继续工作,从服务工作器缓存中呈现。

安全:服务工作器需要 HTTPS

服务工作器是一个强大的特性,它可以充当来自应用的所有网络请求的代理。代理实现可能会导致安全风险。因此,浏览器实施安全的 HTTPS 连接。如果没有 HTTPS 实现的网站试图注册服务工作器,浏览器会忽略该功能。

但是,请注意,在部署到 Http-Server 时,我们没有实现 HTTPS 证书。浏览器不会对本地主机上的 HTTPS 执行这种检查。这是规则的例外。它有助于轻松开发应用。出于开发目的实现 HTTPS 连接可能是一项单调乏味的任务。并且在本地主机上可访问的应用安装并运行在本地机器上;因此,不存在安全风险。

请注意,清单 2-6 显示该应用在两个 IP 地址上可用,如下所示:

  • 127.0.0.1.这代表本地主机。

  • 192.x.x.x。这是机器的本地 IP 地址。它没有安装安全的 HTTPS 证书。

即使 192.x.x.x 是本地 IP 地址,浏览器也无法验证这一点。考虑到连接不是 HTTPS,浏览器不注册服务工作器。参见图 2-5 。浏览器处于脱机模式。尽管有服务工作器,服务工作器也不加载页面。当您将应用联机时,服务工作器不会加载到网络选项卡中。

img/515699_1_En_2_Fig5_HTML.png

图 2-5

服务工作器不在 HTTP 连接上工作

使用纱线全局添加

Yarn 提供了几个命令,包括addlistremove,分别用来安装包、列出包和移除包。这些包安装在给定目录的node_modules中。目录是命令或操作的范围。

然而,由于前缀为yarn global,该命令在全局级别运行。全局级别通常是指登录用户的所有目录/项目。运行清单 2-7 中的命令,查看 Yarn 的全局bin目录。请注意,它位于登录用户的目录下。这是纱线安装的默认路径。

# macOS
% yarn global bin
/Users/logged-in-user/.yarn/bin

# Microsoft Windows
C:> yarn global bin
C:\Users\logged-in-user\AppData\Local\Yarn\bin

Listing 2-7Show Global Yarn bin Directory

如果该目录不包含在环境变量path中,包甚至在全局安装后也不工作。设置路径并重试软件包安装。参见清单 2-8 设置路径。

#On Microsoft Windows, set path with the following command
set PATH=%path%;c:\users\logged-in-user\AppData\Local\Yarn\bin

#On macOS, set path with the following command
export PATH="$(yarn global bin):$PATH"

Listing 2-8Set Path to Yarn Global bin Directory

摘要

这一章是一个入门指南,为离线功能的服务工作器构建一个 Angular 应用。它提供了必备软件和库的列表,以及下载和安装说明。本书中使用的大多数软件和库都是开源和免费的。

Exercise: Creating the Web Arcade Application

在计算机上为 Angular 设置一个开发环境。一定要安装 Node.js。

  1. 选择一个包管理器来安装和管理 Angular 和 TypeScript(或 JavaScript)包。选择使用 NPM 或纱线。更好的是,在未来的练习中坚持你的决定。

  2. 安装和验证 Angular CLI,最好是在全球层面。

  3. 安装 Http-Server,最好是在全局级别。

  4. 创建一个新的 Angular 项目并添加@angular/pwa

  5. 在 Http-Server 上构建和部署项目的第一个版本。

  6. 查看并确保服务工作器在网络不可用时可用。

三、安装 Angular 应用

本章首先提供在 Angular 应用中创建新屏幕和组件的说明。它提供关于组件的介绍性细节,然后提供足够的细节来构建一个离线 Angular 应用。如果您正在寻找关于组件和 Angular 概念的深入信息,请阅读《材料设计的 Angular》一书或参考本书末尾参考资料中提供的 Angular 文档。在本章快结束时,您将把应用打包并安装到客户端设备(桌面或移动设备)上。

Angular 分量

web 应用是由许多网页组成的。在网页中,用户与之交互的视图,包括标签、文本字段、按钮等。,是用 HTML 构建的。文档对象模型(DOM)节点组成一个 HTML 页面。DOM 被组织成一棵树。HTML 页面从一个根节点开始,通常是一个html元素(<html></html>)。它有子节点,子节点有更多的子节点。

可以想象,所有的浏览器都知道这些 HTML 元素。它们有内置的实现来呈现 HTML 页面中的每个元素。比如浏览器遇到一个input元素(<input />),就显示一个文本字段;浏览器将以粗体显示strong元素(<strong>text</strong>)中的文本。

然而,我们是否局限于 HTML 中预定义的元素?如果您想创建封装视图和行为的新元素,该怎么办?假设您想为 Web Arcade 构建一个骰子。

创建一个组件

Angular 组件使开发人员能够构建自定义元素。组件是 Angular 应用的构建块。本节提供了如何创建新组件的说明。

正如您在第二章中看到的,您将使用 Angular CLI 来创建和管理 Angular 应用。当您在前一章中设置开发环境时,您已经安装了 Angular CLI,所以它应该可以使用了。

要创建新组件,请运行以下命令:

% ng generate component components/dice

正如您之前看到的,ng可执行文件运行 Angular CLI。

  • 带 Angular CLI 的generate命令创建文件。

  • 接下来,component集合在与generate一起使用时指定如何添加组件文件。

  • 第三个参数指定组件名dice。在值前面加上components/会在文件夹components下创建它。如果文件夹不存在,它将创建该文件夹。

Note

或者,您可以使用下面的简短形式,使用g表示生成,使用c表示组件。

% ng g c components/dice

Angular CLI 在名为src/app/components/dice的新文件夹中生成以下文件。见图 3-1 。

img/515699_1_En_3_Fig1_HTML.png

图 3-1

使用 Angular CLI 生成的组件文件

  • dice.component.html:用于标记的 HTML 模板文件。要在网页中创建视图,可以使用 HTML。骰子的一面是用 HTML 模板创建的。

  • 样式表文件包含组件外观的 SASS 样式。它包括颜色、文本装饰、边距等。

  • dice.component.spec.ts:一个用于单元测试的 TypeScript 文件。

  • dice.component.ts:用于dice组件的功能实现的 TypeScript 文件。

网络街机:创建一个骰子

本节详细介绍了dice组件的代码。这是对创建一个dice组件的固执己见的看法。请参考“练习”部分,了解关于构建骰子的其他想法。

组件的样式

样式表管理外观、颜色、字体等。,用于组件。样式可以应用于组件的元素。将样式表的范围限定在组件本地是一个很好的做法。这是 Angular 应用中的默认行为。本节详细介绍了如何为dice组件创建样式表。

注意,在 Web Arcade 代码示例中,src/assets目录中有六个 PNG 文件,分别对应于骰子的每一面。在样式表中使用这些图像来显示骰子的侧面。首先在样式表中为每一方创建变量。考虑清单 3-1 中的变量列表,这些变量带有指向骰子图像侧面的 URL 引用。冒号的左边(:)是变量名。冒号的右边是一个值,在本例中是骰子的 PNG 图像。使用url()功能将图像文件包含在 CSS 中。

// Variables in SASS
$side1:url('/assets/side1.png')
$side2:url('/assets/side2.png')
$side3:url('/assets/side3.png')
$side4:url('/assets/side4.png')
$side5:url('/assets/side5.png')
$side6:url('/assets/side6.png')

Listing 3-1Sides of the Die Images

接下来,为骰子的每一面创建 CSS 类,可用于div元素。参见清单 3-2 。

div.img-1
  background-image: $side1

div.img-2
  background-image: $side2

div.img-3
  background-image: $side3

div.img-4
  background-image: $side4

div.img-5
  background-image: $side5

div.img-6
  background-image: $side6

Listing 3-2Code for the CSS Class That Defines Each Side of a Die

每个img-x类(例如img-6)都是一个 CSS 类。它的前缀是div 。在 HTML 中,CSS 类img-6只能应用在div元素上。

Note

记住,SASS 文件不使用花括号或分号。注意元素和类名下 CSS 样式的缩进。也就是说,名为$side6的样式背景图像与div.img-6相关,因为它缩进了一个制表符,表明它与 CSS 类相关。

TypeScript:组件的功能逻辑

每个组件至少有一个 TypeScript 文件和一个 TypeScript 类。组件的功能/行为逻辑写在这个类中。例如,考虑掷骰子并生成 1 到 6 之间的随机数的逻辑。这就是dice组件的功能/行为逻辑。本节介绍如何为dice组件创建 TypeScript 类。

组件的输出(事件发射器)

到目前为止,您已经看到该组件有一个样式表来显示骰子的六个面中的任何一面。当你掷骰子时,它会抽取六个数字中的一个。利用dice组件的代码可能需要掷骰子的结果。把这当成输出。用输出装饰器为输出创建一个EventEmitter对象。在这个对象上使用emit()函数来输出组件外部的值。继续阅读进一步的解释和代码示例。

组件的输入

该组件还可能允许从组件外部设置值。只要是合法值(1 到 6 之间的值),组件就可以显示在骰子上。把这当成输入。创建一个类级变量,用Input()修饰它。这充当组件的属性。对于此属性,代码可以使用组件提供输入值。

考虑清单 3-3 和图 3-2 中显示的 TypeScript。注意用粗体突出显示的文本,第 7 行和第 8 行。Input()装饰器允许从组件外部设置变量值。Output()装饰器启用从组件发出的rollResult值。

img/515699_1_En_3_Fig2_HTML.png

图 3-2

骰子和应用组件之间的输入和输出

import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
1\. @Component({
2\.  selector: 'wade-dice',
3\.  templateUrl: './dice.component.html',
4\.  styleUrls: ['./dice.component.sass']
5\. })
6\. export class DiceComponent implements OnInit {
7\.   @Input() draw: string = '';
8\.   @Output() rollResult = new EventEmitter<number>();
9\.   constructor() { }
10\.  ngOnInit(): void { }
11\. }

Listing 3-3Dice Component TypeScript File

还要注意清单 3-3 ,第 1 行到第 5 行。组件装饰器为组件指定元数据。

  • 如前所述,组件是可重用的定制元素。在 HTML 文件中使用组件时,您将使用该值引用组件。在本例中,参见<wade-dice></wade-dice>

    注意 wade-是为 Web 归档代码样本中的所有组件选择的任意前缀。Angular 使用的默认是app

  • template-url:指组件使用的 HTML 文件。见图 3-1 。它显示了为dice组件创建的 HTML 文件。

  • style-urls:指组件使用的样式表文件。可以有多个样式表。因此,它是一个值数组。见图 3-1 。它显示了为dice组件创建的 SASS 文件。

注意第 9 行中显示的构造函数。它实例化了 TypeScript 类。第 10 行中的函数ngOnInit(),是在构造函数之后调用的 Angular 生命周期钩子。因此,当这个函数被调用时,类变量已经被实例化了。这是设置 Angular 组件上下文的理想位置。

在当前的dice组件案例中,我们通过滚动骰子来设置上下文,生成 1 到 6 之间的随机数,并显示骰子的那一面。或者,dice组件也允许您从组件外部设置一个值。考虑上市 3-4 。

01: @Input() draw: string = '';
02: @Output() rollResult = new EventEmitter<number>();
03:
04: constructor() { }
05:
06: ngOnInit(): void {
07:   if(this.draw){
08:     this.showOnDice(+this.draw);
09:   } else {
10:     this.rollDice();
11:   }
12: }

Listing 3-4ngOnInit() Hook for the Component

Note

TypeScript 类变量和方法(函数)是用this关键字访问的。

注意第 1 行和第 7 行。this.draw是组件的输入属性。如果向元件提供输入,则在骰子上显示该值。使用组件的 Angular 代码明确提供了一个值。你不需要掷骰子来产生一个随机数。参见清单 3-5 。

另一方面,当没有输入时,滚动骰子,产生一个随机数,并在骰子上显示该值。参见清单 3-6 。

Note

注意清单 3-4 第 8 行的+前缀。输入属性是一个字符串值。+前缀将字符串值转换为数字。

/*
     At class level, a variable selectedDiceSideCssClass is declared.

selectedDiceSideCssClass: string = '';
/*
01:  // show the given number (draw parameter) on the dice
02:   showOnDice(draw: number){
03:     // the css class img-x show appropriate side on the dice.
04:     switch (draw) {
05:       case 1: {
06:         this.selectedDiceSideCssClass = 'img-1';
07:         break;
08:       }
09:       case 2: {
10:         this.selectedDiceSideCssClass = 'img-2';
11:         break;
12:       }
13:       case 3: {
14:         this.selectedDiceSideCssClass = 'img-3';
15:         break;
16:       }
17:       case 4: {
18:         this.selectedDiceSideCssClass = 'img-4';
19:         break;
20:       }
21:       case 5: {
22:         this.selectedDiceSideCssClass = 'img-5';
23:         break;
24:       }
25:       case 6: {
26:         this.selectedDiceSideCssClass = 'img-6';
27:         break;
28:       }
29:       default: {
30:         break;
31:       }
32:     }
33:   }

Listing 3-5Show the Given Number on a Die

记住清单 3-1 和 3-2 中的样式表。他们定义了 CSS 类img-1img-6,显示描述骰子六个面的图像。清单 3-5 为一个名为selectedDiceSideCssClass的变量设置了一个合适的 CSS 类名。您将在 HTML 模板中使用这个 CSS 类。

要滚动骰子(无输入时),使用功能rollDice()

01: rollDice(){
02:     let i = 0;
03:
04:     // run the provided function 25 times depicting a rolloing dice
05:     const interval = setInterval(() => {
06:
07:       // random number generator for numbers between 1 and 6
08:       let randomDraw = Math.round((Math.random()*5) + 1);
09:       this.showOnDice(randomDraw);
10:
11:       // After 25, clear the interval so that the dice doesn't roll next time.
12:       if(i > 25) {
13:         clearInterval(interval);
14:         this.rollResult.emit(randomDraw);
15:       }
16:
17:       i += 1;
18:
19:     }, 100);
20: }

Listing 3-6Roll the Die

该函数试图模仿滚动骰子。因此,它在骰子上设置值 25 次(任意次数)。它每 100 毫秒运行一次代码。看到 5 和 19 之间的线。

  • 一个 JavaScript 函数接受一个回调函数作为第一个参数。

  • 第二个参数指示第一个回调函数运行之前的毫秒数。

为了更容易理解,请参见清单- 3-7 中的一小段空白代码。

setInterval(() => { }, // first parameter, callback
 100 // second parameter, interval duration in
     // milliseconds
);

Listing 3-7The setInterval() Function

要生成随机数,请参考清单 3-6 中的第 8 行。

  • Math.random()生成一个介于 0 和 1 之间的值。

  • 将这个数乘以 5 会将值限制在 0 到 5 之间。

  • 骰子不显示小数值;因此,使用 JavaScript 函数Math.round()对数字进行四舍五入。

  • 骰子不显示零;因此,加 1。

Note

什么时候用rollResult?想象一下,在棋盘游戏中,根据骰子抽出的数字移动一个棋子。棋盘游戏组件使用来自dice组件的随机数结果。dice组件发出棋盘游戏的号码。

在一个例子中,假设骰子抽取 6。一篇关于垄断的文章应该移动六个位置。dice组件显示 6 并发出数字。垄断组件接收 6,并将棋子移动 6 个位置。

请参考 TypeScript 类文件的完整代码段。参见清单 3-8 。

import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'wade-dice',
  templateUrl: './dice.component.html',
  styleUrls: ['./dice.component.sass']
})
export class DiceComponent implements OnInit {

  @Input() draw: string = '';
  @Output() rollResult = new EventEmitter<number>();

  selectedDiceSideCssClass: string = '';

  constructor() { }

  ngOnInit(): void {
    if(this.draw){
      this.showOnDice(+this.draw);
    } else {
      this.rollDice();
    }
  }

  // show the given number (draw parameter) on the dice
  showOnDice(draw: number){
    // the css class img-x show appropriate side on the dice.
    switch (draw) {
      case 1: {
        this.selectedDiceSideCssClass = 'img-1';
        break;
      }
      case 2: {
        this.selectedDiceSideCssClass = 'img-2';
        break;
      }
      case 3: {
        this.selectedDiceSideCssClass = 'img-3';
        break;
      }
      case 4: {
        this.selectedDiceSideCssClass = 'img-4';
        break;
      }
      case 5: {
        this.selectedDiceSideCssClass = 'img-5';
        break;
      }
      case 6: {
        this.selectedDiceSideCssClass = 'img-6';
        break;
      }
      default: {
        break;
      }
    }
  }

// generate a random number between 1 and 6
// and set on the dice
  rollDice(){
    let i = 0;

// run the provided function 25 times depicting a rolling dice
    const interval = setInterval(() => {

      // random number generator for numbers between 1 and 6
      let randomDraw = Math.round((Math.random()*5) + 1);
      this.showOnDice(randomDraw);

      // After 25, clear the interval so that the dice doesn't roll next time.
      if(i > 25) {
        clearInterval(interval);
        this.rollResult.emit(randomDraw);
      }
      i += 1;
    }, 100);
  }
}

Listing 3-8Dice Component, TypeScript File

HTML 模板

关于用户与之交互的视图,请参考清单 3-9 中的 HTML 模板。

<div
  class="dice"
  [ngClass]="selectedDiceSideCssClass"
></div>

Listing 3-9Dice Component, HTML Template

注意 CSS 类值dice是静态的。它提供了在骰子滚动时不会动态改变的填充、高度和宽度。

另一方面,ngClass使用属性绑定来动态设置 CSS 类。这是一个 Angular 属性指令。ngClass将通过变量selectedDiceSideCssClass提供的 CSS 类应用到div元素*上。*参见清单 3-8 中的showOnDice()功能。它有条件地选择一个 CSS 类名。

Note

属性指令允许您更改 DOM 元素的外观和行为。ngClass是 Angular 提供的一个内置指令,用于动态更新 CSS 类。这对于动态控制元素的外观非常有用。

属性绑定在 HTML 属性上启用单向数据绑定 TypeScript 变量。

现在,dice组件已经可以使用了。转到app.component.html并删除导致第二章图 2-3 的默认内容。它是用 Angular CLI 创建的应用的占位符。记住,dice组件的选择器是wade-dice *。*在 HTML 模板的 app 组件中使用。参见清单 3-10 。

<div class="container align-center">
   <wade-dice></wade-dice>
</div>

Listing 3-10App Component of the HTML Template

Note

在这一点上,div和两个 CSS 类containeralign-center没有太大的意义。它们有助于在页面上更好地呈现内容。

记住清单 3-4 中的代码。如果您没有为draw属性(输入)提供一个值,dice组件会生成一个随机数。因此,清单 3-10 掷骰子,生成一个随机数,并将其设置在骰子上。相反,如果你使用属性draw,它不会掷骰子。它只显示了骰子的第 4 面。

<wade-dice draw="4"></wade-dice>

结果如图 3-3 所示。

img/515699_1_En_3_Fig3_HTML.jpg

图 3-3

使用应用组件中的骰子组件

服务工作器配置

接下来,您已经准备好安装应用了。完成后,我们可以回顾如何在桌面上安装应用。

在第二章中,当你安装@angular/pwa时,它创建了以下配置。ng add @angular/pwa命令添加了@angular/service-worker包。考虑应用的以下更新:

  1. 该命令将manifest.webmanifest文件添加到应用中。当您在桌面或移动浏览器上加载应用时,它会在此配置文件的帮助下识别渐进式 web 应用。ng add @angular/pwa命令更新index.html并添加一个到这个配置文件的链接。

  2. 该命令将ngsw-config.json文件添加到应用中。这是服务工作器的 Angular 特定配置文件。它由 CLI 和构建过程使用。它为应用配置缓存行为。

  3. 在 Angular 的应用模块中,它导入服务工作器模块并注册它。

Note

加载 web 应用时,每次都会从 web 服务器获取ngsw-config.json文件。它不与服务工作器一起缓存。它有助于识别对应用的更改,并获取一个全新的版本。

创建图标

PWA 需要各种图标文件。请记住,它现在是一个可安装的应用。你需要不同分辨率的启动图标。这些图标用在移动设备主屏幕上的应用中,桌面上的快捷方式,Windows 任务栏或 macOS Dock 上,等等。默认图标是一个 Angular 标志。您可以使用位于web-arcade/src/assets/icons的 Web Arcade 代码示例中的图标。

服务工作器应用需要具有以下分辨率(像素)的图标:

  • 72 × 72

  • 96 × 96

  • 128 × 128

  • 144 × 144

  • 152 × 152

  • 192 × 192

  • 384 × 384

  • 512 × 512

将图标复制到文件夹<your-project-folder>/src/assets/icons。运行 build 命令。请注意,图标和应用包一起被复制到可部署目录中。确保 Http-Server 正在dist/web-arcade目录中运行,以便 URLhttp://localhost:8080继续为应用提供服务。

在支持服务工作器的新浏览器窗口中启动应用。请注意,Web Arcade 应用有一个安装按钮。图 3-4 显示了桌面上 Google Chrome 的安装选项。

img/515699_1_En_3_Fig4_HTML.jpg

图 3-4

安装维修工人

Note

在新的浏览器窗口(或选项卡)中启动应用可确保旧版本的应用不会由服务工作器缓存提供。相反,它在 web 服务器上识别更新的应用包,并下载新的应用。

单击安装。它现在可以在桌面上使用。见图 3-5 。

img/515699_1_En_3_Fig5_HTML.png

图 3-5

macOS 上安装的应用

摘要

本章继续构建 Web Arcade 示例应用。在这个过程中,本章详细介绍了 Angular 和 service worker 概念。它还为骰子开发了样式表,最后在桌面和移动设备上安装应用,并列出由包@angular/pwa添加的配置。

Exercise

  • 本章中描述的代码示例没有显示重复掷骰子的按钮。给dice组件添加一个按钮,再次滚动。使用 click 事件来处理按需掷骰子。

  • 用 CSS 创建骰子的侧面,而不是使用图像。

  • 创建一个 8 面或 12 面的模具。

  • 探索滚动骰子的动画。

四、服务工作器

服务工作器在您的浏览器后台运行。它们为现代 web 应用提供了基础,并且可以安装、脱机工作,在低带宽情况下也是可靠的。本章介绍了维修工人。它讨论了服务工作器的缓存功能以及如何在 Angular 应用中使用它们。它详细描述了服务工作器的生命周期。接下来,本章将讨论 Angular 在与维修人员合作时的配置和功能。它解释了如何为 Web Arcade 示例应用实现缓存。最后,它提供了关于浏览器兼容性的细节。

服务工作器是运行在浏览器上的网络代理。他们可以拦截从浏览器发出的网络请求。这些请求包括应用的 JavaScript 包文件、样式表、图像、字体、数据等。您可以对服务工作器进行编程,以响应来自缓存的请求。这使得 web 应用能够适应网络速度和连接丢失。传统的 web 应用在失去连接时会返回“page not found”错误,与此不同,服务工作器使应用能够利用已安装和缓存的资源。您可以对应用进行编程,以加载缓存的数据或显示优雅的错误信息。即使在低带宽情况下,服务工作器也能让您构建流畅、响应迅速的应用,并提供出色的用户体验。

即使在应用或浏览器关闭后,服务工作器仍会保留。要查看在职服务工作器的列表,请导航至 Google Chrome 上的页面chrome://inspect/#service-workers或 Microsoft Edge 上的页面edge://inspect/#service-workers。见图 4-1 。请注意使用服务工作器的应用中包括 Angular 的 Angular.io 在内的热门网站。还要注意,Web Arcade 示例应用的开发 URL localhost:8080 已经注册了一个服务工作器。

img/515699_1_En_4_Fig1_HTML.png

图 4-1

在谷歌浏览器上检查服务工作器

Note

要查看您在计算机上访问的各种 web 应用注册的所有服务工作器,请启动“服务工作器内部信息”页面。注意,第一个 URLchrome://inspect/#service-workers只列出了活动的服务工作器。通过在 Google Chrome 上导航到chrome://serviceworker-internals(或在 Microsoft Edge 上导航到edge://serviceworker-internals)来访问服务工作器的内部信息。

请注意,此页面将来可能会被弃用。chrome://inspect/#service-workers URL 可能包括所有服务工作器调试特性。

服务工作器生命周期

本节详细介绍了服务工作器生命周期及其在后台运行的状态(在浏览器上)。参见图 4-2 ,该图描绘了一个服务工作器的生命周期。它从注册一个新的服务工作器开始。使用服务工作器的 web 应用在浏览器中加载时注册。“注册”可以在每次用户加载应用时发生。如果服务工作器已经注册,浏览器会忽略新的注册。

成功注册服务工作器会触发安装事件。典型的安装事件处理缓存逻辑。所有静态资源,包括应用包、图像、字体和样式表,都在安装过程中被缓存。这些是可配置的。

服务工作器安装是原子性的。下载和缓存一个或多个资源失败会导致事件完全出错。下次用户访问该网站时,它会尝试再次安装。这是为了确保没有部分安装的应用导致不可预见的问题和错误。

当应用打开时,安装的服务工作器被激活。它在后台运行,充当所有网络调用的代理。根据应用逻辑和配置,您可以从缓存中提供数据。如果在缓存中找不到数据,则调用网络服务并通过网络检索数据。

如果应用或服务工作程序不在使用中,则服务工作程序会终止以节省内存。需要时,它会激活服务工作器。注意图 4-1 (在浏览器窗口中)中的终止按钮,用于手动终止一个活动的维修工人。您可以使用它来强制关闭服务工作器。这有助于服务工作器重新开始工作。如果您的计算机资源不足,您可以终止服务工作以节省内存。另外,请注意 Inspect 链接,它启动开发工具,允许您探索网络资源和应用源代码。

工作流程和事件的描述见图 4-2 。

img/515699_1_En_4_Fig2_HTML.png

图 4-2

服务工作器生命周期

Angular 应用中的服务工作器

Angular 使得在应用中使用服务工作器和缓存特性变得更加容易。Angular scaffolds 提供了上一节“服务工作器生命周期”中描述的许多功能,尤其是缓存特性。本节详细介绍了整合服务工作器的开箱即用的 Angular 功能。

当您将@angular/pwa添加到项目中时,Angular CLI 会生成ngsw-config.json。它为 Angular 应用提供了一种服务工作器配置。Angular 构建过程使用这种配置。配置的一个方面是要缓存和安装的静态和动态资源的列表。静态资源包括组成应用的 JavaScript 包文件、样式表、图像、字体和图标。典型的动态资源包括数据响应。

一个ngsw-config.json文件包括以下几个部分:

  • 请记住,Web Arcade Angular 应用是可安装的,并且维护版本。配置中的该字段提供了应用版本的简要描述。更新应用时,使用此字段提供有关软件升级和版本的有意义的详细信息。

  • index:指定 Angular 应用和单页应用(SPA)的根 HTML 文件。在 Web Arcade 示例应用中,它是src目录中的index.html。有了这个字段索引,您就提供了一个到应用起点的链接。正如您接下来将看到的,Web Arcade 使用服务工作器缓存该文件。

  • assetGroups:这是对资产的配置,通常是 JavaScript 应用包、样式表、字体、图像、图标等。这些资源可以是 Angular 项目的一部分,也可以从远程位置下载,如内容交付网络(CDN)。

    • 注意列表 4-1 中 Web Arcade 的ngsw-config文件。它包括构成应用的文件,即index.html,所有的 JavaScript 包,以及 CSS 文件。它还包括图像、图标、字体等资产。

Note

记住,在 Web Arcade 中,SASS 文件编译成 CSS。服务工作器正在处理 Angular 应用的构建输出。所有的文件,包括 JavaScript 包,编译的 CSS,图片等。,相对于dist目录(yarn build命令的输出)。

  1. 可以配置多个assetGroups。请注意,该字段是一个数组。您可以列出带有配置细节的 JSON 对象。一个assetGroup对象定义了以下字段:

    1. name:这是一个任意的资产组名称。

    2. resources: The resources are the files or URLs to be cached by the service worker. As mentioned, the files could be JavaScript files, CSS stylesheets, images, icons, etc. On the other hand, for resources such as fonts (and a few other libraries), you may use CDN locations, which are URLs.

      1. files:这是为服务工作器配置的要缓存的文件数组。

      2. urls:这是为服务工作器配置的要缓存的 URL 数组。

      在构建时,您不太可能知道要缓存的每个文件。因此,该配置允许您使用文件和 URL 模式。有关更多详细信息,请参见“模式匹配要缓存的资源”一节。

    3. installMode:安装模式决定当浏览器上没有服务工作器的现有版本时,如何首次缓存资源。它支持两种缓存模式。

      1. prefetch:在开头缓存所有的资源、文件和 URL。服务工作器不等待应用请求资源。当应用请求时,资源在缓存中随时可用。

        这种方法对于根index.html文件、核心应用包、主样式表等非常有用。但是,预取安装模式可能会占用大量带宽。

        当没有提供配置值时,预取是默认的安装模式。

      2. lazy:仅当应用第一次请求资源时才缓存资源。如果配置了特定的资源,但从未请求过,则不会缓存该资源。它是高效的。但是,该资源只有在第二次使用后才能脱机使用。

  2. updateMode:更新模式决定当发现新版本的应用时如何缓存资源。这适用于已经安装在浏览器中的服务工作器(Angular 应用)。如您所知,与典型的 web 应用不同,服务工作器支持缓存 Angular 应用。它还允许您发现并安装可用的更新。它支持两种缓存模式。

    1. prefetch:更新应用时,下载并缓存所有资源、文件和 URL。服务工作器不等待应用请求资源。当应用请求某个资源时,该资源在缓存中随时可用。

      Default:当没有提供配置值时,使用为installMode设置的值。

    2. lazy:仅当应用第一次请求资源时才缓存资源。如果配置了特定的资源,但从未请求过,则不会缓存该资源。这是高效的。但是,该资源只有在第二次使用后才能脱机使用。

      如果installModeprefetch,该配置值将被覆盖。为了在懒惰模式下真正缓存,installMode也需要懒惰。

  3. dataGroups:assetGroups支持缓存应用资产,主要是静态资源,dataGroups帮助缓存动态数据请求。它是数据组对象的数组。可以配置多个dataGroups。您可以列出带有配置细节的 JSON 对象。一个dataGroup对象定义了以下字段:

    1. name:这是一个数据组的任意标题。

    2. urls:这是一个字符串数组,用于配置 URL 列表或匹配 URL 的模式列表。与assetGroups不同,该模式不支持与?匹配,因为它是 URL 中查询字符串的常用字符。

    3. version:这有助于识别dataGroup资源新版本的可用性。服务工作器丢弃旧版本的缓存,获取新数据,并缓存新的 URL 响应。如果没有提供版本,则默认为 1。

      对数据组进行版本控制非常有用,尤其是当资源与旧的 URL 响应不兼容时。

    4. cacheConfig:定义数据缓存策略的配置。它包括以下字段:

      1. maxSize:定义缓存数据大小的上限。通过设计来限制大小是一个很好的做法。浏览器(像其他平台一样)为每个应用管理和分配内存。如果应用超出上限,整个数据集和缓存都可能被收回。因此,设计一个系统来限制缓存大小,并防止由于驱逐导致的不可预见的结果。

      2. maxAge : dataCache本质上是动态的。通常情况下,数据在源位置会发生变化。缓存数据时间过长可能会导致应用使用过时的字段和记录。服务工作器配置提供了一种定期自动清除数据的机制,确保应用不使用过时的数据。举个例子,假设利率每天更新一次。这意味着缓存的利率值需要在 24 小时内到期。另一方面,用户的个人资料图片很少更新。因此,它们可以在缓存中存储更长时间。

        您可以使用以下一项或多项来限定最大年龄值:

        d代表天。比如用7d七天。

        h代表小时。比如用12h12 小时。

        m代表分钟。比如用30m30 分钟。

        s代表秒。比如用10s十秒。

        u代表毫秒,比如用500u代表半秒。

        您可以混合搭配来创建一个复合值。例如,2d12h30m代表 2 天 12 小时 30 分钟。

      3. timeout:根据dataCache策略(见下一条),数据请求经常试图通过网络使用响应。只有当网络请求耗时太长(或失败)时,它才使用缓存的响应。

        超时定义了一个值,超过该值后,服务工作人员将忽略网络请求,并使用缓存的值进行响应。

        您可以使用以下一项或多项来限定超时值:

        d代表天。比如用7d七天。

        h代表小时。比如用12h12 小时。

        m代表分钟。比如用30m30 分钟。

        s代表秒。比如用10s十秒。

        u代表毫秒,例如使用500u代表半秒。

        您可以混合搭配来创建一个复合值。例如,2d12h30m 代表 2 天 12 小时 30 分钟。

      4. 服务工作器可以使用以下两种策略之一:

        • 对于少数数据请求,Angular 应用可能会优先考虑性能,指示服务工作器使用缓存的响应。由于响应来自本地缓存,因此返回速度更快。新的网络服务请求仅在maxAge之后发送(参见前面关于maxAge的要点)。在一个例子中,它对于每晚更新的利率请求很有用。想象一下maxAge设置为 1d,服务工作器使用缓存 24 小时,之后缓存过期。

        • freshness:在许多情况下,Angular 应用会将服务工作器配置为在使用缓存数据之前,首先通过网络获取数据。想象一下,在慢速网络上,如果数据请求超时,服务工作器会使用缓存,以便应用仍然可用。

Web Arcade 的服务工作器配置

考虑列出 4-1 。这是为 Web Arcade 项目生成的默认配置文件。

  • 注意第 4 行的字段assetGroups。第 5 行和第 17 行之间是第一个资产组对象。该对象详细描述了服务工作器要缓存的资源。

    1. 字段nameassetGroup的任意标题(第 6 行)。它使用一个任意的名字app,这个名字代表了主要的应用资源,比如 JavaScript 应用包、样式表、index.html文件等等。

    2. 第 9 行和第 12 行之间的前几个资源文件包括以下内容:

      1. 与应用标题一起显示的收藏夹图标。

      2. Web Arcade 应用的根 HTML 文件。

      3. web 清单配置,它将应用标识为渐进式 web 应用。

    3. 注意第 13 行和第 14 行的星号,指示如何缓存所有的 JavaScript 和 CSS 文件(文件名以jscss结尾)。请参阅“模式匹配资源到缓存”一节,了解更多关于模式匹配资源到缓存的信息。

  • 注意第 7 行的安装模式是prefetch。它使服务工作器能够在开始时下载所有资产,而不管它们是否被立即利用。在一个示例中,一些 CSS 或 JS 文件可能在加载时不使用。它们只能在导航到不同的路由或页面后使用。但是,预回迁安装模式会下载整个文件列表。

    考虑到这些文件构成了应用,在开始时下载整个资产组是合适的。不要总是使用这种安装模式,因为它可能会导致大量的网络请求,降低应用的速度并产生冗余的网络流量。

  • 注意第 18 行和第 28 行之间的第二个资产组。

    1. 该资产组被命名为assets(第 19 行)。这些是静态资源,通常包括图像、图标、字体等。

    2. 资源文件包括/assets *文件夹下的所有文件。*见第 24 行。注意通配符星号的用法。指目录assets下的所有文件和目录。请记住,在assets目录中,Web Arcade 的每一面都有六个骰子图像。

    3. 见第 25 行。它指示应用缓存给定扩展名列表中的所有文件。扩展名列表指示字体和图像文件。

  • 注意第 20 行*上的安装模式是lazy。*它使服务工作器能够仅在需要时下载文件。与第一个资产组不同,服务工作器仅在应用请求时才开始下载文件。

--- ngsw-config.json ---

01: {
02:   "$schema": "./node_modules/@angular/service-worker/config/schema.json",
03:   "index": "/index.html",
04:   "assetGroups": [
05:     {
06:       "name": "app",
07:       "installMode": "prefetch",
08:       "resources": {
09:         "files": [
10:           "/favicon.ico",
11:           "/index.html",
12:           "/manifest.webmanifest",
13:           "/*.css",
14:           "/*.js"
15:         ]
16:       }
17:     },
18:     {
19:       "name": "assets",
20:       "installMode": "lazy",
21:       "updateMode": "prefetch",
22:       "resources": {
23:         "files": [
24:           "/assets/**",
25:           "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
26:         ]
27:       }
28:     }
29:   ]
30: }

Listing 4-1ngsw-config.json File

Note

第 21 行中的更新模式用于更新应用的服务工作程序的新版本。第六章详细介绍了更新服务工作器的方法和策略。

模式匹配资源到缓存

在清单 4-1 中,注意第 8、9、22 和 23 行中的资源文件路径。他们遵循一种模式。可以想象,在开发应用时,不可能单独列出所有的资源(文件和 URL)。该列表可以是动态的。即使它们都是已知的,在一个大型项目中列出每一项资产也是一项乏味的工作。

使用模式匹配列出资源。以下是模式匹配到文件或 URL 的链接的一些语法:

img/515699_1_En_4_Fig3_HTML.jpg

图 4-3

资产目录

  • 使用两个星号(**)来匹配路径段。这通常是为了包括所有文件和子目录。在一个例子中,assets目录有另一个名为icons的子目录和一个芯片图像列表。见图 4-3 。要包含assets下的所有文件和目录,请使用/assets/**

  • 要包含任何文件名或任何数量的字符,请使用单个星号(*)。这匹配零个或多个字符。它不包括子目录。

    1. 在一个例子中,assets/*包括目录资产中的所有文件。但是,它不包括图标目录。要显式包含icons目录,可以使用assets/icons/*,它包含了assets/icons/目录下的所有文件。

    2. 在另一个例子中,假设您只需要在目录图标中包含 PNG 文件。你可以使用assets/icons/*.png。这将选择图 4-3 中的所有图标文件。如果目录有文件,比如说icon.jpeg,就会被排除。这是假设。注意,第 13 行和第 14 行遵循相似的模式匹配,包括所有的.js (JavaScript)和.css (CSS)文件。

    3. 您可以重写第 24 行,如清单 4-2 所示。

// Comment- rewriting "/assets/**",

"files": [
  "/assets/*.png",
  "/assets/icons/*.png"
]

Listing 4-2ngsw-config.json File

这是一个特殊的指令,包括目录/assets/assets/icons *下所有扩展名为.png的文件。*最初的语句是通用的,它包括了assets目录下的所有内容。

当您可以包含assets目录下的所有内容时,为什么要编写类似于清单 4-2 的特定模式呢?记住服务工作器的安装阶段。如果单个文件下载失败,它要么全部安装,要么什么都不安装。尽管这不会影响应用的功能,但服务工作器的安装会推迟到下次应用重新加载时进行。这种情况可能发生在低带宽网络上。配置通用模式可能会包含不必要的文件,当这些文件无法下载时,可能会导致服务工作器安装出现问题。因此,尽你所能,具体一点是个好习惯。然而,如果您知道assets目录下的所有内容无论如何都需要缓存,那么使用通用规则并简化配置。通常,这取决于您是使用通用模式还是特定模式。

Note

匹配模式时,两个行项目可能匹配一个文件。一旦找到匹配项,服务工作程序就会缓存或排除某个文件。它不会继续在接下来的几个项目中寻找模式。

假设/assets/**在数组的顶部。匹配assets下的所有文件,特定规则永不运行。因此,在列表底部指定通用规则;特定的规则应该在数组的开头。

到目前为止,您已经看到了包含文件的模式。您可以使用感叹号(!)来匹配排除文件。在一个例子中,假设您想要排除缓存所有的地图文件。映射文件包含 JavaScript 代码的符号,这有助于调试文件的缩小版本。它用于调试,对于用户来说,通过服务工作器缓存这些文件没有任何价值。因此,排除模式为!/**/*.map的地图文件。

请注意,您选择了带有*.map的地图文件,并在开头用感叹号将其排除。

Note

要匹配单个字符,使用?。我们很少能如此具体地知道一个文件或目录名中的字符数。因此,在ngsw-config.json中很少使用。

浏览器支持

考虑图 4-4 ,它描述了服务工作器的浏览器支持及其特性。注意,数据是在 Mozilla 上捕获的。org 网站,在 https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker 。对于 web 技术来说,这是一个可靠的开源平台。Mozilla 是开放网络的倡导者,也是包括 Firefox 浏览器在内的安全免费互联网技术的先驱。

img/515699_1_En_4_Fig4_HTML.png

图 4-4

服务工作器浏览器支持

Note

CanIUse.com 是浏览器兼容性的另一个优质信息来源。作为 Mozilla 的替代产品,请尝试使用 https://caniuse.com/serviceworkers 了解更多信息。

摘要

本章介绍了服务工作器和服务工作器生命周期。它们是在浏览器后台运行的代理。服务工作器拦截来自应用的所有网络请求。服务工作器缓存静态和动态资源,并以编程方式使用缓存的应用脚本、图像、数据等。它们使应用即使在与网络断开连接和在低速网络上也能运行。

Exercise

  • 在 Web Arcade 中选择并使用谷歌字体。启用服务工作器缓存字体文件。不要将字体复制到项目中。使用 CDN 位置。

  • 假设一个生产应用的发布被安排在每个季度一次。不管服务工作器功能更新的频率如何,图标、图像和样式表可能每个季度都会发生变化。每 12 周使此类资源的缓存过期。

  • 浏览并查看安装在您最喜欢的浏览器中的所有服务工作器。

五、服务工作器缓存数据

服务工作器用于缓存数据响应。到目前为止,您已经看到了如何创建一个新的 Angular 应用,如何将应用配置为可安装的,以及如何缓存应用,以便即使在脱机时也可以访问它。本章介绍了如何缓存来自 HTTP 服务的数据响应。

本章首先创建一个新组件来检索和显示来自 HTTP 服务的数据。接下来,讨论如何创建一个接口,作为服务和 Angular 应用之间的契约。接下来,您将学习如何创建 Node.js Express 模拟服务,为 Angular 应用提供数据。它在 Angular 应用之外的独立进程中运行。本章详细介绍了如何创建一个 Angular 服务,它使用一个现成的HttpClient服务来调用 HTTP 服务。

既然您已经集成了 HTTP 服务并访问了数据,本章将详细介绍如何配置 Web Arcade 来缓存数据响应。它详细描述了配置,并展示了一个带有模拟离线浏览器的缓存数据响应。

记住,网络街机是一个在线游戏系统。想象一个列出应用上可用棋盘游戏的屏幕,如图 5-1 所示。按照说明来构建这个组件。它在 HTML 表格中显示数据。在加载页面时,组件调用服务来检索 Web Arcade 棋盘游戏。

img/515699_1_En_5_Fig1_HTML.jpg

图 5-1

棋盘游戏列表

添加一个组件来列出棋盘游戏

首先创建一个列出棋盘游戏的组件。记住第三章中的“Angular 组件”一节。通过运行以下命令创建一个新组件。它将为新组件搭建支架。

% ng generate component components/board-games

之前,您在App组件*中使用了dice组件。*更新它以使用新的组件,如清单 5-1 所示。注意被称为wade-dicedice组件已经被注释了。

<div class="container align-center">
   <!-- <wade-dice></wade-dice> -->
   <wade-board-games></wade-board-games>
</div>

Listing 5-1Use the Board Component

Note

Angular 单页应用(SPAs)使用路由在具有独立组件的两个页面之间导航。清单 5-1 是临时的,所以本章的重点是数据缓存。第章第八章介绍 Angular 路由。

定义棋盘游戏的数据结构

接下来,定义棋盘游戏页面的数据结构。您创建一个 TypeScript 接口来定义数据结构。它定义了棋盘游戏数据对象的形状。TypeScript 使用一个接口来定义一个契约,这在 Angular 应用中以及与提供棋盘游戏数据的外部远程服务一起使用时非常有用。

TypeScript 接口强制执行棋盘游戏所需的字段列表。如果由于远程服务中的问题或 Angular 应用中的错误导致所需字段丢失,您将会注意到一个错误。接口充当 Angular 应用和外部 HTTP 服务之间的契约。

运行以下命令创建接口。它在名为common的新目录中创建了一个名为board-games-entity.ts的新文件。典型地,数据结构/实体在 Angular 应用中使用。因此,将这个目录命名为common

ng generate interface common/board-games-entity

清单 5-2 定义了棋盘游戏的特定区域。远程服务应该返回相同的字段。组件对数据使用这种形状和结构。将代码添加到board-games-entity.ts

export interface BoardGamesEntity {
   title: string;
   description: string;
   age: string;
   players: string;
   origin: string;
   link: string;
   alternateNames: string;
}

/* Multiple games data returned, hence creating an Array */
export interface GamesEntity {
   boardGames: Array<BoardGamesEntity>;
}

Listing 5-2Interfaces for Board Games

BoardGamesEntity代表单一的棋盘游戏。考虑到 Web Arcade 将有多个游戏,GamesEntity包括了一系列的棋盘游戏。后来,GamesEntity可以扩展到网络街机系统中其他类别的游戏。

模拟数据服务

一个典型的服务是从数据库或后端系统中检索和更新数据,这超出了本书的范围。然而,为了与 RESTful 数据服务集成,本节将详细介绍如何开发模拟响应和数据对象。模拟服务以 JavaScript 对象符号(JSON)格式返回棋盘游戏数据。它可以很容易地与在前面的“向列表棋盘游戏添加组件*”一节中创建的 Angular 组件集成。*

您将使用 Node.js 的 Express 服务器来开发模拟服务。按照这些说明创建新服务。

使用 Express application generator 轻松生成 Node.js Express 服务。运行以下命令进行安装:

npm install --save-dev express-generator

# (or)

yarn add --dev express-generator

Note

注意带有npm命令的--save-dev选项和带有yarn命令的--dev选项。它在package.jsondev-dependencies安装这个包,使它成为一个开发工具。它不会包含在生产版本中,这有助于减少内存占用。参见清单 5-3 ,第 15 行。

01: {
02:  "name": "web-arcade",
03:  "version": "0.0.0", /* removed code for brevity */
04:  "dependencies": {
05:    "@angular/animations": "~12.0.1",
06:    /* removed code for brevity */
07:    "zone.js": "~0.11.4"
08:  },
09:  "devDependencies": {
10:    "@angular-devkit/build-angular": "~12.0.1",
11:    "@angular/cli": "~12.0.1",
12:    "@angular/compiler-cli": "~12.0.1",
13:    "@types/jasmine": "~3.6.0",
14:    "@types/node": "¹².11.1",
15:    "express-generator": "⁴.16.1",
16:    "jasmine-core": "~3.7.0",
17:    "karma": "~6.3.0",
18:    "karma-chrome-launcher": "~3.1.0",
19:    "karma-coverage": "~2.0.3",
20:    "karma-jasmine": "~4.0.0",
21:    "karma-jasmine-html-reporter": "¹.5.0",
22:    "typescript": "~4.2.3"
23:  }
24: }

Listing 5-3Package.json dev-dependencies

接下来,为模拟服务创建一个新目录;命名为mock-services(一个任意的名字)。将目录更改为mock-services。运行以下命令创建新的快速服务。它构建了新的 Node.js Express 应用。

npx express-generator

Note

npx命令首先检查包的本地node_modules。如果找不到,该命令会将包下载到本地缓存并运行该命令。

前面的命令运行,即使没有前面步骤(npm install --save-dev express-generator)中的dev-dependency安装。如果您不打算经常运行这个命令,您可以跳过dev-dependency安装。

接下来,运行mock-services目录中的npm install(或yarn install)。

在 JSON 文件中创建和保存棋盘游戏数据。代码示例将其保存到[application-directory]/mock-services/data/board-games.json。服务器端 Node.js 服务将这些字段和值返回给 Angular 应用。该结构与清单 5-2 中定义的角接口结构相匹配。参见清单 5-4 。

{
   "boardGames": [
       {
           "title": "Scrabble",
           "description": "A crossword game commonly played with English alphabets and words",
           "age": "5+",
           "players": "2 to 5",
           "origin": "Started by an architect named Alfred Mosher Butts in the year 1938",
           "link": "https://simple.wikipedia.org/wiki/Scrabble",
           "alternateNames": "Scrabulous (a version of the game on Facebook)"
       },

       {
           "title": "Checkers",
           "description": "Two players start with dark and light colored pieces. The pieces move diagonally.",
           "age": "3+",
           "players": "Two players",
           "origin": "12th century France",
           "link": "https://simple.wikipedia.org/wiki/Checkers",
           "alternateNames": "Draughts"
       }

/* You may extend additional mock games data*/
   ]
}

Listing 5-4Board Games Mock Data

接下来,更新模拟服务应用以返回以前的棋盘游戏数据。在mock-services/routes下创建一个名为board-games.js的新文件。添加清单 5-5 中的代码。

01: var express = require('express'); // import express
02: var router = express.Router(); // create a route
03: var boardGames = require('../data/board-games.json');
04:
05: /* GET board games listing. */
06: router.get('/', function(req, res, next) {
07:     res.setHeader('Content-Type', 'application/json');
08:     res.send(boardGames);
09: });
10:
11: module.exports = router;

Listing 5-5New API Endpoint That Returns Mock Board Games Data

考虑以下解释:

  • 第 3 行在一个变量上导入并设置棋盘游戏模拟数据。

  • 第 6 到 9 行创建了返回棋盘游戏数据的端点。

  • 注意第 6 行中的get()函数。端点响应 HTTP GET 调用,该调用通常用于检索数据(与创建、更新或删除相对)。

  • 第 7 行将响应内容类型设置为application/json,确保客户端浏览器准确地解释响应格式。

  • 第 8 行用棋盘游戏数据响应客户机。

  • 第 11 行导出了封装服务端点的路由器实例。

接下来,端点需要与路由相关联,以便在客户端请求数据时调用前面的代码。在服务应用(mock-services/app.js)的根目录下编辑app.js。将清单 5-6 中粗体显示的代码行(第 9 行和第 25 行)添加到文件中。

07: var indexRouter = require('./routes/index');
08: var usersRouter = require('./routes/users');
09: var boardGames = require('./routes/board-games');
10:
11: var app = express();
12:
13: // view engine setup
14: app.set('views', path.join(__dirname, 'views'));
15: app.set('view engine', 'jade');
16:
17: app.use(logger('dev'));
18: app.use(express.json());
19: app.use(express.urlencoded({ extended: false }));
20: app.use(cookieParser());
21: app.use(express.static(path.join(__dirname, 'public')));
22:
23: app.use('/', indexRouter);
24: app.use('/users', usersRouter);
25: app.use('/api/board-games', boardGames);

Listing 5-6Integrate the New Board Games Endpoint

考虑以下解释:

  • 第 9 行导入了在前面的清单 5-5 中导出的棋盘游戏路由实例。

  • 第 25 行将路由/api/board-games添加到应用中。当客户端调用这个端点时,新服务被调用。

使用命令npm start *运行模拟服务。*默认情况下,它在端口 3000 上运行 Node.js Express 服务应用。通过访问http://localhost:3000/api/board-games *访问新端点。*见图 5-2 。

img/515699_1_En_5_Fig2_HTML.jpg

图 5-2

通过浏览器访问棋盘游戏端点

Note

请注意,您正在一个单独的端口 3000 上运行服务应用。记住,在前面的例子中,Angular 应用运行在端口 8080(使用 Http-Server)和 4200(使用在内部使用 Webpack 的ng serve命令)上。运行在其中一个端口上的 Angular 应用应该连接到运行在端口 3000 上的服务实例。

调用 Angular 应用中的服务

本节详细介绍了如何更新 Angular 应用来使用 Node.js 服务中的数据。在典型的应用中,Node.js 服务是从数据库或其他服务访问数据的服务器端远程服务。

在 Angular 应用中配置服务

Angular 提供了一种简单的方法来配置各种值,包括远程服务 URL。在 Angular 项目中,注意目录src/environment。默认情况下,您将看到以下内容:

  • environment.ts:这是开发人员在本地主机上使用的调试构建配置。通常,ng serve命令会使用它。

  • environment.prod.ts:这是针对生产部署的。运行ng build(或yarn buildnpm run build)使用这个配置文件。

编辑文件src/environments/environment.ts并添加清单 5-7 中的代码。它有一个到服务端点的相对路径。

1: export const environment = {
2:     boardGameServiceUrl: `/api/board-games`,
3:     production: false,
4:   };
5:

Listing 5-7Integrate the New Board Games Endpoint

考虑以下解释:

  • 第 2 行添加了服务端点的相对路径。在调用服务时,您将导入并使用配置字段boardGameServiceUrl

  • 第 3 行将production设置为false。记住,文件environment.tsng serve命令一起使用,后者在 Webpack 的帮助下运行一个调试版本。它在替换环境文件environment.prod.ts中被设置为true

创建有 Angular 的服务

Angular 服务是可重用的代码单元。Angular 提供了创建服务、实例化服务以及将服务注入组件和其他服务的方法。Angular 服务有助于分离关注点。Angular 组件主要关注表示逻辑。另一方面,您可以将服务用于其他不包括表示的可重用功能。请考虑以下示例:

  • 服务可以用于在组件之间共享数据。想象一个有用户列表的屏幕。假设列表由一个UserList组件显示。用户可以选择一个用户。应用导航到另一个屏幕,加载另一个组件,比如说UserDetails。“用户详细信息”组件显示系统中用户的附加信息。用户详细信息组件需要关于所选用户的数据,以便它可以检索和显示附加信息。

您可以使用服务来共享选定的用户信息。第一个组件将选定的用户详细信息更新到公共服务。第二个组件从同一个服务中检索数据。

Note

服务是在组件之间共享数据的一种简单易行的方式。然而,对于大型应用,建议采用 Redux 模式。它有助于维护应用状态,确保单向数据流,提供选择器以便于访问 Redux 存储中的状态,并具有更多功能。对于 Angular,NgRx 是一个流行的库,它实现了 Redux 模式及其概念。

组件如何共享同一个服务实例?有关如何提供 Angular 服务以及如何在 Angular 应用中管理服务实例的详细信息,请参见下一节。

  • 服务可以用来聚集和转换 JSON 数据。Angular 应用可能从各种数据源获取数据。创建一个具有可重用功能的服务来聚合和返回数据。这使得组件可以很容易地将 JSON 对象用于表示。

  • 服务用于从远程 HTTP 服务中检索数据。在这一章中,您已经构建了一个与 Angular 应用共享棋盘游戏数据的服务。在单独的进程中运行的 Node.js Express 服务器(理想情况下在远程服务器上)通过 HTTP GET 调用共享这些数据。

通过运行以下 Angular CLI 命令创建新服务。您将使用这个服务来调用在上一节中构建的api/board-games服务。

ng generate service common/games

CLI 命令创建新的游戏服务。它在目录common中创建以下文件:

  • common/games.services.ts:添加 Angular 服务代码的 TypeScript 文件,Angular 服务代码对游戏数据进行 HTTP 调用

  • common/games.services.spec.ts:针对games.service.ts中函数的单元测试文件

考虑为游戏服务列出 5-8 。添加一个名为getBoardNames()的新函数来调用 HTTP 服务。

01: @Injectable({
02:     providedIn: 'root'
03: })
04: export class GamesService {
05:
06:     constructor() { }
07:
08:     getBoardGames(){
09:     }
10: }

Listing 5-8Angular Service Skeleton Code

提供服务

注意第 1 行到第 3 行中的代码语句。这些行包含Injectable装饰器,而provideIn位于根级别。Angular 为整个应用共享一个实例。以下是备选方案:

  • 在模块级提供:服务实例在模块内可用并共享。后面的章节给出了更多关于 Angular 模块的细节。

  • 在组件级提供:服务实例被创建并可用于组件及其所有子组件。

服务一旦提供,就需要注入。一个服务可以被注入到一个组件或另一个服务中。在当前示例中,棋盘游戏组件需要数据,以便列出游戏供用户查看。注意,在前面的清单 5-8 中,代码创建了一个名为getBoardGames()的新函数,用于从远程 HTTP 服务中检索列表。

GamesService注入BoardGamesComponent,如清单 5-9 第 5 行所示。构造函数创建了一个名为gameService的新字段,类型为GamesService。该语句将服务注入到组件中。

01: export class BoardGamesComponent implements OnInit {
02:
03:     games = new Observable<GamesEntity>();
04:
05:     constructor(private gameService: GamesService) { }
06:
07:     ngOnInit(): void {
08:       this.games = this.gameService.getBoardGames();
09:     }
10:
11:   }

Listing 5-9Inject Games Service into a Component

Note

第 7 行的ngOnInit()函数是一个 Angular 生命周期钩子。它在框架完成组件及其属性的初始化后被调用。这个函数非常适合在组件中进行额外的初始化,包括服务调用。

清单 5-9 中的第 8 行调用检索棋盘游戏数据的服务函数。该数据是组件初始化的一部分,因为组件的主要功能是显示游戏列表。

HttpClient 服务

接下来,调用远程 HTTP 服务。Angular 提供的HttpClient服务是@angular/common/http *套餐的一部分。*它提供了一个 API 来调用各种 HTTP 方法,包括 GET、POST、PUT 和 DELETE。

作为先决条件,从@angular/common/http导入HttpClientModule。将它(HttpClientModule)添加到 Angular 模块的导入列表中,如清单 5-10 ,第 7 行和第 13 行所示。

01: import {HttpClientModule} from '@angular/common/http';
02:
03: @NgModule({
04:   declarations: [
05:  // pre-existing declaratoins
06:   ],
07:   imports: [
08:    // pre-existing imports
09:     BrowserModule,
10:     HttpClientModule,
11:     AppRoutingModule,
12:
13:   ],
14:   providers: [],
15:   bootstrap: [AppComponent]
16: })
17: export class AppModule { }
18:

Listing 5-10Import HttpClientModule

请记住清单 5-5 (第 6 行)中的服务通过 GET 调用将数据返回给 Angular 应用。因此,我们将在HttpClient实例上使用get()函数来调用服务。记住,我们已经创建了函数getBoardGames()作为GamesService的一部分(参见清单 5-8 ,第 8 行)

接下来,将HttpClient服务注入到GamesService中,并使用get() API 进行 HTTP 调用。参见清单 5-11 。

01: import { Injectable } from '@angular/core';
02: import { HttpClient } from '@angular/common/http';
03: import { environment } from 'src/environments/environment';
04: import { GamesEntity } from './board-games-entity';
05: import { Observable } from 'rxjs';
06:
07:
08: @Injectable({
09:   providedIn: 'root'
10: })
11: export class GamesService {
12:
13:   constructor(private client: HttpClient) { }
14:
15:   getBoardGames(): Observable<GamesEntity>{
16:     return this
17:       .client
18:       .get<GamesEntity>(environment.boardGameServiceUrl);
19:   }
20: }
21:

Listing 5-11GamesService Injects and Uses HttpClient

考虑以下解释:

  • 第 13 行将HttpClient注入GamesService *。*注意这个字段的名字(HttpClient的一个实例)是client。它是一个私有字段,因此只能在服务类中访问。

  • 第 16 到 18 行的语句调用了client.get() API。因为客户机是该类的一个字段,所以使用this关键字来访问它。

  • get()函数接受一个参数,即服务的 URL。注意第 3 行中环境对象的 import 语句。它导入从环境配置文件导出的对象。参见清单 5-7 。它是环境配置文件之一。使用配置中的boardGameServiceUrl字段(列表 5-11 ,第 18 行)。您可能在环境文件中配置了多个 URL。

  • 请注意,get()函数应该检索GamesEntity。它是在清单 5-2 中创建的。

  • getBoardGames()函数返回一个Observable<GamesEntity> Observable 对于异步函数调用很有用。远程服务可能需要一些时间来返回数据,比如几毫秒或者几秒钟。因此,服务函数返回一个可观察的。订户提供函数回调。一旦数据可用,观察对象就执行函数回调。

  • 注意,第 16 行返回了get()函数调用的输出。它返回一个指定类型的Observable。您在第 18 行指定了类型GamesEntity。因此,它返回一个类型为GamesEntityObservable。与第 15 行getBoardGames()的返回类型匹配。

现在,服务功能准备好了。再次回顾清单 5-9 ,它是一个组件 TypeScript 类。它调用服务函数并将类型Observable<GamesEntity>的返回值设置为一个类字段。class 字段使用 HTML 模板中返回的对象。模板文件在页面上呈现棋盘游戏列表。参见清单 5-12 。

01: <div>
02:     <table>
03:         <tr>
04:             <th> Title </th>
05:             <th> History </th>
06:         </tr>
07:         <ng-container *ngFor="let game of (games | async)?.boardGames">
08:             <tr>
09:                 <td>
10:                     <strong>
11:                         {{game.title}}
12:                     </strong>
13:                     <span>{{game.alternateNames}}</span>
14:                 </td>
15:                 <td>{{game.origin}}</td>
16:             </tr>
17:             <tr >
18:                 <td class="last-cell" colspan="2">{{game.description}}</td>
19:             </tr>
20:         </ng-container>
21:
22:     </table>
23: </div>

Listing 5-12Board Games Component Template Shows List of Games

考虑以下解释:

  • 该模板将列表呈现为 HTML 表格。

  • 注意,在第 7 行中,*ngFor指令遍历了boardGames *。*见清单 5-2 。注意,boardGames是接口GamesEntity上的一个数组。

  • 该模板显示了实体中每个游戏的字段。请参见第 11、13、15 和 18 行。它们显示了字段titlealternateNamesorigindescription

  • 记住,类字段games是用服务返回的值设置的。该字段在模板中使用。见第 7 行。

  • 注意第 7 行带有async ( | async)的管道。它应用在Observable上。记住,服务返回一个Observable。如前所述,Observable对于异步函数调用非常有用。远程服务可能需要几毫秒或者几秒钟的时间来返回数据。当数据可用时,换句话说,当从服务获得数据时,模板使用games Observable上的字段boardGames

缓存棋盘游戏数据

到目前为止,我们已经创建了一个 HTTP 服务来提供棋盘游戏数据,创建了一个 Angular 服务来使用 HTTP 服务获取数据,并添加了一个新组件来显示列表。现在,配置服务工作器来缓存棋盘游戏数据(甚至其他 HTTP 服务响应)。

记住,在上一章中,我们列出了各种 Angular 维修工人的配置。如您所见,Angular 使用一个名为ngsw-config.json的文件进行服务工作器配置。在本节中,您将添加一个dataGroups部分来缓存 HTTP 服务数据。请参见清单 5-13 了解缓存棋盘游戏数据的新配置。

01: "dataGroups": [{
02:     "name": "data",
03:     "urls": [
04:       "api/board-games"
05:     ],
06:     "cacheConfig": {
07:       "maxAge": "36h",
08:       "timeout": "10s",
09:       "maxSize": 100,
10:       "strategy":"performance"
11:     }
12:   }]

Listing 5-13Data Groups Configuration for a Service Worker in an Angular Application

考虑以下解释:

  • 第 4 行配置服务 URL 来缓存数据。它是一个数组,我们可以在这里配置多个 URL。

  • URL 支持匹配模式。例如,您可以使用api/*来配置所有的 URL。

  • 作为缓存配置的一部分(cacheConfig),参见第 10 行。将strategy设置为performance。这指示服务工作器首先使用缓存的响应以获得更好的性能。或者,您可以使用freshness,它首先进入网络,仅在应用离线时使用缓存。

  • 注意maxAge被设置为 36 小时,在此之后,服务工作器清除缓存的响应(棋盘游戏)。缓存数据时间过长可能会导致应用使用过时的字段和记录。服务工作器配置提供了一种定期自动清除数据的机制,确保应用不会使用过时的数据。

  • 超时设置为 10 秒。这个要看strategy。假设strategy被设置为freshness,10 秒钟后,服务工作器使用缓存的响应。

  • maxSize设置为 100 条记录。通过设计来限制大小是一个很好的做法。浏览器(像其他平台一样)为每个应用管理和分配内存。如果应用超出上限,整个数据集和缓存都可能被收回。

清单 5-13 有一个单一的数据组配置对象。随着我们进一步开发应用,额外的服务可能会有稍微不同的缓存需求。例如,玩家列表可能需要是最新的。如果你的朋友加入了街机,你更愿意看到她被列出来而不是显示旧的列表。因此,您可以将策略更改为freshness *。*将这个 URL 配置作为另一个对象添加到dataGroups数组中。另一方面,对于适合当前配置的服务,将 URL 添加到第 4 行的urls字段。

运行 Angular 构建并启动 Http-Server 来查看变化。请参见以下命令:

yarn build && http-server dist/web-arcade --proxy http://localhost:3000

请参见图 5-3 了解服务工作器缓存的服务响应。

img/515699_1_En_5_Fig3_HTML.png

图 5-3

服务工作器缓存的服务响应

Angular 模块

传统上,Angular 有自己的模块化系统。新框架(Angular 2 和更高版本)使用 NgModules 为应用带来模块化。Angular 模块封装了包括组件、服务、管道等在内的指令。创建 Angular 模块以对特征进行逻辑分组。见图 5-4 。

img/515699_1_En_5_Fig4_HTML.png

图 5-4

Angular 模块

所有 Angular 应用至少使用一个根模块。通常,该模块被命名为AppModule,并在src/app/app.module.ts中定义。一个模块可以导出一个或多个功能。应用中的其他模块可以导入导出的组件和服务。

Note

Angular 模块独立于 JavaScript (ES6)模块。它们相辅相成。Angular 应用同时使用 JavaScript 模块和 Angular 模块。

摘要

本章提供了为棋盘游戏列表创建新组件的说明。通过这个代码示例,它演示了服务工作器如何缓存来自 HTTP 服务的数据响应。它提供了使用 Angular CLI 创建棋盘游戏组件的说明。您还更新了应用以使用这个新组件来代替dice

它还定义了 Angular 应用和外部 HTTP 服务之间的数据契约,详细介绍了如何创建 Node.js Express 服务来为 Angular 应用提供数据,并介绍了 Angular 服务。

Exercise

  • 在 Node.js Express 应用中创建一个新的路由,用于显示拼图游戏列表。

  • 创建一个 Angular 服务来使用新的 jigsaw puzzles 服务端点并检索数据。

  • 确保最新的拼图数据对用户可用。仅在用户离线或失去连接时缓存。

  • 对于新服务,将配置为如果服务在一分钟后没有响应,则使用缓存中的数据。

六、升级应用

到目前为止,您已经创建了一个 Angular 应用、注册的服务工作器和缓存的应用资源。本章详细介绍了如何发现应用的更新,如何与用户通信,以及如何处理事件以顺利升级到下一版本。

本章广泛使用 Angular 的SwUpdate服务,该服务提供了识别和升级应用的现成特性。它从包含(导入和注入)服务的指令开始。接下来,它详细介绍了如何识别可用的升级并激活升级。它还详细介绍了如何定期检查升级。最后,本章详细介绍了如何处理边缘情况,即浏览器清理未使用的脚本时的错误场景。

考虑到 Web Arcade 应用是可安装的,您需要一种机制来寻找更新,通知用户应用的新版本,并执行升级。服务工作器管理 Angular 应用的安装和缓存。本章详细介绍了如何使用SwUpdate,这是 Angular 提供的一种开箱即用的服务,旨在简化服务工作器的沟通。当新版本的应用可用、下载和激活时,它提供对事件的访问。您可以使用此服务中的功能定期检查更新。

SwUpdate 入门

本节向您展示如何通过导入和注入服务来开始使用SwUpdate服务。SwUpdate服务是 Angular 模块ServiceWorkerModule的一部分,已经在AppModule的导入列表中引用。在app.module.ts文件中验证示例应用中的代码。当您运行第二章中的 Angular CLI ng add @angular/pwa命令时,它已包含在内。考虑清单 6-1 ,第 15 行和第 22 行。

01: import { NgModule } from '@angular/core';
02: import { BrowserModule } from '@angular/platform-browser';
03: import { environment } from '../environments/environment';
04: import { ServiceWorkerModule } from '@angular/service-worker';
05: import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
06:
07: import { AppComponent } from './app.component';
08:
09: @NgModule({
10:   declarations: [
11:     AppComponent,
12:   ],
13:   imports: [
14:     BrowserModule,
15:     ServiceWorkerModule.register('ngsw-worker.js', {
16:       enabled: environment.production,
17:       // Register the ServiceWorker as soon as the app is stable
18:       // or after 30 seconds (whichever comes first).
19:       registrationStrategy: 'registerWhenStable:30000'
20:     }),
21:     BrowserAnimationsModule
22:   ],
23:   providers: [],
24:   bootstrap: [AppComponent]
25: })
26: export class AppModule { }
27:

Listing 6-1ServiceWorkerModule Imported in AppModule

确保ServiceWorkerModule如图所示导入(粗体)。这使得SwUpdate服务可以随时使用。创建一个新的 Angular 服务来封装代码,以便识别服务工作器的新版本、与用户通信以及管理细节。这有助于代码的可重用性和关注点的分离。要安装该服务,请运行以下命令:

ng g s common/sw-communication

Note

在代码片段中,g是“生成”的缩写,s是“服务”的缩写。

可以把前面的命令改写成ng generate service common/sw-communication

Angular CLI 命令在目录common中创建了名为SwCommunicationService的新服务。默认情况下,它是在根级别提供的。将SwUpdate服务导入并注入SwCommunicationService,如清单 6-2 所示。

01: import { Injectable } from '@angular/core';
02: import { SwUpdate } from '@angular/service-worker';
03:
04: @Injectable({
05:   providedIn: 'root'
06: })
07: export class SwCommunicationService {
08:   constructor(private updateSvc: SwUpdate)
09:   }
10: }

Listing 6-2Scaffolded SwCommunicationService

2、8 号线导入注入SwUpdate角服务。第 5 行提供了根级别的服务。虽然服务是在根级别提供的,但它还没有在应用中使用。与 Web Arcade 中创建的其他服务不同,它需要在后台运行,当您启动应用时或定期运行。因此,在根组件AppComponent中导入并注入swCommunicationService,如清单 6-3 ,第 2 行和第 9 行所示。

01: import { Component } from '@angular/core';
02: import { SwCommunicationService } from 'src/app/common/sw-communication.service';
03: @Component({
04:   selector: 'app-root',
05:   templateUrl: './app.component.html',
06:   styleUrls: ['./app.component.sass']
07: })
08: export class AppComponent {
09:   constructor(private commSvc: SwCommunicationService){
10:   }
11: }

Listing 6-3Import and Inject SwCommunicationService in AppComponent

识别应用的更新

接下来,更新SwCommunicationService以识别应用的更新版本是否可用。SwUpdate服务提供了一个名为available的可观察对象。订阅这个可观察的。当服务工作器识别出应用的更新版本时,就会调用它。

Note

此时,您已经知道服务器上有可用的升级。您尚未下载并激活它。

考虑列出 6-4 。

1: export class SwCommunicationService {
2:   constructor(private updateSvc: SwUpdate
3:    ){
4:     this.updateSvc.available.subscribe( i => {
5:       console.log('A new version of the application available', i.current, i.available);
6:     });
7:   }
8: }

Listing 6-4Identify a New Version of the Application

考虑以下解释:

img/515699_1_En_6_Fig1_HTML.png

图 6-1

基于可观察数据的结果

  • 如何在updateSvc上使用对象available(SwUpdate的一个对象),见第 4 行。它是可观察的,当应用的新版本可用时,它发送值。

  • 第 5 行打印currentavailable对象。考虑图 6-1 中的结果。请注意消息中的版本号。

Note

清单 6-4 不会启动对新版本的检查。当一个新版本被识别时,订阅回调(第 5 行)运行。请参阅“检查新版本”一节,定期检查新版本。

另外,新版本还没有下载和激活。

  • 记住应用的ngsw-config.json中的appData字段。(参见第四章。)对象currentavailable包括来自appData *的数据。*在示例应用中,我们添加了一个描述应用变化的字段名称。图 6-1 中的结果打印出currentavailable对象。它们是应用当前版本和新版本中来自ngsw-config.json的字段。ngsw-config.json见清单 6-5 。
01: {
02:   "appData": {"name": "New games available and few bug fixes"},
03:   "$schema": "./node_modules/@angular/service-worker/config/schema.json",
04:   "index": "/index.html",
05:   "assetGroups": [
06:     {
07:       // Removed code for brevity. See code sample for the complete file.
42:     }]
43: }

Listing 6-5ngsw-config.json with appData

如前所述,available观察者验证服务工作器的新版本是否已经准备好并可用。这并不意味着它还在使用中。您可以提示用户更新应用。这为用户提供了完成当前工作流的机会。例如,在 Web Arcade 中,您不希望在用户玩游戏时重新加载并升级到新版本。

识别更新被激活的时间

到目前为止,您已经确定了升级是否可用。此步骤允许您识别已激活的升级。一旦服务工作器开始提供新版本应用的内容,就会触发activated观察。

考虑在SwCommunicationService中列出使用activated可观测值的 6-6 。

01: export class SwCommunicationService {
02:
03:     constructor(private updateSvc: SwUpdate) {
04:       this.updateSvc
05:         .available
06:         .subscribe( i => {
07:             console.log('A new version of the application available', i.current, i.available);
08:       });

09:       this.updateSvc
10:         .activated
11:         .subscribe( i =>
12:             console.log('A new version of the application activated', i.current, i.previous));
13:     }
14:
15: }

Listing 6-6Activated Observable on SwUpdate

请参见第 9 行到第 13 行。请注意,您订阅了activated可观察值。它在激活应用的新版本时被触发。见第 12 行的console.log。这印出了currentprevious和*。*类似于available可观察对象上的currentavailable对象(见清单 6-4 ),激活前。由于激活的可观察对象是在激活后触发的,所以可用版本现在是当前版本。结果如图 6-2 所示。

img/515699_1_En_6_Fig2_HTML.png

图 6-2

激活的可观察对象的结果

Note

类似于available可观察对象,currentprevious对象包括来自ngsw-config.jsonappData,用于应用的各个版本。

使用 SwUpdate 服务激活

当用户在新窗口中打开应用时,服务工作器会检查是否有新版本可用。服务工作器可能仍然从缓存中加载应用。这是因为新版本可能尚未下载和激活。它触发了available事件。对available可观察对象的subscribe回调将被调用(类似于清单 6-4 )。通常,下一次用户试图在新窗口中打开应用时,服务工作器和应用的较新版本被提供。确切的行为取决于配置和其他一些因素。

但是,一旦知道有新版本可用,您可以选择激活新版本。SwUpdate服务提供了activateUpdate() API 来激活应用的新版本。该函数返回一个promise<void>??。激活更新后,调用承诺的成功回调。考虑清单 6-7 ,它激活了更新。

Note

该部分并不提示用户选择更新新版本。当您进行到下一部分时,您将添加代码来提醒用户有新版本的应用可用。

01: export class SwCommunicationService {
02:
03:     constructor(private updateSvc: SwUpdate) {
04:       this.updateSvc
05:         .available
06:         .subscribe( i => {
07:             console.log('A new version of the application available', i.current, i.available);
08:             this.updateSvc
09:                 .activateUpdate()
10:                 .then( () => {
11:                     console.log("activate update is successful");
12:                     window.location.reload();
13:                 });
14:       });
15:
16:       this.updateSvc
17:         .activated
18:         .subscribe( i => console.log('A new version of the application activated', i.current, i.previous));
19:     }
20:   }

Listing 6-7Activate Update with the SwUpdate Service

请参见第 8 行和第 13 行。第 9 行调用了activateUpdate()函数。它回报一个承诺。对返回的承诺调用then()函数。您提供了一个回调,该回调在承诺完成后被调用。参见第 11 行,它显示了一条消息“激活更新成功”

请注意,当有更新可用时,会调用activateUpdate()。在available可观察订阅中调用activateUpdate()方法。请参见第 6 行和第 14 行。这是对available可观察对象的订阅回调。

第 12 行在更新激活后重新加载应用(浏览器窗口)。成功更新后重新加载浏览器是一个很好的做法。在少数情况下,如果窗口在服务工作器和缓存刷新后没有重新加载,延迟加载路由可能会中断。

检查新版本

SwUpdates服务可以在服务器上检查应用的新版本(服务工作器配置)。如前所述,到目前为止,如果新版本可用或被激活,您已经收到了事件。如果新版本可用,您激活了它。但是,我们依赖浏览器和服务工作器的内置机制来寻找更新。

可以在SwUpdate的实例上调用函数checkForUpdates()来查找服务器上的更新。如果有新版本,它将触发available观察。如果您预计用户会让应用长时间保持打开状态,有时是几天,那么这个函数就特别有用。也有可能您预期对应用进行频繁的更新和部署。您可以设置一个时间间隔并定期检查更新。

考虑到checkForUpdates()与间隔一起使用,在检查更新之前确保 Angular 应用完全启动并稳定是很重要的。过于频繁地使用此功能可能会导致 Angular 应用不稳定。因此,检查应用是否稳定并使用checkForUpdates是一个好的实践,如清单 6-8 所示。它被添加到SwCommunicationService中。

01: import { SwUpdate } from '@angular/service-worker';
02: import { ApplicationRef, Injectable } from '@angular/core';
03: import { first} from 'rxjs/operators';
04: import { interval, concat } from 'rxjs';
05:
06: @Injectable({
07:   providedIn: 'root'
08: })
09: export class SwCommunicationService {
10:
11:   constructor(private updateSvc: SwUpdate,
12:     private appRef: ApplicationRef) {
13:
14:     let isApplicationStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true));
15:     let isReadyForVersionUpgrade$ = concat( isApplicationStable$, interval (12 * 60 * 60 * 1000)); // //twelve hours in milliseconds
16:     isReadyForVersionUpgrade$.subscribe( () => {
17:       console.log("checking for version upgrade...")
18:       this.updateSvc.checkForUpdate();
19:     });
20:   }
21: }
22:

Listing 6-8Invoking CheckForUpdates at Regular Intervals

考虑以下解释:

  • 你注入ApplicationRef来验证 Angular app 是否稳定。第 2 行和第 12 行分别导入和注入该类。

  • 第 13 行验证应用是否稳定。字段isStable的类型为Observable<boolean>。订阅时,当应用稳定时返回 true。第 14 行将可观察值赋给局部变量isApplicationStable$

  • 在第 15 行,interval()函数返回一个Observable<number> *。*该函数接受以毫秒为单位的持续时间作为参数。在指定的时间间隔后调用订阅回调。请注意,代码片段以毫秒为单位指定了 12 小时。

  • 第 15 行将isApplicationStable$interval()返回的可观察值连接起来。由此产生的可观测值被设置为isReadyForVersionUpgrade$ *。*订阅此可观察。当应用稳定并且经过了指定的时间间隔(12 小时)时,调用success回调。

Note

concat函数现已被弃用。如果您使用的是 RxJS 版本 8,使用concatWith()函数连接观察值。

  • 在第 18 行,在可观察对象isReadyForVersionUpgrade$subscribe回调中,您使用SwUpdate实例检查更新。

  • 简单回顾一下,当应用稳定时,每 12 小时检查一次升级。checkForUpdate()函数可能触发available订户,该订户在成功激活新版本的应用后调用触发activate订户的activateUpdate()

通知用户新版本

请注意,当识别出服务工作器的新版本时,当前代码会重新加载应用。它还没有提醒用户,并允许她选择何时重新加载。要提供此选项,请将应用与 Snackbar 组件集成在一起。这是角材库中可用的现成元件。Angular 材料为 Angular 应用提供材料设计实现。

我们为什么选择 Snackbar 组件?典型的警报会阻止用户工作流。在对警报采取措施之前,用户无法继续使用该应用。当没有用户的决定,工作流无法继续时,这样的范例工作得很好。例如,它可能是一个错误场景,这对于用户来说是非常重要的,用户需要确认并做出纠正。

另一方面,当新版本的服务工作程序(和应用)可用时,您不希望中断当前的用户会话。用户可以继续使用当前版本,直到手头的任务完成。例如,如果用户正在 Web Arcade 上玩游戏,则继续玩,直到游戏完成。当用户认为重新加载窗口是合适的时候,她可以选择响应警告。

Snackbar 组件非常适合我们的场景。它在应用的一角显示警告,而不会干扰页面的其余部分。默认情况下,警告显示在页面底部,靠近中央。见图 6-3 。如前所述,您应该允许用户单击 Snackbar 组件上的按钮来安装应用的新版本。

img/515699_1_En_6_Fig3_HTML.png

图 6-3

警告用户有新版本的应用可用的 Snackbar 组件

要安装 Snackbar 组件,请向 Web Arcade 应用添加 Angular 材质。运行以下命令:

ng add @angular/material

Angular Material 是一个 UI 组件,其他组件和样式表都符合 Google 的材质设计。首先,CLI 会提示您选择一个主题。见图 6-4 。

img/515699_1_En_6_Fig4_HTML.jpg

图 6-4

选择有 Angular 的材质主题

接下来,CLI 执行以下操作:

  1. 提示您选择有 Angular 的材料排版。这是一个关于使用有 Angular 的材料字体、默认字体大小等的决定。

  2. 提示您包含与 Angular 材料构件交互的浏览器动画。动画为用户动作提供视觉反馈,例如动画化点击按钮、转换内容标签等。

结果见清单 6-9 。

Package successfully installed.
? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink        [ Preview: https://material.angular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? Yes
? Set up browser animations for Angular Material? Yes
UPDATE package.json (1403 bytes)
✔ Packages installed successfully.
UPDATE src/app/app.module.ts (1171 bytes)
UPDATE angular.json (3636 bytes)
UPDATE src/index.html (759 bytes)
UPDATE node_modules/@angular/material/prebuilt-themes/indigo-pink.css (77575 bytes)

Listing 6-9Result Installing Angular Material

向 Web Arcade 添加 Angular 材质允许您使用各种 Angular 材质组件。您将只导入和使用所需的组件。整个组件库不会增加包的大小。

要使用 Snackbar 组件,请将MatSnackBarModule导入 Web Arcade 中的AppModule。考虑上市 6-10 。

01: import { MatSnackBarModule } from '@angular/material/snack-bar';
02:
03: @NgModule({
04:   declarations: [
05:     AppComponent,
06:     // You may have more components
07:   ],
08:   imports: [
09:     BrowserModule,
10:     AppRoutingModule,
11:     MatSnackBarModule,
12:     // You may have additional modules imported
13:   ],
14:   providers: [],
15:   bootstrap: [AppComponent]
16: })
17: export class AppModule { }

Listing 6-10Add the Snackbar Module to the Web Arcade Application

参见第一行,它从 Snackbar 组件的 Angular Material 模块导入MatSnackBarModule。另请参见第 11 行,它将模块导入 Web Arcade 上的AppModule

接下来,将 Snackbar 组件导入并注入到SwCommunication服务中。请记住,当应用的新版本可用时,您会向用户显示一个警告。它在SwCommunication服务的构造函数中被识别。见清单 6-11 。

01: import { Injectable } from '@angular/core';
02: import { SwUpdate } from '@angular/service-worker';
03: import { MatSnackBar } from '@angular/material/snack-bar';
04:
05: @Injectable({
06:   providedIn: 'root'
07: })
08: export class SwCommunicationService {
10:   constructor(private updateSvc: SwUpdate,
11:     private snackbar: MatSnackBar) {
13:     this.updateSvc.available.subscribe( i => {
14:       let message = i?.available?.appData as { "name": string };
15:       console.log('A new version of the application available', i.current, i.available);
16:       let snackRef = this.snackbar
17:         .open(`A new version of the app available. ${message.name}. Click to install the application`, "Install new version");
18:     });
19:   }
20: }

Listing 6-11Alert with a Snackbar that a new version of the application is available

考虑以下解释:

  • 第 3 行从 Angular Material 的snackbar模块导入 Snackbar 组件。它被注入到第 11 行的服务中。

  • 注意第 14 行和第 18 行之间的success回调。它在available上可以观察到。如前所述,当一个更新就绪时,就会触发这个观察者功能。

  • 见第 16 行。open()函数显示了一个 Snackbar 组件。您需要提供两个参数,即显示在警报上的消息(Snackbar 组件)和 Snackbar 组件上操作(或按钮)的标题。重新查看图 6-3 以匹配代码和结果。

  • 注意,open 函数也返回一个 Snackbar 引用对象。当用户单击 Snackbar 组件上的按钮时,您将使用此对象来执行操作。

  • 注意message.name被插入第 17 行。message对象是在前一行 14 上获得的。注意它是ngsw-config.json上的appData物体。这是为应用的每个版本升级提供友好消息的一种方式,并在用户选择重新加载和安装新版本时向用户显示信息。见图 6-3 。来自appData的消息说,“我们给街机增加了更多的游戏。”

接下来,使用由open()函数返回的 Snackbar 组件引用,如第 16 行所示。参考对象被命名为snackRef。您使用snackRef *上的onAction()功能。*这返回了另一个可观察值。顾名思义,当用户在 Snackbar 组件上执行一个操作时,就会触发 observer 回调函数。请注意,在前面的代码示例中,您有一个操作,即 Snackbar 组件上的按钮“Install new version”。因此,当这个观察者被调用时,您知道用户点击了按钮并可以执行安装。见清单 6-12 。使用修改后的代码,只有在用户通过单击 Snackbar 组件上的按钮选择安装后,您才能执行安装。

01:
02: // include this snippet after snackRef created in the SwCommunicationService
03: snackRef
04: .onAction()
05: .subscribe (() => {
06:   console.log("Snackbar action performed");
07:   this.updateSvc.activateUpdate().then( () => {
08:     console.log("activate update invoked");
09:     window.location.reload();
10:   });
11: });

Listing 6-12Add Snackbar Module to Web Arcade

考虑以下解释:

  • 第 4 行调用了snackRef对象上的onAction()函数。你在返回的对象上链接subscribe()函数。如前所述,onAction()函数返回一个可观察值。

  • 作为观察者提供的成功回调调用SwUpdate对象上的activateUpdate()函数。当用户在 Snackbar 组件上执行操作时,调用第 6 行和第 10 行之间的观察者回调。

  • 请记住,第 6 行和第 10 行之间的代码与清单 6-6 相同,它在识别出新版本后立即执行安装。

在不可恢复的情况下管理错误

用户可能有一段时间没有返回到机器上的应用。浏览器将清空缓存并占用磁盘空间。当用户返回到应用时(在缓存被清理之后),服务工作器可能没有它需要的所有脚本。与此同时,假设在服务器上部署了一个新版本的应用。现在,浏览器无法(从缓存中)获取已删除的脚本,甚至无法从服务器获取。这导致应用处于不可恢复的状态。对用户来说,只剩下一个选择:升级到最新版本。

SwUpdate服务提供了一个名为unrecoverable的可观察对象来处理这样的场景,如清单 6-13 所示。当出现不可恢复的状态时,添加一个错误处理程序。通知用户并重新加载浏览器窗口以清除错误。与前面的代码示例类似,在SwCommunicationService构造函数中添加代码。

01: export class SwCommunicationService {
02:
03:   constructor(private updateSvc: SwUpdate,
04:     private snackbar: MatSnackBar) {
05:     this.updateSvc.unrecoverable.subscribe( i => {
06:       console.log('The application is unrecoverable', i.reason);
07:       let snackRef = this.snackbar
08:       .open(`We identified an error loading the application. Use the following reload button. If the error persists, clear cache and reload the application`,
09:         "Reload");
10:     snackRef
11:       .onAction()
12:       .subscribe (() => {
13:         this.updateSvc.activateUpdate().then( () => {
14:           window.location.reload();
15:         });
16:      });
17:     });
18:   }
19: }

Listing 6-13Handle the Unrecoverable State

考虑以下解释:

  • 见第 5 行。它在SwUpdate(即updateSvc)的实例上订阅了unrecoverable观察者。可观察对象的成功回调位于第 6 行和第 17 行之间。

  • You open a Snackbar component, which alerts the user about the error at the bottom center of the page. See Figure 6-5.

    img/515699_1_En_6_Fig5_HTML.png

    图 6-5

    警告用户不可恢复的错误的 Snackbar 组件

  • 注意第 6 行的原因字段。它有关于不可恢复状态的附加信息。您可以在浏览器控制台中打印和使用这些信息,或者将其记录到一个中心位置,以便进一步调查。

摘要

可安装和缓存的 web 应用非常强大。它们使用户能够在低带宽和离线情况下访问应用。然而,您还需要构建无缝升级应用的特性。如您所知,对于 Web Arcade,服务工作器管理缓存和离线访问功能。

本章详细介绍了如何无缝升级 Angular 应用并与服务工作器进行沟通。它广泛使用 Angular 的SwUpdate服务,该服务为识别和激活新版本的应用提供了许多现成的功能。它使用可观测量;当新版本的应用可用或激活时,您可以进行订阅。

Exercise

  • 本章使用 Snackbar 组件在应用的新版本可用时发出警报。激活升级后显示警告。在ngsw-config.json中包含来自appData的信息。

  • 扩展ngsw-config.json以显示关于发布的附加信息。包括增强功能和错误修复的详细信息。让它接近真实世界的用例。用户希望看到有关升级的详细信息摘要。

  • 探索将 Snackbar 组件定位在页面上的不同位置(与默认的底部居中相对)。

  • 探索使用除 Snackbar 组件之外的更多组件向用户发出警报。构建更适合不同用例的体验。请记住,我们使用了 Snackbar 组件,因为它不会中断和阻塞活动用户的工作流。然而,Snackbar 组件也用于警告不可恢复的错误场景。因此,为这种错误情况选择适当的警报组件和机制。