Angular-项目第三版-全-
Angular 项目第三版(全)
原文:
zh.annas-archive.org/md5/a5e00c8078625cc7b0d1e54adc65ace7译者:飞龙
前言
Angular 是一个流行的 JavaScript 框架,可以在包括 Web、桌面和移动在内的广泛平台上运行。它提供了一系列丰富的功能,并拥有广泛的工具,使其在开发者中非常受欢迎。本更新的第三版 Angular Projects 将教你如何使用 Angular 构建高效和优化的 Web 应用程序。
你将从创建 10 个不同的真实世界 Web 应用程序开始,探索框架的基本特性。每个应用程序都将展示如何将 Angular 与不同的库和工具集成。随着你的进步,你将学习如何在构建问题跟踪系统、PWA 天气应用程序、移动照片地理标记应用程序、组件 UI 库等多个令人兴奋的项目的同时,实现流行的技术,如 Angular Router、Scully、Electron、Angular 的服务工作者、Nx 的单仓库工具、NgRx 等。在结论章节中,你将学会使用 schematics 自定义 Angular CLI 命令。
在本书结束时,你将具备根据你或你的客户需求使用各种不同技术构建 Angular 应用程序所需的技能。
本书面向对象
如果你是一位具有 Angular 入门级经验的开发者,并且希望熟练掌握处理 Angular 可能遇到的各种用例的基本工具,那么这本 Angular 开发书籍就是为你准备的。假设你具备 Web 应用程序开发的基础知识,以及使用 ES6 或 TypeScript 的基本经验。
本书涵盖内容
第一章,在 Angular 中创建你的第一个 Web 应用程序,探讨了 Angular 框架的主要特性,并教你了解构成典型 Angular 应用程序的基本构建块。你将研究 Angular 生态系统中可用的不同工具和 IDE 扩展,以增强开发者的工作流程和体验。
第二章,使用 Scully 和 Angular Router 构建 SPA 应用程序,探讨了 Angular 应用程序基于单页应用(SPA)架构,通常我们有多页内容,这些内容由不同的 URL 或路由提供服务。另一方面,Jamstack 是一种新兴的热门技术,它允许你构建快速、静态的网站,并直接从 CDN 上提供服务。在本章中,我们将使用 Angular Router 在 Angular 应用程序中实现路由功能。我们还将使用 Scully,这是 Angular 最佳静态站点生成器,来创建一个采用 Jamstack 架构的个人博客。
第三章,使用响应式表单构建问题跟踪系统,介绍了我们如何构建问题跟踪管理系统,并使用 Angular 响应式表单向系统中添加新问题。我们将使用来自 VMware 的 Clarity Components 设计我们的表单,并包含内置和自定义验证。我们还将对表单中的值变化做出反应,并据此采取行动。
第四章,使用 Angular Service Worker 构建 PWA 天气应用程序,讨论了 Web 应用程序的用户体验对于所有用户来说并不相同,尤其是在网络覆盖和连接性较差的地方。当我们构建 Web 应用程序时,我们应该考虑到所有类型的网络。在本章中,我们将创建一个使用 OpenWeather API 显示指定地区天气的应用程序。我们将学习如何将应用程序部署到 Firebase Hosting。我们还将探索使用 Angular service worker 的 PWA 技术,以在离线时提供无缝的用户体验。
第五章,使用 Electron 构建桌面 WYSIWYG 编辑器,这是一个跨平台的 JavaScript 框架,用于使用 Web 技术构建桌面应用程序。当与 Angular 结合使用时,它可以产生真正高性能的应用。在本章中,我们将创建一个可以在桌面上运行的 WYSIWYG 编辑器。我们将构建一个 Angular 应用程序,并将其与流行的 WYSIWYG Angular 库 ngx-wig 集成,然后使用 Electron 将其打包为桌面应用程序。数据将通过 Node.js API 在文件系统中本地持久化。
第六章,使用 Capacitor 和 3D 地图构建移动照片地理标记应用程序,介绍了 Capacitor,这是 Ionic 框架提供的一项服务,可以将任何 Web 应用程序(如使用 Angular 创建的应用程序)转换为原生应用程序。其主要优势是我们可以使用相同的代码库构建原生移动应用程序和 Web 应用程序。Cesium 是一个流行的 JavaScript 框架,用于构建 3D 地图。在本章中,我们将使用 Capacitor 为我们拍摄的照片构建一个地理标记的移动应用程序。我们将使用各种 Ionic 插件在指定位置拍照并将其持久化到 Cloud Firestore。然后,我们将在 Cesium 3D 查看器中显示所有拍摄照片的列表。
第七章,使用 Angular 为 GitHub 个人资料构建 SSR 应用程序,深入探讨了搜索引擎优化(SEO),这是任何网站当今的一个关键方面。谁不想在通过社交媒体分享时让他们的网站看起来很好看?客户端 Web 应用程序的真正挑战是优化它,这可以通过在服务器上渲染内容来实现。在本章中,我们将学习如何使用 GitHub API 创建 GitHub 个人资料应用程序。然后,我们将在服务器上渲染它,并学习如何将状态传输到浏览器。我们还将看到如何动态设置页面标题和附加元数据。
第八章,使用 Nx Monorepo 工具和 NgRx 构建 Enterprise Portal,介绍了 monorepo 架构,这是一种在单个存储库下处理多个应用程序时流行的技术,它为开发过程提供了速度和灵活性。在本章中,我们将使用 Nx monorepo 开发工具创建两个门户:一个用于最终用户,他们可以在地图上选择一个兴趣点(POI)并访问它;另一个用于管理员检查特定 POI 的访问统计信息。应用程序状态使用 NgRx 进行管理。
第九章,使用 Angular CLI 和 Angular CDK 构建组件 UI 库,讨论了企业组织通常需要跨不同 Web 应用程序使用的自定义 UI 库。Angular CDK 提供了创建可访问性和高性能 UI 组件的广泛功能。在本章中,我们将使用 Angular CDK 和 Bulma CSS 框架创建两个不同的组件。我们还将将它们打包成一个单一的 Angular 库,并学习如何在 npm 上发布它们,以便在不同的应用程序中重用。我们还将研究如何将每个组件用作 Angular 元素。
第十章,使用 Schematics 自定义 Angular CLI 命令,介绍了组织在创建 Angular 实体(如组件或服务)时通常遵循不同的指南。Angular Schematics 可以通过扩展 Angular CLI 命令和提供自定义自动化来帮助他们。在本章中,我们将学习如何使用 Angular Schematics API 来构建我们自己的命令集,用于生成组件和服务。我们将构建一个用于创建包含 Tailwind CSS 框架的 Angular 组件的 schematic,我们还将构建一个默认使用内置 HTTP 客户端的 Angular 服务。
为了充分利用本书
您需要在您的计算机上安装 Angular 16 的版本,最好是最新版本。所有代码示例都已在 Windows 操作系统上的 Angular 16.0.0 上进行过测试,但它们也应该适用于 Angular 16 的任何未来版本。
下载示例代码文件
本书代码包托管在 GitHub 上,网址为 github.com/PacktPublishing/Angular-Projects-Third-Edition。我们还有其他来自我们丰富图书和视频目录的代码包,可在 github.com/PacktPublishing/ 找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/UbmtQ。
使用的约定
本书使用了多种文本约定。
CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”
代码块按以下方式设置:
getWeather(city: string): Observable<Weather> {
const options = new HttpParams()
.set('units', 'metric')
.set('q', city)
.set('appId', this.apiKey);
return this.http.get<Weather>(this.apiUrl + 'weather', { params: options });
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
**import** **{** **HttpClientModule** **}** **from****'@angular/common/http'****;**
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
任何命令行输入或输出都按以下方式编写:
ng generate service weather
粗体: 表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。例如:“从管理面板中选择系统信息。”
警告或重要提示看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 发送电子邮件至feedback@packtpub.com,并在邮件主题中提及本书的标题。如果您对本书的任何方面有疑问,请通过questions@packtpub.com与我们联系。
勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/submit-errata,点击提交勘误,并填写表格。
盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并提供材料的链接。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《Angular Projects,第三版》,我们很乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
您喜欢在路上阅读,但无法携带您的印刷书籍到处走?您的电子书购买是否与您选择的设备不兼容?
不要担心,现在,随着每本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何地点、任何设备上阅读。直接从您喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不会就此停止,您还可以获得独家折扣、时事通讯和每日收件箱中的精彩免费内容。
按照以下简单步骤获取好处:
- 扫描二维码或访问下面的链接

packt.link/free-ebook/9781803239118
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的邮箱
第一章:在 Angular 中创建您的第一个 Web 应用程序
Angular 是一个流行的现代 JavaScript 框架,可以在不同的平台上运行,包括网络、桌面和移动。Angular 应用程序是用 TypeScript 编写的,它是 JavaScript 的超集,提供了诸如强类型和面向对象技术之类的语法糖。
Angular 应用程序是使用 Angular 团队制作的命令行工具 Angular CLI 创建和开发的。它自动化了许多开发任务,如脚手架、测试和部署 Angular 应用程序,这些任务手动配置将花费大量时间。
Angular 框架的流行度在很大程度上反映了其广泛的工具支持。Visual Studio Code (VS Code) 编辑器包含各种扩展,这些扩展在处理 Angular 时可以增强开发体验。
在本章中,我们将涵盖以下主题:
-
Angular CLI 简介
-
探索 VS Code 中丰富的 Angular 工具生态系统
-
创建我们的第一个 Angular 应用程序
-
与 Angular 框架交互
-
使用 Nx Console 自动化 Angular CLI 命令
必要的背景理论和上下文
Angular 框架是一个跨平台 JavaScript 框架,可以在各种环境中运行,包括网络、服务器、移动和桌面。它由一系列 JavaScript 库组成,我们可以使用这些库来构建高性能和可扩展的 Web 应用程序。Angular 应用程序的架构基于组件的分层表示。组件是 Angular 应用程序的基本构建块。它们代表并控制网页的特定部分,称为 视图。以下是一些组件的示例:
-
一系列博客文章
-
一个问题报告表单
-
一个天气显示小部件
Angular 应用程序组件可以按逻辑组织成树状结构:

图 1.1 – 组件树
按照惯例,Angular 应用程序通常有一个主组件,称为 AppComponent。树中的每个组件都可以通过应用程序编程接口与其兄弟组件进行通信和交互,该接口由每个组件定义。
Angular 应用程序可以有许多被称为 模块 的功能。每个模块对应于特定应用程序领域或工作流程的单个功能块。Angular 模块用于将具有相似功能的 Angular 组件分组:

图 1.2 – 模块层次结构
在前面的图中,虚线圆圈代表 Angular 模块。按照惯例,Angular 应用程序通常有一个主模块,称为 AppModule。如果模块希望使用其功能的一部分,它们可以导入 Angular 应用程序中的其他模块。
模块的功能可以进一步分析为特性的展示逻辑和业务逻辑。Angular 组件应仅处理展示逻辑,并将业务逻辑任务委托给服务。Angular 框架通过内置的依赖注入(DI)机制为组件提供 Angular 服务。
Angular DI 框架使用特殊用途的对象,称为注入器,来隐藏向 Angular 应用程序提供依赖项的大部分复杂性。组件不需要知道 Angular 服务的实际实现。它们只需要从注入器请求即可。
一个 Angular 服务应遵循单一职责原则,并且不要跨越不同模块之间的边界。以下是一些服务的示例:
-
使用 HTTP 协议从后端 API 访问数据
-
与浏览器本地存储的交互
-
错误日志记录
-
数据转换
在构建 Angular 应用程序时,Angular 开发者不需要记住如何创建组件、模块和服务。幸运的是,Angular CLI 可以通过提供命令行界面来帮助我们完成这些任务。
Angular CLI 简介
Angular CLI 是 Angular 团队创建的一个工具,它改善了构建 Angular 应用程序的开发者体验。它隐藏了搭建和配置 Angular 应用程序的复杂性,同时允许开发者专注于他们最擅长的事情——编码!在我们开始使用 Angular CLI 之前,我们需要在我们的系统中设置以下先决条件:
-
Node.js:建立在 Chrome v8 引擎之上的 JavaScript 运行时。您可以从
nodejs.org下载任何长期支持(LTS)版本。 -
npm:Node.js 运行的包管理器。
然后,我们可以使用命令行中的 npm 安装 Angular CLI:
npm install -g @angular/cli
我们使用 -g 选项全局安装 Angular CLI,因为我们希望从任何操作系统路径创建 Angular 应用程序。
在某些操作系统中安装 Angular CLI 可能需要管理员权限。
要验证 Angular CLI 是否已正确安装,我们可以在命令行中运行以下命令:
ng version
之前的命令将报告我们系统中安装的 Angular CLI 版本。Angular CLI 通过 ng 命令提供命令行界面,这是 Angular CLI 的二进制可执行文件。它可以接受各种选项,包括以下内容:
-
serve:构建并服务 Angular 应用程序。 -
build:构建 Angular 应用程序。 -
test:运行 Angular 应用程序的单元测试。 -
generate:生成新的 Angular 实体,例如组件或模块。 -
add:安装与 Angular 框架兼容的第三方库。 -
new:创建新的 Angular 应用程序。
之前提到的选项是最常见的。如果您想查看所有可用的命令,请在命令行中执行以下命令:
ng help
之前的命令将显示 Angular CLI 支持的所有命令列表。
Angular 工具生态系统充满了扩展和实用工具,可以在我们开发 Angular 应用程序时帮助我们。在下一节中,我们将了解其中一些与 VS Code 一起工作的扩展。
探索 VS Code 中 Angular 工具的丰富生态系统
在VS Code Marketplace中有许多扩展可用,可以增强 Angular 工具生态系统。在本节中,我们将了解其中最受欢迎的扩展,这些扩展可以显著帮助我们进行 Angular 开发:
-
Nx Console
-
Angular 语言服务
-
Angular Snippets
-
Angular Evergreen
-
Material Icon Theme
前面的列表并不全面;一些扩展已经包含在Angular Essentials扩展包中。然而,您可以在marketplace.visualstudio.com/search?term=angular&target=VSCode上浏览更多 VS Code 的 Angular 扩展。
Nx Console
Nx Console 是由 Nrwl 团队开发的一个 VS Code 扩展,它提供了一个图形用户界面,用于覆盖 Angular CLI。它包含大多数 Angular CLI 命令,并使用 Angular CLI 内部执行每个命令。我们将在“使用 Nx Console 构建我们的应用程序”部分了解更多关于这个扩展的信息。
Angular Language Service
Angular 语言服务扩展在编辑 Angular 应用程序中的 HTML 模板时提供了各种增强功能,包括以下内容:
-
代码自动补全
-
编译错误信息
-
跳转到定义技术
代码自动补全是帮助我们找到在输入 HTML 内容时使用正确属性或方法的功能。它通过在我们开始输入时显示建议列表来实现:

图 1.3 – 代码补全
在前面的屏幕截图中,当我们开始输入单词descr时,Angular 语言服务建议使用description组件属性。请注意,代码补全仅适用于组件中的公共属性和方法。
在开发 Web 应用程序时,最常见的问题之一是在应用程序达到生产状态之前检测到错误。这个问题可以通过 Angular 编译器部分解决,该编译器在构建用于生产的 Angular 应用程序时启动。此外,Angular 语言服务可以通过在应用程序达到编译过程之前显示编译错误信息来进一步解决这个问题:

图 1.4 – 编译错误信息
例如,如果我们不小心拼错了组件的属性或方法名称,Angular 语言服务将显示适当的错误信息。
Angular Snippets
Angular Snippets扩展包含了一组 Angular 代码片段,用于 TypeScript 和 HTML。在 TypeScript 中,我们可以使用它在一个空白 TypeScript 文件中创建组件、模块或服务:

图 1.5 – 新 Angular 组件片段
在 HTML 模板中,我们可以使用此扩展来创建有用的 Angular 元素,例如 *ngFor 指令,以在 HTML 中循环列表:

图 1.6 – *ngFor 碎片
由于 Angular CLI 的广泛流行和功能,使用它来在 TypeScript 中生成 Angular 元素看起来更方便。然而,Angular Snippets 在 HTML 部分做得很好,那里有更多需要记住的内容。
Angular Evergreen
使 Angular 框架如此稳定的一个主要因素是它遵循基于语义版本控制的定期发布周期。如果我们希望我们的 Angular 应用程序充满最新功能和修复,我们必须定期更新它们。但如何最有效地保持更新呢?我们可以使用 Angular Evergreen 扩展!
它比较 Angular CLI 项目的 Angular 和 Angular CLI 版本与最新版本,并提醒您是否需要更新:

图 1.7 – Angular Evergreen
它提供了一个易于使用的用户界面来执行以下命令:
-
将 Angular 依赖项升级到 最新 版本
-
将 Angular 依赖项升级到 下一个 版本
-
升级所有 npm 依赖项
Angular Evergreen 是始终与您的 Angular 项目保持更新的完美扩展。
材料图标主题
列表中最后一个扩展在提高开发者生产力方面添加的价值很小。相反,它通过修改 VS Code 的图标主题来关注可发现性和美学观点。
材料图标主题包含大量基于 Google 材料设计的图标。它可以理解项目中每种文件类型并自动显示相关图标。例如,Angular 模块用红色 Angular 图标表示,而组件则用蓝色 Angular 图标表示。
VS Code 有一个默认的文件图标主题,称为 Seti。一旦您安装了材料图标主题,它将提示您选择您想要激活的主题:

图 1.8 – 选择文件图标主题
选择 材料图标主题将自动更新当前 Angular 项目的图标。
材料图标主题已安装并全局应用于 VS Code,因此您无需为每个 Angular CLI 项目单独激活它。
现在,当您打开您的 Angular 项目时,您将一眼就能理解每个文件的类型,即使其名称没有完全显示在屏幕上。
项目概述
在这个项目中,我们将使用 Angular CLI 从头开始创建一个新的 Angular 应用程序。然后,我们将与 Angular 框架的核心功能交互,对我们的应用程序进行简单的更改。最后,我们将学习如何使用 Nx Console 扩展来构建和托管我们的应用程序。
构建时间:15 分钟。
入门
完成此项目所需的软件工具如下:
-
Git:一个免费且开源的分布式版本控制系统。您可以从
git-scm.com下载它。 -
VS Code:一个您可以从
code.visualstudio.com下载的代码编辑器。 -
Angular CLI:我们在 必要背景理论及环境 部分介绍了 Angular 的命令行界面。
-
GitHub 资源:本章的代码,您可以在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter01文件夹中找到。
创建我们的第一个 Angular 应用程序
要创建一个全新的 Angular 应用程序,我们必须执行 Angular CLI 的 ng new 命令,并将应用程序名称作为选项传递:
ng new my-app
ng new 命令用于创建新的 Angular 应用程序或新的 Angular 工作空间。Angular 工作空间是一个包含一个或多个 Angular 应用程序的 Angular CLI 项目,其中一些可以是 Angular 库。因此,当我们执行 ng new 命令时,我们默认创建一个包含 Angular 应用程序的 Angular 工作空间。
在前面的命令中,我们的 Angular 应用程序名称是 my-app。执行命令后,Angular CLI 将提出一些问题,尽可能收集有关我们想要创建的应用程序性质的信息:
-
初始时,它会询问我们是否想启用 Angular 分析:
Would you like to share pseudonymous usage data about this project with the Angular Team at Google under Google's Privacy Policy at https://policies.google.com/privacy. For more details and how to change this setting, see https://angular.io/analytics. (y/N)Angular CLI 只会在我们创建第一个 Angular 项目时询问一次前面的问题,并将其全局应用于系统。然而,我们可以在特定的 Angular 工作空间中稍后更改设置。
-
接下来,它将询问我们是否想在 Angular 应用程序中启用路由:
Would you like to add Angular routing? (y/N)Angular 中的路由全部关于使用 URL 在 Angular 应用程序组件之间导航。我们在这个项目中不关心路由,所以按 Enter 接受默认值。
-
然后,Angular CLI 会提示我们选择我们想要在 Angular 应用程序中使用的样式格式:
Which stylesheet format would you like to use? (Use arrow keys)
从可用的样式表列表中选择一个格式并按 Enter。
Angular CLI 启动您的 Angular 应用程序的创建过程,该过程包括以下步骤:
-
为典型的 Angular CLI 项目搭建必要的文件夹结构
-
安装所需的 npm 依赖项和 Angular 包
-
在 Angular CLI 项目中初始化 Git
这个过程可能需要一些时间,具体取决于您的网络速度。一旦完成,您应该在运行ng new Angular CLI 命令的路径中看到一个名为my-app的新文件夹。
现在,运行我们的 Angular 应用程序并看到它实际运行的时刻终于到来了:
-
打开一个终端窗口并导航到
my-app文件夹。 -
运行以下 Angular CLI 命令:
ng serve上述命令将构建 Angular 应用程序并启动一个内置的 Web 服务器,我们可以使用它来预览应用程序。Web 服务器以监视模式启动;每当我们的代码发生变化时,它会自动重新构建 Angular 应用程序。第一次构建 Angular 应用程序时,完成需要相当长的时间,因此我们必须有耐心。当我们在终端窗口中看到以下消息时,我们知道过程已经完成且没有错误:
![图 1.11 – Angular 构建输出]()
图 1.9 – Angular 构建输出
-
启动您喜欢的浏览器并导航到
http://localhost:4200以预览您全新的 Angular 应用程序:

图 1.10 – 最小 Angular 应用程序
Angular CLI 默认创建一个最小的 Angular 应用程序,为我们提供 Angular 项目的起点。它包含一些现成的 CSS 样式和 HTML 内容,我们将在下一节中学习如何根据我们的规格进行更改。
与 Angular 框架交互
在使用 Angular 时,真正的乐趣在于我们开始与框架本身打交道。毕竟,理解 Angular 的工作原理和编写应用程序代码才是最重要的。
应用程序源代码位于 Angular CLI 项目的根目录下的src\app文件夹中。它包含构建和测试我们的 Angular 应用程序所需的所有文件,包括一个组件和一个模块。组件是 Angular 应用程序的主要组件:
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'my-app';
}
以下属性描述了 Angular 组件:
-
selector:用于在 HTML 内容中标识和声明组件的唯一名称。它是一个 HTML 标签,就像任何原生 HTML 元素一样,例如<app-root></app-root>。Angular CLI 默认在组件选择器中提供
app-前缀。在从头创建新的 Angular CLI 应用程序时,我们可以使用--prefix选项来使用自定义前缀。自定义前缀可以基于组织名称或特定产品的名称,这有助于避免与其他库或模块冲突。 -
templateUrl:指向包含组件 HTML 内容的 HTML 文件的路径,称为组件模板。 -
styleUrls:指向包含组件 CSS 样式的样式表文件的路径列表。
前面的属性使用 @Component 装饰器定义。它是一个装饰 TypeScript 类的函数,并识别它为 Angular 组件。AppComponent 类的 title 属性是一个包含字符串值的公共属性,可以在组件模板中使用。
我们的 Angular 应用程序的主模块使用一个类似的装饰器 @NgModule 来定义其属性:
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Angular 模块的装饰器定义了一组可以用于配置模块的属性。最常见的一些如下:
-
declarations:定义属于 Angular 模块的 Angular 组件。Angular 模块中存在的每个组件必须添加到declarations数组中。 -
imports:定义包含 Angular 模块所需功能的其他 Angular 模块。
现在,让我们通过修改我们的 Angular 应用程序的代码来试试水。我们将更改以下在应用程序启动时显示的问候信息,使其更有意义:

图 1.11 – 欢迎信息
首先,我们需要找到上一张截图中的消息声明位置。Angular 应用程序的主组件是默认在应用程序启动时加载的组件。
应用程序主模块的 bootstrap 属性指示在 Angular 应用程序启动时显示的组件。我们很少需要更改此属性。该组件的选择器默认在 index.html 文件中使用。
因此,消息应该在 app.component.ts 文件中声明。让我们看一下:
-
打开 VS Code 编辑器并从主菜单中选择 文件 | 打开文件夹…。
-
找到我们创建的 Angular 应用程序的
my-app文件夹并选择它。 -
从 资源管理器 窗格导航到
src\app文件夹并选择app.component.ts文件。 -
在
AppComponent类中找到title属性并将其值更改为Angular Projects:title = '**Angular Projects**'; -
如果应用程序没有运行,请在终端窗口中运行
ng serve,然后使用浏览器导航到http://localhost:4200。我们的 Angular 应用程序现在应该显示以下问候信息:

图 1.12 – 欢迎信息
title 属性绑定到主组件的模板。如果我们打开 app.component.html 文件并转到第 344 行,我们将看到以下 HTML 代码:
<span>{{ title }} app is running!</span>
围绕 title 属性的 {{}} 语法称为插值。在插值过程中,Angular 框架读取包含的组件属性值,将其转换为文本,并在屏幕上打印出来。
Angular CLI 提供了丰富的命令集合,以协助我们在日常开发过程中。然而,许多开发者发现使用命令行很困难,更倾向于图形化方法。在下一节中,我们将学习如何使用 Nx 控制台,它是 Angular CLI 的图形用户界面。
使用 Nx Console 自动化 Angular CLI 命令
Angular CLI 是一个具有各种命令的命令行工具。每个命令可以根据我们想要完成的任务接受广泛的各种选项和参数。记住这些命令及其选项是一项艰巨且耗时的工作。在这种情况下,Angular 工具生态系统可以派上用场。VS Code 市场包含许多有用的扩展,我们可以安装它们来帮助我们进行 Angular 开发。其中之一就是 Nx 控制台,它提供了一个 Angular CLI 的用户界面。要在您的环境中安装 Nx 控制台,请按照以下步骤操作:
- 打开 VS Code 并在侧边栏中点击 扩展 菜单:

图 1.13 – VS Code 扩展
-
在出现的 扩展 面板中,键入
Nx Console。 -
在第一个项目上点击 安装 按钮来安装 Nx Console 扩展。
Nx Console 扩展现在已在全球环境中安装,因此我们可以在任何 Angular 项目中使用它。它是最常见的 Angular CLI 命令的图形表示。目前,它支持以下命令(括号中显示的是相关的 Angular CLI 命令):
-
生成:生成新的 Angular 艺术品,如组件和模块(
ng generate)。 -
运行:运行在 Angular CLI 工作区的
angular.json配置文件中定义的架构目标(ng run)。 -
构建:构建 Angular 应用程序(
ng build)。 -
运行:构建并运行 Angular 应用程序(
ng serve)。 -
测试:运行 Angular 应用程序的单元测试(
ng test)。
Nx 控制台几乎可以实现我们使用 Angular CLI 可以做到的所有事情。真正的好处是开发者不需要记住所有 Angular CLI 命令选项,因为它们都在图形界面中得到了表示。让我们看看它是如何做到的:
- 使用 VS Code 打开
my-app文件夹,并在侧边栏中点击 Nx 控制台 菜单:

图 1.14 – Nx 控制台
- 从 项目 面板中选择 运行 命令,然后点击 播放 按钮来执行它:

图 1.15 – serve 命令
- VS Code 在编辑器底部打开一个集成终端并执行 ng serve 命令:

图 1.16 – VS Code 集成终端
这是我们从终端窗口使用 Angular CLI 时运行的相同命令。
Nx 控制台内部使用 任务 来运行 Angular CLI 命令。任务是 VS Code 的内置机制,允许我们运行脚本或启动外部进程,而无需直接与命令行交互。
Nx 控制台扩展出色地减轻了记住 Angular CLI 命令的负担。VS Code 市场上有许多针对 Angular 开发者的扩展,这些扩展补充了 Nx 控制台的工作。
摘要
在本章中,我们学习了 Angular 框架的基本原则,并简要概述了 Angular 架构。我们看到了一些流行的 VS Code 扩展,我们可以使用这些扩展来增强我们在使用 Angular 进行开发时的体验。
然后,我们学习了如何使用 Angular CLI,这是 Angular 生态系统中的一个强大工具,可以从头开始构建新的 Angular 应用程序。我们还通过修改典型 Angular CLI 应用程序的 Angular 组件,首次与 Angular 代码进行了交互。最后,我们安装了 Nx 控制台扩展,并学习了如何构建我们的应用程序。
在下一章中,我们将探讨 Angular 路由,并学习如何使用它来创建个人博客,使用 Scully 静态网站生成器。
实践问题
让我们看看几个实践问题:
-
Angular 应用程序的基本构建块是什么?
-
我们如何将功能相似的组件分组?
-
在 Angular 应用程序中谁处理业务逻辑任务?
-
我们可以使用哪个 Angular CLI 命令来创建新的 Angular 应用程序?
-
我们可以使用哪个 Angular CLI 命令来提供 Angular 应用程序?
-
我们如何在 HTML 中声明 Angular 组件?
-
我们如何在模块中声明 Angular 组件?
-
我们在 HTML 模板上绑定文本时使用什么语法?
-
使用 Nx 控制台有什么好处?
-
我们在 Angular 代码中用哪个扩展来执行静态分析?
进一步阅读
这里有一些链接,可以帮助我们巩固本章所学的内容:
-
基本 Angular 概念简介:
angular.io/guide/architecture -
Nx 控制台:
nx.dev/core-features/integrate-with-editors#vscode-plugin:-nx-console -
Angular 精要:
marketplace.visualstudio.com/items?itemName=johnpapa.angular-essentials -
Angular Evergreen:
expertlysimple.io/get-evergreen
第二章:使用 Scully 和 Angular Router 构建 SPA 应用程序
Angular 应用程序遵循单页应用程序(SPA)架构,其中可以使用浏览器中的 URL 激活网页的不同视图。任何对该 URL 的更改都可以被 Angular 路由器拦截并转换为可以激活特定 Angular 组件的路由。
Scully是一个流行的基于Jamstack架构的静态网站生成器。它可以很好地与 Angular 路由器合作,根据每个路由预渲染 Angular 应用程序的内容。
在本章中,我们将结合 Angular 和 Scully 创建一个个人博客。以下主题将被涵盖:
-
在 Angular 应用程序中设置路由
-
创建我们博客的基本布局
-
配置我们应用程序的路由
-
使用 Scully 添加博客功能
-
在主页上显示博客文章
必要的背景理论和上下文
在 Web 开发的早期,客户端应用程序与底层服务器基础设施高度耦合。当我们想要通过 URL 访问网站页面时,涉及到许多机械操作。
浏览器会将请求的 URL 发送到服务器,服务器应该响应与该 URL 匹配的 HTML 文件。这是一个复杂的过程,会导致延迟和往返时间的差异。
现代 Web 应用程序使用 SPA 架构消除了这些问题。客户端只需要从服务器请求一次单个 HTML 文件。浏览器 URL 的任何后续更改都由客户端基础设施内部处理。在 Angular 中,路由器负责拦截应用程序内的 URL 请求并根据定义的路由配置处理它们。
Jamstack 是一种热门的新兴技术,允许我们创建快速和安全的 Web 应用程序。它可以用于任何应用程序类型,从电子商务网站到软件即服务(SaaS)Web 应用程序,甚至个人博客。Jamstack 的架构基于以下支柱:
-
性能:页面在生产过程中生成和预渲染,消除了等待内容加载的需要。
-
扩展性:内容是静态文件,可以从任何地方提供,甚至可以从提高应用程序性能的内容分发网络(CDN)提供商那里提供。
-
安全性:服务器端过程的无服务器性质以及内容已经是静态的事实消除了针对服务器基础设施的潜在攻击。
Scully 是第一个采用 Jamstack 方法的 Angular 静态网站生成器。它本质上在构建时生成 Angular 应用程序的页面,以便在请求时立即可用。
项目概述
在这个项目中,我们将使用 Angular 框架构建一个个人博客,并使用 Scully 网站生成器增强其 Jamstack 特性。最初,我们将构建一个新的 Angular 应用程序并启用其路由功能。然后,我们将通过添加一些基本组件来创建我们应用程序的基本布局。一旦我们有一个可工作的 Angular 应用程序,我们将使用 Scully 为其添加博客支持。然后,我们将使用 Markdown 文件创建一些博客文章,并在我们应用程序的首页上显示它们。以下图表展示了项目的架构概述:

图 2.1 – 项目架构
构建时间:1 小时。
入门
完成此项目所需的以下软件工具:
-
Angular CLI:Angular 的命令行界面,您可以在
angular.io/cli找到。 -
GitHub 资料库:本章的相关代码,您可以在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter02文件夹中找到。
在 Angular 应用程序中设置路由
我们将通过从头开始创建一个新的 Angular 应用程序来启动我们的项目。在终端窗口中执行以下 Angular CLI 命令以创建一个新的 Angular 应用程序:
ng new my-blog --routing --style=scss
我们使用 ng new 命令创建一个新的 Angular 应用程序,传递以下选项:
-
my-blog: 我们想要创建的 Angular 应用程序的名称。Angular CLI 将在执行命令的路径中创建一个my-blog文件夹。在终端窗口中运行的每个命令都应该在这个文件夹内执行。
-
--routing: 启用 Angular 应用程序中的路由功能。 -
--style=scss: 配置 Angular 应用程序在处理 CSS 样式时使用 SCSS 样式表格式。
当我们在 Angular 应用程序中启用路由时,Angular CLI 将从 @angular/router npm 包中导入几个工件到我们的应用程序中:
-
它创建了
app-routing.module.ts文件,这是我们的应用程序的主要路由模块:import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; const routes: Routes = []; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } -
它将
AppRoutingModule导入我们的应用程序的主要模块app.module.ts:import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; **import** **{** **AppRoutingModule** **}** **from****'./app-routing.module'****;** import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, **AppRoutingModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
我们配置了我们的应用程序以使用 SCSS 样式表格式。我们不会手动创建应用程序的样式,而是将使用 Bootstrap CSS 库:
-
在终端窗口中执行以下命令以安装 Bootstrap:
npm install bootstrap在前面的命令中,我们使用
npm可执行文件从 npm 注册表中安装bootstrap包。 -
在我们 Angular 应用程序的
src文件夹中存在的styles.scss文件顶部添加以下import语句:@import "bootstrap/scss/bootstrap";
@import CSS rule accepts the absolute path of the bootstrap.scss file as an option without adding the extension.
在以下部分,我们将学习如何通过创建组件(如页眉和页脚)来创建我们博客的基本布局。
创建我们博客的基本布局
一个博客通常包含一个包含所有主要网站链接的标题,以及一个包含版权信息和其他有用链接的页脚。在 Angular 的世界里,这两个都可以表示为单独的组件。
标题组件仅使用一次,因为它在我们应用程序启动时添加,并且始终作为网站的主菜单进行渲染。在 Angular 中,我们通常创建一个名为core的模块,按照惯例,以保持此类组件或服务在我们的应用程序中的中心位置。要创建模块,我们使用 Angular CLI 的generate命令:
ng generate module core
之前的命令将在我们的应用程序的src\app\core文件夹中创建模块。要创建标题组件,我们将使用相同的命令,传递不同的选项集:
ng generate component header --path=src/app/core --module=core --export
之前的命令将在src\app\core\header文件夹内创建所有必要的组件文件。它还会在core.module.ts文件中声明HeaderComponent,并将其添加到exports属性中,以便其他模块可以使用它:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
**import** **{** **HeaderComponent** **}** **from****'./header/header.component'****;**
@NgModule({
declarations: [
**HeaderComponent**
],
imports: [
CommonModule
],
**exports****: [**
**HeaderComponent**
**]**
})
export class CoreModule { }
标题组件应显示我们博客的主要链接。打开标题组件的header.component.html模板文件,并用以下片段替换其内容:
<nav class="navbar navbar-expand navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand">Angular Projects</a>
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link">Articles</a>
</li>
<li class="nav-item">
<a class="nav-link">Contact</a>
</li>
</ul>
</div>
</nav>
页脚组件可以在 Angular 应用程序中使用多次。目前,我们希望在应用程序的主页上显示它。在未来,我们可能还希望在可供博客访客使用的登录页上显示它。在这种情况下,页脚组件应该是可重用的。当我们想要将将在整个应用程序中重用的组件分组时,我们通常按照惯例创建一个名为shared的模块。使用 Angular CLI 的generate命令来创建模块:
ng generate module shared
之前的命令将在src\app\shared文件夹中创建shared模块。现在,可以使用以下命令创建页脚组件:
ng generate component footer --path=src/app/shared --module=shared --export
之前的命令将在src\app\shared\footer文件夹内创建页脚组件的所有必要文件。它还会在shared.module.ts文件的declarations和exports属性中添加FooterComponent:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
**import** **{** **FooterComponent** **}** **from****'./footer/footer.component'****;**
@NgModule({
declarations: [
**FooterComponent**
],
imports: [
CommonModule
],
**exports****: [**
**FooterComponent**
**]**
})
export class SharedModule { }
页脚组件的内容应包含关于我们博客的版权信息。
让我们看看如何将此信息添加到我们的组件中:
-
打开
footer.component.ts文件,在FooterComponent类中添加一个currentDate属性,并将其初始化为一个新的Date对象:currentDate = new Date(); -
打开页脚组件的
footer.component.html模板文件,并用以下内容替换其内容:<nav class="navbar fixed-bottom navbar-light bg-light"> <div class="container-fluid"> <p>Copyright @{{currentDate | date: 'y'}}. All Rights Reserved</p> </div> </nav>
之前的代码使用插值来在屏幕上显示currentDate属性的值。它还使用内置的date管道来仅显示当前日期的年份。
管道是 Angular 框架的内置功能,它对组件属性的可视表示形式应用转换。属性的底层值保持不变。
我们已经创建了博客的必要组件。现在,是时候在屏幕上显示它们了:
-
打开应用程序的主模块,即
app.module.ts文件,并将CoreModule和SharedModule添加到@NgModule装饰器的imports属性中:@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, **CoreModule****,** **SharedModule** ], providers: [], bootstrap: [AppComponent] }) -
在文件的顶部为每个模块添加适当的
import语句:import { CoreModule } from './core/core.module'; import { SharedModule } from './shared/shared.module'; -
打开主组件的
app.component.html模板文件,并用以下 HTML 片段替换其内容:<app-header></app-header> <app-footer></app-footer>
在前面的片段中,我们通过使用它们的 CSS 选择器添加了标题和页脚组件。
如果我们运行 Angular CLI 的 serve 命令来预览应用程序,我们应该得到以下结果:

图 2.2 – 基本布局
我们已经完成了我们博客应用程序的基本布局,看起来很棒!但是标题中包含两个我们尚未覆盖的额外链接。我们将在下一节中学习如何使用路由来激活这些链接。
为我们的应用程序配置路由
我们在前一节中创建的标题组件包含两个链接:
-
文章: 显示博客文章列表
-
联系: 显示博客所有者的个人信息
之前的链接也将成为我们应用程序的主要功能。因此,我们需要为每个功能创建一个 Angular 模块。
当你设计你的网站并需要决定将使用哪些 Angular 模块时,查看网站的主菜单。菜单中的每个链接都应该是一个不同的功能,因此是一个不同的 Angular 模块。
按照惯例,包含特定功能功能的 Angular 模块被称为 功能模块。
创建联系页面
让我们从创建我们的联系功能开始:
-
创建一个将成为我们联系功能家的模块:
ng generate module contact -
创建一个将成为
contact模块主要组件的组件:ng generate component contact --path=src/app/contact --module=contact --export --flat我们将
--flat选项传递给generate命令,这样 Angular CLI 就不会为我们的组件创建一个单独的文件夹,就像之前的例子一样。由于contact组件将是我们的模块中唯一的组件,所以没有必要单独创建它。 -
打开
contact.component.html文件并添加以下 HTML 内容:<div class="card mx-auto text-center border-light" style="width: 18rem;"> <img src="img/angular.png" class="card-img-top" alt="Angular logo"> <div class="card-body"> <h5 class="card-title">Angular Projects</h5> <p class="card-text"> A personal blog created with the Angular framework and the Scully static site generator </p> <a href="https://angular.io/" target="_blank" class="card-link">Angular</a> <a href="https://scully.io/" target="_blank" class="card-link">Scully</a> </div> </div>
在前面的代码中,我们使用了 angular.png 图像,你可以在随附的 GitHub 仓库项目的 src\assets 文件夹中找到它。
Angular CLI 项目的 assets 文件夹用于静态内容,如图像、字体或 JSON 文件。
我们已经创建了我们的联系功能。下一步是将它添加到我们的 Angular 应用程序的主页上:
-
打开
app-routing.module.ts文件,并在routes属性中添加一个新的路由配置对象:import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; **import** **{** **ContactComponent** **}** **from****'****./contact/contact.component'****;** const routes: Routes = [ **{** **path****:** **'contact'****,** **component****:** **ContactComponent** **}** ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }上述代码表明,当浏览器的 URL 指向
contact路径时,我们的应用程序将激活并在屏幕上显示ContactComponent。路由模块的routes属性包含相应功能模块的路由配置。它是一个路由配置对象数组,其中每个对象定义了组件类和激活它的 URL 路径。 -
在
AppModule的@NgModule装饰器的imports数组中添加ContactModule,以便能够使用它:@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, CoreModule, SharedModule, **ContactModule** ], providers: [], bootstrap: [AppComponent] })不要忘记在文件顶部添加对
ContactModule的相应import语句。 -
路由组件,就像
ContactComponent一样,需要一个可以加载的地方。打开app.component.html文件,并添加<router-outlet>指令:<app-header></app-header> **<****div****class****=****"****container"****>** **<****router-outlet****></****router-outlet****>** **</****div****>** <app-footer></app-footer>
现在,我们需要将我们创建的路由配置与页眉组件的实际链接连接起来:
-
打开
header.component.html文件,并将routerLink指令添加到相应的锚 HTML 元素中:<li class="nav-item"> <a **routerLink****=****"/contact"****routerLinkActive****=****"active"** class="nav-link">Contact</a> </li>
routerLink directive points to the path property of the route configuration object. We have also added the routerLinkActive directive, which sets the active class on the anchor element when the specific route is activated.
注意,routerLink指令的值包含一个前导/,而我们所定义的路由配置对象的path属性则没有。根据情况,省略/会给路由带来不同的含义。
-
routerLink和routerLinkActive指令是 Angular Router 包的一部分。我们需要在core.module.ts文件中导入RouterModule才能使用它们:import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HeaderComponent } from './header/header.component'; **import** **{** **RouterModule** **}** **from****'@angular/router'****;** @NgModule({ declarations: [ HeaderComponent ], imports: [ CommonModule, **RouterModule** ], exports: [ HeaderComponent ] }) export class CoreModule { }
现在,我们已经准备好预览我们新的联系页面!如果我们使用ng serve运行应用程序并点击联系链接,我们应该看到以下输出:

图 2.3 – 联系页面
在以下部分,我们将构建我们博客页眉中文章链接的功能。
添加文章页面
负责在我们博客中显示文章的功能将是articles模块。它也将是连接 Angular 和 Scully 之间的模块。我们将使用 Angular CLI 的generate命令来创建该模块:
ng generate module articles --route=articles --module=app-routing
在之前的命令中,我们传递了一些额外的路由选项:
-
--route:定义我们功能的 URL 路径 -
--module:指示将定义激活我们功能的路由配置对象的路由模块
当执行命令时,Angular CLI 执行了额外的操作,而不仅仅是创建模块:
-
它在
src\app\articles文件夹中创建了一个路由组件,该组件将默认由路由导航对象激活。它是我们功能的着陆页,并将显示博客文章列表,正如我们将在在主页上显示博客数据部分中看到的那样。 -
它创建了一个名为
articles-routing.module.ts的路由模块,其中包含我们模块的路由配置。 -
在主应用模块的路由配置中添加了一个新的路由配置对象,该对象激活了我们的模块。
articles-routing.module.ts文件包含articles模块的路由配置:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ArticlesComponent } from './articles.component';
const routes: Routes = [{ path: '', component: ArticlesComponent }];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ArticlesRoutingModule { }
它使用forChild方法导入RouterModule,将路由配置传递给 Angular 路由器。如果我们查看应用程序的主路由模块,我们会看到它采用了一种略有不同的方法:
app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ContactComponent } from './contact/contact.component';
const routes: Routes = [
{ path: 'contact', component: ContactComponent },
**{** **path****:** **'articles'****,** **loadChildren****:** **() =>****import****(****'./articles/articles.module'****).****then****(****m** **=>** **m.****ArticlesModule****) }**
];
@NgModule({
imports: [**RouterModule****.****forRoot****(routes)**],
exports: [RouterModule]
})
export class AppRoutingModule { }
在功能模块中使用forChild方法,而forRoot方法应仅在主应用程序模块中使用仅此而已。
articles模块的路由配置只包含一个激活ArticlesComponent的路由。该路由的路径设置为空字符串,以表示它是路由模块的默认路由。这实际上意味着每当该模块被加载时,ArticlesComponent将被激活。但我们的应用程序是如何加载articles模块的呢?
主路由模块的第二路由包含一个路由配置对象,该对象不激活组件而是激活一个模块。它使用loadChildren方法在导航触发articles路径时动态加载ArticlesModule。
loadChildren属性的import函数接受 TypeScript 模块文件的相对路径,不包括扩展名。
之前的方法称为懒加载,它提高了 Angular 应用程序的启动和整体性能。它为每个懒加载的模块创建一个单独的包,在请求时加载,减少了最终包的大小和应用程序的内存消耗。让我们将新路由连接到我们的标题组件:
-
打开
header.component.html文件,并将以下routerLink和routerLinkActive指令添加到Articles锚点 HTML 元素中:<li class="nav-item"> <a **routerLink****=****"/articles"****routerLinkActive****=****"active"** class="nav-link">Articles</a> </li> -
运行
ng serve并使用您喜欢的浏览器预览您的应用程序。 -
打开您浏览器的开发者工具,点击Articles链接,并检查网络选项卡:

图 2.4 – 懒加载 Angular 模块
在其他请求中,你应该看到一个名为src_app_articles_articles_module_ts.js的请求。这是当你点击Articles链接时加载的懒加载文章模块的包。
现在我们已经准备好将我们的出色 Angular 应用程序转换成一个专业的博客网站。
在我们继续之前,让我们向app-routing.module.ts文件添加一些额外的路由:
const routes: Routes = [
{ path: 'contact', component: ContactComponent },
{ path: 'articles', loadChildren: () => import('./articles/articles.module').then(m => m.ArticlesModule) },
**{** **path****:** **''****,** **pathMatch****:** **'full'****,** **redirectTo****:** **'articles'** **},**
**{** **path****:** **'**'****,** **redirectTo****:** **'articles'** **}**
];
我们添加了一个默认路由,在访问博客时自动将博客用户重定向到articles路径。此外,我们还创建了一个新的路由配置对象,其路径设置为**,它也会导航到articles路径。**语法称为通配符路由,当路由器无法将请求的 URL 与定义的路由匹配时,它会被触发。
首先定义最具体的路由,然后添加任何通用的路由,例如默认路由和通配符路由。Angular 路由器按照我们定义的顺序解析路由配置,并遵循“首次匹配即获胜”的策略来选择一个。
我们已经在 Angular 应用程序中启用了并配置了路由。在下一节中,我们将建立添加博客功能所需的基础设施。
使用 Scully 添加博客功能
我们的应用程序目前还没有关于博客文章的任何特定逻辑。它是一个典型的 Angular 应用程序,使用路由。然而,通过添加路由配置,我们已经为使用 Scully 添加博客支持奠定了基础。
Scully 至少需要在 Angular 应用程序中定义一个路由才能正确工作。
首先,我们需要在我们的应用程序中安装 Scully。
安装 Scully 库
我们将使用 npm CLI 的 install 命令在我们的 Angular 应用程序中安装 Scully:
npm install @scullyio/init @scullyio/ng-lib @scullyio/scully @scullyio/scully-plugin-puppeteer --force
之前的命令下载并安装了 Scully 在我们的 Angular 应用程序中正确工作所需的所有必要的 npm 包。
Scully 库与 Angular 16 不完全兼容,截至本文撰写时为止。在上一个命令中,我们使用了 --force 选项来忽略来自 Angular 版本不兼容的任何警告。
打开 app.module.ts 文件并导入 ScullyLibModule:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ContactModule } from './contact/contact.module';
import { CoreModule } from './core/core.module';
import { SharedModule } from './shared/shared.module';
**import** **{** **ScullyLibModule** **}** **from****'@scullyio/ng-lib'****;**
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
CoreModule,
SharedModule,
ContactModule,
**ScullyLibModule**
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
ScullyLibModule 是 Scully 库的主要模块;它包含 Scully 需要的各种 Angular 服务和指令。
在 Angular CLI 工作区的根目录中为 Scully 库创建一个配置文件,内容如下:
scully.my-blog.config.ts
import { ScullyConfig } from '@scullyio/scully';
export const config: ScullyConfig = {
projectRoot: "./src",
projectName: "my-blog",
outDir: './dist/static',
routes: {
}
};
配置文件包含 Scully 在过程中需要了解的关于我们的 Angular 应用程序的信息:
-
projectRoot:包含 Angular 应用程序源代码的路径 -
projectName:Angular 应用程序的名称 -
outDir:Scully 生成的文件输出路径Scully 的输出路径必须与 Angular CLI 为您的 Angular 应用程序捆绑输出的路径不同。后者可以在
angular.json文件中配置。 -
routes:它包含用于访问我们的博客文章的路由配置。Scully 将自动填充它,正如我们将在下一节中看到的。
由于我们已经成功地在 Angular 应用程序中安装了 Scully,我们现在可以配置它来初始化我们的博客。
初始化我们的博客页面
Scully 为初始化 Angular 应用程序(如博客)提供了一个特定的 Angular CLI 模板,通过使用 Markdown (.md) 文件:
ng generate @scullyio/init:markdown --project my-blog
之前的命令将通过一系列问题(括号内显示默认值)启动博客的配置过程:
-
将博客模块的名称输入为
posts:What name do you want to use for the module? (blog)这将创建一个名为
posts的新 Angular 模块。 -
留空 slug 选择,并按 Enter 键接受默认值:
What slug do you want for the markdown file? (id)slug 是每篇帖子的唯一标识符,它在模块的路由配置对象中定义。
-
将路径设置为
mdfiles,这是 Scully 将用于存储我们的实际博客文章文件的位置:Where do you want to store your markdown files?这将在我们的 Angular CLI 项目的根路径内创建一个
mdfiles文件夹。默认情况下,它还会为了我们的方便创建一篇博客文章。我们将在 在主页上显示博客数据 这一部分学习如何创建自己的。 -
将路由名称设置为
posts以访问我们的博客文章:Under which route do you want your files to be requested?
路由的名称是创建的路由配置对象的 path 属性。
Scully 在执行前面的命令时执行各种操作,包括创建 posts 模块的路由配置:
posts-routing.module.ts
import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {PostsComponent} from './posts.component';
const routes: Routes = [
{
path: ':id',
component: PostsComponent,
},
{
path: '**',
component: PostsComponent,
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class PostsRoutingModule {}
第一条路由的 path 属性设置为 :id 并激活 PostsComponent。冒号字符表示 id 是一个路由参数。id 参数与 Scully 配置中早先定义的 slug 属性相关。Scully 通过为每篇我们创建的博客文章创建一个路由来工作。它使用 posts 模块的路线配置和主应用程序模块来构建 Scully 配置文件中的 routes 属性:
routes: {
'/posts/:id': {
type: 'contentFolder',
id: {
folder: "./mdfiles"
}
},
}
PostsComponent 是一个 Angular 组件,用于渲染每篇博客的详细信息。根据您的需求,组件的模板文件可以进一步自定义:
posts.component.html
<h3>ScullyIo content</h3>
<hr>
<!-- This is where Scully will inject the static HTML -->
<scully-content></scully-content>
<hr>
<h4>End of content</h4>
您可以自定义除 <scully-content></scully-content> 行之外的前一个模板文件中的所有内容,该行是 Scully 内部使用的。
到目前为止,我们已经完成了 Scully 在我们的 Angular 应用程序中的安装和配置。现在是项目的最后一部分!在下一节中,我们将让 Angular 和 Scully 合作,并在我们的 Angular 应用程序中显示博客文章。
在主页上显示博客文章
我们希望用户一登录我们的博客网站就能看到可用的博客文章列表。根据我们定义的默认路由路径,ArticlesComponent 是我们博客的着陆页。Scully 提供了 ScullyRoutesService,这是一个 Angular 服务,我们可以在我们的组件中使用它来获取关于它将根据博客文章创建的路由的信息。让我们在我们的着陆页上使用这个服务:
-
打开
articles.component.ts文件,并按如下修改import语句:import { Component, **OnInit** } from '@angular/core'; **import** **{** **ScullyRoute****,** **ScullyRoutesService** **}** **from****'@scullyio/ng-lib'****;** **import** **{** **Observable****, map }** **from****'rxjs'****;** -
将
OnInit接口添加到ArticlesComponent类实现的接口列表中:export class ArticlesComponent **implements****OnInit** { } -
在
ArticlesComponent类的constructor中注入ScullyRoutesService:constructor(private scullyService: ScullyRoutesService) { } -
创建以下组件属性:
posts$: Observable<ScullyRoute[]> | undefined; -
实现
ngOnInit方法:ngOnInit(): void { this.posts$ = this.scullyService.available$.pipe( map(posts => posts.filter(post => post.title)) ); } -
打开
articles.component.html文件并添加以下 HTML 代码:<div class="list-group mt-3"> <a *ngFor="let post of posts$ | async" [routerLink]="post.route" class="list-group-item list-group-item-action"> <div class="d-flex w-100 justify-content-between"> <h5 class="mb-1">{{post.title}}</h5> </div> <p class="mb-1">{{post['description']}}</p> </a> </div>
在前面的步骤中涉及了许多 Angular 技巧,所以让我们一点一点地分解它们。
当我们想在组件中使用 Angular 服务时,我们只需从 Angular 框架中请求它。如何?通过将其添加到组件的 constructor 中的属性。组件不需要了解服务是如何实现的。
ngOnInit 方法是 OnInit 接口的一部分,该接口由我们的组件实现。当 Angular 框架初始化组件时,它会被调用,并为我们提供了一个钩子来添加要执行的定制逻辑。
为组件提供初始化逻辑的 Angular 服务应该在 ngOnInit 方法中调用,而不是在 constructor 中,因为当对组件进行单元测试时,提供这些服务的模拟更容易。
ScullyRoutesService 的 available$ 属性被称为 可观察对象,当订阅它时返回所有由 Scully 生成的可用路由。为了避免显示与博客文章无关的路由,例如 contact 路由,我们从 available$ 属性中过滤掉结果。
在组件模板中,我们使用 Angular 内置的 *ngFor 指令和 async 管道来订阅 HTML 中的 posts$ 可观察对象。然后我们可以使用 post 模板引用变量访问每个项目,并使用插值来显示 title 和 description。
最后,我们为每个锚点元素添加一个 routerLink 指令,以便在点击时导航到相应的博客文章。注意,routerLink 被括号 [] 包围。这种括号语法称为 属性绑定,我们使用它来将 HTML 元素的属性绑定到一个变量。在我们的情况下,我们将 routerLink 指令绑定到 post 变量的 route 属性。
现在我们终于完成了拼图的每一块,我们可以看到我们的博客网站正在运行:
-
运行 Angular CLI 的
build命令来构建我们的 Angular 应用程序:ng build -
执行以下命令来构建 Scully 并生成我们的博客路由:
npx scully --project my-blog上述命令将在
src\assets文件夹中创建一个scully-routes.json文件。它包含我们的 Angular 应用程序的路由,并且是 Scully 运行时所需的。首次运行 Scully 可执行文件时,会提示您收集匿名错误以改进其服务。
-
运行以下命令来提供我们的博客:
npx scully serve --project my-blog
上述命令将启动两个网络服务器:一个包含使用 Scully 构建的网站的静态预渲染版本,另一个是应用程序的 Angular 实时版本:

图 2.5 – 服务器上的我们的应用程序
如果我们打开浏览器并导航到 http://localhost:1668,我们将看不到任何博客文章。使用 Scully 创建的博客文章不会在 ScullyRoutesService 的 available$ 属性中返回,除非我们发布它。要发布博客文章,我们执行以下操作:
-
导航到 Scully 创建的
mdfiles文件夹并打开你找到的唯一.md文件。该文件名和内容可能因 Scully 创建它的日期而异:--- title: 2023-06-22-posts description: 'blog description' published: false slugs: - ___UNPUBLISHED___lj738su6_7mqWyfNdmNCwovaCCi2tZItsDKMPJGcG --- # 2023-06-22-postsScully 在文件顶部关闭和结束
---行之间定义了一组属性,代表关于博客文章的元数据。您也可以添加自己的作为键值对。 -
删除
slugs属性并将published属性设置为true:--- title: 2023-06-22-posts description: 'blog description' published: true --- # 2023-06-22-posts -
运行以下命令以强制 Scully 重新生成我们应用的路线:
npx scully --project my-blog我们需要每次在博客相关文件中做出更改时都执行前面的命令。
-
执行
npx scully serve --project my-blog命令并导航到预览生成的网站。
我们现在可以看到一篇博客文章,这是当我们安装 Scully 时创建的默认文章。让我们再创建一篇:
-
运行以下 Angular CLI 的
generate命令:ng generate @scullyio/init:post --name="Angular and Scully"在前面的命令中,我们使用
@scullyio/init:post规范,传递我们想要创建的文章的名称作为选项。 -
将新博客文章的目标文件夹设置为
mdfiles:What's the target folder for this post? (blog) -
Scully 将在指定的文件夹内创建一个名为
angular-and-scully.md的 Markdown 文件。打开该文件并更新其内容,使其与以下内容相同:--- title: 'Angular and Scully' description: 'How to build a blog with Angular and Scully' published: true --- # Angular and Scully Angular is a robust JavaScript framework that we can use to build excellent and performant web applications. Scully is a popular static website generator that empowers the Angular framework with Jamstack characteristics. You can find more about them in the following links: - https://angular.io - https://scully.io - https://www.jamstack.org -
运行
npx scully --project my-blog以为新建的博客文章创建一个路由。Scully 还会更新scully-routes.json文件以包含新的路由。
如果我们现在预览我们的应用,它应该看起来像以下这样:

图 2.6 – 博客文章列表
如果我们点击其中一个博客条目,我们将导航到所选的博客文章。当前屏幕上显示的内容是博客文章路由的预渲染版本:

图 2.7 – 博客文章详情
为了验证这一点,导航到你的 Angular 项目的 dist 文件夹,在那里你会找到两个文件夹:
-
my-blog: 这包含了我们应用的 Angular 实时版本。当我们执行ng buildAngular CLI 命令时,它会构建我们的应用并将捆绑文件输出到这个文件夹中。 -
static: 这包含了一个由 Scully 生成的 Angular 应用程序的预渲染版本,当我们运行npx scully --project my-blog命令时。
如果我们导航到 static 文件夹,我们会看到 Scully 为我们的 Angular 应用程序的每个路由创建了一个文件夹。每个文件夹都包含一个 index.html 文件,它代表了从该路由激活的组件。
index.html 文件的内容是由 Scully 自动生成的,并且表现得好像我们正在实时运行我们的应用并导航到该组件。
现在,您可以将您的 Angular 应用程序上传到您选择的 CDN 或 Web 服务器,您的博客将立即准备好!接下来,您只需练习您的写作技巧,以创建优秀的博客内容。
概述
在本章中,我们学习了如何将 Angular 框架与 Scully 库结合使用来创建个人博客。
我们看到了 Angular 如何使用内置的路由包来增强 Web 应用程序的内部导航。我们还学习了如何将 Angular 应用程序组织成模块以及如何在这些模块间导航。
我们使用 Scully 库将 Jamstack 引入到我们的 Angular 应用程序中,并看到了如何轻松地将我们的应用程序转换为预渲染的博客。我们使用 Scully 界面创建了一些博客文章并在屏幕上显示它们。
在下一章中,我们将探讨 Angular 框架的另一个令人兴奋的特性——表单。我们将学习如何使用它们并构建一个问题跟踪系统。
实践问题
让我们看看几个实践问题:
-
我们在 Angular 应用程序中使用哪个库进行路由?
-
我们如何在 HTML 锚点元素中添加路由功能?
-
我们使用哪个 Angular 管道进行日期格式化?
-
Angular CLI 应用程序中的
assets文件夹的目的是什么? -
我们使用哪个路由属性来实现模块的懒加载?
-
我们使用哪个 npm CLI 命令来安装 Scully?
-
我们使用哪个服务来获取 Scully 路由?
-
属性绑定是什么?
-
我们在 HTML 中使用哪个 Angular 指令来遍历数组?
-
标准 Angular 应用程序与 Scully 应用程序之间的区别是什么?
进一步阅读
这里有一些链接,可以帮助我们巩固本章所学的内容:
-
Angular 路由:
angular.io/guide/router -
Angular 功能模块:
angular.io/guide/module-types -
Angular 内置管道:
angular.io/api?type=pipe -
Bootstrap CSS:
getbootstrap.com -
Jamstack:
jamstack.org -
Scully:
scully.io -
掌握 Markdown:
guides.github.com/features/mastering-markdown
第三章:使用反应式表单构建问题跟踪系统
Web 应用程序使用 HTML 表单从用户那里收集数据并进行验证,例如在登录应用程序、执行搜索或完成在线支付时。Angular 框架提供了两种类型的表单,反应式和模板驱动型,我们可以在 Angular 应用程序中使用。
在本章中,我们将构建一个用于管理和跟踪问题的系统。我们将使用 Angular 反应式表单来报告新问题。我们还将使用来自 VMware 的 Clarity 设计系统来设计我们的表单和显示我们的问题。
我们将涵盖以下主题:
-
在 Angular 应用程序中安装 Clarity 设计系统
-
显示问题概述
-
报告新问题
-
标记问题为已解决
-
启用新问题建议
必要的背景理论和上下文
Angular 框架提供了两种类型的表单,我们可以使用:
-
模板驱动型:在 Angular 应用程序中设置它们非常简单。模板驱动的表单扩展性不好,且难以测试,因为它们是在组件模板中定义的。
-
反应式:它们基于反应式编程方法。反应式表单在组件的 TypeScript 类中操作,并且比模板驱动型表单更容易测试和扩展。
在本章中,我们将亲身体验反应式表单方法,这是 Angular 社区中最受欢迎的方法。
Angular 组件可以从外部源(如 HTTP 或其他 Angular 组件)获取数据。在后一种情况下,它们通过公共 API 与具有数据的组件交互:
-
@Input():用于向组件传递数据。 -
@Output():用于接收通知或从组件获取数据。
Clarity 是一个包含构建 Web 应用程序的一套 UX 和 UI 指南的设计系统。它还包含一个包含这些指南的专有 HTML 和 CSS 框架。幸运的是,我们不需要使用这个框架,因为 Clarity 已经提供了各种基于 Angular 的 UI 组件,我们可以在我们的 Angular 应用程序中使用。
项目概述
在此项目中,我们将使用反应式表单和 Clarity 构建 Angular 应用程序来管理和跟踪问题。最初,我们将以表格形式显示问题列表,我们可以对其进行排序和筛选。然后我们将创建一个表单,允许用户报告新问题。最后,我们将创建一个模态对话框来解决问题。我们还将更进一步,在报告问题时启用建议,以帮助用户避免重复输入。以下图表展示了项目的架构概述:

图 3.1 – 项目架构
构建时间:1 小时
开始
完成此项目所需的以下软件工具:
-
Angular CLI:Angular 的命令行界面,您可以在
angular.io/cli找到 -
GitHub 材料:本章的相关代码,您可以在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter03文件夹中找到
在 Angular 应用程序中安装 Clarity
让我们通过搭建一个新的 Angular 应用程序来开始创建我们的问题跟踪系统:
ng new issue-tracker --defaults
我们使用 Angular CLI 的 ng new 命令创建一个具有以下特性的新 Angular 应用程序:
-
issue-tracker:Angular 应用程序的名称。 -
--defaults:此选项禁用应用程序的 Angular 路由,并将样式表格式设置为 CSS。
现在,我们需要在我们的 Angular 应用程序中安装 Clarity 库:
-
导航到创建的
issue-tracker文件夹,并运行以下命令来安装它:npm install @cds/core @clr/angular @clr/ui --save -
打开
angular.json文件,并在styles数组中添加 Clarity CSS 样式:"styles": [ **"node_modules/@clr/ui/clr-ui.min.css"****,** "src/styles.css" ] -
最后,在主应用程序模块
app.module.ts中导入ClarityModule和BrowserAnimationsModule:import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; **import** **{** **ClarityModule** **}** **from****'@clr/angular'****;** **import** **{** **BrowserAnimationsModule** **}** **from** **'@angular/platform-browser/animations'****;** @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, **ClarityModule****,** **BrowserAnimationsModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
现在我们已经完成了 Clarity 在我们的应用程序中的安装,我们可以开始使用它构建美观的设计。在接下来的部分,我们将首先创建一个用于显示我们问题的列表。
显示问题概述
我们的 Angular 应用程序将负责管理和跟踪问题。当应用程序启动时,我们应该显示系统中所有待处理问题的列表。待处理问题定义为那些尚未解决的问题。我们将遵循的过程可以进一步分析如下:
-
获取待处理问题
-
使用数据网格可视化问题
获取待处理问题
首先,我们需要创建一个机制来获取所有待处理问题:
-
使用 Angular CLI 的
generate命令创建一个名为issues的 Angular 服务:ng generate service issues之前的命令将在我们的 Angular CLI 项目的
src\app文件夹中创建一个issues.service.ts文件。 -
每个问题都将具有定义类型的特定属性。我们需要使用以下 Angular CLI 命令创建一个 TypeScript 接口:
ng generate interface issue之前的命令将在项目的
src\app文件夹中创建一个issue.ts文件。 -
打开
issue.ts文件,并在Issue接口中添加以下属性:export interface Issue { **issueNo****:** **number****;** **title****:** **string****;** **description****:** **string****;** **priority****:** **'low'** **|** **'high'****;** **type****:** **'Feature'** **|** **'Bug'** **|** **'Documentation'****;** **completed?:** **Date****;** }completed属性是一个问题被解决时的日期。我们将其定义为可选的,因为新问题将不会设置此属性。 -
打开我们在第一步中创建的 Angular 服务,并添加一个
issues属性来存储我们的数据。同时,创建一个getPendingIssues方法,它将返回所有未完成的问题:import { Injectable } from '@angular/core'; **import** **{** **Issue** **}** **from****'./issue'****;** @Injectable({ providedIn: 'root' }) export class IssuesService { **private****issues****:** **Issue****[] = [];** constructor() { } **getPendingIssues****():** **Issue****[] {** **return****this****.****issues****.****filter****(****issue** **=>** **!issue.****completed****);** **}** }在前面的代码中,我们将
issues属性初始化为一个空数组。如果您想从示例数据开始,可以使用本章 GitHub 材料中存在的src\assets文件夹中的mock-issues.ts文件,并按以下方式导入:import { issues } from '../assets/mock-issues';
在接下来的部分,我们将创建一个用于显示这些问题的组件。
在数据网格中可视化问题
我们将使用 Clarity 库的数据网格 UI 组件以表格格式显示数据。数据网格还提供了开箱即用的过滤和排序机制。首先,让我们创建一个将托管数据网格的 Angular 组件:
-
使用 Angular CLI 的
generate命令创建组件:ng generate component issue-list -
打开我们应用程序的主要组件模板
app.component.html,并用以下 HTML 代码替换其内容:<div class="main-container"> <div class="content-container"> <div class="content-area"> <app-issue-list></app-issue-list> </div> </div> </div>一旦 Angular 应用程序启动,问题列表将显示在主组件中。
-
目前,
<app-issue-list>组件不显示任何问题数据。我们必须将其与我们在 获取待处理问题 部分创建的 Angular 服务连接起来。打开issue-list.component.ts文件,并在IssueListComponent类的constructor中注入IssuesService:import { Component } from '@angular/core'; **import** **{** **IssuesService** **}** **from****'../issues.service'****;** @Component({ selector: 'app-issue-list', templateUrl: './issue-list.component.html', styleUrls: ['./issue-list.component.css'] }) export class IssueListComponent { **constructor****(****private** **issueService: IssuesService****) { }** } -
创建一个名为
getIssues的方法,该方法将调用注入的服务中的getPendingIssues方法,并将返回值保存在issues组件属性中:import { Component } from '@angular/core'; **import** **{** **Issue** **}** **from****'../issue'****;** import { IssuesService } from '../issues.service'; @Component({ selector: 'app-issue-list', templateUrl: './issue-list.component.html', styleUrls: ['./issue-list.component.css'] }) export class IssueListComponent { **issues****:** **Issue****[] = [];** constructor(private issueService: IssuesService) { } **private****getIssues****() {** **this****.****issues** **=** **this****.****issueService****.****getPendingIssues****();** **}** } -
最后,在
ngOnInit组件方法中调用getIssues方法,以在组件初始化时获取所有待处理问题:import { Component, **OnInit** } from '@angular/core'; import { Issue } from '../issue'; import { IssuesService } from '../issues.service'; @Component({ selector: 'app-issue-list', templateUrl: './issue-list.component.html', styleUrls: ['./issue-list.component.css'] }) export class IssueListComponent **implements****OnInit** { issues: Issue[] = []; constructor(private issueService: IssuesService) { } **ngOnInit****():** **void** **{** **this****.****getIssues****();** **}** private getIssues() { this.issues = this.issueService.getPendingIssues(); } }
我们已经在我们的组件中实现了获取问题数据的过程。现在我们只需要在模板中显示它。打开 issue-list.component.html 文件,并用以下 HTML 代码替换其内容:
<clr-datagrid>
<clr-dg-column [clrDgField]="'issueNo'" [clrDgColType]="'number'">Issue No</clr-dg-column>
<clr-dg-column [clrDgField]="'type'">Type</clr-dg-column>
<clr-dg-column [clrDgField]="'title'">Title</clr-dg-column>
<clr-dg-column [clrDgField]="'description'">Description</clr-dg-column>
<clr-dg-column [clrDgField]="'priority'">Priority</clr-dg-column>
<clr-dg-row *clrDgItems="let issue of issues">
<clr-dg-cell>{{issue.issueNo}}</clr-dg-cell>
<clr-dg-cell>{{issue.type}}</clr-dg-cell>
<clr-dg-cell>{{issue.title}}</clr-dg-cell>
<clr-dg-cell>{{issue.description}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-danger]="issue.priority === 'high'">{{issue.priority}}</span>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{issues.length}} issues</clr-dg-footer>
</clr-datagrid>
在前面的代码片段中,我们使用了 Clarity 库的几个 Angular 组件:
-
<clr-datagrid>: 定义一个表格。 -
<clr-dg-column>: 定义表格的一列。每一列使用clrDgField指令绑定到该列表示的问题的属性名。clrDgField指令提供了排序和过滤功能,而无需在 TypeScript 类文件中编写任何代码。排序仅适用于基于字符串的内容。如果我们想按不同的原始类型排序,我们必须使用clrDgColType指令并指定特定的类型。 -
<clr-dg-row>: 定义表格的一行。它使用*clrDgItems指令遍历问题,并为每个问题创建一行。 -
<clr-dg-cell>: 每一行包含一组<clr-dg-cell>组件,用于通过插值显示每列的值。在最后一列中,当问题具有高优先级时,我们添加label-danger类以表明其重要性。 -
<clr-dg-footer>: 定义表格的页脚。在这种情况下,它显示问题的总数。
如果我们使用 ng serve 运行我们的 Angular 应用程序,输出将如下所示:

图 3.2 – 待处理问题的概述
在前面的屏幕截图中,应用程序使用 mock-issues.ts 文件中的示例数据。
Clarity 库的数据网格组件具有丰富的功能,我们可以在我们的 Angular 应用程序中使用这些功能。在下一节中,我们将学习如何使用响应式表单来报告新问题。
报告新问题
我们问题跟踪系统的主要功能之一是能够报告新问题。我们将使用 Angular 反应式表单来创建添加新问题的表单。该功能可以进一步细分为以下任务:
-
在 Angular 应用程序中设置反应式表单
-
创建报告问题表单
-
在列表中显示新问题
-
验证问题细节
让我们从在 Angular 应用程序中介绍反应式表单开始。
在 Angular 应用程序中设置反应式表单
反应式表单定义在 Angular 框架的 @angular/forms npm 包中。要将它们添加到我们的 Angular 应用程序中:
-
打开
app.module.ts文件并导入ReactiveFormsModule:import { ReactiveFormsModule } from '@angular/forms'; -
将
ReactiveFormsModule添加到@NgModule装饰器的imports数组中:@NgModule({ declarations: [ AppComponent, IssueListComponent ], imports: [ BrowserModule, ClarityModule, BrowserAnimationsModule, **ReactiveFormsModule** ], providers: [], bootstrap: [AppComponent] })
ReactiveFormsModule 包含了我们与表单一起工作所需的所有必要的 Angular 指令和服务,正如我们将在下一节中看到的。
创建报告问题表单
现在我们已经在 Angular 应用程序中介绍了反应式表单,我们可以开始构建我们的表单:
-
创建一个名为
issue-report的新 Angular 组件:ng generate component issue-report -
打开
issue-report.component.ts文件并添加以下import语句:import { FormControl, FormGroup } from '@angular/forms';在这个语句中,
FormControl代表表单的单个控件,而FormGroup用于将单个控件组合成一个逻辑表单表示。 -
创建以下接口,它将代表我们表单的结构:
interface IssueForm { title: FormControl<string>; description: FormControl<string>; priority: FormControl<string>; type: FormControl<string>; } -
在 TypeScript 类中声明一个
issueForm属性,其类型为FormGroup<IssueForm>:issueForm = new FormGroup<IssueForm>({ title: new FormControl('', { nonNullable: true }), description: new FormControl('', { nonNullable: true }), priority: new FormControl('', { nonNullable: true }), type: new FormControl('', { nonNullable: true }) });我们将所有控件初始化为空字符串,因为表单将用于从头创建新问题。我们还通过使用
nonNullable属性明确声明所有控件默认不接受空值。 -
我们现在必须将我们创建的
FormGroup对象与相应的 HTML 元素关联。打开issue-report.component.html文件,并用以下 HTML 代码替换其内容:<h3>Report an issue</h3> <form clrForm *ngIf="issueForm" [formGroup]="issueForm"> <clr-input-container> <label>Title</label> <input clrInput formControlName="title" /> </clr-input-container> <clr-textarea-container> <label>Description</label> <textarea clrTextarea formControlName="description"></textarea> </clr-textarea-container> <clr-radio-container clrInline> <label>Priority</label> <clr-radio-wrapper> <input type="radio" value="low" clrRadio formControlName="priority" /> <label>Low</label> </clr-radio-wrapper> <clr-radio-wrapper> <input type="radio" value="high" clrRadio formControlName="priority" /> <label>High</label> </clr-radio-wrapper> </clr-radio-container> <clr-select-container> <label>Type</label> <select clrSelect formControlName="type"> <option value="Feature">Feature</option> <option value="Bug">Bug</option> <option value="Documentation">Documentation </option> </select> </clr-select-container> </form>formGroup和clrForm指令将 HTML<form>元素与issueForm属性关联,并将其标识为 Clarity 表单。formControlName指令用于通过名称将 HTML 元素与表单控件关联。每个控件也使用 Clarity 容器元素定义。例如,
title输入控件是一个包含<input>HTML 元素的<clr-input-container>组件。每个原生 HTML 元素都根据其类型附加了一个 Clarity 指令。例如,
<input>HTML 元素包含一个clrInput指令。 -
最后,给我们的
issue-report.component.css文件添加一些样式:.clr-input, .clr-textarea { width: 30%; } button { margin-top: 25px; }
现在我们已经创建了表单的基本结构,我们将学习如何提交其细节:
-
在 HTML
<form>元素的结束标签之前添加一个 HTML<button>元素:<button class="btn btn-primary" type="submit">Create</button>我们将其类型设置为
submit,以便在点击按钮时触发表单提交。 -
打开
issues.service.ts文件并添加一个createIssue方法,该方法将新问题插入到issues数组中:createIssue(issue: Issue) { issue.issueNo = this.issues.length + 1; this.issues.push(issue); }在将问题添加到
issues数组之前,我们自动为问题分配一个新的issueNo属性。当前
issueNo属性是根据issues数组的长度来计算的。一个更好的方法是实现一个生成器机制来创建唯一且随机的issueNo值。 -
返回到
issue-report.component.ts文件,并添加以下import语句:import { Issue } from '../issue'; import { IssuesService } from '../issues.service'; -
将
IssuesService类注入到 TypeScript 类的constructor中:constructor(private issueService: IssuesService) { } -
添加一个新的组件方法,该方法将调用注入的服务中的
createIssue方法:addIssue() { this.issueService.createIssue(this.issueForm.getRawValue() as Issue); }我们使用
issueForm对象的getRawValue属性传递每个表单控件值,该对象将为我们提供对底层表单模型的访问。我们还将其类型转换为Issue接口,因为我们已经知道其值将代表问题对象的属性。 -
打开
issue-report.component.html文件,并将表单的ngSubmit事件绑定到addIssue组件方法:<form clrForm *ngIf="issueForm" [formGroup]="issueForm" **(****ngSubmit****)=****"addIssue()"**>
当我们点击表单的 Create 按钮时,将触发 ngSubmit 事件。
我们已经完成了将新问题添加到系统的所有过程。在下一节中,我们将学习如何在待处理问题表中显示新创建的问题。
在列表中显示新问题
显示和创建新问题是委托给不同的 Angular 组件的两个任务。当我们使用 IssueReportComponent 创建新问题时,我们需要通知 IssueListComponent 以在表格中反映这一变化。首先,让我们看看如何配置 IssueReportComponent 以实现这种通信:
-
打开
issue-report.component.ts文件,并使用@Output()装饰器添加一个EventEmitter属性:@Output() formClose = new EventEmitter();可以从
@angular/corenpm 包中导入Output和EventEmitter符号。 -
在创建问题后,立即在
addIssue组件方法中调用formClose输出属性的emit方法:addIssue() { this.issueService.createIssue(this.issueForm.getRawValue() as Issue); **this****.****formClose****.****emit****();** } -
在组件模板中添加第二个 HTML
<button>元素,并在其click事件上调用formClose.emit方法:<button class="btn" type="button" (click)="formClose.emit()">Cancel</button>
IssueListComponent 现在可以绑定到 IssueReportComponent 的 formClose 事件,并在任何按钮被点击时收到通知。让我们来看看如何实现:
-
打开
issue-list.component.ts文件,并在IssueListComponent类中添加以下属性:showReportIssue = false;showReportIssue属性将切换报告问题表单的显示。 -
添加以下组件方法,当报告问题表单发出
formClose事件时将被调用:onCloseReport() { this.showReportIssue = false; this.getIssues(); }之前的方法会将
showReportIssue属性设置为false,这样报告问题表单就不再可见,而是显示待处理问题的表格。它还会再次获取问题以刷新表格中的数据。 -
打开
issue-list.component.html文件,并在模板顶部添加一个 HTML<button>元素。当点击按钮时,将显示报告问题表单:<button class="btn btn-primary" (click)="showReportIssue = true">Add new issue</button> -
在
<ng-container>元素内组合按钮和数据网格。如*ngIfAngular 指令所示,当报告问题表单不可见时,<ng-container>元素的 内容将被显示:**<****ng-container** *******ngIf****=****"showReportIssue === false"****>** <button class="btn btn-primary" (click)="showReportIssue = true">Add new issue</button> <clr-datagrid> <clr-dg-column [clrDgField]="'issueNo'" [clrDgColType]="'number'">Issue No</clr-dg-column> <clr-dg-column [clrDgField]="'type'">Type</clr-dg-column> <clr-dg-column [clrDgField]="'title'">Title</clr-dg-column> <clr-dg-column [clrDgField]="'description'">Description</clr-dg-column> <clr-dg-column [clrDgField]="'priority'">Priority</clr-dg-column> <clr-dg-row *clrDgItems="let issue of issues"> <clr-dg-cell>{{issue.issueNo}}</clr-dg-cell> <clr-dg-cell>{{issue.type}}</clr-dg-cell> <clr-dg-cell>{{issue.title}}</clr-dg-cell> <clr-dg-cell>{{issue.description}}</clr-dg-cell> <clr-dg-cell> <span class="label" [class.label-danger]="issue.priority === 'high'">{{issue.priority}}</span> </clr-dg-cell> </clr-dg-row> <clr-dg-footer>{{issues.length}} issues</clr-dg-footer> </clr-datagrid> **</****ng-container****>**<ng-container>元素是一个 Angular 组件,它不会在屏幕上渲染,用于组合 HTML 元素。 -
在模板末尾添加
<app-issue-report>组件,并使用*ngIf指令在showReportIssue属性为 true 时显示它。同时将其formClose事件绑定到onCloseReport组件方法:<app-issue-report *ngIf="showReportIssue === true" (formClose)="onCloseReport()"></app-issue-report>
我们已经成功连接了所有点,完成了报告问题表单与显示问题的表格之间的交互。现在,是时候将它们付诸实践了:
-
使用
ngserve运行 Angular 应用程序。 -
点击 添加新问题 按钮,并输入新问题的详细信息:

图 3.3 – 报告问题表单
- 点击 创建 按钮,新的问题应该会出现在表格中:

图 3.4 – 待处理问题
- 重复步骤 2 和 3 而不填写任何详细信息,你将注意到表格中添加了一个空的问题。
可以创建一个空的问题,因为我们没有在我们的报告问题表单上定义任何必填字段。在下一节中,我们将学习如何完成这项任务,并添加验证到我们的表单以避免意外的行为。
验证问题详情
当我们使用报告问题表单创建问题时,我们可以留空表单控件值,因为我们还没有添加任何验证规则。要在表单控件中添加验证,我们使用来自 @angular/forms npm 包的 Validators 类。在构建表单时,每个表单控件实例都会添加一个验证器。在这种情况下,我们将使用 required 验证器来表示表单控件必须有一个值:
-
打开
issue-report.component.ts文件,并从@angular/formsnpm 包导入Validators:import { FormControl, FormGroup, **Validators** } from '@angular/forms'; -
在所有控件(除了问题的
description)中设置Validators.required静态属性:issueForm = new FormGroup<IssueForm>({ title: new FormControl('', { nonNullable: true, **validators****:** **Validators****.****required** }), description: new FormControl('', { nonNullable: true }), priority: new FormControl('', { nonNullable: true, **validators****:** **Validators****.****required** }), type: new FormControl('', { nonNullable: true, **validators****:** **Validators****.****required** }) });我们可以为表单控件使用各种验证器,例如 min、max 和 email。如果我们想在表单控件中设置多个验证器,我们可以在一个数组中添加它们。
-
当我们在表单中使用验证器时,我们需要向用户提供视觉指示。打开
issue-report.component.html文件,并为每个必填表单控件添加<clr-control-error>组件:<clr-input-container> <label>Title</label> <input clrInput formControlName="title" /> **<****clr-control-error****>****Title is required****</****clr-control-error****>** </clr-input-container> <clr-textarea-container> <label>Description</label> <textarea clrTextarea formControlName="description"></textarea> </clr-textarea-container> <clr-radio-container clrInline> <label>Priority</label> <clr-radio-wrapper> <input type="radio" value="low" clrRadio formControlName="priority" /> <label>Low</label> </clr-radio-wrapper> <clr-radio-wrapper> <input type="radio" value="high" clrRadio formControlName="priority" /> <label>High</label> </clr-radio-wrapper> **<****clr-control-error****>****Priority is required****</****clr-control-error****>** </clr-radio-container> <clr-select-container> <label>Type</label> <select clrSelect formControlName="type"> <option value="Feature">Feature</option> <option value="Bug">Bug</option> <option value="Documentation">Documentation</option> </select> **<****clr-control-error****>****Type is required****</****clr-control-error****>** </clr-select-container><clr-control-error>Clarity 组件在表单中提供验证消息。当触摸无效控件时,它会显示。当至少有一个验证规则被违反时,控件是无效的。 -
用户可能只是偶尔触摸表单控件来查看验证消息。因此,我们需要在表单提交时考虑这一点并相应地操作。为了克服这种情况,我们将在表单提交时将所有表单控件标记为已触摸:
addIssue() { **if** **(****this****.****issueForm** **&&** **this****.****issueForm****.****invalid****) {** **this****.****issueForm****.****markAllAsTouched****();** **return****;** **}** this.issueService.createIssue(this.issueForm.getRawValue() as Issue); this.formClose.emit(); } markAllAsTouched method of the issueForm property to mark all controls as touched when the form is invalid. Marking controls as touched makes validation messages appear automatically. Additionally, we use a return statement to prevent the creation of the issue when the form is invalid. -
运行
ng serve以启动应用程序。在标题输入框内点击,然后移出表单控件:![图 3.4 – 标题验证消息]()
图 3.5 – 标题验证消息
在标题输入框下方应出现一条消息,说明我们尚未输入任何值。Clarity 库中的验证消息由文本和红色感叹号图标在验证的表单控件中表示。
-
现在,点击创建按钮:

图 3.6 – 表单验证消息
所有验证消息将同时出现在屏幕上,表单将不会提交。响应式表单中的验证确保了我们的 Angular 应用程序拥有流畅的用户体验。在下一节中,我们将学习如何使用 Clarity 创建模态对话框,并使用它来解决列表中的问题。
解决问题
建立问题跟踪系统的主要思想是问题应该在某个时候得到解决。我们将在我们的应用程序中创建一个用户工作流程来完成这项任务。我们将能够直接从待解决问题列表中解决问题。在解决问题时,应用程序将使用模态对话框向用户请求确认:
-
创建一个 Angular 组件来托管对话框:
ng generate component confirm-dialog -
打开
confirm-dialog.component.ts文件,并按以下方式修改它:import { Component, **EventEmitter****,** **Input****,** **Output** } from '@angular/core'; @Component({ selector: 'app-confirm-dialog', templateUrl: './confirm-dialog.component.html', styleUrls: ['./confirm-dialog.component.css'] }) export class ConfirmDialogComponent { **@Input****()** **issueNo****:** **number** **|** **null** **=** **null****;** **@Output****() confirm =** **new****EventEmitter****<****boolean****>();** }我们使用
@Input()装饰器来获取问题编号并在组件模板中显示它。confirm事件将发出一个boolean值,以指示用户是否确认解决了问题。 -
创建两个方法,这两个方法将调用
confirm输出属性的emit方法,要么是true,要么是false:agree() { this.confirm.emit(true); this.issueNo = null; } disagree() { this.confirm.emit(false); this.issueNo = null; }
这两个方法都将issueNo属性设置为null,因为该属性也将控制模态对话框是否打开。所以,我们希望在两种情况下都关闭对话框。
我们已经设置了对话框组件的 TypeScript 类。现在让我们通过其模板将其连接起来。打开confirm-dialog.component.html文件,并用以下内容替换其内容:
<clr-modal [clrModalOpen]="issueNo !== null" [clrModalClosable]="false">
<h3 class="modal-title">
Resolve Issue #
{{issueNo}}
</h3>
<div class="modal-body">
<p>Are you sure you want to close the issue?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="disagree()">Cancel</button>
<button type="button" class="btn btn-danger" (click)="agree()">Yes, continue</button>
</div>
</clr-modal>
Clarity 模态对话框由一个<clr-modal>组件和一组具有特定类的 HTML 元素组成:
-
modal-title:显示当前问题编号的对话框标题。 -
modal-body:对话框的主要内容。 -
modal-footer:对话框的页脚,通常用于添加该对话框的动作。我们目前添加了两个 HTML<button>元素,并将它们的click事件分别绑定到agree和disagree组件方法。
不论是打开还是关闭,对话框的当前状态由绑定到issueNo输入属性的clrModalOpen指令指示。当该属性为 null 时,对话框是关闭的。clrModalClosable指令指示对话框不能通过任何其他方式关闭,只能通过程序化地通过issueNo属性关闭。
根据我们的规范,我们希望用户能够直接从列表中解决问题。让我们找出如何将我们创建的对话框与待解决问题列表集成:
-
打开
issues.service.ts文件并添加一个新方法来设置问题的completed属性:completeIssue(issue: Issue) { const selectedIssue: Issue = { ...issue, completed: new Date() }; const index = this.issues.findIndex(i => i === issue); this.issues[index] = selectedIssue; }之前的方法首先创建了一个我们想要解决的问题的副本,并将其
completed属性设置为当前日期。然后它在issues数组中找到初始问题,并用克隆实例替换它。 -
打开
issue-list.component.ts文件并在 TypeScript 类中添加一个selectedIssue属性和一个onConfirm方法:selectedIssue: Issue | null = null; onConfirm(confirmed: boolean) { if (confirmed && this.selectedIssue) { this.issueService.completeIssue(this.selectedIssue); this.getIssues(); } this.selectedIssue = null; }只有当
confirmed参数为真时,onConfirm方法才会调用issueService属性的completeIssue方法。在这种情况下,它还会调用getIssues方法来刷新表格数据。selectedIssue属性持有我们想要解决的问题的Issue对象,并且每次调用onConfirm方法时都会重置。 -
打开
issue-list.component.html文件并在<clr-dg-row>组件内添加一个操作溢出组件:<clr-dg-row *clrDgItems="let issue of issues"> **<****clr-dg-action-overflow****>** **<****button****class****=****"action-item"** **(****click****)=****"selectedIssue = issue"****>****Resolve****</****button****>** **</****clr-dg-action-overflow****>** <clr-dg-cell>{{issue.issueNo}}</clr-dg-cell> <clr-dg-cell>{{issue.type}}</clr-dg-cell> <clr-dg-cell>{{issue.title}}</clr-dg-cell> <clr-dg-cell>{{issue.description}}</clr-dg-cell> <clr-dg-cell> <span class="label" [class.label-danger]="issue.priority === 'high'">{{issue.priority}}</span> </clr-dg-cell> </clr-dg-row>Clarity 组件
<clr-dg-action-overflow>在每一行表格中添加一个下拉菜单。该菜单包含一个按钮,当点击时,将selectedIssue属性设置为当前问题。 -
最后,在模板末尾添加
<app-confirm-dialog>组件:<app-confirm-dialog *ngIf="selectedIssue" [issueNo]="selectedIssue.issueNo" (confirm)="onConfirm($event)"></app-confirm-dialog>我们将
selectedIssue的issueNo属性传递给对话框组件的输入绑定。我们还将
onConfirm组件方法绑定到confirm事件,以便我们可以在用户同意或不同意时得到通知。$event参数是 Angular 中的一个保留关键字,包含事件绑定结果,这取决于 HTML 元素类型。在这种情况下,它包括确认的boolean结果。
我们已经将所有部件放在一起以解决问题。让我们试一试:
-
运行
ng serve并在http://localhost:4200打开应用程序。 -
如果您没有任何问题,请使用 添加新问题 按钮创建一个。
-
点击某一行的操作菜单并选择 解决。菜单是位于 问题编号 列旁边的三个垂直点图标:

图 3.7 – 操作菜单
- 在出现的对话框中,点击 是,继续 按钮:

图 3.8 – 解决问题对话框
点击按钮后,对话框将关闭,问题不应再在列表中可见。
我们为我们的应用程序用户提供了解决问题的方法。我们的问题跟踪系统现在已完整并准备好投入使用!有时,用户可能因为匆忙而报告了已经报告的问题。在下一节中,我们将学习如何利用高级响应式表单技术来帮助他们。
开启新问题建议
响应式表单 API 包含一个机制,用于在特定表单控件值发生变化时接收通知。我们将在我们的应用程序中使用它来在报告新问题时查找相关的问题。更具体地说,当用户开始在标题表单控件中输入时,我们将显示建议问题的列表:
-
打开
issues.service.ts文件并添加以下方法:getSuggestions(title: string): Issue[] { if (title.length > 3) { return this.issues.filter(issue => issue.title.indexOf(title) !== -1); } return []; }前面的方法接受一个问题的标题作为参数,并搜索包含相同标题的任何问题。当
title参数超过三个字符长时,搜索机制被触发,以限制结果到一个合理的数量。 -
打开
issue-report.component.ts文件并从@angular/corenpm 包导入OnInit实体:import { Component, EventEmitter, **OnInit**, Output } from '@angular/core'; -
创建一个新的组件属性来保存建议的问题:
suggestions: Issue[]= []; -
将
OnInit接口添加到IssueReportComponent类实现的接口列表中:export class IssueReportComponent **implements****OnInit** { -
FormGroup对象的controls属性包含所有表单控件作为键值对。键是控件的名称,值是实际的表单控件对象。我们可以通过访问其名称(在这种情况下为title)来获取关于表单控件值变化的通知,以下是这样做的:ngOnInit(): void { this.issueForm.controls.title.valueChanges.subscribe(title => { this.suggestions = this.issueService.getSuggestions(title); }); }每个控件都公开一个
valueChanges可观察对象,我们可以订阅它并获取一个连续的值流。valueChanges可观察对象在用户开始在表单的title控件中输入时立即发出新值。当发生这种情况时,我们将getSuggestions方法的结果设置在suggestions组件属性中。 -
要在组件的模板上显示建议的问题,打开
issue-report.component.html文件,并在<clr-input-container>元素之后添加以下 HTML 代码:<div class="clr-row" *ngIf="suggestions.length"> <div class="clr-col-lg-2"></div> <div class="clr-col-lg-6"> <clr-stack-view> <clr-stack-header>Similar issues</clr-stack-header> <clr-stack-block *ngFor="let issue of suggestions"> <clr-stack-label>#{{issue.issueNo}}:{{issue.title}}</clr-stack-label> <clr-stack-content>{{issue.description}}</clr-stack-content> </clr-stack-block> </clr-stack-view> </div> </div>我们使用 Clarity 库中的
<clr-stack-view>组件以键值对的形式显示建议的问题。键由<clr-stack-header>组件指示,显示问题的标题和编号。《clr-stack-content》组件指示值并显示问题描述。
只有当有可用的建议问题时,我们才会显示类似的问题。
运行 ng serve 并打开报告问题表单以创建新问题。当你开始在 标题 输入框中输入时,应用程序将建议任何与你试图创建的问题相关的任何问题:

图 3.9 – 类似的问题
用户现在将看到是否有任何类似的问题,并避免报告重复的问题。
摘要
在本章中,我们使用响应式表单和 Clarity 设计系统构建了一个 Angular 应用程序来管理和跟踪问题。
首先,我们在 Angular 应用程序中安装了 Clarity,并使用数据网格组件显示待处理问题的列表。然后,我们介绍了响应式表单,并使用它们来构建报告新问题的表单。我们在表单中添加了验证,以向用户提供必填字段的视觉指示并防止不希望的行为。
如果我们的用户能够解决问题,问题跟踪系统才会高效。我们使用 Clarity 构建了一个模态对话框来解决问题。最后,我们通过在报告新问题时建议相关问题来改进了应用程序的 UX。
在下一章中,我们将使用 Angular 服务工作者构建一个用于天气的渐进式网络应用程序。
练习
创建一个 Angular 组件来编辑现有问题的详细信息。该组件应显示问题编号,并允许用户更改标题、描述和优先级。标题和描述应为必填字段。
用户应能够通过待处理问题列表中的操作菜单访问上一个组件。向操作菜单中添加一个新按钮以打开编辑问题表单。
用户完成更新问题后,表单应关闭,并刷新待处理问题的列表。
您可以在 exercise 分支的 Chapter03 文件夹中找到练习的解决方案:github.com/PacktPublishing/Angular-Projects-Third-Edition/tree/exercise。
进一步阅读
-
Angular 表单:
angular.io/guide/forms-overview -
验证响应式表单:
angular.io/guide/form-validation#validating-input-in-reactive-forms -
向组件传递数据:
angular.io/guide/component-interaction#pass-data-from-parent-to-child-with-input-binding -
从组件获取数据:
angular.io/guide/component-interaction#parent-listens-for-child-event -
开始使用 Clarity:
clarity.design/documentation/get-started
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第四章:使用 Angular 服务工作者构建 PWA 天气应用程序
我们可以使用不同类型的设备访问网络应用程序,例如桌面、移动或平板电脑,以及各种类型的网络,如宽带、Wi-Fi 和蜂窝网络。网络应用程序应该无缝工作,并独立于用户的设备和网络提供相同的用户体验。
渐进式网络应用(PWA) 是考虑到上述因素构建的应用程序。一种流行的技术是 服务工作者,它可以提高 Web 应用程序的加载时间。在本章中,我们将使用 Angular 框架的服务工作者实现来构建一个 PWA,使用 OpenWeather API 显示城市的天气。
我们将详细介绍以下主题:
-
设置 OpenWeather API
-
显示天气数据
-
使用服务工作者启用离线模式
-
通过应用内通知保持最新
-
使用 Firebase Hosting 部署我们的应用程序
必要的背景理论和上下文
传统的网络应用程序通常托管在 Web 服务器上,并且任何给定时间对任何用户都是立即可用的。本地应用程序安装在用户的设备上,可以访问其本地资源,并且可以与任何网络无缝工作。PWA 横跨 Web 和本地应用程序的两个世界,并具有两者的特征,总结如下:
-
能力:它可以访问本地保存的数据,并与连接到用户设备的外围硬件交互。
-
可靠性:它可以在任何网络连接中提供相同的性能和体验,即使在网络连接和覆盖范围较低的地区。
-
可安装性:它可以在用户的设备上安装,可以直接从主屏幕启动,并与其他已安装的本地应用程序交互。
将网络应用程序转换为 PWA 涉及多个步骤和技术。其中最重要的一项是配置服务工作者。服务工作者是一种在 Web 浏览器上运行的机制,充当应用程序与外部 HTTP 端点或其他应用程序内资源(如 JavaScript 和 CSS 文件)之间的代理。服务工作者的主要任务是拦截对这些资源的请求,并通过提供缓存的或实时响应来对其采取行动。
服务工作者在浏览器标签页关闭后仍然保持持久。
Angular 框架提供了一个服务工作者的实现,我们可以使用它将我们的 Angular 应用程序转换为 PWA。
它还包含一个内置的 HTTP 客户端,我们可以使用它通过 HTTP 与服务器通信。Angular HTTP 客户端公开了一个基于观察器的 API,具有所有标准 HTTP 方法,如 POST 和 GET。观察器基于观察者模式,这是响应式编程的核心。在观察者模式中,多个称为观察者的对象可以订阅观察器并接收有关其状态变化的任何通知。观察器通过异步发射事件流来向观察者发送更改。Angular 框架使用一个名为RxJS的库,其中包含用于处理观察器的各种工具。其中之一是一组称为操作符的函数,可以对观察器应用各种操作,如转换和过滤。接下来,让我们对我们的项目进行概述。
项目概述
在这个项目中,我们将构建一个 PWA 来显示城市的天气状况。最初,我们将学习如何配置 OpenWeather API,我们将使用它来获取天气数据。然后,我们将学习如何使用 API 在 Angular 组件中显示天气信息。我们将了解如何使用服务工作者将我们的 Angular 应用程序转换为 PWA。我们还将为我们的应用程序实现更新通知机制。最后,我们将把我们的 PWA 部署到 Firebase Hosting 提供者。以下图表展示了项目的架构概述:

图 4.1 – 项目架构
构建时间:90 分钟
入门
完成此项目所需的以下软件工具:
-
Angular CLI:Angular 的命令行界面,您可以在
angular.io/cli找到。 -
GitHub 材料:本章的相关代码可以在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter04文件夹中找到。
设置 OpenWeather API
OpenWeather 团队创建了 OpenWeather API,该 API 包含来自全球 200,000 多个城市的当前和历史天气信息。它还支持更详细信息的天气预报数据。
我们需要首先获取一个 API 密钥,才能开始使用 OpenWeather API:
-
导航到 OpenWeather API 网站
openweathermap.org/api。您将看到 OpenWeather 团队提供的所有可用 API 的列表。
-
找到当前天气数据部分,并点击订阅按钮。
您将被重定向到包含服务可用定价方案的页面。每个方案支持每分钟和每月不同的 API 调用组合。对于这个项目,我们将使用免费级别。
-
点击获取 API 密钥按钮。
您将被重定向到服务的注册页面。
-
完成所有必填信息,并点击创建账户按钮。
将确认消息发送到您用于创建账户的电子邮件地址。
-
找到确认电子邮件并点击 验证您的电子邮件 按钮以完成注册。
您将很快收到来自 OpenWeather 的另一封电子邮件,其中包含有关您当前订阅的详细信息,包括您的 API 密钥以及您将与 API 通信的 HTTP 端点。
API 密钥可能需要一些时间才能激活,通常在您可以使用它之前需要几个小时。
一旦 API 密钥被激活,我们就可以在 Angular 应用程序中使用它。我们将在下一节中学习如何做到这一点。
显示天气数据
在本节中,我们将创建一个 Angular 应用程序来显示给定城市的天气信息。用户将在输入字段中输入城市的名称,应用程序将使用 OpenWeather API 获取指定城市的天气数据。我们将更详细地介绍以下主题:
-
设置 Angular 应用程序
-
与 OpenWeather API 通信
-
显示城市的天气信息
让我们从创建 Angular 应用程序开始,接下来的部分将介绍如何进行。
设置 Angular 应用程序
我们将使用 Angular CLI 的 ng new 命令从头创建一个新的 Angular 应用程序:
ng new weather-app --style=scss --routing=false
上述命令将创建一个新的 Angular CLI 应用程序,具有以下属性:
-
weather-app:Angular 应用程序的名称 -
--style=scss:表示我们的 Angular 应用程序将使用 SCSS 样式表格式 -
--routing=false:禁用应用程序中的 Angular 路由
用户应在输入字段中输入城市的名称,并且该城市的天气信息应以卡片布局进行可视化。Angular Material 库提供了一套 UI 组件来满足我们的需求,包括输入和卡片。
Angular Material 组件遵循 Material Design 原则,并由 Angular 的 Components 团队维护。我们可以使用以下 Angular CLI 命令安装 Angular Material 库:
ng add @angular/material --theme=indigo-pink --animations=enabled --typography
上述代码使用了 Angular CLI 的 ng add 命令,并传递了额外的配置选项:
-
@angular/material:Angular Material 库的 npm 包名。它还将安装 Angular CDK 包,这是一个用于构建 Angular Material 的行为和交互的集合。这两个包都将添加到应用程序的package.json文件的dependencies部分中。 -
--theme=indigo-pink:我们想要使用的 Angular Material 主题的名称。添加主题涉及修改 Angular CLI 工作区的几个文件。它将 CSS 主题文件的条目添加到angular.json配置文件中:@angular/material/prebuilt-themes/indigo-pink.css它还包括
index.html文件中的 Material Design 图标:<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">Angular Material 随带一套预定义的主题,我们可以使用。或者,我们可以构建一个符合我们特定需求的自定义主题。
-
--animations=enabled:通过将BrowserAnimationsModule导入主应用程序模块app.module.ts来在应用程序中启用浏览器动画:import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; **import** **{** **BrowserAnimationsModule** **}** **from****'@angular/platform-browser/animations'****;** @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, **BrowserAnimationsModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } -
--typography:在应用程序中全局启用 Angular Material 字体排印。字体排印定义了文本内容的显示方式,并默认使用 Roboto 字体,该字体包含在index.html文件中:<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">它向 HTML 文件的
<body>标签中添加以下类:<body **class****=****"mat-typography"**> <app-root></app-root> </body>它还向应用程序的全局
styles.scss文件中添加了一些 CSS 样式:html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
我们现在拥有了构建 Angular 应用程序的所有组件。在下一节中,我们将创建一个与 OpenWeather API 交互的机制。
与 OpenWeather API 通信
应用程序应通过 HTTP 与 OpenWeather API 交互以获取天气数据。让我们看看我们如何在应用程序中设置这种类型的通信:
-
首先,我们必须创建一个接口来描述我们将从 API 获取的数据类型。使用以下 Angular CLI 命令创建一个:
ng generate interface weather上述命令将在 Angular CLI 项目的
src\app文件夹中创建weather.ts文件。 -
打开
weather.ts文件并按以下方式修改它:export interface Weather { weather: WeatherInfo[], main: { temp: number; pressure: number; humidity: number; }; wind: { speed: number; }; sys: { country: string }; name: string; } interface WeatherInfo { main: string; icon: string; }每个属性都对应于 OpenWeather API 响应中的天气字段。您可以在
openweathermap.org/current#parameter上找到每个字段的描述。然后,我们必须设置 Angular 框架提供的内置 HTTP 客户端。
-
打开
app.module.ts文件并将HttpClientModule添加到@NgModule装饰器的imports数组中:**import** **{** **HttpClientModule** **}** **from****'@angular/common/http'****;** import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, BrowserAnimationsModule, **HttpClientModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } -
使用以下 Angular CLI 命令创建一个新的 Angular 服务:
ng generate service weather上述命令将在 Angular CLI 项目的
src\app文件夹中创建weather.service.ts文件。 -
打开
weather.service.ts文件并将HttpClient服务注入到其constructor中:**import** **{** **HttpClient** **}** **from****'@angular/common/http'****;** import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class WeatherService { constructor(**private** **http: HttpClient**) { } } -
将以下属性添加以定义 OpenWeather API 的端点 URL 和我们的 API 密钥:
private apiUrl = 'https://api.openweathermap.org/data/2.5/'; private apiKey = '<Your API key>';将
apiKey属性的值替换为您拥有的 API 密钥。-
在服务中添加一个方法,该方法接受城市名称作为单个参数并查询 OpenWeather API 以获取该城市的天气:
getWeather(city: string): Observable<Weather> { const options = new HttpParams() .set('units', 'metric') .set('q', city) .set('appId', this.apiKey); return this.http.get<Weather>(this.apiUrl + 'weather', { params: options }); }
getWeather方法使用HttpClient服务的get方法,它接受两个参数。第一个参数是 OpenWeather API 的 URL 端点。第二个参数是一个options对象,用于将额外的配置传递给请求,例如带有params属性的 URL 查询参数。我们使用
HttpParams对象的构造函数并调用其set方法为要添加到 URL 的每个查询参数。在我们的例子中,我们传递q参数用于城市名称,appId用于 API 密钥,以及我们想要使用的units类型。您可以在openweathermap.org/current#data上了解更多关于支持的单位信息。我们使用
set方法创建查询参数,因为HttpParams对象是不可变的。为每个要传递的参数调用构造函数将引发错误。我们还在
get方法中将响应数据类型设置为Weather。请注意,getWeather方法不返回Weather数据,而是一个此类型的Observable。 -
-
在文件顶部添加以下
import语句:import { HttpClient, **HttpParams** } from '@angular/common/http'; import { Injectable } from '@angular/core'; **import** **{** **Observable** **}** **from****'rxjs'****;** **import** **{** **Weather** **}** **from****'./weather'****;**
我们创建的 Angular 服务包含与 OpenWeather API 交互所需的所有必要组件。在下一节中,我们将创建一个 Angular 组件来发起请求并显示数据。
显示城市的天气信息
用户应该能够使用我们应用程序的 UI 并输入他们想要查看天气详情的城市名称。应用程序将使用该信息查询 OpenWeather API,并将请求结果以卡片布局的形式显示在 UI 上。让我们开始构建一个 Angular 组件来创建所有这些类型的交互:
-
使用以下 Angular CLI 命令创建 Angular 组件:
ng generate component weather -
打开主组件的模板,
app.component.html,并用新组件的选择器<app-weather>替换其内容:<app-weather></app-weather> -
打开
app.module.ts文件,并将 Angular Material 库中的以下模块添加到@NgModule装饰器的imports数组中:@NgModule({ declarations: [ AppComponent, WeatherComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, **MatIconModule****,** **MatInputModule****,** **MatCardModule** ], providers: [], bootstrap: [AppComponent] })还需要在文件顶部添加必要的
import语句:import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; -
打开
weather.component.ts文件,为Weather类型创建一个weather属性,并将WeatherService注入到WeatherComponent类的constructor中:import { Component } from '@angular/core'; **import** **{** **Weather** **}** **from****'****../weather'****;** **import** **{** **WeatherService** **}** **from****'../weather.service'****;** @Component({ selector: 'app-weather', templateUrl: './weather.component.html', styleUrls: ['./weather.component.scss'] }) export class WeatherComponent { **weather****:** **Weather** **|** **undefined****;** **constructor****(****private** **weatherService: WeatherService****){ }** } -
创建一个组件方法,它订阅
WeatherService的getWeather方法,并将结果分配给weather组件属性:search(city: string) { this.weatherService.getWeather(city).subscribe(weather => this.weather = weather); }
我们已经完成了与组件的 TypeScript 类文件的协作。现在让我们将其连接到其模板。打开weather.component.html文件,并用以下 HTML 代码替换其内容:
<mat-form-field>
<input matInput placeholder="Enter city" #cityCtrl (keydown.enter)="search(cityCtrl.value)">
<mat-icon matSuffix (click)="search(cityCtrl.value)">search</mat-icon>
</mat-form-field>
<mat-card *ngIf="weather">
<mat-card-header>
<mat-card-title>{{weather.name}}, {{weather.sys.country}}</mat-card-title>
<mat-card-subtitle>{{weather.weather[0].main}}</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="img/{{weather.weather[0].icon}}@2x.png" [alt]="weather.weather[0].main">
<mat-card-content>
<h1>{{weather.main.temp | number:'1.0-0'}} ℃</h1>
<p>Pressure: {{weather.main.pressure}} hPa</p>
<p>Humidity: {{weather.main.humidity}} %</p>
<p>Wind: {{weather.wind.speed}} m/s</p>
</mat-card-content>
</mat-card>
上述模板由 Angular Material 库中的几个组件组成,包括一个包含以下子元素的<mat-form-field>组件:
-
一个用于输入城市名称的
<input>HTML 元素。当用户完成编辑并按下Enter键时,它会调用search组件方法,并将cityCtrl变量的值属性作为参数传递。cityCtrl变量是一个模板引用变量,表示原生 HTML<input>元素的实体对象。 -
<mat-icon>组件在输入元素的末尾显示一个放大镜图标,如matSuffix指令所示。点击时,它还会调用search组件的方法。
cityCtrl模板引用变量由#表示,并在组件模板内部任何地方都可以访问。
<mat-card> 组件以卡片布局展示信息,并且仅在 weather 组件属性有值时显示。它由以下子元素组成:
-
<mat-card-header>:卡片的头部。它由一个<mat-card-title>组件组成,显示城市名称和国家代码,以及一个<mat-card-subtitle>组件,显示当前的天气状况。 -
mat-card-image:显示天气状况图标的卡片图片,以及作为替代文本的描述。 -
<mat-card-content>:卡片的主要内容。它显示当前天气的温度、压力、湿度和风速。温度以没有小数点的形式显示,如number管道所示。
现在我们来增加一些样式,让我们的组件更有趣:
weather.component.scss
:host {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding-top: 25px;
}
mat-form-field {
width: 20%;
}
mat-icon {
cursor: pointer;
}
mat-card {
margin-top: 30px;
width: 250px;
}
h1 {
text-align: center;
font-size: 2.5em;
}
:host 选择器是 Angular 独特的 CSS 选择器,它针对托管我们的组件的 HTML 元素,在我们的例子中,是 <app-weather> HTML 元素。
如果我们使用 ng serve 运行我们的应用程序,导航到 http://localhost:4200,并在 Athens 中搜索天气信息,我们应该在屏幕上得到以下输出:

图 4.2 – 应用程序输出
恭喜!现在,你拥有了一个完全工作的 Angular 应用程序,它可以显示特定城市的天气信息。该应用程序由一个单一的 Angular 组件组成,通过 Angular 服务使用 HTTP 与 OpenWeather API 进行通信。我们学习了如何使用 Angular Material 来美化我们的组件,并让我们的用户在使用我们的应用程序时获得愉悦的体验。但当我们离线时会发生什么?应用程序是否按预期工作?用户的体验是否保持不变?让我们在下一节中找出答案。
通过服务工作者启用离线模式
来自任何地方的用户现在都可以访问我们的 Angular 应用程序,以获取他们感兴趣的任何城市的天气信息。当我们说“任何地方”时,我们指的是任何网络类型,例如宽带、蜂窝(3G/4G/5G)和 Wi-Fi。考虑一个用户处于覆盖范围低或频繁断网的地方的情况。我们的应用程序会如何表现?让我们通过实验来找出答案:
-
使用 Angular CLI 的
ng serve命令运行 Angular 应用程序。 -
打开你喜欢的浏览器,导航到
http://localhost:4200,这是 Angular CLI 项目的默认地址和端口号。你应该能看到输入字段,用于输入城市的名称:

图 4.3 – 输入城市名称
- 打开你浏览器的开发者工具,并导航到 网络 选项卡。将 Throttling 下拉菜单的值设置为 Offline:

图 4.4 – 离线网络模式
- 尝试刷新您的浏览器。您将看到您已从互联网断开连接的指示,如下面的截图所示:

图 4.5 – 无互联网连接(Google Chrome)
在低质量互联网连接的地区,这种情况是标准的。那么,我们能为这样的用户做些什么呢?幸运的是,Angular 框架包含了一个服务工作者的实现,当在离线模式下运行时,它可以显著提高我们应用程序的用户体验。它可以缓存某些应用程序部分并相应地提供它们,而不是进行实际请求。
Angular 服务工作者也可以用于具有大网络延迟连接的环境。考虑在这种类型的网络中也使用服务工作者来改善用户的体验。
运行以下 Angular CLI 命令以在我们的 Angular 应用程序中启用服务工作者:
ng add @angular/pwa
上述命令将根据 PWA 支持相应地转换 Angular CLI 工作区:
-
它将
@angular/service-workernpm 包添加到应用程序的package.json文件的dependencies部分。 -
它在应用程序的
src文件夹中创建manifest.webmanifest文件。该清单文件包含有关应用程序的信息,这些信息是安装和运行原生应用程序所需的。它还将其添加到angular.json文件的build选项的assets数组中。 -
它在项目根目录中创建
ngsw-config.json文件,这是服务工作者配置文件。我们使用它来定义特定配置的工件,例如哪些资源被缓存以及如何缓存。您可以在以下链接中找到有关服务工作者配置的更多详细信息:angular.io/guide/service-worker-config#service-worker-configuration。 -
配置文件也在
angular.json文件的build配置中的ngswConfigPath属性中设置。 -
它在
angular.json文件的build配置中将serviceWorker属性设置为true。 -
它在
app.module.ts文件中注册服务工作者:@NgModule({ declarations: [ AppComponent, WeatherComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, MatIconModule, MatInputModule, MatCardModule, **ServiceWorkerModule****.****register****(****'ngsw-worker.js'****, {** **enabled****: !****isDevMode****(),** **// Register the ServiceWorker as soon as the application is stable** **// or after 30 seconds (whichever comes first).** **registrationStrategy****:** **'registerWhenStable:30000'** **})** ], providers: [], bootstrap: [AppComponent] }) -
ngsw-worker.js文件是包含服务工作者实际实现的 JavaScript 文件。当构建我们的应用程序时,它会为我们自动创建。Angular 使用ServiceWorkerModule类的register方法在我们的应用程序中注册它。 -
它为当应用程序作为原生应用安装到用户设备上时使用创建几个图标。
-
它在
index.html文件的<head>元素中包含清单文件和一个theme-color的<meta>标签:<link rel="manifest" href="manifest.webmanifest"> <meta name="theme-color" content="#1976d2">
现在我们已经完成了服务工作者的安装,是时候测试它了!在继续之前,我们应该安装一个外部网络服务器,因为 Angular CLI 的内置功能不支持服务工作者。一个不错的选择是 http-server:
-
运行 npm 客户端的
install命令来安装http-server:npm install -D http-server上述命令将
http-server作为我们的 Angular CLI 项目的开发依赖项安装。 -
使用 Angular CLI 的
ng build命令构建 Angular 应用程序。 -
打开 Angular CLI 工作区的
package.json文件,并将以下条目添加到scripts属性中:"scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", **"server"****:** **"http-server -p 8080 -c-1 dist/weather-app"** } -
使用以下命令启动 HTTP 网络服务器:
npm run server上述命令将在端口 8080 上启动 http-server 并禁用缓存。
-
打开您的浏览器并导航到
http://localhost:8080。建议在私密或隐身模式下打开页面,以避免服务工作者出现意外行为。
-
重复本节开头为切换到离线模式所遵循的过程。
-
如果您现在刷新页面,您会注意到应用程序按预期工作。
服务工作者为我们做了所有工作,整个过程如此无缝,以至于我们无法判断我们是处于在线还是离线状态。您可以通过检查 网络 选项卡来验证这一点:
![图 4.5 – 服务工作者(离线模式)]()
图 4.6 – 服务工作者(离线模式)
大小 列中的 (ServiceWorker) 值表示服务工作者为我们提供了应用程序的缓存版本。
我们已成功安装服务工作者,并更接近将我们的应用程序转换为 PWA。在下一节中,我们将学习如何通知用户应用程序的潜在更新。
保持应用程序内通知的更新
当我们想在网络应用程序中应用更改时,我们进行更改并构建应用程序的新版本。然后,应用程序被部署到网络服务器,每个用户都可以立即访问最新版本。但 PWA 是不同的。
当我们部署 PWA 的新版本时,服务工作者必须相应地采取行动并应用特定的更新策略。它应该通知用户新版本或立即安装它。我们遵循哪种更新策略取决于我们的需求。在这个项目中,我们想向用户显示提示,并让他们决定是否想要更新应用程序。让我们看看如何在我们的应用程序中实现这个功能:
-
打开
app.module.ts文件,并将MatSnackBarModule添加到@NgModule装饰器的imports数组中:**import** **{** **MatSnackBarModule** **}** **from****'@angular/material/snack-bar'****;** @NgModule({ declarations: [ AppComponent, WeatherComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, MatIconModule, MatInputModule, MatCardModule, **MatSnackBarModule**, ServiceWorkerModule.register('ngsw-worker.js', { enabled: !isDevMode(), // Register the ServiceWorker as soon as the application is stable // or after 30 seconds (whichever comes first). registrationStrategy: 'registerWhenStable:30000' }) ], providers: [], bootstrap: [AppComponent] })MatSnackBarModule是一个 Angular Material 模块,它允许我们与 snack bars 进行交互。snack bar 是一个通常出现在页面底部的弹出窗口,用于通知目的。 -
打开
app.component.ts文件,并将OnInit接口添加到AppComponent类实现的接口中:import { Component, **OnInit** } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent **implements****OnInit** { title = 'weather-app'; } -
在
AppComponent类的constructor中注入MatSnackBar和SwUpdate服务:import { Component, OnInit } from '@angular/core'; **import** **{** **MatSnackBar** **}** **from****'@angular/material/snack-bar'****;** **import** **{** **SwUpdate****,** **VersionReadyEvent** **}** **from****'@angular/service-worker'****;** @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { title = 'weather-app'; **constructor****(****private** **updates: SwUpdate,** **private** **snackbar: MatSnackBar****) {}** }MatSnackBar服务是从MatSnackBarModule中公开的 Angular 服务。SwUpdate服务是服务工作者的一部分,包含我们可以用来通知用户应用程序更新过程的可观察对象。 -
创建以下
ngOnInit方法:ngOnInit() { this.updates.versionUpdates.pipe( filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'), switchMap(() => this.snackbar.open('A new version is available!', 'Update now').afterDismissed()), filter(result => result.dismissedByAction), map(() => this.updates.activateUpdate().then(() => location.reload())) ).subscribe(); }ngOnInit方法是OnInit接口的一个实现方法,在组件初始化时被调用。SwUpdate服务包含一个versionUpdates可观察属性,我们可以使用它来获取通知,当我们的应用程序有新版本可用时。通常,我们倾向于订阅可观察对象,但在这个情况下,我们没有这样做。相反,我们订阅了pipe方法,这是一个用于组合多个操作符的 RxJS 操作符。 -
在
app.component.ts文件的顶部添加以下import语句:import { filter, map, switchMap } from 'rxjs';
在我们之前定义的 ngOnInit 方法内部有很多事情在进行,所以让我们将其分解成几个部分以进一步理解它。pipe 操作符组合了四个 RxJS 操作符:
-
filter:我们使用它来过滤掉从versionUpdates可观察对象发出的除表示版本准备安装之外的所有值。` -
switchMap: 当我们的应用程序有新版本可用时,会调用此方法。它使用snackbar属性的open方法来显示带有操作按钮的 snack bar,并订阅其afterDismissed可观察对象。afterDismissed可观察对象在 snack bar 通过点击操作按钮或使用其 API 方法程序化关闭时发出。 -
filter: 当使用操作按钮关闭 snack bar 时,会调用此方法。 -
map: 这调用updates属性的activateUpdate方法来应用应用程序的新版本。一旦应用程序已更新,它将重新加载浏览器窗口以使更改生效。
让我们看看更新到新版本的整个过程:
-
运行以下 Angular CLI 命令来构建 Angular 应用程序:
ng build -
启动 HTTP 服务器以提供应用程序:
npm run server -
打开一个私密或隐身浏览器窗口,并导航到
http://localhost:8080。 -
在不关闭浏览器窗口的情况下,让我们在我们的应用程序中引入一个更改并添加一个 UI 标题。运行 Angular CLI 的
generate命令来创建一个组件:ng generate component header -
打开
app.module.ts文件并导入以下 Angular Material 模块:**import** **{** **MatButtonModule** **}** **from****'@angular/material/button'****;** **import** **{** **MatToolbarModule** **}** **from****'@angular/material/toolbar'****;** @NgModule({ declarations: [ AppComponent, WeatherComponent, HeaderComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, MatIconModule, MatInputModule, MatCardModule, MatSnackBarModule, **MatButtonModule****,** **MatToolbarModule****,** ServiceWorkerModule.register('ngsw-worker.js', { enabled: !isDevMode(), // Register the ServiceWorker as soon as the application is stable // or after 30 seconds (whichever comes first). registrationStrategy: 'registerWhenStable:30000' }) ], providers: [], bootstrap: [AppComponent] }) -
打开
header.component.html文件,并创建一个包含两个 HTML<button>元素的<mat-toolbar>组件,每个按钮都包含一个<mat-icon>组件:<mat-toolbar color="primary"> <span>Weather App</span> <span class="spacer"></span> <button mat-icon-button> <mat-icon>refresh</mat-icon> </button> <button mat-icon-button> <mat-icon>share</mat-icon> </button> </mat-toolbar> -
将以下 CSS 样式添加到
header.component.scss文件中,以便将按钮定位在标题的右端:.spacer { flex: 1 1 auto; } -
打开
app.component.html文件,并在顶部添加<app-header>组件:**<****app-header****></****app-header****>** <app-weather></app-weather> -
重复步骤 1 和 2,并刷新指向
http://localhost:8080的浏览器窗口。几秒钟后,你应在页面底部看到以下通知:

图 4.7 – 新版本通知
- 点击 立即更新 按钮,等待浏览器窗口重新加载,你应该能看到你的更改:

图 4.8 – 应用程序输出
我们的 Angular 应用程序已经开始转变为 PWA 应用程序。随着 Angular 服务工作者提供的缓存机制,我们添加了安装我们应用程序新版本的机制。在下一节中,我们将学习如何在我们的设备上本地部署和安装我们的应用程序。
使用 Firebase Hosting 部署我们的应用程序
Firebase 是由 Google 提供的托管解决方案,我们可以使用它来部署我们的 Angular 应用程序。Firebase 团队投入了大量精力创建了一个 Angular CLI 脚本,用于通过单个命令部署 Angular 应用程序。在深入了解之前,让我们学习如何设置 Firebase Hosting:
-
使用 Google 账户登录 Firebase,网址为
console.firebase.google.com。 -
点击 添加项目 按钮以创建新的 Firebase 项目。
-
输入项目名称,
weather-app,然后点击 继续 按钮。Firebase 在项目名称下方生成一个独特的标识符,例如 weather-app-b11a2,该标识符将在项目的主机 URL 中使用。
-
禁用项目中 Google Analytics 的使用,然后点击 创建项目 按钮。
-
一旦项目创建完成,屏幕上将会显示以下内容:

图 4.9 – Firebase 项目创建
- 点击 继续 按钮,你将被重定向到你的新 Firebase 项目仪表板。
我们现在已经完成了 Firebase Hosting 的配置。现在是时候将其与我们的 Angular 应用程序集成:
-
在终端窗口中运行以下命令来安装 Firebase 工具:
npm install -g firebase-tools -
在相同的终端窗口中运行以下命令以使用 Firebase CLI 进行身份验证:
firebase login -
最后,运行以下 Angular CLI 命令来在 Angular CLI 项目中安装
@angular/firenpm 包:ng add @angular/fire上述命令将找到库的最新版本,并提示我们安装它。
-
首先,它将询问我们想要启用 Firebase 的哪些功能:
? What features would you like to setup?确保选择了
ng deploy -- hosting选项,然后按 Enter。 -
然后,它将询问我们想要使用哪个 Firebase 账户:
? Which Firebase account would you like to use?确保选择了之前使用的账户,然后按 Enter。
-
在下一个问题中,我们将选择我们将要部署应用程序的项目:
? Please select a project:选择我们之前创建的
weather-app项目,然后按 Enter。 -
最后,我们必须选择将托管我们应用程序的网站:
? Please select a hosting site:
选择我们之前创建的托管网站,然后按 Enter。
之前的流程将根据需要修改 Angular CLI 工作区,以适应其部署到 Firebase:
-
它将在根目录下创建一个
.firebaserc文件,其中包含所选 Firebase 项目的详细信息。 -
它将在根目录下创建一个
firebase.json文件,这是 Firebase 配置文件。配置文件指定了将部署到 Firebase 的文件夹以及任何重写规则。默认部署的文件夹是当运行
ng build命令时由 Angular CLI 创建的dist输出文件夹。 -
它将在
angular.json配置文件中添加一个deploy目标。
要部署应用程序,我们只需要运行一个 Angular CLI 命令,Angular CLI 将处理其余部分:
ng deploy
之前的命令将构建并将应用程序部署到所选的 Firebase 项目。一旦部署完成,Angular CLI 将报告以下信息:
-
项目控制台:Firebase 项目的仪表板。
-
托管 URL:已部署应用程序版本的 URL。它由 Firebase 项目的唯一标识符和 Firebase 自动添加的
.web.app后缀组成。
服务工作者需要以 HTTPS 协议提供服务才能作为 PWA 正确工作,除了用于开发的 localhost 之外。Firebase 默认使用 HTTPS 托管 Web 应用程序。
现在我们已经部署了我们的应用程序,让我们看看我们如何将其作为 PWA 安装到我们的设备上:
-
导航到托管 URL,然后在浏览器地址栏旁边点击 安装 weather-app 按钮:
![包含文本的图片 自动生成的描述]()
图 4.10 – 安装应用程序(Google Chrome)
安装按钮可能在其他浏览器的不同位置。
浏览器将提示我们安装应用程序。
-
点击 安装 按钮,应用程序将在我们的设备上以原生窗口的形式打开:
![图片 B18465_04_11.png]
图 4.11 – PWA
它还会在我们的设备上创建一个启动应用程序的快捷方式。恭喜!我们现在有一个完整的 PWA,可以显示城市的天气信息。
摘要
在本章中,我们构建了一个显示特定城市天气信息的 PWA。
初始时,我们设置了 OpenWeather API 以获取天气数据,并从头创建了一个 Angular 应用程序来集成它。我们学习了如何使用 Angular 框架内置的 HTTP 客户端与 OpenWeather API 进行通信。我们还安装了 Angular Material 库,并使用了一些现成的 UI 组件来构建我们的应用程序。
在创建 Angular 应用程序后,我们介绍了 Angular 服务工作者并使其能够离线工作。我们学习了如何与服务工作者交互并为我们的应用程序提供更新通知。最后,我们将应用程序的生产版本部署到 Firebase Hosting 并将其安装到我们的设备上。
在下一章中,我们将学习如何使用 Electron(PWAs 的主要竞争对手)创建 Angular 桌面应用程序。
练习
使用 OpenWeather API 显示所选城市的每周天气预报。OpenWeather API 提供了5 天/3 小时预报集合,可以用于此。该集合为每天每 3 小时返回一次预报,因此,对于每周预报,你只需关注每天中午 12:00 的天气即可。预报应以网格列表的形式显示为卡片组件,并应位于城市当前天气下方。
你可以在github.com/PacktPublishing/Angular-Projects-Third-Edition/tree/exercise的exercise分支的Chapter04文件夹中找到练习的解决方案。
进一步阅读
-
OpenWeather API:
openweathermap.org/api -
Angular Material:
material.angular.io -
Angular HTTP 客户端:
angular.io/guide/http -
Angular 服务工作者:
angular.io/guide/service-worker-getting-started -
与 Angular 服务工作者通信:
angular.io/guide/service-worker-communications -
HTTP 服务器:
www.npmjs.com/package/http-server -
Firebase Hosting:
firebase.google.com/docs/hosting -
Angular 部署:
angular.io/guide/deployment#automatic-deployment-with-the-cli
第五章:使用电子构建桌面 WYSIWYG 编辑器
Web 应用程序传统上是用 HTML、CSS 和 JavaScript 构建的。它们的使用也广泛扩展到使用Node.js的服务器开发。近年来,出现了各种工具和框架,它们使用 HTML、CSS 和 JavaScript 来创建桌面和移动应用程序。在本章中,我们将探讨如何使用 Angular 和电子创建桌面应用程序。
电子是一个 JavaScript 框架,用于使用 Web 技术构建原生桌面应用程序。结合 Angular 框架,我们可以创建快速且高性能的 Web 应用程序。在本章中,我们将构建一个桌面WYSIWYG编辑器,并涵盖以下主题:
-
为 Angular 添加 WYSIWYG 编辑器库
-
在工作区中集成电子
-
Angular 与电子之间的通信
-
打包桌面应用程序
必要的背景理论和上下文
电子框架是一个跨平台框架,用于构建 Windows、Linux 和 Mac 桌面应用程序。许多流行的应用程序,如 Visual Studio Code、Skype 和 Slack,都是使用电子框架制作的。电子框架建立在 Node.js 和 Chromium 之上。Web 开发者可以利用他们现有的 HTML、CSS 和 JavaScript 技能来创建桌面应用程序,而无需学习新的语言,如 C++或 C#。
电子应用程序与 PWA 应用程序有许多相似之处。考虑为以下场景构建电子应用程序,如高级文件系统操作或当你需要为你的应用程序提供更原生的外观和感觉时。另一个用例是当你为你的主要桌面产品构建补充工具并希望将它们一起发布时。
电子应用程序由两个进程组成:
-
主进程:这通过 Node.js API 与本地资源进行交互。
-
渲染器:负责管理应用程序的用户界面。
电子应用程序只能有一个主进程,该进程与一个或多个渲染进程进行通信。每个渲染进程与其他进程完全隔离运行。
电子框架提供了ipcMain和ipcRenderer接口,我们可以使用这些接口与这些进程进行交互。交互是通过进程间通信(IPC)完成的,这是一种通过基于 Promise 的 API 在公共通道上安全异步交换消息的机制。
项目概述
在此项目中,我们将构建一个桌面 WYSIWYG 编辑器,其内容保留在文件系统中。最初,我们将使用 ngx-wig,一个流行的 WYSIWYG Angular 库,将其构建为 Angular 应用程序。然后,我们将使用 Electron 将其转换为桌面应用程序,并学习如何在 Angular 和 Electron 之间同步内容。我们还将了解如何将编辑器的内容持久化到文件系统中。最后,我们将打包我们的应用程序为一个可执行的单一文件,该文件可以在桌面环境中运行。以下图表描述了项目的架构概述:

图 5.1 – 项目架构
构建时间:1 小时。
开始使用
完成此项目所需的软件工具如下:
-
Angular CLI:Angular 的命令行界面,您可以在
angular.io/cli找到。 -
Visual Studio Code:一个代码编辑器,您可以从
code.visualstudio.com下载。 -
GitHub 材料:本章的代码可以在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter05文件夹中找到。
为 Angular 添加 WYSIWYG 编辑器库
我们将通过创建一个作为 Angular 应用程序的 WYSIWYG 编辑器来启动我们的项目。使用 Angular CLI 从头开始创建一个新的 Angular 应用程序:
ng new my-editor --defaults
我们将以下选项传递给 ng new 命令:
-
my-editor: 定义应用程序的名称 -
--defaults: 定义 CSS 为应用程序的首选样式表格式,并禁用路由,因为我们的应用程序将仅由一个包含编辑器的组件组成
WYSIWYG 编辑器是一种富文本编辑器,例如 Microsoft Word。我们可以使用 Angular 框架从头开始创建一个,但这将非常耗时,我们只会重蹈覆辙。Angular 生态系统包含大量用于此目的的库。其中之一是 ngx-wig 库,它没有外部依赖,只有 Angular!让我们将库添加到我们的应用程序中,并学习如何使用它:
-
使用
npm客户端从 npm 包注册库安装ngx-wig:npm install ngx-wig -
打开
app.module.ts文件,并将NgxWigModule添加到@NgModule装饰器的imports数组中:import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; **import** **{** **NgxWigModule** **}** **from****'****ngx-wig'****;** import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, **NgxWigModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }NgxWigModule是 ngx-wig 库的主要模块。 -
创建一个新的 Angular 组件,该组件将托管我们的 WYSIWYG 编辑器:
ng generate component editor -
打开新创建的组件的模板文件,
editor.component.html,并用以下 HTML 片段替换其内容:<ngx-wig placeholder="Enter your content"></ngx-wig>NgxWigModule提供了一组 Angular 服务和组件,我们可以在应用程序中使用。该模块的主要组件是<ngx-wig>组件,它显示实际的 WYSIWYG 编辑器。它公开了一组我们可以设置的输入属性,例如编辑器的占位符。 -
打开
app.component.html文件,并用<app-editor>组件替换其内容:<app-editor></app-editor> -
打开
styles.css文件,该文件包含 Angular 应用程序的全局样式,并添加以下样式以使编辑器可停靠并占满整个页面:html, body { margin: 0; width: 100%; height: 100%; } .ng-wig, .nw-editor-container, .nw-editor { display: flex !important; flex-direction: column; height: 100% !important; overflow: hidden; } -
打开 Angular 应用程序的主要 HTML 文件
index.html,并从<head>元素中移除<base>标签。浏览器使用<base>标签通过相对 URL 引用脚本和 CSS 文件。保留它将使我们的桌面应用程序失败,因为它将直接从本地文件系统中加载所有必要的资源。我们将在集成 Angular 与 Electron部分了解更多。
让我们看看我们已经取得了哪些成果。运行ng serve并导航到http://localhost:4200以预览应用程序:

图 5.2 – 应用程序输出
我们的应用程序包括以下内容:
-
一个带有按钮的工具栏,允许我们应用不同的样式到编辑器的内容
-
一个用作编辑器主要容器的文本区域,用于添加内容
我们现在已经使用 Angular 创建了一个具有完全可操作的 WYSIWYG 编辑器的 Web 应用程序。在下一节中,我们将学习如何使用 Electron 将其转换为桌面应用程序。
在工作空间中集成 Electron
Electron 框架是一个我们可以使用以下命令安装的 npm 包:
npm install -D electron
之前的命令将在 Angular CLI 工作空间中安装最新版本的electron npm 包。它还将在我们的项目package.json文件的devDependencies部分添加相应的条目。
Electron 被添加到package.json文件的devDependencies部分,因为它是我们应用程序的开发依赖项。它仅用于将我们的应用程序作为桌面应用程序准备和构建,而不是在运行时使用。
Electron 应用程序在 Node.js 运行时上运行,并使用 Chromium 浏览器进行渲染。一个 Node.js 应用程序至少有一个 JavaScript 文件,通常称为index.js或main.js,它是应用程序的主要入口点。由于我们使用 Angular 和 TypeScript 作为我们的开发堆栈,我们将首先创建一个单独的 TypeScript 文件,最终将其编译成 JavaScript:
-
在 Angular CLI 工作空间的
src文件夹内创建一个名为electron的文件夹。electron文件夹将包含任何与 Electron 相关的源代码。我们可以将我们的应用程序视为两个不同的平台。Web 平台是 Angular 应用程序,位于
src\app文件夹中。桌面平台是 Electron 应用程序,位于src\electron文件夹中。这种方法有许多优点,包括它强制我们在应用程序中分离关注点,并允许它们独立于彼此独立开发。从现在起,我们将它们称为 Angular 和 Electron 应用程序。 -
在
electron文件夹内创建一个main.ts文件,并包含以下内容:import { app, BrowserWindow } from 'electron'; function createWindow () { const mainWindow = new BrowserWindow({ width: 800, height: 600 }); mainWindow.loadFile('index.html'); } app.whenReady().then(() => { createWindow(); });在前面的代码中,我们首先从
electronnpm 包中导入BrowserWindow和app模块。BrowserWindow类用于为我们的应用程序创建桌面窗口。我们通过构造函数传递一个options对象来定义窗口的尺寸,该对象设置了窗口的width和height值。然后我们调用loadFile方法,将作为参数传递我们想要在窗口内加载的 HTML 文件。我们在
loadFile方法中传递的index.html文件是 Angular 应用程序的主 HTML 文件。它是使用文件协议加载的,这就是为什么我们在为 Angular 添加 WYSIWYG 编辑器库部分中移除了<base>标签。app对象是我们桌面应用程序的全局对象,就像网页上的window对象一样。它暴露了一个whenReadyPromise,当它解析时,允许我们运行应用程序的任何初始化逻辑,包括创建窗口。 -
在
electron文件夹内创建一个tsconfig.json文件,并添加以下内容:{ "extends": "../../tsconfig.json", "compilerOptions": { "importHelpers": false }, "include": [ "**/*.ts" ] }main.ts文件必须编译成 JavaScript,因为浏览器不理解 TypeScript。编译过程称为转译,需要一个 TypeScript 配置文件。配置文件包含驱动 TypeScript 转译器的选项,转译器负责转译过程。上述 TypeScript 配置文件使用
include属性定义了 Electron 源代码文件的路径,并将importHelpers属性设置为false。如果我们启用
importHelpers标志,它将包括来自tslib库的帮助程序,从而导致包的大小更大。 -
运行以下命令以安装Webpack CLI:
npm install -D webpack-cliWebpack CLI 从命令行调用流行的模块打包器webpack。我们将使用 webpack 来构建和打包我们的 Electron 应用程序。
-
使用以下命令安装
ts-loadernpm 包:npm install -D ts-loader
ts-loader库是一个 webpack 插件,可以加载 TypeScript 文件。
我们已经创建了将我们的 Angular 应用程序转换为桌面应用程序所需的全部组件,使用 Electron。我们只需要将它们组合起来以构建和运行我们的桌面应用程序。协调 Electron 应用程序的主要组件是我们需要在 Angular CLI 工作区的根目录中创建的 webpack 配置文件:
webpack.config.js
const path = require('path');
const src = path.join(process.cwd(), 'src', 'electron');
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: path.join(src, 'main.ts'),
output: {
path: path.join(process.cwd(), 'dist', 'my-editor'),
filename: 'shell.js'
},
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
options: {
configFile: path.join(src, 'tsconfig.json')
}
}
]
},
target: 'electron-main'
};
前面的文件使用以下选项配置了我们的应用程序中的 webpack:
-
mode:指示我们当前正在开发环境中运行。 -
devtool:启用源映射文件生成,用于调试目的。 -
entry:指示 Electron 应用程序的主入口点,即main.ts文件。 -
输出: 定义了从 webpack 生成的 Electron 包的路径和文件名。path属性指向 Angular CLI 创建 Angular 应用包所使用的相同文件夹。filename属性设置为shell.js,因为 webpack 默认生成的文件名为main.js,这将会与 Angular 应用生成的main.js文件冲突。 -
module: 指示 webpack 加载ts-loader插件来处理 TypeScript 文件。 -
target: 表示我们目前正在 Electron 的主进程中运行。
Webpack 模块打包器现在包含了构建和打包 Electron 应用所需的所有信息。另一方面,Angular CLI 负责构建 Angular 应用。让我们看看我们如何将它们结合起来并运行我们的桌面应用:
-
运行以下命令来安装
concurrentlynpm 包:npm install -D concurrentlyconcurrently库使我们能够同时执行多个进程。在我们的案例中,它将允许我们并行运行 Angular 和 Electron 应用。
-
打开
package.json文件,并在scripts属性中添加一个新条目:"scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", **"start:desktop"****:****"concurrently \"ng build --delete-output-path=false --watch\" \"webpack --watch\""** **}**start:desktop脚本使用 Angular CLI 的ng build命令构建 Angular 应用,并使用webpack命令构建 Electron 应用。两个应用都使用--watch选项以监视模式运行,所以每次我们更改代码,应用都会重新构建以反映更改。当我们修改 Angular 应用时,Angular CLI 默认会删除
dist文件夹。我们可以使用--delete-output-path=false选项来防止这种行为,因为 Electron 应用也是在同一个文件夹中构建的。我们没有将 webpack 配置文件传递给
webpack命令,因为它默认假设文件名为webpack.config.js。 -
点击 Visual Studio Code 侧边栏中存在的运行菜单:

图 5.3 – 运行菜单
- 在出现的运行和调试面板中,从下拉菜单中选择添加配置…选项:

图 5.4 – 运行和调试面板
-
Visual Studio Code 将打开一个下拉菜单,允许我们选择运行应用的环境。选择{} Node.js: Electron Main配置。
-
在打开的
launch.json文件中,将program属性的值设置为${workspaceFolder}/dist/my-editor/shell.js。program属性指向 Electron 包文件的绝对路径。
现在我们已经准备好运行我们的桌面应用并预览它。运行以下命令来构建应用:
npm run start:desktop
之前的命令将首先构建 Electron 应用,然后是 Angular 应用。等待 Angular 构建完成,从运行和调试面板的下拉菜单中选择Electron Main选项,然后点击播放按钮来预览应用:

图 5.5 – 应用程序窗口
在前面的屏幕截图中,我们可以看到我们的带有 WYSIWYG 编辑器的 Angular 应用程序托管在原生桌面窗口中。它包含以下我们在桌面应用程序中通常会发现的特点:
-
带有图标的标题
-
主菜单
-
最小化、最大化和关闭按钮
Angular 应用程序在 Chromium 浏览器内部渲染。为了验证这一点,点击 视图 菜单项并选择 切换开发者工具 选项。
干得好!你已经成功创建了自己的桌面 WYSIWYG 编辑器。在下一节中,我们将学习如何进行 Angular 和 Electron 之间的交互。
Angular 和 Electron 之间的通信
根据项目的规格,WYSIWYG 编辑器的内容需要保存在本地文件系统中。此外,内容将在应用程序启动时从文件系统加载。
Angular 应用程序使用渲染进程处理 WYSIWYG 编辑器与数据之间的任何交互,而 Electron 应用程序使用主进程管理文件系统。因此,我们需要建立一个 IPC 机制,以便在两个 Electron 进程之间进行通信,如下所示:
-
配置 Angular CLI 工作区
-
与编辑器交互
-
与文件系统交互
让我们先设置 Angular CLI 项目以支持所需的通信机制。
配置 Angular CLI 工作区
我们需要修改几个文件来配置我们应用程序的工作区:
-
打开位于
src\electron文件夹中的main.ts文件,并在BrowserWindow构造函数中相应地设置webPreferences属性:function createWindow () { const mainWindow = new BrowserWindow({ width: 800, height: 600, **webPreferences****: {** **nodeIntegration****:** **true****,** **contextIsolation****:** **false** **}** }); mainWindow.loadFile('index.html'); }之前的标志将在渲染进程中启用 Node.js 并公开
ipcRenderer接口,这是我们与主进程通信所需的。 -
运行以下命令来安装
ngx-electronyzernpm 包:npm install ngx-electronyzer
ngx-electronyzer 库允许我们将 Electron API 集成到 Angular 应用程序中。
Angular 和 Electron 应用程序现在已准备好通过 IPC 机制相互交互。让我们首先在 Angular 应用程序中实现必要的逻辑。
与编辑器交互
Angular 应用程序负责管理 WYSIWYG 编辑器。编辑器的内容通过 Electron 的渲染进程与文件系统保持同步。让我们了解如何使用渲染进程:
-
使用以下 Angular CLI 命令创建一个新的 Angular 服务:
ng generate service editor -
打开
editor.service.ts文件,并从ngx-electronyzernpm 包中注入ElectronService类:import { Injectable } from '@angular/core'; **import** **{** **ElectronService** **}** **from****'ngx-electronyzer'****;** @Injectable({ providedIn: 'root' }) export class EditorService { constructor(**private** **electronService: ElectronService**) { } }ElectronService类公开了部分 Electron API,包括我们目前感兴趣的ipcRenderer接口。 -
创建一个方法,该方法将被调用来从文件系统获取编辑器的内容:
getContent(): Promise<string> { return this.electronService.ipcRenderer.invoke('getContent'); }我们使用
ipcRenderer属性的invoke方法,将通信通道的名称作为参数传递。getContent方法的返回值是一个Promise对象,其类型为string,因为编辑器的内容是原始文本数据。invoke方法通过getContent通道与主进程建立连接。在 与文件系统交互 部分,我们将看到如何设置主进程以响应该通道中的invoke方法调用。 -
创建一个方法,当需要将编辑器的内容保存到文件系统时将被调用:
setContent(content: string) { this.electronService.ipcRenderer.invoke('setContent', content); }setContent方法再次调用ipcRenderer对象的invoke方法,但使用不同的通道名称。它还使用invoke方法的第二个参数将数据传递给主进程。在这种情况下,content参数将包含编辑器的内容。我们将在 与文件系统交互 部分看到如何配置主进程以处理数据。 -
打开
editor.component.ts文件并创建一个myContent属性来保存编辑器数据。同时,在EditorComponent类的constructor中注入EditorService,并添加来自@angular/corenpm 包的OnInit接口:import { Component, **OnInit** } from '@angular/core'; **import** **{** **EditorService** **}** **from****'../editor.service'****;** @Component({ selector: 'app-editor', templateUrl: './editor.component.html', styleUrls: ['./editor.component.css'] }) export class EditorComponent **implements****OnInit** { **myContent =** **''****;** **constructor****(****private** **editorService: EditorService****) {}** } -
创建一个方法,该方法调用
editorService变量的getContent方法,并在ngOnInit方法内部执行它:ngOnInit(): void { this.getContent(); } private async getContent() { this.myContent = await this.editorService.getContent(); }我们使用
async/await语法,它允许我们在基于 Promise 的方法调用中同步执行我们的代码。 -
创建一个方法,该方法调用
editorService变量的setContent方法:saveContent(content: string) { this.editorService.setContent(content); } -
让我们将我们创建的方法与组件的模板绑定起来。打开
editor.component.html文件并添加以下绑定:<ngx-wig placeholder="Enter your content" **[****ngModel****]=****"myContent"** **(****contentChange****)=****"saveContent($event)"**></ngx-wig>我们使用
ngModel指令将编辑器的模型绑定到myContent组件属性,该属性将用于最初显示内容。我们还使用contentChange事件绑定,以便在编辑器内容更改时(即用户输入时)保存编辑器的内容。 -
ngModel指令是@angular/formsnpm 包的一部分。将FormsModule导入到app.module.ts文件中以便使用:import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; **import** **{** **FormsModule** **}** **from****'@angular/forms'****;** import { NgxWigModule } from 'ngx-wig'; import { AppComponent } from './app.component'; import { EditorComponent } from './editor/editor.component'; @NgModule({ declarations: [ AppComponent, EditorComponent ], imports: [ BrowserModule, NgxWigModule, **FormsModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
我们已经实现了 Angular 应用程序与主进程通信的所有逻辑。现在是时候实现通信机制的另一端,即 Electron 应用程序及其主进程了。
与文件系统交互
主进程通过内置在 Electron 框架中的 fs 库与文件系统进行交互。让我们看看如何使用它:
-
打开位于
src\electron文件夹中的main.ts文件并导入以下组件:import { app, BrowserWindow, **ipcMain** } from 'electron'; **import** ***** **as** **fs** **from****'fs'****;** **import** ***** **as** **path** **from****'path'****;**fs库负责与文件系统交互。path库提供了用于处理文件和文件夹路径的实用工具。ipcMain对象允许我们与 Electron 的主进程进行交互。 -
创建一个变量来保存包含编辑器内容的文件路径:
const contentFile = path.join(app.getPath('userData'), 'content.html');保存编辑器内容的文件是位于预留的
userData文件夹中的content.html文件。userData文件夹是一个特殊用途的系统文件夹的别名,每个操作系统不同,用于存储特定于应用程序的文件,如配置。您可以在www.electronjs.org/docs/api/app#appgetpathname找到有关userData文件夹和其他系统文件夹的更多详细信息。app对象的getPath方法是跨平台的,用于获取特殊文件夹的路径,例如用户的家目录或应用程序数据。 -
调用
ipcMain对象的handle方法以在getContent通道中开始监听请求:ipcMain.handle('getContent', () => { if (fs.existsSync(contentFile)) { const result = fs.readFileSync(contentFile); return result.toString(); } return ''; });当主进程接收到此通道的请求时,它使用
fs库的existsSync方法检查包含编辑器内容的文件是否已存在。如果存在,它将使用readFileSync方法读取它,并将内容返回给渲染进程。 -
再次调用
handle方法,但这次是为setContent通道:ipcMain.handle('setContent', ({}, content: string) => { fs.writeFileSync(contentFile, content); }); writeFileSync method of the fs library to write the value of the content property in the file.
现在我们已经连接了 Angular 和 Electron 应用程序,是时候预览我们的 WYSIWYG 桌面应用程序了:
-
执行
start:desktopnpm 脚本,并按 F5 运行应用程序。 -
使用编辑器和其工具栏输入一些内容,例如以下内容:

图 5.6 – 编辑器内容
- 关闭应用程序窗口并重新运行应用程序。如果一切正常,您应该看到编辑器中输入的内容。
恭喜!您已经通过向其添加持久性功能来丰富了您的 WYSIWYG 编辑器。在下一节中,我们将采取创建桌面应用程序的最后一步,并学习如何打包和分发它。
打包桌面应用程序
网络应用程序通常被打包并部署到托管服务器。另一方面,桌面应用程序被打包并打包成一个可轻松分发的单个可执行文件。打包我们的 WYSIWYG 应用程序需要以下步骤:
-
配置生产模式下的 webpack
-
使用 Electron 打包器
我们将在下一节中更详细地介绍它们。
配置生产环境下的 webpack
我们已经为开发环境创建了一个 webpack 配置文件。我们现在需要为生产环境创建一个新的配置文件。这两个配置文件将共享一些功能,所以让我们先创建一个通用的配置文件:
-
在 Angular CLI 工作区的根目录中创建一个
webpack.dev.config.js文件,内容如下:const path = require('path'); const baseConfig = require('./webpack.config'); module.exports = { ...baseConfig, mode: 'development', devtool: 'source-map', output: { path: path.join(process.cwd(), 'dist', 'my-editor'), filename: 'shell.js' } }; -
从
webpack.config.js文件中移除mode、devtool和output属性。 -
打开
package.json文件,并在start:desktop脚本中传递新的 webpack 开发配置文件:"start:desktop": "concurrently \"ng build --delete-output-path=false --watch\" \"webpack **--config webpack.dev.config.js** --watch\"" -
在 Angular CLI 工作区的根目录中创建一个
webpack.prod.config.js文件,内容如下:const path = require('path'); const baseConfig = require('./webpack.config'); module.exports = { ...baseConfig, output: { path: path.join(process.cwd(), 'dist', 'my-editor'), filename: 'main.js' } };与开发环境的 webpack 配置文件相比,主要区别在于我们将
output包的filename改为了main.js。Angular CLI 在生产环境中向 Angular 应用的main.js文件中添加一个哈希数字,因此不会发生冲突。 -
在
package.json文件的scripts属性中添加一个新的条目,以在生产模式下构建我们的应用程序:"scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "start:desktop": "concurrently \"ng build --delete-output-path=false --watch\" \"webpack --config webpack.dev.config.js --watch\"", **"build:electron"****:** **"ng build && webpack --config webpack.prod.config.js"** }build:electron脚本同时以生产模式构建 Angular 和 Electron 应用程序。
我们已经完成了打包桌面应用程序所需的所有配置。在下一节中,我们将学习如何将其转换为针对每个操作系统的单个包。
使用 Electron 打包器
Electron 框架拥有开源社区创建和维护的各种工具。其中之一是 electron-packager 库,我们可以使用它将我们的桌面应用程序打包成每个操作系统(Windows、Linux 和 macOS)的单个可执行文件。让我们看看如何将其集成到我们的开发工作流程中:
-
运行以下命令将
electron-packager作为我们的项目的开发依赖项安装:npm install -D electron-packager -
在
package.json文件的scripts属性中添加一个新的条目以打包我们的应用程序:"scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "start:desktop": "concurrently \"ng build --delete-output-path=false --watch\" \"webpack --config webpack.dev.config.js --watch\"", "build:electron": "ng build && webpack --config webpack.prod.config.js", **"package"****:****"electron-packager dist/my-editor --out=dist --asar"** }在前面的脚本中,
electron-packager将读取dist/my-editor文件夹中的所有文件,将它们打包,并将最终的包输出到dist文件夹。--asar选项指示打包器将所有文件存档为 ASAR 格式,类似于 ZIP 或 TAR 文件。 -
在
src\electron文件夹中创建一个package.json文件,并添加以下内容:{ "name": "my-editor", "main": "main.js" }electron-packager库要求输出文件夹中存在一个package.json文件,该文件指向 Electron 应用程序的主入口文件。 -
打开
webpack.prod.config.js文件,并在plugins属性中添加CopyWebpackPlugin:const path = require('path'); const baseConfig = require('./webpack.config'); **const****CopyWebpackPlugin** **=** **require****(****'copy-webpack-plugin'****);** module.exports = { ...baseConfig, output: { path: path.join(process.cwd(), 'dist', 'my-editor'), filename: 'main.js' }, **plugins****: [** **new****CopyWebpackPlugin****({** **patterns****: [** **{** **context****: path.****join****(process.****cwd****(),** **'src'****,** **'electron'****),** **from****:** **'****package.json'** **}** **]** **})** **]** };我们使用
CopyWebpackPlugin在生产模式下构建应用程序时将package.json文件从src\electron文件夹复制到dist\my-editor文件夹。 -
运行以下命令以在生产模式下构建应用程序:
npm run build:electron -
现在运行以下
npm命令来打包它:npm run package前面的命令将为当前运行的操作系统打包应用程序,这是
electron-packager库的默认行为。您可以通过传递额外的选项来更改此行为,这些选项可以在库的 GitHub 仓库中找到,列在 进一步阅读 部分。 -
导航到 Angular CLI 工作区的
dist文件夹。您将找到一个名为my-editor-{OS}的文件夹,其中{OS}是您的当前操作系统及其架构。例如,在 Windows 上,它将是my-editor-win32-x64。打开文件夹,您将得到以下文件:

图 5.7 – 应用程序包(Windows)
在前面的屏幕截图中,my-editor.exe 文件是我们桌面应用程序的可执行文件。我们的应用程序代码不包含在这个文件中,而是在 resources 文件夹中的 app.asar 文件中。我们的应用程序代码不包含在这个文件中,而是在 resources 文件夹中的 app.asar 文件中。
运行可执行文件,桌面应用程序应该可以正常打开。您可以整个文件夹上传到服务器,或者通过其他任何方式分发。现在,您的 WYSIWYG 编辑器可以触及更多用户,例如那些大部分时间都在离线状态的用户。太棒了!
摘要
在本章中,我们使用 Angular 和 Electron 构建了一个桌面 WYSIWYG 编辑器。最初,我们创建了一个 Angular 应用程序,并添加了 ngx-wig,一个流行的 Angular WYSIWYG 库。然后,我们学习了如何构建 Electron 应用程序,并实现了 Angular 和 Electron 应用程序之间的数据交换通信机制。最后,我们学习了如何打包我们的应用程序以进行打包,并准备好分发。
在下一章中,我们将学习如何使用 Angular 和 Ionic 构建一个移动照片地理标记应用程序。
实践问题
让我们看看几个实践问题:
-
哪个类负责在 Electron 中创建桌面窗口?
-
我们如何在 Electron 中的主进程和渲染进程之间进行通信?
-
哪些标志可以启用在渲染进程中使用 Node.js?
-
我们如何在 Angular 应用程序中加载 Electron?
-
我们在 Angular 应用程序中与 Electron 交互时使用哪个接口?
-
我们如何从 Angular 应用程序向主 Electron 进程传递数据?
-
我们在 Electron 中使用哪个包进行文件系统操作?
-
我们使用哪个库来打包 Electron 应用程序?
进一步阅读
这里有一些链接,可以帮助我们巩固本章所学的内容:
-
Electron:
www.electronjs.org -
Electron 快速入门:
www.electronjs.org/docs/tutorial/quick-start -
ngx-wig:www.npmjs.com/package/ngx-wig -
Webpack 配置:
webpack.js.org/configuration -
ts-loader:webpack.js.org/guides/typescript -
ngx-electronyzer:www.npmjs.com/package/ngx-electronyzer -
文件系统 API:
nodejs.org/api/fs.html -
electron-packager:www.npmjs.com/package/electron-packager -
concurrently:www.npmjs.com/package/concurrently
第六章:使用 Capacitor 和 3D 地图构建移动照片地理标记应用程序
Angular 是一个跨平台的 JavaScript 框架,可用于构建不同平台(如 Web、桌面和移动)的应用程序。此外,它允许开发者使用相同的代码库并将相同的 Web 技术应用于每个平台,从而享受相同的使用体验和性能。在本章中,我们将探讨如何使用 Angular 构建移动应用程序。
Ionic是一个流行的 UI 工具包,允许我们使用如 Angular 等 Web 技术构建移动应用程序。Capacitor库通过使它们能够在 Android 和 iOS 设备上本地运行,极大地增强了 Ionic 应用程序。在本章中,我们将使用这两种技术构建一个移动应用程序,用于拍摄带有地理标记的照片并在 3D 地图上显示它们。
我们将详细介绍以下主题:
-
使用 Ionic 创建移动应用程序
-
使用 Capacitor 拍照
-
在Firebase中存储数据
-
使用CesiumJS预览照片
必要的背景理论和上下文
电容器是一个原生移动运行时,使我们能够使用包括 Angular 在内的 Web 技术构建 Android 和 iOS 应用程序。它为 Web 应用程序提供了一个抽象 API 层,以便与移动操作系统的原生资源进行交互。它不包括 UI 层或任何其他与用户界面交互的方式。
Ionic 是一个包含我们可以用于使用 Capacitor 构建的应用程序中的 UI 组件的移动框架。Ionic 的主要优势是我们可以在所有原生移动平台上维护单个代码库。也就是说,我们只需编写一次代码,它就可以在任何地方工作。Ionic 支持所有流行的 JavaScript 框架,包括 Angular。
Firebase 是由 Google 提供的后端即服务(BaaS)平台,其中包含用于构建应用程序的工具和服务。Cloud Firestore是 Firebase 提供的一种数据库解决方案,它具有灵活和可扩展的 NoSQL 文档导向数据库,可用于 Web 和移动应用程序。Firebase Storage是一种服务,允许我们与存储机制进行交互并上传或下载文件。
CesiumJS 是一个用于在浏览器中创建交互式 3D 地图的 JavaScript 库。它是一个开源、跨平台的库,使用 WebGL,并允许我们在多个平台上共享地理空间数据。它由Cesium提供支持,这是一个用于构建高质量和性能优异的 3D 地理空间应用的平台。
项目概述
在这个项目中,我们将构建一个可以根据当前位置拍照并在地图上预览照片的移动应用程序。最初,我们将学习如何使用 Angular 和 Ionic 创建移动应用程序。然后,我们将使用 Capacitor 通过移动设备的相机拍照,并通过 GPS 标记当前位置。我们将把这些照片及其位置数据上传到 Firebase。最后,我们将使用 CesiumJS 在 3D 球上加载位置数据,并预览照片。以下图表展示了项目的架构概述:

图 6.1 – 项目架构
在本章中,你将学习如何使用 Angular 和 Ionic 构建移动应用程序。为了跟进项目并预览你的应用程序,你必须遵循你的开发环境(Android 或 iOS)的入门指南,你可以在 进一步阅读 部分找到。
构建时间:2 小时
入门
完成此项目,你需要以下软件和工具:
-
对于 Android 开发:Android Studio 以及最新的 Android SDK。
-
对于 iOS 开发:Xcode 以及 iOS SDK 和 Xcode 命令行工具。
-
一个物理移动设备。
-
Angular CLI:Angular 的命令行界面,你可以在
angular.io/cli找到。 -
GitHub 资源:本章相关代码可在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter06文件夹中找到。
使用 Ionic 创建移动应用程序
建立我们的应用程序的第一步是使用 Ionic 工具包创建一个新的移动应用程序。我们将从以下任务开始构建我们的应用程序:
-
应用程序脚手架
-
构建主菜单
Ionic 创建从零开始的新移动应用程序的过程非常直接,无需输入任何代码。
应用程序脚手架
创建新的 Ionic 应用程序,请完成以下步骤:
-
使用以下命令安装我们需要的 Ionic 工具:
npm install -g @ionic/cli native-run cordova-resIonic CLI 用于构建和运行 Ionic 移动应用程序。
native-run库用于在移动设备和模拟器上运行原生库。cordova-res库为我们生成原生移动设备的应用程序图标和启动画面。 -
运行以下命令以创建一个新的 Angular 应用程序,该应用程序使用 Ionic 的
sidemenu起始模板,并添加了 Capacitor:ionic start phototag sidemenu --type=angular --capacitor -
之前的命令将询问你是否想使用 Angular 模块或独立组件。选择
Standalone并按 Enter。
Ionic 将为我们创建一个包含一些现成数据的示例应用程序。在下一节中,我们将学习如何根据我们的需求对其进行修改。
构建主菜单
我们将根据我们的规格开始构建应用程序的主菜单:
-
在 VSCode 中加载我们在上一节中构建的 Ionic 项目,并打开应用程序的主 HTML 文件
index.html。 -
在
<title>标签中添加您应用程序的名称:<title>**Phototag** App</title> -
打开主组件的模板文件
app.component.html,并删除第二个<ion-list>元素。<ion-list>元素用于在列表视图中显示项目。 -
在
<ion-list-header>元素中添加您应用程序的名称,并相应地更改<ion-note>元素的文本:<ion-list-header>**Phototag**</ion-list-header> <ion-note>**Capture geotagged photos**</ion-note><ion-list-header>元素是列表的标题。<ion-note>元素是一个用于提供额外信息的文本元素,例如列表的副标题。 -
打开主组件的 TypeScript 文件
app.component.ts,并按如下方式修改AppComponent类:export class AppComponent { public appPages = [ { title: 'Take a photo', url: '/capture', icon: 'camera' }, { title: 'View gallery', url: '/view', icon: 'globe' } ]; constructor() {} }appPages属性包含我们应用程序的所有页面。每个页面都有一个title,一个可访问的url,以及一个icon。我们的应用程序将包含两个页面,一个用于使用相机拍照,另一个用于在地图上显示它们。 -
运行 Ionic CLI 的
serve命令以启动应用程序:ionic serve上述命令将构建您的应用程序并在默认浏览器中打开
http://localhost:8100。您应该在应用程序的侧边菜单中看到以下输出:
![图 6.8 – 主菜单]()
图 6.2 – 主菜单
假设您调整浏览器窗口大小以获得更真实的移动设备视图或使用模拟器,例如 Google Chrome 开发者工具中的设备工具栏。在这种情况下,您必须点击应用程序菜单按钮才能看到前面的图像。
我们已经学习了如何使用 Ionic CLI 创建新的 Ionic 应用程序并根据我们的需求进行修改。
如果我们尝试点击菜单项,我们会注意到没有任何反应,因为我们还没有为每种情况创建必要的页面。在下一节中,我们将学习如何通过构建第一页的功能来完成此任务。
使用 Capacitor 拍照
我们应用程序的第一页将允许用户使用相机拍照。我们将使用 Capacitor 运行时来获取对相机原生资源的访问权限。为了实现页面,我们需要采取以下行动:
-
创建用户界面。
-
与电容器交互。
让我们开始构建页面的用户界面。
创建用户界面
我们应用程序中的每个页面都是一个不同的 Angular 组件。要在 Ionic 中创建 Angular 组件,我们可以使用 Ionic CLI 的 generate 命令:
ionic generate page capture
之前的命令将执行以下操作:
-
创建一个名为
capture的 Angular 组件。 -
创建相关路由文件。
让我们现在开始构建新页面的逻辑:
-
首先,当用户打开应用程序时,使我们的页面成为默认页面。打开
app.routes.ts文件,并将routes属性的第一个条目更改为:{ path: '', redirectTo: '**capture**', pathMatch: 'full', }空路径称为 默认 路由路径,当我们的应用程序启动时被激活。
redirectTo属性告诉 Angular 重定向到capture路径,这将加载我们创建的页面。你也可以删除
folder/:id路径,因为它不再需要,并且从应用程序中删除整个src\app\folder目录,这是 Ionic 模板布局的一部分。 -
打开
capture.page.html文件并按照以下方式替换第一个<ion-toolbar>元素的全部内容:<ion-header [translucent]="true"> <ion-toolbar> **<****ion-buttons****slot****=****"start"****>** **<****ion-menu-button****color****=****"primary"****></****ion-menu-button****>** **</****ion-buttons****>** <ion-title>**Take a photo**</ion-title> </ion-toolbar> </ion-header><ion-toolbar>元素是<ion-header>元素的一部分,它是页面的顶部导航栏。它包含一个<ion-menu-button>元素,用于切换应用程序的主菜单,以及一个<ion-title>元素,描述页面的标题。 -
按照以下方式修改第二个
<ion-toolbar>元素的标题:<ion-title size="large">**Take a photo**</ion-title>当页面展开时,将显示第二个
<ion-header>元素,主菜单将显示在屏幕上。 -
在第二个标题之后立即添加以下 HTML 代码:
<div id="container"> <strong class="capitalize">Take a nice photo with your camera</strong> <ion-fab vertical="center" horizontal="center" slot="fixed"> <ion-fab-button> <ion-icon name="camera"></ion-icon> </ion-fab-button> </ion-fab> </div>它包含一个
<ion-fab-button>元素,当点击时,将打开设备的相机来拍照。 -
最后,让我们给我们的页面添加一些酷炫的样式。打开
capture.page.scss文件并输入以下 CSS 样式:#container { text-align: center; position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%); } #container strong { font-size: 20px; line-height: 26px; } #container ion-fab { margin-top: 60px; }
让我们使用 ionic serve 运行应用程序,以快速预览我们迄今为止所构建的内容:

图 6.3 – 捕获页面
页面上的相机按钮需要打开相机来拍照。在以下部分,我们将学习如何使用 Capacitor 与相机交互。
与 Capacitor 交互
在我们的应用程序中拍照涉及使用 Capacitor 库中的两个 API。Camera API 将打开相机来拍照,而 Geolocation API 将从 GPS 读取当前位置。让我们看看我们如何在应用程序中使用这两个 API:
-
执行以下
npm命令来安装两个 API:npm install @capacitor/camera @capacitor/geolocation -
使用以下 Ionic CLI 命令创建 Angular 服务:
ionic generate service photo -
打开
photo.service.ts文件并添加以下import语句:import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import { Geolocation } from '@capacitor/geolocation'; -
在
PhotoService类中创建一个方法来从 GPS 设备读取当前位置:private async getLocation() { const location = await Geolocation.getCurrentPosition(); return location.coords; }Geolocation对象的getCurrentPosition方法包含一个coords属性,其中包含各种基于位置的数据,如纬度和经度。 -
创建另一个方法,该方法调用
getLocation方法并打开设备的相机来拍照:async takePhoto() { await this.getLocation(); await Camera.getPhoto({ resultType: CameraResultType.DataUrl, source: CameraSource.Camera, quality: 100 }); }我们使用
Camera对象的getPhoto方法并传递一个配置对象来定义每张照片的属性。resultType属性表示照片将以 data URL 格式保存,以便稍后轻松地将其保存到 Firebase。source属性表示我们将使用相机设备来获取照片,而quality属性定义了实际照片的质量。 -
打开
capture.page.ts文件并在CapturePage类的constructor中注入PhotoService:import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { IonicModule } from '@ionic/angular'; **import** **{** **PhotoService** **}** **from****'../photo.service'****;** @Component({ selector: 'app-capture', templateUrl: './capture.page.html', styleUrls: ['./capture.page.scss'], standalone: true, imports: [IonicModule, CommonModule, FormsModule] }) export class CapturePage implements OnInit { constructor(**private** **photoService: PhotoService**) { } ngOnInit() { } } -
创建一个组件方法,该方法将调用
photoService变量的takePhoto方法:openCamera() { this.photoService.takePhoto(); } -
打开
capture.page.html文件,并将<ion-fab-button>元素的click事件绑定到openCamera组件方法:<ion-fab-button **(****click****)=****"openCamera()"**> <ion-icon name="camera"></ion-icon> </ion-fab-button>
我们现在已经添加了所有必要的组件来使用设备的相机拍照。让我们尝试在真实设备上运行应用程序以测试与相机的交互:
-
首先,我们需要使用以下 Ionic CLI 命令构建我们的应用程序:
ionic build
上述命令将在项目根目录中创建一个www文件夹,其中包含你的应用程序包。
-
运行以下命令以在所选平台的发展环境中打开应用程序:
ionic cap open <os>在上一个命令中,
<os>可以是android或ios。执行后,它将根据你针对的平台打开相应的本地移动项目,Android Studio 或 Xcode,具体取决于目标平台。然后必须使用 IDE 来运行本地应用程序。每次你想重新构建应用程序时,都必须运行
ionic cap copy命令,以将应用程序包从www文件夹复制到本地移动项目。 -
点击相机按钮。应用程序可能会要求你允许使用 GPS 和相机。或者,在继续之前,你可能需要在设备上启用位置设置。
你可能需要在开发环境的本地移动项目中添加额外的权限。检查 Capacitor 网站上 API 的相关文档。
我们应用程序的第一页现在有一个简洁的界面,允许用户与相机交互。我们还创建了一个 Angular 服务,确保与 Capacitor 的无缝交互以获取基于位置的数据并拍照。在下一节中,我们将看到如何使用 Firebase 将它们保存在云端。
在 Firebase 中存储数据
应用程序将能够将照片及其位置存储在 Firebase 中。我们将使用存储服务上传我们的照片,并使用 Cloud Firestore 数据库来保存它们的位置。我们将在以下任务中进一步扩展我们的应用程序:
-
创建 Firebase 项目
-
集成AngularFire库
首先,我们必须为我们的应用程序设置一个新的 Firebase 项目。
创建 Firebase 项目
我们可以使用位于console.firebase.google.com的Firebase 控制台设置和配置 Firebase 项目:
- 点击添加项目按钮以创建一个新的 Firebase 项目:

图 6.4 – 创建新的 Firebase 项目
-
为你的项目输入一个名称,然后点击继续按钮:
![图 6.11 – 输入项目名称]()
图 6.5 – 输入项目名称
Firebase 为你的项目生成一个唯一的标识符,它位于项目名称下方,并在各种 Firebase 服务中使用。
-
禁用此项目的Google Analytics并点击创建项目按钮:

图 6.6 – 禁用 Google Analytics
-
等待创建新项目并点击继续按钮。您将被重定向到您的新项目仪表板,其中包含一系列选项:
![图 6.13 – 选择应用程序类型]()
图 6.7 – 选择应用程序类型
点击带有代码图标的第三个选项,将 Firebase 添加到 Web 应用程序中。
-
在应用昵称字段中输入您的应用程序名称,然后点击注册应用按钮:

图 6.8 – 应用程序注册
-
Firebase 将生成一个配置,我们将在后面的移动应用程序中使用:
const firebaseConfig = { apiKey: "<Your API key>", authDomain: "<Your project auth domain>", projectId: "<Your project ID>", storageBucket: "<Your storage bucket>", messagingSenderId: "<Your messaging sender ID>", appId: "<Your application ID>" };复制
firebaseConfig对象并点击继续到控制台按钮。Firebase 配置也可以在
https://console.firebase.google.com/project/<project-id>/settings/general处稍后访问,其中project-id是您的 Firebase 项目 ID。 -
在仪表板控制台中,选择Cloud Firestore选项以在您的应用程序中启用 Cloud Firestore。
-
点击创建数据库按钮以创建一个新的 Cloud Firestore 数据库:

图 6.9 – 创建数据库
-
选择数据库的操作模式。为了开发目的,选择以测试模式启动并点击下一步按钮:
![包含文本的图像 自动生成的描述]()
图 6.10 – 选择操作模式
选择模式相当于为您的数据库设置规则。测试模式允许快速设置,并保持您的数据公开 30 天。当您准备好将应用程序移入生产时,您可以相应地修改数据库规则以使您的数据私有。
-
根据您的区域设置选择数据库的位置,然后点击启用按钮。
恭喜!您已创建了一个新的 Cloud Firestore 数据库。在下一节中,我们将学习如何使用新的数据库通过我们的移动应用程序保存数据。
集成 AngularFire 库
AngularFire 库是一个 Angular 库,我们可以在 Angular 应用程序中使用它来与 Firebase 家族产品(如 Cloud Firestore 和存储服务)交互。要在我们的应用程序中安装它:
-
在终端窗口中运行以下命令以安装Firebase 工具:
npm install -g firebase-tools -
在相同的终端窗口中运行以下命令以使用 Firebase CLI 进行身份验证:
firebase login -
最后,运行以下 Angular CLI 命令以在您的 Angular CLI 项目中安装
@angular/firenpm 包:ng add @angular/fire上述命令将找到库的最新版本,并提示我们安装它。
-
首先,它会询问我们想要启用 Firebase 的哪些功能:
? What features would you like to setup?确保只选择 Firestore 选项并按 Enter。
-
然后,它将询问我们想要使用哪个 Firebase 账户:
? Which Firebase account would you like to use?确保选择你之前使用的账户,并按 Enter。
-
在下一个问题中,我们将选择我们将要使用 Firestore 的项目:
? Please select a project:选择我们之前创建的
phototag项目并按 Enter。 -
最后,我们必须选择已启用 Firestore 的应用程序:
? Please select an app:选择我们之前创建的
phototag应用程序并按 Enter。前面的命令可能会抛出一个错误,表明
app.module.ts文件不存在。请忽略它并继续下一步。 -
打开
main.ts文件并添加以下import语句:import { provideFirebaseApp, initializeApp } from '@angular/fire/app'; import { getFirestore, provideFirestore } from '@angular/fire/firestore'; import { getStorage, provideStorage } from '@angular/fire/storage'; -
最后,修改
bootstrapApplication方法中的providers数组如下:bootstrapApplication(AppComponent, { providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, importProvidersFrom(IonicModule.forRoot({})), provideRouter(routes), **importProvidersFrom(****provideFirebaseApp****(****() =>****initializeApp****(<firebaseConfig>))),** **importProvidersFrom(****provideFirestore****(****() =>****getFirestore****())),** **importProvidersFrom(****provideStorage****(****() =>****getStorage****()))** ] });
将 <firebaseConfig> 替换为你在上一节中复制的 Firebase 配置对象。
现在我们来看看我们如何在应用程序中使用 AngularFire 库:
-
打开
photo.service.ts文件并添加以下import语句:import { Firestore, collection, addDoc } from '@angular/fire/firestore'; import { Storage, ref, uploadString, getDownloadURL } from '@angular/fire/storage';Firestore服务包含我们与 Cloud Firestore 数据库交互所需的所有必要方法。Storage服务包含将文件上传到存储服务的方法。 -
将这两个服务注入到
PhotoService类的constructor中:constructor(**private** **firestore: Firestore,** **private** **storage: Storage**) {} -
创建以下方法以在 Firebase 中保存照片:
private async savePhoto(dataUrl: string, latitude: number, longitude: number) { const name = new Date().getUTCMilliseconds().toString(); const storageRef = ref(this.storage, name); await uploadString(storageRef, dataUrl, 'data_url'); const photoUrl = await getDownloadURL(storageRef); const photoCollection = collection(this.firestore, 'photos'); await addDoc(photoCollection, { url: photoUrl, lat: latitude, lng: longitude }) }首先,我们为我们的照片创建一个随机的
name,并使用uploadString方法将其上传到 Firebase 存储中。一旦上传完成,我们使用getDownloadURL方法获取可下载的 URL,该 URL 可用于访问该照片。最后,我们使用addDoc方法将新照片添加到 Firestore 数据库的photocollection属性中。 -
修改
takePhoto方法以调用我们在上一步中创建的savePhoto方法:async takePhoto() { **const** **{latitude, longitude} =** await this.getLocation(); **const** **cameraPhoto =** await Camera.getPhoto({ resultType: CameraResultType.DataUrl, source: CameraSource.Camera, quality: 100 }); **if** **(cameraPhoto.****dataUrl****) {** **await****this****.****savePhoto****(cameraPhoto.****dataUrl****, latitude, longitude);** **}** }
我们现在可以检查照片拍摄过程的完整功能:
-
运行以下 Capacitor 命令以将应用程序包复制到原生移动项目中:
ionic cap copy -
使用 Capacitor 的
open命令打开原生移动项目,并使用相应的 IDE 运行项目。 -
打开应用程序的 Firebase 控制台,并在 构建 部分中选择 存储 选项。点击 开始 按钮,选择 以测试模式开始 选项,然后点击 下一步。最后,点击 完成 以完成设置云存储的过程。
-
使用应用程序拍摄一张好照片。为了验证你的照片是否已成功上传到 Firebase,请刷新 Firebase 控制台中的页面。你应该会看到一个如下条目:

图 6.11 – Firebase 存储
- 类似地,在 构建 部分中选择 Firestore 数据库 选项,你应该会看到以下内容:

图 6.12 – Cloud Firestore
在前面的屏幕截图中,1oFxxWgQseIwqWUrYBkN条目是包含实际文件 URL 及其位置数据的照片的逻辑对象。
我们应用的第一页现在功能完整。我们已经完成了从拍摄和上传照片到 Firebase,包括其位置数据的全过程。我们首先设置和配置了 Firebase 项目,最后通过学习如何使用 AngularFire 库与该项目交互来完成。在下一节中,我们将通过实现应用的第二页来达到最终目标。
使用 CesiumJS 预览照片
我们应用的下个功能将是将我们用相机拍摄的所有照片显示在 3D 地图上。CesiumJS 库提供了一个带有 3D 地球仪的查看器,我们可以用它来可视化各种事物,例如特定位置上的图像。我们应用的新功能将包括以下内容:
-
配置 CesiumJS
-
在查看器上显示照片
我们将首先学习如何设置 CesiumJS 库。
配置 CesiumJS
CesiumJS 库是一个我们可以安装的 npm 包,用于开始使用 3D 地图和可视化:
-
运行以下
npm命令来安装 CesiumJS:npm install cesium -
打开
angular.json配置文件,并在build架构师选项的assets数组中添加以下条目:{ "glob": "**/*", "input": "node_modules/cesium/Build/Cesium/Workers", "output": "/assets/cesium/Workers" }, { "glob": "**/*", "input": "node_modules/cesium/Build/Cesium/ThirdParty", "output": "/assets/cesium/ThirdParty" }, { "glob": "**/*", "input": "node_modules/cesium/Build/Cesium/Assets", "output": "/assets/cesium/Assets" }, { "glob": "**/*", "input": "node_modules/cesium/Build/Cesium/Widgets", "output": "/assets/cesium/Widgets" }上述条目将把所有 CesiumJS 源文件复制到应用
assets文件夹内的cesium文件夹中。 -
还要将 CesiumJS 小部件样式表文件添加到
build部分的styles数组中:"styles": [ **"node_modules/cesium/Build/Cesium/Widgets/widgets.css"****,** "src/theme/variables.scss", "src/global.scss" ]CesiumJS 的查看器包含一个带有小部件的工具栏,包括搜索栏和下拉菜单以选择特定类型的地图,例如 Bing Maps 或 Mapbox。
-
打开我们应用的主入口点文件
main.ts,并添加以下行:(window as Record<string, any>)['CESIUM_BASE_URL'] = '/assets/cesium/';CESIUM_BASE_URL全局变量指示 CesiumJS 源文件的位置。 -
使用以下
npm命令安装自定义 webpack 构建器:npm install -D @angular-builders/custom-webpack构建器是一个扩展 Angular CLI 默认功能的 Angular 库。
@angular-builders/custom-webpack构建器允许我们在构建应用时提供额外的 webpack 配置文件。在需要包含其他 webpack 插件或覆盖现有功能的情况下,这非常有用。 -
在项目的根文件夹中创建一个名为
extra-webpack.config.js的新 webpack 配置文件,并添加以下内容:module.exports = { resolve: { fallback: { "https": false, "zlib": false, "http": false, "url": false } }, module: { unknownContextCritical: false } };配置文件将确保 webpack 只会尝试加载它能够理解的 CesiumJS 代码。CesiumJS 使用一种格式,无法使用 webpack 进行静态分析。
-
打开
angular.json文件,并将build架构师部分的builder属性更改为使用自定义 webpack 构建器:"builder": "**@angular-builders/custom-webpack:browser**" -
在
build部分的options属性中定义自定义 webpack 配置文件的路径:"customWebpackConfig": { "path": "./extra-webpack.config.js" } -
还要配置
serve架构师部分以使用自定义 webpack 构建器:"serve": { "builder": "**@angular-builders/custom-webpack:dev-server**", "configurations": { "production": { "browserTarget": "app:build:production" }, "development": { "browserTarget": "app:build:development" }, "ci": { "progress": false } }, "defaultConfiguration": "development" }
现在我们已经完成了 CesiumJS 库的配置,我们可以开始创建我们功能的页面:
-
使用以下 Ionic CLI 命令创建一个新的页面:
ionic generate page view -
打开
view.page.html文件并修改第一个<ion-header>元素,使其包含一个菜单切换按钮:<ion-header [translucent]="true"> <ion-toolbar> **<****ion-buttons****slot****=****"start"****>** **<****ion-menu-button****color****=****"primary"****></****ion-menu-button****>** **</****ion-buttons****>** <ion-title>**View gallery**</ion-title> </ion-toolbar> </ion-header> -
修改
<ion-content>元素标题,并添加一个<div>元素作为我们的查看器容器:<ion-content [fullscreen]="true"> <ion-header collapse="condense"> <ion-toolbar> <ion-title size="large">**View gallery**</ion-title> </ion-toolbar> </ion-header> **<****div****#mapContainer****></****div****>** </ion-content>#mapContainer是我们用来在模板中声明元素别名的模板引用变量。 -
打开
view.page.scss文件并设置地图容器元素的大小:div { height: 100%; width: 100%; } -
让我们现在创建我们的查看器。打开
view.page.ts文件并按以下方式修改它:import { **AfterViewInit**, Component, **ElementRef**, OnInit, **ViewChild** } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { IonicModule } from '@ionic/angular'; **import** **{** **Viewer** **}** **from****'cesium'****;** @Component({ selector: 'app-view', templateUrl: './view.page.html', styleUrls: ['./view.page.scss'], standalone: true, imports: [IonicModule, CommonModule, FormsModule] }) export class ViewPage implements OnInit, **AfterViewInit** { **@ViewChild****(****'mapContainer'****)** **content****:** **ElementRef** **|** **undefined****;** constructor() { } ngOnInit() { } **ngAfterViewInit****() {** **const** **viewer =** **new****Viewer****(****this****.****content****?.****nativeElement****);** **}** }我们在组件的
ngAfterViewInit方法内部创建一个新的Viewer对象。ngAfterViewInit方法在组件视图加载完成后被调用,它定义在AfterViewInit接口中。Viewer类的构造函数接受一个参数,即我们想要在上面创建查看器的原生 HTML 元素。在我们的情况下,我们想要将其附加到我们之前创建的地图容器元素上。因此,我们使用@ViewChild装饰器通过传递模板引用变量名称作为参数来引用该元素。 -
使用
ionic serve运行应用程序,并从主菜单点击查看相册选项。你应该看到以下输出:

图 6.13 – 查看相册页面
我们现在已成功在我们的应用程序中配置了 CesiumJS 库。在下一节中,我们将看到如何从中受益并在 CesiumJS 查看器的 3D 地球上显示我们的照片。
在查看器上显示照片
为了使我们的应用程序准备就绪,我们需要做的下一件事是在地图上显示我们的照片。我们将从 Firebase 获取所有照片并将它们添加到查看器中指定的位置。让我们看看我们如何实现这一点:
-
使用以下 Ionic CLI 命令创建一个新的 Angular 服务:
ionic generate service cesium -
打开
cesium.service.ts文件并添加以下import语句:import { Firestore, collection, getDocs } from '@angular/fire/firestore'; import { Cartesian3, Color, PinBuilder, Viewer } from 'cesium'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -
在
CesiumService类的constructor中注入Firestore服务并创建一个viewer属性,我们将使用它来存储我们的Viewer对象:export class CesiumService { **private****viewer****:** **Viewer** **|** **undefined****;** constructor(**private** **firestore: Firestore**) { } } -
创建一个
register方法来设置viewer属性:register(viewer: Viewer) { this.viewer = viewer; } -
创建一个方法来从 Cloud Firestore 获取
photos集合:private async getPhotos() { const photoCollection = collection(this.firestore, 'photos'); return await getDocs(photoCollection); }在前面的方法中,我们调用
getDocs方法来获取photos集合的数据。 -
为添加所有照片到查看器创建以下方法:
async addPhotos() { const pinBuilder = new PinBuilder(); const photos = await this.getPhotos(); photos.forEach(photo => { const entity = { position: Cartesian3.fromDegrees(photo.get('lng'), photo.get('lat')), billboard: { image: pinBuilder.fromColor(Color.fromCssColorString('#de6b45'), 48).toDataURL() }, description: `<img width="100%" style="margin:auto; display: block;" src="img/${photo.get('url')}" />` }; this.viewer?.entities.add(entity); }); }在查看器中,每张照片的位置将以标记的形式显示。因此,我们首先需要初始化一个
PinBuilder对象。前面的方法调用getPhotos方法从 Cloud Firestore 获取所有照片。对于每张照片,它创建一个包含position的entity对象,这是照片在度数中的位置,以及一个显示 48 像素大小标记的billboard属性。它还定义了一个description属性,当点击标记时将显示照片的实际图像。每个
entity对象都通过其add方法添加到查看器的entities集合中。 -
每张照片的描述都显示在信息框内。打开包含应用程序全局样式的
global.scss文件,并为信息框添加以下 CSS 样式:.cesium-infoBox, .cesium-infoBox-iframe { height: 100% !important; width: 100%; } -
现在,让我们使用页面中的
CesiumService。打开view.page.ts文件,并将CesiumService类注入到ViewPage类的constructor中:import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { IonicModule } from '@ionic/angular'; import { Viewer } from 'cesium'; **import** **{** **CesiumService** **}** **from****'../cesium.service'****;** @Component({ selector: 'app-view', templateUrl: './view.page.html', styleUrls: ['./view.page.scss'], standalone: true, imports: [IonicModule, CommonModule, FormsModule] }) export class ViewPage implements OnInit, AfterViewInit { @ViewChild('mapContainer') content: ElementRef | undefined; constructor(**private** **cesiumService: CesiumService**) { } ngOnInit() { } ngAfterViewInit() { const viewer = new Viewer(this.content?.nativeElement); } } -
修改
ngAfterViewInit方法以注册查看器并添加照片:ngAfterViewInit() { **this****.****cesiumService****.****register****(****new****Viewer****(****this****.****content****?.****nativeElement****));** **this****.****cesiumService****.****addPhotos****();** }
我们现在可以查看地图上的照片了:
-
使用
ionic serve命令运行应用程序。 -
使用应用程序拍摄漂亮的照片,最好在不同的地点。
-
从主菜单中选择查看相册选项,你应该得到以下输出:

图 6.14 – 地图上的照片
- 点击地图上的一个标记,你应该能看到你的照片:

图 6.15 – 照片显示
现在我们有一个完整的移动应用程序,用于拍摄带有地理标记的照片并在地图上显示它们。我们看到了如何设置 CesiumJS 库并从 Cloud Firestore 获取我们的照片。CesiumJS 查看器的 API 为我们在地图上可视化照片和与之交互提供了简单的方法。
摘要
在本章中,我们构建了一个用于拍照、标记当前位置并在 3D 地图上显示照片的移动应用程序。最初,我们学习了如何使用 Ionic 框架创建新的移动应用程序。我们在本地构建了应用程序,并集成了 Capacitor 以与相机和 GPS 设备交互。相机用于拍照,GPS 用于标记位置。
之后,我们使用了 Firebase 服务将我们的照片文件和数据存储在云端。最后,我们学习了如何从 Firebase 检索存储的照片,并使用 CesiumJS 库在 3D 球上显示它们。
在下一章中,我们将探讨在 Angular 中预渲染内容的另一种方法。我们将使用服务器端渲染技术来创建一个 GitHub 站点。
实践问题
-
我们可以使用哪个工具包在 Capacitor 应用中创建 UI?
-
在 Capacitor 应用中,我们使用哪种方法用相机拍照?
-
在 Capacitor 应用中,我们如何读取当前的位置?
-
我们如何使用 Ionic 添加菜单切换按钮?
-
我们使用哪个 Capacitor 命令来同步应用程序包与原生移动项目?
-
在 Cloud Firestore 中,测试模式和发布模式有什么区别?
-
我们如何使用 AngularFire 库初始化应用程序?
-
我们使用哪种方法从 Cloud Firestore 集合中获取数据?
-
我们如何使用 CesiumJS 库创建一个标记?
-
我们如何使用 CesiumJS 将经纬度转换为度?
进一步阅读
-
开始使用 Capacitor:
capacitorjs.com/docs/getting-started -
Capacitor 的 Android 入门指南:
capacitorjs.com/docs/android#getting-started -
Capacitor 的 iOS 入门指南:
capacitorjs.com/docs/ios#getting-started -
使用 Ionic 进行 Angular 开发:
ionicframework.com/docs/angular/overview -
AngularFire 库文档:
firebaseopensource.com/projects/angular/angularfire2 -
CesiumJS 快速入门指南:
cesium.com/docs/tutorials/quick-start -
CesiumJS 和 Angular 文章:
cesium.com/blog/2018/03/12/cesium-and-angular
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第七章:使用 Angular 为 GitHub 资料构建 SSR 应用程序
一个典型的 Angular 应用程序遵循单页应用程序(SPA)的方法,其中每个页面都在浏览器的 DOM 中创建,同时用户与应用程序进行交互。一个网络服务器托管应用程序,并在应用程序启动时仅提供主页面,通常称为index.html。
服务器端渲染(SSR)是一种与 SPA 完全不同的应用程序渲染方法。它在用户在运行时请求页面时使用服务器进行预渲染页面。在服务器上渲染内容极大地提高了 Web 应用程序的性能,并改善了其搜索引擎优化(SEO)能力。要在 Angular 应用程序中执行 SSR,我们使用一个名为Angular Universal的库。
在本章中,我们将学习如何通过构建使用GitHub API的资料应用程序来从 Angular Universal 中获益。我们将涵盖以下主题:
-
使用 GitHub API 构建 Angular 应用程序
-
集成 Angular Universal
-
在构建过程中预渲染内容
-
提升 SEO 能力
重要的背景理论和上下文
一个 Angular 应用程序由几个页面组成,这些页面在我们使用应用程序时由 Angular 框架在浏览器 DOM 中动态创建。Angular Universal 使 Angular 框架能够在应用程序运行时在服务器上静态地创建这些页面。换句话说,它可以创建一个完全静态的 Angular 应用程序版本,即使不需要启用 JavaScript 也可以运行。在服务器上预渲染应用程序有以下优点:
-
它允许网络爬虫索引应用程序,并在社交媒体网站上使其可发现和可链接。
-
这使得应用程序在移动和其他性能较低的设备上可用,这些设备无法在其侧执行 JavaScript。
-
它通过快速加载第一页并在后台同时加载实际客户端页面(首次内容绘制(FCP))来提高用户体验。
GitHub API 是一个用于与 GitHub 数据交互的 HTTP REST API。它可以通过提供的开箱即用的认证机制公开或私下使用。
对 GitHub API 的无授权请求限制为每小时 60 次。有关可用认证方法的概述,您可以在docs.github.com/en/rest/overview/authenticating-to-the-rest-api找到更多详细信息。
我们使用@angular/common/http npm 包中可用的内置 HTTP 客户端在 Angular 中进行 HTTP 通信。在 SSR 应用程序中与 HTTP 交互可能会导致由于在 FCP 时页面预渲染而导致的 HTTP 请求重复。然而,Angular Universal 可以通过称为TransferState的机制克服这种类型的重复。
项目概述
在这个项目中,我们将为我们的 GitHub 用户资料构建一个个人资料应用程序。我们最初将使用 Angular CLI 来搭建一个与 GitHub API 交互的 Angular 应用程序。我们将学习如何使用 GitHub API 并获取特定用户的数据。我们还将使用Bootstrap CSS库来美化我们的应用程序并创建一个美观的用户界面。
在创建我们的 Angular 应用程序后,我们将使用 Angular Universal 将其转换为服务器端渲染的应用程序。我们将了解如何安装和配置 Angular Universal,并学习如何在构建时进行预渲染。
然后,我们将配置我们的应用程序以在最受欢迎的社会平台上正确地使用 SEO 进行渲染。以下图表展示了项目的架构概述:

图 7.1 – 项目架构
构建时间:2 小时
入门
完成此项目所需的先决条件和软件工具如下:
-
GitHub 账户:一个有效的 GitHub 用户账户。
-
Angular CLI:Angular 的 CLI,您可以在
angular.io/cli找到。 -
GitHub 材料:本章的相关代码可以在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter07文件夹中找到。
使用 GitHub API 构建 Angular 应用程序
GitHub 包含一个 API,我们可以使用它来获取有关 GitHub 用户资料的各项信息。我们正在构建的 Angular 应用程序将与 GitHub API 通信并显示我们 GitHub 资料的简要个人资料。我们的应用程序将包含以下功能:
-
仪表板:这将是应用程序的着陆页,它将显示我们的 GitHub 资料的摘要。
-
信息:这将显示关于我们的个人信息。
-
仓库:这将显示我们的公共仓库列表。
-
组织:这将显示我们作为成员的 GitHub 组织列表。
本章截图显示的每个功能的结果输出将根据您的 GitHub 资料而有所不同。
仪表板将是应用程序的主页,它将包含所有其他功能。我们将在下一节学习如何构建仪表板页面。
构建仪表板
在我们开始创建应用程序的主要功能之前,我们需要通过运行以下命令来搭建和配置一个 Angular 应用程序:
ng new gh-portfolio --routing=false --style=scss
以下命令将使用 Angular CLI 的ng new命令,传递以下选项:
-
gh-portfolio:我们想要创建的 Angular 应用程序的名称 -
--routing=false:禁用路由,因为我们的应用程序将只包含一个页面 -
--style=scss:配置 Angular 应用程序在处理 CSS 样式时使用 SCSS 样式表格式
我们将使用 Bootstrap CSS 库来为我们的投资组合应用程序进行样式设计。让我们看看如何在刚刚创建的 Angular CLI 应用程序中安装和配置它:
-
执行以下
npm命令安装 Bootstrap CSS 库:npm install bootstrap -
打开
src\styles.scss文件并导入 Bootstrap SCSS 样式表:@import "bootstrap/scss/bootstrap";styles.scss文件包含应用于应用程序的全局 CSS 样式。@importCSS 规则接受我们想要加载的样式表的绝对路径。当我们使用
@import规则导入样式表格式时,我们省略了文件的扩展名。 -
执行以下命令安装Bootstrap Icons,这是一个免费的开源图标库:
npm install bootstrap-iconsBootstrap Icons 可以使用多种格式,如 SVG 或字体。在这个项目中,我们将使用后者。
-
将 Bootstrap Icons 库的字体图标格式导入到
styles.scss文件中:@import "bootstrap/scss/bootstrap"; **@import****"bootstrap-icons/font/bootstrap-icons"****;**
我们已经创建了 Angular 应用程序并添加了必要的样式元素。现在我们准备开始创建 Angular 应用程序的主页:
-
从官方 Angular 文档的
angular.io/presskit的媒体包中下载您选择的 Angular 标志。 -
将下载的标志文件复制到 Angular CLI 工作区的
src\assets文件夹中。assets文件夹用于静态文件,如图像、字体和 JSON 文件。 -
打开
app.component.ts文件,并在AppComponent类中创建一个username属性,该属性以 GitHub 登录名作为值:export class AppComponent { title = 'gh-portfolio'; **username =** **'<Your GitHub login>'****;** } -
打开
app.component.html文件,并用以下 HTML 模板替换其内容:<div class="toolbar d-flex align-items-center"> <img width="40" alt="Angular Logo" src="img/angular.png" /> <span>Welcome to my GitHub portfolio</span> <a class="ms-auto p-2" target="_blank" rel="noopener" href="https://github.com/{{username}}" title="GitHub"> <i class="bi-github"></i> </a> </div>在前面的模板中,我们定义了我们的应用程序的标题。它包含一个锚点元素,链接到我们的 GitHub 个人资料。我们还使用了 Bootstrap Icons 集中的
bi-github类添加了 GitHub 图标。 -
在应用程序的标题之后插入以下 HTML 片段:
<div class="content d-flex flex-column"> <div class="row"> <div class="col-sm-3"></div> <div class="col-sm-9"> <div class="row"> <div class="col-12 col-sm-12"></div> </div> <div class="row"> <div class="col-12 col-sm-12"></div> </div> </div> </div> </div> col-sm-3 class selector will display the *personal information* feature. The element with the col-sm-9 class selector will be split into two rows, one for the *repositories* and another for the *organizations* features. -
打开
app.component.scss文件,并为应用程序的标题和内容添加以下 CSS 样式:.toolbar { height: 60px; background-color: #1976d2; color: white; font-weight: 600; } .toolbar img { margin: 0 16px; } .toolbar i { font-size: 1.5rem; color: white; margin: 0 16px; } .toolbar a { margin-bottom: 5px; } .toolbar i:hover { opacity: 0.8; } .content { margin: 52px auto 32px; padding: 0 16px; } -
运行
ng serve以启动应用程序并导航到http://localhost:4200。应用程序的标题应如下所示:

图 7.2 – 应用程序标题
我们的投资组合应用程序的主页现在准备好了。它包含一个标题和一个用于添加主要功能的空容器元素。在下一节中,我们将开始构建应用程序的个人信息功能。
显示个人信息
我们应用程序的第一个功能将是显示来自 GitHub 个人资料的个人信息,例如全名、个人照片和一些社交媒体链接。在创建功能之前,我们首先需要配置我们的应用程序,使其能够与 GitHub API 通信:
-
打开应用程序的主模块,即
app.module.ts文件,并将HttpClientModule类添加到@NgModule装饰器的imports数组中:import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; **import** **{** **HttpClientModule** **}** **from****'@angular/common/http'****;** import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, **HttpClientModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }HttpClientModule类是内置 HTTP 库的主要 Angular 模块,它导出了与 HTTP 资源交互所需的所有必要服务。 -
使用以下 Angular CLI 命令创建一个新的 Angular 服务:
ng generate service github -
打开
github.service.ts文件,将HttpClient服务注入到GithubService类的constructor中:**import** **{** **HttpClient** **}** **from****'@angular/common/http'****;** import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class GithubService { constructor(**private** **http: HttpClient**) { } }HttpClient类是 Angular 内置 HTTP 客户端的服务,它提供了与 HTTP 交互的所有主要方法,例如GET、POST和PUT。 -
在
GithubService类中添加以下属性:readonly username = '<Your GitHub login>'; private apiUrl = 'https://api.github.com';确保将
username属性的值设置为您的 GitHub 登录名。 -
修改
app.component.ts文件,使其使用GithubService中的username属性:import { Component, **OnInit** } from '@angular/core'; **import** **{** **GithubService** **}** **from****'./github.service'****;** @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent **implements****OnInit** { title = 'gh-portfolio'; **username =** **''****;** **constructor****(****private** **githubService: GithubService****) {}** **ngOnInit****():** **void** **{** **this****.****username** **=** **this****.****githubService****.****username****;** **}** }
我们的应用程序与 GitHub API 之间的所有交互都将委托给GithubService。现在,让我们专注于构建我们的功能:
-
执行以下 Angular CLI 命令来创建我们功能的新 Angular 组件:
ng generate component personal-info -
使用以下 Angular CLI 命令创建一个
user接口,以定义组件的数据模型:ng generate interface user -
打开
user.ts文件,并将以下属性添加到User接口中:export interface User { **avatar_url****:** **string****;** **name****:** **string****;** **blog****:** **string****;** **location****:** **string****;** **bio****:** **string****;** **twitter_username****:** **string****;** **followers****:** **number****;** } -
打开
github.service.ts文件,并添加以下import语句:import { Observable } from 'rxjs'; import { User } from './user'; -
创建一个新方法,从 GitHub API 获取我们个人资料详情:
getUser(): Observable<User> { return this.http.get<User>(`${this.apiUrl}/users/${this.username}`); } -
打开
personal-info.component.ts文件,并相应地修改import语句:import { Component, **OnInit** } from '@angular/core'; **import** **{** **Observable** **}** **from****'rxjs'****;** **import** **{** **GithubService** **}** **from****'../github.service'****;** **import** **{** **User** **}** **from****'../user'****;** -
将
GithubService注入到PersonalInfoComponent类的constructor中,并创建一个组件属性以获取getUser方法的结果:export class PersonalInfoComponent implements OnInit { user$: Observable<User> | undefined; constructor(private githubService: GithubService) {} ngOnInit(): void { this.user$ = this.githubService.getUser(); } } -
打开
personal-info.component.html文件,将其内容替换为以下 HTML 模板:<div class="card" *ngIf="user$ | async as user"> <img [src]="user.avatar_url" class="card-img-top" alt="{{user.name}} photo"> <div class="card-body"> <h5 class="card-title">{{user.name}}</h5> <p class="card-text">{{user.bio}}</p> </div> <ul class="list-group list-group-flush"> <li class="list-group-item" title="Location"> <i class="bi-geo me-2"></i>{{user.location}} </li> <li class="list-group-item" title="Followers"> <i class="bi-people me-2"></i>{{user.followers}} </li> </ul> <div class="card-body"> <a href="https://www.twitter.com/{{user.twitter_username}}" class="card-link">Twitter</a> <a [href]="user.blog" class="card-link">Personal blog</a> </div> </div>在前面的模板中,我们使用
async管道,因为user$属性是一个可观察对象,我们需要订阅它以获取其值。async管道的主要优点是当组件被销毁时,它会自动取消订阅可观察对象,从而避免潜在的内存泄漏。我们还创建了
user别名,以便在组件模板的各个位置轻松引用它。 -
打开
app.component.html文件,并将<app-personal-info>组件添加到具有col-sm-3类选择器的元素中:<div class="col-sm-3"> **<****app-personal-info****></****app-personal-info****>** </div>
如果我们运行ng serve来预览应用程序,我们应该在页面左侧看到个人信息面板:

图 7.3 – 个人信息
我们的投资组合应用程序的第一个功能现在已完成。它显示了我们 GitHub 个人资料的个人信息、简短的个人简介和一些社交网络链接。在下一节中,我们将构建应用程序的仓库功能。
列出用户仓库
GitHub 用户资料包含用户拥有的仓库列表,称为 sources,以及另一个贡献的仓库列表,称为 forks。
我们应用程序的仓库功能将仅显示源仓库。
repositories 和 organizations 功能将具有类似的用户界面。因此,我们需要为这两个功能创建一个组件:
-
执行以下 Angular CLI 命令来创建一个新的组件:
ng generate component panel -
打开
panel.component.ts文件,并使用@Input装饰器定义两个输入属性:import { Component, **Input** } from '@angular/core'; @Component({ selector: 'app-panel', templateUrl: './panel.component.html', styleUrls: ['./panel.component.scss'] }) export class PanelComponent { **@Input****() caption =** **''****;** **@Input****() icon =** **''****;** } -
打开
panel.component.html文件,并用以下 HTML 模板替换其内容:<div class="card mb-4"> <div class="card-header"> <i class="bi bi-{{icon}} me-1"></i> {{caption}} </div> <div class="card-body"> <ng-content></ng-content> </div> </div>面板组件是一个 Bootstrap 卡片元素,由标题和主体组成。标题使用
caption和icon输入属性来显示带图标的文本。主体使用<ng-content>Angular 组件来定义一个占位符,其中将显示我们的功能内容。
我们现在可以使用面板组件来创建我们的功能:
-
创建一个接口来表示 GitHub 仓库的数据模型:
ng generate interface repository -
打开
repository.ts文件,并在Repository接口中添加以下属性:export interface Repository { **name****:** **string****;** **html_url****:** **string****;** **description****:** **string****;** **fork****:** **boolean****;** **stargazers_count****:** **number****;** **language****:** **string****;** **forks_count****:** **number****;** } -
打开
github.service.ts文件,并导入Repository接口:import { Repository } from './repository'; -
现在,是我们对服务进行重构的时候了。我们将用于获取仓库的 URL 与
getUser方法的 URL 类似。将那个方法的 URL 提取为GithubService类的一个属性:export class GithubService { readonly username = '<Your GitHub login>'; **private** **userUrl =** **'https://api.github.com/users/'** **+** **this****.****username****;** constructor(private http: HttpClient) { } getUser(): Observable<User> { return this.http.get<User>(**this****.****userUrl**); } } -
创建一个新的方法来获取当前 GitHub 用户的仓库:
getRepos(): Observable<Repository[]> { return this.http.get<Repository[]>(this.userUrl + '/repos'); }
现在我们已经为从 GitHub API 获取用户仓库创建了先决条件,我们可以开始构建将显示这些仓库的组件:
-
执行以下命令使用 Angular CLI 创建一个新的 Angular 组件:
ng generate component repositories -
打开
repositories.component.ts文件,并相应地修改import语句:import { Component, **OnInit** } from '@angular/core'; **import** **{** **Observable** **}** **from****'rxjs'****;** **import** **{ map }** **from****'rxjs/operators'****;** **import** **{** **GithubService** **}** **from****'../github.service'****;** **import** **{** **Repository** **}** **from****'../repository'****;** -
在
RepositoriesComponent类的constructor中注入GithubService,并创建一个组件属性以获取getRepos方法的结果:export class RepositoriesComponent implements OnInit { repos$: Observable<Repository[]> | undefined; constructor(private githubService: GithubService) { } ngOnInit(): void { this.repos$ = this.githubService.getRepos().pipe( map(repos => repos.filter(repo => !repo.fork)) ); } }在前面的类中,我们使用 RxJS 的
pipe操作符将getRepos方法返回的可观察对象与map操作符结合,以过滤掉fork仓库并仅获取源。过滤是通过使用数组的标准filter方法完成的。 -
打开
repositories.component.html文件,并用以下 HTML 模板替换其内容:<app-panel caption="Repositories" icon="archive"> <div class="row row-cols-1 row-cols-md-3 g-4"> <div class="col p-2" *ngFor="let repo of repos$ | async"> <div class="card h-100"> <div class="card-body"> <h5 class="card-title"> <a [href]="repo.html_url">{{repo.name}}</a> </h5> <p class="card-text">{{repo.description}}</p> </div> </div> </div> </div> </app-panel>在前面的模板中,我们将组件的主要内容包裹在
<app-panel>组件内部,并为标题设置了caption和icon属性。我们的组件遍历
repos$可观察对象,并显示每个仓库的name和description。名称是一个锚点元素,指向仓库的实际 GitHub URL。 -
在具有
card-body类选择器的元素之后立即添加以下列表:<ul class="list-group list-group-flush list-group-horizontal"> <li class="list-group-item border-0"> <i class="bi-code me-2"></i> {{repo.language}} </li> <li class="list-group-item border-0"> <i class="bi-star me-2"></i> {{repo.stargazers_count}} </li> <li class="list-group-item border-0"> <i class="bi-diagram-2 me-2"></i> {{repo.forks_count}} </li> </ul> language of each repository, how many have starred it, and how many have forked it. -
打开
app.component.html文件,并在第一个 HTML 元素中使用col-12 col-sm-12类选择器添加<app-repositories>组件:<div class="col-sm-9"> <div class="row"> <div class="col-12 col-sm-12"> **<****app-repositories****></****app-repositories****>** </div> </div> <div class="row"> <div class="col-12 col-sm-12"></div> </div> </div> -
运行
ng serve来预览应用程序,你应该能在个人信息功能旁边看到新的面板:

图 7.4 – 仓库
我们应用程序的第二个功能已经完成。它显示了我们 GitHub 个人资料中存在的公共仓库列表。我们的应用程序现在还提供了一个面板组件,我们可以在下一节中使用它来构建应用程序的组织功能。
可视化组织成员资格
一个 GitHub 用户可以是 GitHub 组织的成员。我们的应用程序将显示用户组织列表以及每个组织的附加信息。
让我们开始构建我们的组织列表:
-
创建一个接口来定义组织的属性:
ng generate interface organization -
打开
organization.ts文件,并在Organization接口中添加以下属性:export interface Organization { **login****:** **string****;** **description****:** **string****;** **avatar_url****:** **string****;** } -
打开
github.service.ts文件并导入Organization接口:import { Organization } from './organization'; -
创建一个新的方法来获取当前 GitHub 用户的组织:
getOrganizations(): Observable<Organization[]> { return this.http.get<Organization[]>(this.userUrl + '/orgs'); } -
执行以下命令来创建我们的功能对应的 Angular 组件:
ng generate component organizations -
打开
organizations.component.ts文件并相应地修改import语句:import { Component, **OnInit** } from '@angular/core'; **import** **{** **Observable** **}** **from****'rxjs'****;** **import** **{** **GithubService** **}** **from****'../github.service'****;** **import** **{** **Organization** **}** **from****'../organization'****;** -
在
OrganizationsComponent类的constructor中注入GithubService,并将其getOrganizations方法的结果设置为可观察的组件属性:export class OrganizationsComponent implements OnInit { orgs$: Observable<Organization[]> | undefined; constructor(private githubService: GithubService) { } ngOnInit(): void { this.orgs$ = this.githubService.getOrganizations(); } } -
打开
organizations.component.html文件,并用以下 HTML 模板替换其内容:<app-panel caption="Organizations" icon="diagram-3"> <div class="list-group"> <a href="https://www.github.com/{{org.login}}" class="list-group-item list-group-item-action" *ngFor="let org of orgs$ | async"> <div class="row"> <img [src]="org.avatar_url"> <div class="col-sm-9"> <div class="d-flex w-100 justify-content-between"> <h5 class="mb-1">{{org.login}}</h5> </div> <p class="mb-1">{{org.description}}</p> </div> </div> </a> </div> </app-panel>在前面的 HTML 模板中,我们将组件的主要内容放在
<app-panel>组件内部,传递适当的caption和icon。我们显示每个组织的login和description。每个组织都被一个指向组织 GitHub 页面的锚元素包裹。 -
打开
organizations.component.scss文件,为组织徽标添加以下 CSS 样式:img { width: 60px; height: 40px; } -
打开
app.component.html文件,并在第二个元素中使用col-12 col-sm-12类选择器添加<app-organizations>组件:<div class="col-sm-9"> <div class="row"> <div class="col-12 col-sm-12"> <app-repositories></app-repositories> </div> </div> <div class="row"> <div class="col-12 col-sm-12"> **<****app-organizations****></****app-organizations****>** </div> </div> </div> -
运行
ng serve以启动应用程序,你应该能在仓库功能下看到组织列表:

图 7.5 – 组织
我们的应用程序现在为 GitHub 用户的个人资料提供了一个完整的组合。它显示以下内容:
-
个人信息、简短的个人简介和社交媒体链接
-
包含链接到每个仓库以获取更多信息的公共用户仓库列表
-
用户是成员的组织列表,每个组织都有链接以获取更多详细信息
在下一节中,我们将学习如何集成 Angular Universal 并在服务器上渲染我们的应用程序。
集成 Angular Universal
Angular Universal 是一个 Angular 库,它使 Angular CLI 应用程序能够在服务器上渲染。SSR 应用程序可以提高 Angular 应用程序的加载速度并改善第一页的加载。
要在现有的 Angular CLI 应用程序中安装 Angular Universal,我们将使用以下 Angular CLI 命令:
ng add @nguniversal/express-engine
之前的命令使用了 Angular CLI 的ng add命令来安装@nguniversal/express-engine npm 包。@nguniversal/express-engine包是 Angular Universal 库的核心,其核心是一个Node.js Express网络服务器。
当我们执行前面的命令来安装 Angular Universal 时,我们不仅安装了库,还修改了我们的 Angular CLI 工作区中的以下文件:
-
angular.json: 这在architect部分创建新的条目以构建和启用我们的 Angular Universal 应用程序。其中之一是server属性,它负责使用 SSR 构建我们的应用程序。它将生成的包输出到 Angular CLI 应用程序标准输出文件夹内的一个单独的server文件夹中:"server": { "builder": "@angular-devkit/build-angular:server", "options": { "outputPath": "dist/gh-portfolio/server", "main": "server.ts", "tsConfig": "tsconfig.server.json", "inlineStyleLanguage": "scss" }, "configurations": { "production": { "outputHashing": "media" }, "development": { "buildOptimizer": false, "optimization": false, "sourceMap": true, "extractLicenses": false, "vendorChunk": true } }, "defaultConfiguration": "production" }原始应用程序包现在已生成到 Angular CLI 应用程序标准输出文件夹内的
browser文件夹中,如build部分的outputPath属性所述。因此,Angular Universal 应用程序生成相同 Angular 应用程序的两个版本,一个用于服务器,另一个用于浏览器。
-
package.json: 这将添加所有必要的 npm 依赖项,并创建一组 npmscripts以使用 Angular Universal 开始构建:"scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", **"dev:ssr"****:****"ng run gh-portfolio:serve-ssr"****,** **"serve:ssr"****:****"node dist/gh-portfolio/server/main.js"****,** **"build:ssr"****:****"ng build && ng run gh-portfolio:server"****,** **"prerender"****:****"ng run gh-portfolio:prerender"** }包含
:ssr后缀的脚本与构建和提供 Angular Universal 应用程序相关。prerender脚本将在构建时创建 Angular 应用程序的预渲染版本。我们将在构建期间预渲染内容部分了解prerender脚本。 -
server.ts: 这包含将托管我们的个人项目应用程序服务器端渲染版本的 Node.js Express 应用程序。 -
main.server.ts: 这是我们的 Angular Universal 应用程序的主要入口点。 -
app.server.module.ts: 这是服务器端渲染应用程序的主要应用程序模块。 -
tsconfig.server.json: 这是我们的 Angular Universal 应用程序的 TypeScript 配置。
在服务器上渲染 Angular 应用程序时,全局 JavaScript 对象如window和document不可用,因为没有浏览器。Angular 为一些对象提供了抽象 API,例如DOCUMENT注入令牌。如果您需要有条件地启用它们,可以注入PLATFORM_ID令牌,并使用@angular/common npm 包中的isPlatformServer或isPlatformBrowser方法来检查您的应用程序当前正在哪个平台上运行:
import { Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
export class CheckPlatformComponent {
isBrowser: boolean;
constructor( @Inject(PLATFORM_ID) platformId: any) {
this.isBrowser = isPlatformBrowser(platformId);
}
}
我们现在可以使用以下npm命令在服务器上运行我们的 GitHub 个人项目应用程序:
npm run dev:ssr
要在服务器上预览您的 GitHub 个人项目应用程序,请在浏览器中打开http://localhost:4200。
你通常会看到应用程序在之前的版本。那么,我们在这里得到了什么?Angular Universal 应用程序在运行在具有强大处理器和大量内存的开发机器上时,并没有完全发挥其潜力。相反,我们需要在现实世界的情况中运行和预览它们,比如在慢速网络中。我们可以使用 Google Chrome 开发者工具在开发环境中模拟慢速网络:
-
打开 Google Chrome 浏览器。
-
切换到开发者工具并选择网络选项卡。
-
从限制下拉菜单中选择慢速 3G选项。
-
在浏览器地址栏中输入
http://localhost:4200。
服务器首先加载一个静态版本的应用程序以向用户显示,直到实际的 Angular 应用程序在后台加载完成。当在后台完全加载后,Angular Universal 将切换到完整的应用程序。
在下一节中,我们将探讨如何使用预渲染技术进一步提高我们应用程序的加载速度。
构建过程中的预渲染内容
我们 Angular CLI 工作区的package.json文件包含一个prerender npm 脚本,我们可以使用它来改善我们应用程序的首次加载。该脚本从angular.json配置文件的architect部分运行prerender命令,并在构建时间预渲染我们应用程序的内容。让我们看看预渲染将对我们的 GitHub 个人项目应用程序产生什么影响:
-
执行以下
npm命令以生成应用程序的预渲染版本:npm run prerender上述命令将应用程序的生产版本输出到
dist\gh-portfolio\browser文件夹。 -
导航到
dist\gh-portfolio\browser文件夹,你应该会看到两个 HTML 文件,即index.html和index.original.html文件。 -
打开
index.original.html文件,找到<app-root>HTML 元素。这是我们的 Angular 应用程序的主要组件,Angular 将在浏览器中渲染我们应用程序的内容。 -
现在打开
index.html文件,再次查看<app-root>元素。这次主要组件不为空。Angular Universal 在运行时对 GitHub API 进行了所有 HTTP 请求,并预取了应用程序的内容。所有组件模板和样式都已在主 HTML 文件中预渲染,这意味着我们可以在浏览器中查看我们的应用程序,即使没有启用 JavaScript!
-
执行以下命令以启动我们 GitHub 个人项目应用程序的预渲染版本:
npm run serve:ssr上述命令将启动一个 Node.js Express 服务器,该服务器托管我们的应用程序在
http://localhost:4000。 -
在浏览器设置中禁用 JavaScript,并导航到
http://localhost:4000。
我们的 GitHub 个人项目应用程序即使在没有启用 JavaScript 的情况下也能完全正常运行。应用程序的主页也会立即渲染,无需用户等待应用程序加载。
之前的场景非常适合那些无法在设备上启用 JavaScript 的用户。但是,当启用 JavaScript 的用户使用相同的预先渲染版本的应用程序时会发生什么?让我们了解更多关于这一点:
-
在您的浏览器中启用 JavaScript 并切换开发者工具。
-
导航到
http://localhost:4000。乍一看,似乎没有什么不同。然而,由于预先渲染的内容,应用程序立即加载。 -
检查网络标签页,您将注意到以下内容:

图 7.6 – 网络标签页(Google Chrome)
我们的应用程序将所有 HTTP 请求到 GitHub API 初始化,就像它是由浏览器渲染的一样。即使 HTML 页面已经预先渲染了数据,它也会复制应用程序需要的所有 HTTP 请求。为什么是这样呢?
该应用程序为浏览器渲染版本和 SSR 应用程序分别发送一个 HTTP 请求,因为这两个版本有不同的状态。我们可以通过在服务器和浏览器之间共享状态来防止之前的行为。更具体地说,我们可以使用 Angular Universal 库中的一个特殊用途的 Angular 模块TransferHttpCacheModule将服务器的状态传输到浏览器。
如果我们使用TransferHttpCacheModule,服务器将缓存 GitHub API 的响应,浏览器将使用缓存而不是发起新的请求。TransferHttpCacheModule通过在 Angular 应用程序中安装一个HTTP 拦截器来解决该问题,该拦截器忽略最初由服务器处理的 HTTP 请求。
HTTP 拦截器是一个 Angular 服务,它拦截来自 Angular 框架内置 HTTP 客户端的 HTTP 请求和响应。
要在我们的 GitHub 个人项目应用程序中安装TransferHttpCacheModule,请按照以下步骤操作:
-
打开 Angular 应用程序的主模块文件
app.module.ts,并从@nguniversal/commonnpm 包中导入TransferHttpCacheModule:import { TransferHttpCacheModule } from '@nguniversal/common'; -
将
TransferHttpCacheModule类添加到@NgModule装饰器的imports数组中:@NgModule({ declarations: [ AppComponent, PersonalInfoComponent, PanelComponent, RepositoriesComponent, OrganizationsComponent ], imports: [ BrowserModule, HttpClientModule, **TransferHttpCacheModule** ], providers: [], bootstrap: [AppComponent] }) -
执行以下命令以预先渲染您的应用程序:
npm run prerender -
执行以下命令以启动您的预先渲染应用程序:
npm run serve:ssr
如果您预览个人项目应用程序并检查浏览器中的网络标签页,您将注意到它不会发起额外的 HTTP 请求。TransferHttpCacheModule拦截了所有 HTTP 请求并将它们存储在我们的应用程序的TransferState存储中。TransferState 是一个键值存储,可以从服务器传输到浏览器。应用程序的浏览器版本可以稍后直接从存储中读取 HTTP 响应,而不需要额外的调用。
我们现在有了 GitHub 个人资料的完整预渲染版本。但如何进一步优化以便在社交媒体平台上分享?我们将在下一节中了解更多关于 SEO 优化技术的内容。
提升 SEO 能力
SEO 通过优化网站,使其能够被网络爬虫正确索引。网络爬虫是大多数搜索引擎上的专用软件,可以识别和索引网站,以便通过其平台轻松发现和链接。
Angular Universal 通过在构建时预渲染内容,在 SEO 方面做得很好。一些网络爬虫无法执行 JavaScript 并构建 Angular 应用程序的动态内容。使用 Angular Universal 进行预渲染消除了对 JavaScript 的需求,从而允许网络爬虫尽其所能地识别网络应用程序。
我们还可以通过在 Angular 应用程序的主要index.html文件的<head>元素中定义几个标签来帮助 SEO,例如title、viewport和charset:
<head>
**<****meta****charset****=****"utf-8"****>**
**<****title****>****GhPortfolio****</****title****>**
<base href="/">
**<****meta****name****=****"viewport"****content****=****"width=device-width, initial-scale=1"****>**
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
您可以在developer.mozilla.org/docs/Web/HTML/Element/meta/name找到可用的标签列表。
然而,在index.html文件中设置标签是不够的,尤其是当 Angular 应用程序启用了路由并包含多个路由时。Angular 框架提供了一些方便的服务,我们可以使用这些服务来程序化地设置标签。首先,让我们看看如何在我们的应用程序中设置标题标签:
-
打开
app.component.ts文件并添加以下import语句:import { Title } from '@angular/platform-browser'; -
将
Title服务注入到AppComponent类的constructor中:constructor(private githubService: GithubService, **private** **titleService: Title**) {} -
在
ngOnInit方法中调用titleService变量的setTitle方法:ngOnInit(): void { this.username = this.githubService.username; **this****.****titleService****.****setTitle****(****'GitHub portfolio app'****);** } -
运行
npm run dev:ssr以预览应用程序,您应该在浏览器标签中看到标题:

图 7.7 – 浏览器标签页标题
与Title服务类似,我们可以使用Meta服务为我们的应用程序设置元标签:
-
打开
app.component.ts文件并从@angular/platform-browsernpm 包中导入Meta:import { **Meta**, Title } from '@angular/platform-browser'; -
将
Meta服务注入到AppComponent类的constructor中:constructor(private githubService: GithubService, private titleService: Title, **private** **meta: Meta**) {} -
使用
meta变量的addTags方法在ngOnInit方法中添加一些元标签:ngOnInit(): void { this.username = this.githubService.username; this.titleService.setTitle('GitHub portfolio app'); **this****.****meta****.****addTags****([** **{** **name****:** **'description'****,** **content****:** **`****${****this****.username}****'s GitHub portfolio`** **},** **{** **name****:** **'author'****,** **content****:** **this****.****username** **}** **]);** }在前面的代码中,我们添加了两个元标签。第一个设置了包含当前 GitHub 个人资料用户名的
description。第二个将author标签设置为与 GitHub 个人资料的用户名相同。 -
运行
npm run dev:ssr以启动应用程序并导航到http://localhost:4200。 -
使用您的浏览器检查页面,您应该在页面的元素中看到以下元标签:

图 7.8 – 应用程序头部元素
每个流行的社交平台,如 Twitter、Facebook 和 LinkedIn,都需要其自己的元标签,以便 SSR 应用的 URL 可以在其平台上正确显示。
概述
在这个项目中,我们为我们的 GitHub 个人资料构建了一个投资组合应用程序。最初,我们学习了如何在新的 Angular 应用程序中与 GitHub API 交互。我们还使用了 Bootstrap CSS 和 Bootstrap 图标为我们的投资组合应用程序提供美观的用户界面。
我们看到了如何使用 Angular Universal 将我们的 Angular 应用程序转换为 SSR 应用程序。我们学习了当用户拥有低端和性能较慢的设备时,如何从预渲染内容中受益,以及这种技术的潜在陷阱。
我们使用了 Angular 框架提供的某些可用的 SEO 技术来提高我们应用程序的可发现性。
在下一章中,我们将学习关于 monorepo 架构以及我们如何管理 Angular 应用的状态。
练习题
让我们看看一些练习题:
-
我们如何在组件的模板中订阅一个可观察对象?
-
我们使用什么命令来安装 Angular Universal?
-
我们如何通过编程区分浏览器和服务器平台?
-
什么命令可以生成一个 SSR 应用的预渲染版本?
-
我们使用哪个 Angular 模块将状态从服务器传输到浏览器?
-
我们在 Angular 应用中设置标题时使用哪个 Angular 服务?
-
我们在 Angular 应用中设置元标签时使用哪个 Angular 服务?
进一步阅读
这里有一些链接,可以帮助我们巩固本章所学的内容:
-
Angular Universal 指南:
angular.io/guide/universal -
GitHub REST API:
docs.github.com/rest -
Bootstrap CSS:
getbootstrap.com -
Bootstrap 图标:
icons.getbootstrap.com -
Angular HTTP 指南:
angular.io/guide/http -
TransferHttpCacheModule:github.com/angular/universal/blob/master/docs/transfer-http.md
第八章:使用 Nx 单仓库工具和 NgRx 构建 Enterprise Portal
典型的企业应用程序通常由后端和前端系统组成。后端负责与数据库交互以实现数据持久化,并公开 REST API。前端通过 REST 接口与后端系统通信以交换数据。前端系统有时可以由多个应用程序组成,包括网页界面或移动应用程序。将这些应用程序和系统保留在单独的源控制仓库中会扩展不佳,且难以维护和构建。作为替代,我们可以为大型企业应用程序遵循 单仓库 架构,其中每个应用程序位于同一仓库内的不同位置。
在 Angular 生态系统中,Nx 是一个流行的工具,它采用了单仓库架构。将 Nx 单仓库应用程序与状态管理库相结合可以显著提高您的应用程序。对于 Angular 应用程序,NgRx 是一个流行的状态管理库,可以帮助我们维护一致且可管理的全局状态。
在本章中,我们将通过构建一个用于访问 兴趣点(POI) 的企业门户应用程序来研究这两种技术。我们将涵盖以下主题:
-
使用 Nx 创建单仓库应用程序
-
创建用户特定的门户
-
使用 NgRx 管理应用程序状态
-
使用图表可视化数据
重要的背景理论和上下文
Nx 是一套基于单仓库架构构建 Web 应用程序的开发工具和库。一个典型的 Nx 应用程序可以在单个工作区内部包含许多应用程序和共享库。单仓库架构的灵活性允许任何应用程序,无论是后端还是前端,都可以在工作区内部使用相同的库。
在这个项目中,我们只考虑使用 Angular 框架构建的前端应用程序。
Nx 为开发者提供了以下功能:
-
应用程序依赖的集中管理:每个应用程序都使用相同版本的 Angular 框架,这使得一次性更新所有应用程序变得容易。
-
快速构建:Nx 应用程序的构建过程仅涉及已更改的工件,而不对整个单仓库进行完全重建。
-
分布式缓存:每个应用程序构建都可以使用 Nx Cloud 本地或云端缓存,以改善其他构建类似工件的开发者的构建过程。
在大型 Angular 企业应用程序中维护一致的全球状态是繁琐的。使用 @Input 和 @Output 装饰器在 Angular 组件之间通信并不总是可行的,尤其是在许多组件需要共享相同状态时。
NgRx 是一个由 RxJS 库驱动的库,它高效地管理应用程序的全局状态。NgRx 的主要构建块如下:
-
存储:保持应用程序全局状态的中央存储。
-
Reducer:一个监听特定事件并直接与存储交互的函数。Reducer 根据存储中的现有状态推导出新的应用程序状态。
-
动作:由组件和服务派发的一个独特事件,触发 reducer。动作可以是用户或外部源(如 HTTP 调用)发起的任何交互。
-
效果:处理与外部源(如进行 HTTP 调用或与本地存储交换数据)的交互。效果通过隐藏业务逻辑从组件中处理应用程序的副作用。
-
选择器:一个从存储中选择应用程序状态或其特定部分(切片)的函数。选择器支持记忆化技术,即如果用相同的参数调用,它们可以返回相同的状态,这极大地提高了应用程序的性能。
项目概述
在这个项目中,我们将构建一个 Angular 企业应用来管理地图上的 POI 访问。该应用将包括两个门户,其中一个将允许访客从列表中选择一个 POI 并在地图上查看其位置。另一个门户将允许管理员查看每个 POI 的访问流量。
首先,我们将使用 Nx 从头开始构建一个 Angular 应用。然后,我们将通过添加我们应用的基本组件来创建每个门户的框架。在我们搭建好应用之后,我们将开始使用 NgRx 添加访客门户的功能。最后,我们将实现管理员门户,并学习如何使用 Angular 库在图表中可视化数据。以下图表描述了项目的架构概述:

图 8.1 – 项目架构
构建时间:3 小时
入门
完成此项目所需的以下软件工具:
-
Nx 控制台:一个 VSCode 扩展,提供了一个图形界面来与 Nx 一起工作。你可以在第一章,在 Angular 中创建你的第一个 Web 应用中了解更多关于如何安装它的信息。
-
GitHub 材料:本章相关的代码可以在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter08文件夹中找到。
使用 Nx 创建单仓库应用
Nx 为开发者提供了与单仓库一起工作的工具,包括以下内容:
-
create-nx-workspace:一个 npm 包,用于搭建新的 Nx 单仓库应用。
-
Nx CLI:一个命令行界面,可以对单仓库应用运行命令。Nx CLI 扩展了 Angular CLI,提供了更多命令,由于分布式缓存机制,这使它更快。
建议在使用 Nx 单仓库时使用 VSCode 的快速打开功能。生成的文件夹和文件数量将显著增加,这将使导航变得具有挑战性。更多信息请参阅code.visualstudio.com/docs/editor/editingevolved#_quick-file-navigation。
要安装 Nx CLI,请在终端中运行以下命令:
npm install -g nx
上述命令将在我们的系统上全局安装nxnpm 包。现在,我们可以使用以下命令来创建新的 Nx 单仓库工作区:
npx create-nx-workspace packt --appName=tour --preset=angular-monorepo --style=css --linter=eslint --nx-cloud=false --routing
上述命令将执行以下操作:
-
查找
create-nx-workspacenpm 包的最新版本,并请求我们安装它。 -
询问我们是否想在应用程序中使用独立组件。确保选择
No并按Enter键继续。
独立的 Angular 组件是一种更简单、更现代的方法,用于构建不使用 Angular 模块的更组件化的 Angular 应用程序。在这个项目中,我们将默认使用 Angular 模块。
执行create-nx-workspace包涉及以下选项:
-
packt: Nx 单仓库工作区的名称。在大型企业环境中,我们通常使用组织名称。 -
--appName=tour: 应用程序的名称。 -
--preset=angular-monorepo: Nx 支持使用各种 JavaScript 框架构建的应用程序。preset选项定义了我们想构建的应用程序类型。 -
--style=css: 表示我们的应用程序将使用 CSS 样式表格式。 -
--linter=eslint: 将我们的应用程序配置为使用 ESLint 作为默认的代码检查工具。 -
--nx-cloud=false: 禁用 Nx Cloud 为我们应用程序。 -
--routing: 启用应用程序中的 Angular 路由。
创建新的 Nx 工作区可能需要一些时间,因为它会安装企业环境所需的所有必要的包。
工作区创建完成后,我们可以运行它来验证一切是否已正确设置:
-
在 VSCode 编辑器中打开项目,然后在 VSCode 侧边栏中点击Nx 控制台菜单。
-
从项目面板中选择服务命令,然后点击播放按钮执行它:

图 8.2 – 服务选项
- 在浏览器中打开
http://localhost:4200,你应该会看到以下输出:

图 8.3 – 最小 Nx 应用程序
恭喜!您的新应用程序已正确配置!Nx 创建了一个最小骨架应用程序,就像 Angular CLI 一样,以便我们可以在其上构建我们的功能。
在下一节中,我们将通过在我们的工作区中创建管理员和访客门户来深入了解 Nx。
创建用户特定门户
我们的应用程序将包含两个门户,不同的用户将使用它们。访客将能够查看 POI 列表并在地图上选择它们。管理员将能够查看每个 POI 的统计数据。我们将在接下来的章节中了解更多关于如何使用 Nx 的信息:
-
构建访客门户
-
构建管理员门户
每个门户都将是一个独立的 Nx 库,根据在浏览器地址栏中输入的 URL 进行加载。将我们的代码组织到库中允许我们在不同的应用程序之间重用它,并单独构建和测试它。我们将在下一节开始构建访客门户。
构建访客门户
访客门户将是 Nx 工作区内的一个库,默认情况下将被加载。让我们看看如何使用 Nx 控制台构建这个库:
- 从 VSCode 侧边栏运行 Nx 控制台,并从GENERATE & RUN TARGET面板中选择generate选项:

图 8.4 – 生成选项
-
从出现的对话框中选择@nrwl/angular – library选项。@nx/angular命名空间包含我们可以在 Nx 单仓库中执行的 Angular 应用的 schematics。
-
将库的名称输入为
visitor并点击运行按钮:

图 8.5 – 库名称
当你输入库的名称时,你可能已经注意到 Nx 在终端中运行了generate命令。但是,它并没有运行。相反,它模拟了在系统中运行命令的效果,这种技术称为dry run。
Nx 将在我们的工作区libs文件夹内创建visitor库。该库目前还没有任何组件。根据项目规格,访客门户将有一个 POI 列表,用户可以在地图上选择并查看他们的位置。因此,我们需要创建一个具有以下布局的 Angular 组件:

图 8.6 – 访客门户布局
在之前的图中,门户由显示 POI 列表的侧边栏和显示地图的主要内容区域组成。我们不会从头开始创建布局,而是会使用 Angular Material,它包含一些现成的布局,包括带有侧边栏的一个。
在使用 Angular Material 之前,我们需要使用以下命令将其安装到我们的应用程序中:
npm install @angular/material
安装成功完成后,我们可以使用以下命令在 Nx 工作区中配置 Angular Material:
nx generate @angular/material:ng-add --project=tour --theme=deeppurple-amber --animations=enabled --typography
上述命令将为我们的工作区配置@angular/material npm 包,并传递额外的选项。你可以在第四章,使用 Angular Service Worker 构建 PWA 天气应用程序中了解更多关于这些选项的信息。
在我们的项目中配置 Angular Material 也会安装 @angular/cdk npm 包,该包包含用于构建 Angular Material 的特定行为和交互。
地址表单:这使用 Angular Material 表单控件输入地址信息。
Angular Material 库包含以下组件模板,我们可以使用:
-
我们将在第九章 使用 Angular CLI 和 Angular CDK 构建组件 UI 库 中学习如何构建这样的库。
-
导航:这包含一个侧边导航组件以及一个内容占位符和标题栏。
-
仪表板:这由多个 Angular Material 卡片和菜单组件组成,以网格布局组织。
-
表格:这显示了一个具有排序和过滤功能的 Angular Material 表格。
-
树形结构:这表示树视图中的可视文件夹结构。
在我们的案例中,我们将使用 导航 组件,因为我们需要一个侧边栏。让我们看看我们如何生成该组件:
-
从 VSCode 侧边栏打开 Nx 控制台并选择 生成 选项。
-
从出现的对话框中选择 @angular/material – 导航 选项。@angular/material 命名空间包含我们可以运行的脚本来创建 Angular Material 组件。
-
输入组件的名称:

图 8.7 – 组件名称
-
从我们之前创建的 项目 下拉菜单中选择 访客 库:
![图 8.8 – 项目选择]()
图 8.8 – 项目选择
如果选项未显示,请点击 显示更多 按钮。
-
选择 平铺 选项,以便组件不会在单独的文件夹中生成:
![图 8.9 – 平铺选项]()
图 8.9 – 平铺选项
组件将是库的主要组件,因此我们希望将其与其相关的模块文件放在同一个文件夹中。
-
输入组件将被创建的文件夹:
![图 8.10 – 组件文件夹]()
图 8.10 – 组件文件夹
没有必要定义组件将被创建的模块,因为 Angular CLI 可以直接从 路径 选项中推断出来。
-
点击 运行 按钮以生成组件。
Nx 控制台将在 Nx 工作区的 访客 库中创建 访客 组件。我们现在需要将其与工作区的主应用程序连接:
-
打开
app.component.html文件并删除<packt-nx-welcome>选择器。 -
打开
app.routes.ts文件并添加一个路由配置,当 URL 包含tour路径时将加载访客门户:export const appRoutes: Route[] = [ **{** **path****:** **'tour'****,** **loadChildren****:** **() =>****import****(****'@packt/visitor'****).****then****(****m** **=>** **m.****VisitorModule****)** **},** **{** **path****:** **''****,** **pathMatch****:** **'full'****,** **redirectTo****:** **'****tour'** **}** ];路由配置包含两个路径。默认路径,由空字符串表示,重定向到
tour路径。tour路径会懒加载 访客 库的模块。 -
打开
visitor.module.ts文件,并添加一个路由配置来加载我们创建的visitor组件:import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { VisitorComponent } from './visitor.component'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatButtonModule } from '@angular/material/button'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; **import** **{** **RouterModule** **}** **from****'@angular/router'****;** @NgModule({ imports: [CommonModule, MatToolbarModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule, **RouterModule****.****forChild****([** **{** **path****:** **''****,** **component****:** **VisitorComponent** **}** **])** ], declarations: [ VisitorComponent ], }) export class VisitorModule {}
路由配置将在VisitorModule加载后默认激活VisitorComponent,使用步骤 3 中描述的tour路径。
如果我们现在从 Nx 控制台运行serve命令并导航到http://localhost:4200,我们应该看到以下输出:

图 8.11 – 访客门户
Angular 路由将重定向我们到http://localhost:4200/tour并显示访客门户。它目前包含一些 Angular Material 在生成导航组件时输入的演示数据。我们将在使用 NgRx 管理应用程序状态部分重新访问它,以使用 NgRx 实现完整的功能。现在,我们将继续在下一节构建管理员门户。
构建管理员门户
管理员门户将是一个 Nx 库,包含一个组件,就像访客门户一样,但它不会基于 Angular Material 模板。让我们使用 Nx 控制台开始构建库的结构:
-
从 VSCode 侧边栏运行 Nx 控制台并选择generate选项。
-
在出现的对话框中,选择@nx/angular – library选项。
-
输入
admin作为库名称并点击Run按钮:

图 8.12 – 库名称
-
再次点击generate选项并选择@schematics/angular – component选项。@schematics/angular命名空间包含我们可以在 Angular 应用程序中使用 Angular CLI 运行的 schematics。
-
将组件名称与步骤 3 中相同:

图 8.13 – 组件名称
- 从项目下拉列表中选择我们创建的库:

图 8.14 – 项目选择
-
选择flat选项,以便组件将在库的模块文件相同的文件夹中创建:
![图 8.15 – 平铺选项]()
图 8.15 – 平铺选项
如果选项未显示,请点击Show more按钮。
-
输入组件将被创建的文件夹,并点击Run按钮:

图 8.16 – 组件文件夹
Angular CLI 将在admin库的文件夹内创建admin组件。我们现在需要将其连接到主应用程序:
-
打开
app.routes.ts文件,并为admin路径添加一个新的路由配置对象:export const appRoutes: Route[] = [ **{** **path****:** **'****admin'****,** **loadChildren****:** **() =>****import****(****'@packt/admin'****).****then****(****m** **=>** **m.****AdminModule****)** **}**, { path: 'tour', loadChildren: () => import('@packt/visitor').then(m => m.VisitorModule) }, { path: '', pathMatch: 'full', redirectTo: 'tour' } ]; -
打开
admin.module.ts文件,并添加一个路由配置来默认激活AdminComponent:import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AdminComponent } from './admin.component'; **import** **{** **RouterModule** **}** **from****'@angular/router'****;** @NgModule({ imports: [ CommonModule, **RouterModule****.****forChild****([** **{** **path****:** **''****,** **component****:** **AdminComponent** **}** **])** ], declarations: [ AdminComponent ], }) export class AdminModule {} -
使用 Nx 控制台的serve选项运行应用程序,并导航到
http://localhost:4200/admin:

图 8.17 – 管理员门户
页面将显示 admin 库主组件的默认模板。
我们现在已经完成了企业应用程序的脚手架搭建。首先,我们创建了将托管应用程序门户的 Nx 单一代码仓库。然后,我们使用 Nx Console 生成我们的门户及其主组件。我们还安装了 Angular Material 以在组件中使用其 UI 元素。
在下一节中,我们将使用 NgRx 实现访客门户的功能。
使用 NgRx 管理应用程序状态
访客门户将允许用户查看可用的 POI 列表并选择一个查看其在地图上的位置。可用的 POI 列表和 POI 的选择是我们应用程序的全局状态。我们将通过完成以下任务来集成 NgRx 以管理访客门户的应用程序状态:
-
配置状态
-
与存储交互
让我们从配置应用程序状态开始,在下一节中。
配置状态
我们的应用程序将包括一个用于整个应用程序的 root 状态和一个用于访客门户的功能状态。我们将首先执行以下命令来创建 root 状态:
nx generate @nx/angular:ngrx app --root --no-interactive --parent=apps/tour/src/app/app.module.ts
上一条命令使用 Nx CLI 的 generate 命令,传递以下选项:
-
@nx/angular:ngrx:表示我们想要设置 NgRx 状态 -
app:状态名称 -
--root:表示我们想要配置一个根状态 -
--no-interactive:禁用交互式输入提示 -
--parent=apps/tour/src/app/app.module.ts:将状态注册到我们应用程序的主要 Angular 模块中
上一条命令将在 package.json 文件中添加所有必要的 NgRx npm 包并安装它们。它还将修改 app.module.ts 文件以配置所有 NgRx 相关艺术品,如存储和效果。
访客库不会管理访客门户状态的数据。相反,我们将在 Nx 工作区中创建一个新的库来获取和存储功能状态中的数据。执行以下 Nx CLI 命令来创建一个新的库:
nx generate @nrwl/angular:library poi
上一条命令将在我们的 Nx 单一代码仓库中生成 poi 库。现在,我们可以使用以下命令设置功能状态:
nx generate @nx/angular:ngrx poi --no-interactive --parent=libs/poi/src/lib/poi.module.ts --barrels
上一条命令使用 Nx CLI 的 generate 命令注册功能状态,传递额外的选项:
-
@nx/angular:ngrx:表示我们想要设置 NgRx 状态。 -
poi:状态名称。 -
--no-interactive:禁用交互式输入提示。 -
--parent=libs/poi/src/lib/poi.module.ts:将状态注册到我们库的 Angular 模块中。 -
--barrels:表示使用桶文件重新导出 NgRx 艺术品,如选择器和状态。桶文件的名称通常按惯例为index.ts。
上一条命令将在我们的库中创建一个名为 +state 的文件夹(按惯例命名),其中包含以下文件:
-
poi.actions.ts:定义功能状态 NgRx 动作 -
poi.effects.ts: 定义 NgRx 效应用于功能状态 -
poi.models.ts: 定义 POI 数据的实体接口 -
poi.reducer.ts: 定义 NgRx 减法器用于功能状态 -
poi.selectors.ts: 定义 NgRx 选择器用于功能状态
Nx CLI 已经通过在之前的文件中添加必要的内容完成了大部分工作,消除了我们需要的样板代码。我们现在需要在库中创建一个 Angular 服务来获取 POI 数据:
-
打开
poi.models.ts文件并为PoiEntity接口添加以下属性:export interface PoiEntity { id: string | number; // Primary ID name: string; **lat****:** **number****;** **lng****:** **number****;** **description****:** **string****;** **imgUrl****:** **string****;** } -
执行以下命令以生成 Angular 服务:
nx generate service poi --project=poi上述命令将在
poi库中创建一个名为poi的 Angular 服务。 -
打开
poi.service.ts文件并添加以下import语句:import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { PoiEntity } from '..'; -
在
PoiService类的constructor中注入HttpClient并创建一个从assets/poi.json文件获取 POI 数据的方法:export class PoiService { constructor(**private** **http: HttpClient**) {} **getAll****():** **Observable****<****PoiEntity****[]> {** **return****this****.****http****.****get****<****PoiEntity****[]>(****'assets/poi.json'****);** **}** }我们使用 Angular 框架内置的 HTTP 客户端通过发起 GET HTTP 请求来获取 POI 数据。
您可以从 GitHub 仓库的 入门 部分获取
poi.json文件并将其复制到您的工作区中的apps\tour\src\assets文件夹。 -
打开
poi.effects.ts文件并导入map和PoiService实体:import { Injectable, inject } from '@angular/core'; import { createEffect, Actions, ofType } from '@ngrx/effects'; import { switchMap, catchError, of, `map` } from 'rxjs'; import * as PoiActions from './poi.actions'; import * as PoiFeature from './poi.reducer'; `import { PoiService } from '../poi.service';` -
在
PoiEffects类中注入PoiService:private poiService = inject(PoiService); -
将
init$属性修改为使用poiService变量:init$ = createEffect(() => this.actions$.pipe( ofType(PoiActions.initPoi), **switchMap****(****() =>****this****.****poiService****.****getAll****()),** switchMap(pois => of(PoiActions.loadPoiSuccess({ poi: **pois** }))), catchError((error) => { console.error('Error', error); return of(PoiActions.loadPoiFailure({ error })); }) ) );NgRx 效应负责监听存储中派发的所有动作。当派发
PoiActions.initPoi动作时,init$属性被触发并调用poiService变量的getAll方法。init$属性通过ofType操作符的参数知道要监听哪个动作。ofType操作符可以接受多个动作。如果数据获取成功,效应将在存储中触发一个新动作,即
PoiActions.loadPoiSuccess,并将 POI 数据作为负载。如果获取数据时出现失败,它将在存储中触发一个PoiActions.loadPoiFailure动作。 -
打开
app.module.ts文件并从@angular/common/http命名空间导入HttpClientModule。同时将HttpClientModule类添加到@NgModule装饰器的imports数组中。
我们应用程序的全局状态现在已配置并准备好使用。在下一节中,我们将在访客库中创建额外的 Angular 组件,这些组件将与我们的应用程序的功能状态进行交互。
与存储进行交互
访客门户将通过两个 Angular 组件与我们的应用程序的功能状态进行交互。一个组件将显示 POI 列表并允许用户选择一个。另一个组件将在 Google Maps 中显示所选的 POI。
初始时,我们将构建显示 POI 列表的组件:
-
打开
visitor.module.ts文件并添加以下import语句:import { PoiModule } from '@packt/poi'; -
在
@NgModule装饰器的imports数组中添加PoiModule:@NgModule({ imports: [CommonModule, MatToolbarModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule, RouterModule.forChild([ { path: '', component: VisitorComponent } ]), **PoiModule** ], declarations: [ VisitorComponent ], })我们导入
PoiModule以确保在访问者门户加载时,poi 功能状态立即在存储中注册。 -
执行以下 Nx CLI 命令以创建 Angular 组件:
nx generate @schematics/angular:component poi-list --project=visitor -
打开
poi-list.component.ts文件并相应地修改import语句:import { Component, **OnInit** } from '@angular/core'; **import** **{** **Store** **}** **from****'@ngrx/store'****;** **import** **{** **PoiActions****,** **PoiSelectors** **}** **from****'@packt/poi'****;** -
修改
PoiListComponent类,使其在组件初始化时在存储中触发PoiActions.initPoi动作以获取 POI 数据:export class PoiListComponent implements OnInit { constructor(private store: Store) {} ngOnInit(): void { this.store.dispatch(PoiActions.initPoi()); } }我们将操作作为方法执行并将结果传递给
store变量的dispatch方法。 -
创建一个组件属性,它调用
PoiSelectors.selectAllPoi选择器以从存储中列出 POI 数据:pois$ = this.store.select(PoiSelectors.selectAllPoi);我们使用
store变量的select方法来执行选择器。我们没有创建
PoiSelectors.selectAllPoi选择器。在生成 poi 库中的功能状态时,NgRx 为我们完成了这项工作。 -
打开
poi-list.component.html文件并用以下 HTML 模板替换其内容:<mat-action-list *ngFor="let poi of pois$ | async"> <button mat-list-item>{{poi.name}}</button> </mat-action-list>我们使用 Angular Material 库中的
<mat-action-list>组件来显示每个 POI 作为单个操作项。我们使用async管道订阅pois$属性并为每个 POI 创建一个带有mat-list-item指令的<button>元素。 -
打开
visitor.component.html文件并用我们创建的<packt-poi-list>组件替换<mat-nav-list>组件。
使用 Nx Console 启动应用程序,你应该在菜单侧边栏中看到以下输出:

图 8.18 – POI 列表
我们已经创建了 Angular 组件来显示可用的 POI。现在让我们看看如何创建一个组件来在地图上显示 POI 使用 Google Maps。
Angular Material 库包含一个用于 Google Maps 的组件,我们可以在我们的应用程序中使用它:
-
运行以下
npm客户端命令来安装 Google Maps 组件:npm install @angular/google-maps -
打开
visitor.module.ts文件并添加以下import语句:import { GoogleMapsModule } from '@angular/google-maps'; -
将
GoogleMapsModule添加到@NgModule装饰器的imports数组中:@NgModule({ imports: [CommonModule, MatToolbarModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule, RouterModule.forChild([ { path: '', component: VisitorComponent } ]), PoiModule, **GoogleMapsModule** ], declarations: [ VisitorComponent, PoiListComponent ], }) -
打开应用程序的
index.html文件并在<head>元素内添加 Google Maps JavaScript API:<script src="img/js"></script>
现在我们已经安装并注册了 Google Maps 到我们的应用程序中,让我们创建一个将托管它的 Angular 组件:
-
执行以下 Nx CLI 命令以创建一个新的 Angular 组件:
nx generate @schematics/angular:component map --project=visitor -
打开
map.component.ts文件并添加以下import语句:import { Store } from '@ngrx/store'; import { PoiSelectors } from '@packt/poi'; -
在
MapComponent类的constructor中注入Store服务并声明一个属性以从存储中获取选定的 POI:export class MapComponent { **poi$ =** **this****.****store****.****select****(****PoiSelectors****.****selectEntity****);** **constructor****(****private** **store: Store****) { }** } -
打开
map.component.html文件并用以下 HTML 模板替换其内容:<google-map height="100%" width="auto" *ngIf="poi$ | async as poi" [center]="poi"> <map-marker [position]="poi"></map-marker> </google-map>在前面的模板中,我们使用
async管道订阅poi$属性。一旦我们从存储中获取到选定的 POI,我们就显示一个<google-map>组件并将地图的中心设置为 POI 坐标。此外,我们在指定的 POI 坐标上添加了一个标记。 -
打开
visitor.component.html文件并将<!-- Add Content Here -->注释替换为<packt-map>选择器。
我们创建的 Angular 组件将在我们从列表中选择 POI 时立即在地图上显示其位置。如果您尝试从列表中选择一个 POI,您会注意到没有任何反应。这是为什么?
应用程序的全局状态目前不知道何时选择了 POI。我们需要添加必要的代码来设置选中的 POI 并与存储进行交互:
-
打开
poi.actions.ts文件并添加一个新操作以传递选中 POI 的 ID:export const selectPoi = createAction( '[Poi/API] Select Poi', props<{ poiId: string | number }>() ); -
打开
poi.reducer.ts文件,并在reducer属性中添加一个新语句,该语句将监听selectPoi操作并将选中的 POI 保存到存储中:const reducer = createReducer( initialPoiState, on(PoiActions.initPoi, (state) => ({ ...state, loaded: false, error: null })), on(PoiActions.loadPoiSuccess, (state, { poi }) => poiAdapter.setAll(poi, { ...state, loaded: true }) ), on(PoiActions.loadPoiFailure, (state, { error }) => ({ ...state, error })), **on****(****PoiActions****.****selectPoi****,** **(****state, { poiId }****) =>** **({ ...state,** **selectedId****: poiId }))** ); -
打开
poi-list.component.ts文件并导入PoiEntity接口:import { PoiActions, **PoiEntity**, PoiSelectors } from '@packt/poi'; -
创建一个新方法以将
selectPoi操作及其选中的PoiEntity一起分发给存储:selectPoi(poi: PoiEntity) { this.store.dispatch(PoiActions.selectPoi({poiId: poi.id})); } -
打开
poi-list.component.html文件,并将selectPoi方法绑定到<button>元素的click事件:<mat-action-list *ngFor="let poi of pois$ | async"> <button mat-list-item **(****click****)=****"selectPoi(poi)"**>{{poi.name}}</button> </mat-action-list>
要查看新功能的效果,请使用 Nx Console 中的serve选项运行应用程序,并从列表中选择一个 POI。应用程序的输出应如下所示:

图 8.19 – POI 选择
在此项目中,我们以开发模式使用 Google Maps。对于生产环境,您应从developers.google.com/maps/get-started获取 API 密钥,并将其包含在您在index.html文件中加载的 Google Maps JavaScript API 脚本中,作为<script src="img/js?key=YOUR_API_KEY"></script>。
我们现在已经完成了访客门户所需的所有功能。做得好!实现访客门户的基本功能需要与 NgRx 交互以管理我们应用程序的全局状态。
全局状态被分离为应用程序的根状态和访客门户的特征状态。访客库使用后者创建 Angular 组件以显示 POI 列表并选择一个在 Google Maps 中查看:
在下一节中,我们将构建管理员门户以获取每个 POI 的访问统计。
使用图表可视化数据
管理员门户将使用图表显示每个 POI 的流量访问。当访客通过点击地图上的标记访问 POI 时,将生成流量。应用程序将在浏览器的本地存储中持久化访问数据。它将为每次访问记录 POI 的 ID 和总访问次数。管理员门户将包括以下功能:
-
在存储中持久化访问数据
-
显示访问统计
在下一节中,我们将通过实现跟踪访问的机制来开始构建管理员门户。
在存储中持久化访问数据
我们的应用程序目前还没有记录 POI 的交通统计数据。让我们看看我们如何完成这个任务:
-
打开
map.component.html文件,并添加一个<map-info-window>组件:<google-map height="100%" width="auto" *ngIf="poi$ | async as poi" [center]="poi"> <map-marker [position]="poi"></map-marker> **<****map-info-window****>** **<****mat-card****>** **<****mat-card-header****>** **<****mat-card-title****>****{{poi.name}}****</****mat-card-title****>** **</****mat-card-header****>** **<****img****mat-card-image** **[****src****]=****"poi.imgUrl"****>** **<****mat-card-content****>** **<****p****>****{{poi.description}}****</****p****>** **</****mat-card-content****>** **</****mat-card****>** **</****map-info-window****>** </google-map><map-info-window>组件是一个弹出窗口,显示有关当前地图标记的附加信息。它以 Angular Material 卡片组件的形式显示 POI 的标题、图像和描述。<mat-card>组件包含一个由<mat-card-header>组件表示的标题和一个由带有mat-card-image指令的<img>元素表示的图像。<mat-card-content>组件表示卡的正文内容。 -
打开
visitor.module.ts文件,并添加以下import语句:import { MatCardModule } from '@angular/material/card'; -
在
@NgModule装饰器的imports数组中添加MatCardModule类:@NgModule({ imports: [CommonModule, MatToolbarModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule, RouterModule.forChild([ { path: '', component: VisitorComponent } ]), PoiModule, GoogleMapsModule, **MatCardModule** ], declarations: [ VisitorComponent, PoiListComponent, MapComponent ], })MatCardModule类是一个 Angular Material 模块,它公开了我们创建卡片组件所需的所有组件。 -
打开
map.component.ts文件,并相应地修改import语句:import { Component, **ViewChild** } from '@angular/core'; import { Store } from '@ngrx/store'; import { PoiSelectors } from '@packt/poi'; **import** **{** **MapInfoWindow****,** **MapMarker** **}** **from****'@angular/google-maps'****;** -
使用
@ViewChild装饰器声明一个组件属性,以获取信息窗口的引用:@ViewChild(MapInfoWindow) info: MapInfoWindow | undefined; -
创建一个打开信息窗口的方法:
showInfo(marker: MapMarker) { this.info?.open(marker); }在前面的代码中,我们调用信息窗口引用的
open方法,并将相关的地图marker作为参数传递。 -
打开
map.component.html文件,并将showInfo组件方法绑定到<map-marker>组件的mapClick事件:<map-marker **#****marker****=****"mapMarker"** **(****mapClick****)=****"showInfo(marker)"** [position]="poi"></map-marker>我们创建
marker模板引用变量以获取对mapMarker对象的引用,并在showInfo方法中将它作为参数传递。 -
使用 Nx Console 的serve选项运行应用程序,并从列表中选择一个 POI。
-
点击地图上的 POI 标记,你应该得到以下类似的输出:

图 8.20 – 地图信息窗口
我们认为当访客点击地图标记并出现信息窗口时,POI 已被访问。然后我们的应用程序将通知存储该动作以将其保存在本地存储中。让我们创建与存储交互的逻辑:
-
打开
poi.actions.ts文件,并为访问 POI 的功能创建以下操作:export const visitPoi = createAction( '[Poi/API] Visit Poi', props<{ poiId: string | number }>() ) export const visitPoiSuccess = createAction('[Poi/API] Visit Poi Success'); export const visitPoiFailure = createAction( '[Poi/API] Visit Poi Failure', props<{ error: any }>() ); -
打开
poi.effects.ts文件,并创建一个新的效果,该效果监听visitPoi动作,并将指定poiId的总访问次数增加一:visit$ = createEffect(() => this.actions$.pipe( ofType(PoiActions.visitPoi), switchMap(action => { const stat = localStorage.getItem('tour-' + action.poiId); const total = stat ? Number(stat) + 1 : 1; localStorage.setItem('tour-' + action.poiId, total.toString()); return of(PoiActions.visitPoiSuccess()) }), catchError((error) => { console.error('Error', error); return of(PoiActions.visitPoiFailure({ error })); }) ) );在前面的代码中,我们获取以单词
tour-开头的本地存储键,后面跟着 POI ID。如果找到这个键,我们就将其增加一,并更新本地存储。如果没有找到,我们将其初始化为 1。在实际情况下,最好将本地存储的逻辑抽象为一个 Angular 服务,该服务将作为全局
localStorage对象的包装器。我们鼓励你在构建此项目时创建此类服务。 -
打开
map.component.ts文件,并从@packt/poi命名空间导入PoiActions:import { **PoiActions**, PoiSelectors } from '@packt/poi'; -
修改
showInfo组件方法,使其向存储发送visitPoi动作:showInfo(marker: MapMarker, **poiId:** **string** **|** **number**) { **this****.****store****.****dispatch****(****PoiActions****.****visitPoi****({ poiId }));** this.info?.open(marker); } -
最后,打开
map.component.html文件并将选定的 POI ID 传递给showInfo方法:<map-marker #marker="mapMarker" (mapClick)="showInfo(marker**, poi.id**)" [position]="poi" ></map-marker>
我们的应用程序现在可以记录每个 POI 的访问次数并将它们保存在浏览器的本地存储中。在下一节中,我们将创建管理员门户的主组件,该组件利用访问数据。
显示访问统计
管理员门户将在其主组件上显示访问统计,并使用图表进行可视化。我们将使用ng2-charts库在饼图中可视化数据。让我们看看如何在组件中添加所需的功能:
-
使用以下命令安装
ng2-charts库:npm install ng2-charts chart.js前面的命令还将安装
chart.js库,它是ng2-charts库的核心。 -
打开
admin.module.ts文件,并从@packt/poi命名空间导入PoiModule,从ng2-chartsnpm 包导入NgChartsModule:import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AdminComponent } from './admin.component'; import { RouterModule } from '@angular/router'; **import** **{** **PoiModule** **}** **from****'@packt/poi'****;** **import** **{** **NgChartsModule** **}** **from****'ng2-charts'****;** @NgModule({ imports: [ CommonModule, RouterModule.forChild([ { path: '', component: AdminComponent } ]), **PoiModule****,** **NgChartsModule** ], declarations: [ AdminComponent ], }) export class AdminModule {} -
打开
admin.component.ts文件并根据需要修改import语句:import { Component, **OnDestroy****,** **OnInit** } from '@angular/core'; **import** **{** **Store** **}** **from****'@ngrx/store'****;** **import** **{** **PoiActions****,** **PoiEntity****,** **PoiSelectors** **}** **from****'@packt/poi'****;** **import** **{** **Subscription** **}** **from****'rxjs'****;** -
修改
AdminComponent类,使其与应用程序存储交互以获取 POI 数据:export class AdminComponent implements OnInit, OnDestroy { private subscription: Subscription | undefined; constructor(private store: Store) { } ngOnInit(): void { this.subscription = this.store.select(PoiSelectors.selectAllPoi).subscribe(); this.store.dispatch(PoiActions.initPoi()); } ngOnDestroy() { this.subscription?.unsubscribe(); } }在前面的代码中,我们手动使用
subscription属性订阅selectAllPoi选择器,而不是使用async管道。在这种情况下,我们必须在组件的ngOnDestroy生命周期钩子中使用unsubscribe方法手动取消订阅。如果我们不这样做,我们可能会在我们的应用程序中引入内存泄漏。
现在我们已经设置了与存储的交互,我们可以从本地存储中获取统计数据并创建我们的饼图:
-
执行以下 Nx CLI 命令以在
admin库中创建一个服务:nx generate service admin --project=admin -
打开
admin.service.ts文件并添加以下import语句:import { PoiEntity } from '@packt/poi'; -
创建一个方法来从浏览器的本地存储中获取所有保存的流量统计数据:
getStatistics(pois: PoiEntity[]): number[] { return pois.map(poi => { const stat = localStorage.getItem('tour-' + poi.id) ?? 0; return +stat; }); }在前面的方法中,我们根据每个 POI 的
id属性获取其流量。然后通过添加+前缀将stat属性转换为数字。 -
打开
admin.component.ts文件并添加以下import语句:import { AdminService } from './admin.service'; import { ChartDataset } from 'chart.js'; -
声明组件属性,用于显示在饼图上的标签和实际数据,并将
AdminService注入到AdminComponent类的constructor中:export class AdminComponent implements OnInit, OnDestroy { private subscription: Subscription | undefined; **dataSets****:** **ChartDataset****[] = [];** **labels****:** **string****[] = [];** constructor(private store: Store, **private** **adminService: AdminService**) { } ngOnInit(): void { this.subscription = this.store.select(PoiSelectors.selectAllPoi).subscribe(); this.store.dispatch(PoiActions.initPoi()); } ngOnDestroy() { this.subscription?.unsubscribe(); } } -
创建一个组件方法来设置图表的标签和数据:
private buildChart(pois: PoiEntity[]) { this.labels = pois.map(poi => poi.name); this.dataSets = [{ data: this.adminService.getStatistics(pois) }] }图表标签是 POI 的标题,数据来自
adminService变量的getStatistics方法。 -
在
selectAllPoi选择器的subscribe方法内部调用buildChart方法:ngOnInit(): void { this.subscription = this.store.select(PoiSelectors.selectAllPoi).subscribe(**pois** **=>****this****.****buildChart****(pois)**); this.store.dispatch(PoiActions.initPoi()); } -
最后,打开
admin.component.html文件并用以下 HTML 模板替换其内容:<div class="chart" *ngIf="dataSets.length"> <canvas height="100" baseChart [datasets]="dataSets" [labels]="labels" type="pie"> </canvas> </div>在前面的模板中,我们使用
baseChart指令将<canvas>元素转换为图表。图表通过type属性设置为pie类型。
如果我们现在使用 Nx 控制台运行我们的应用程序,从地图中访问一个 POI,并切换到 http://localhost:4200/admin URL,我们应该看到以下输出:

图 8.21 – POI 统计信息
现在管理员可以全面了解每个 POI 的访问情况。我们的管理员门户现在已经完成。访客门户可以与存储交互并在浏览器的本地存储中保存每个 POI 的访问统计信息。然后管理员门户可以获取并显示这些数据在饼图上。
摘要
在这个项目中,我们构建了一个企业门户应用程序,用于在地图上访问 POI 并显示每个 POI 的访问统计信息。首先,我们看到了如何使用 Nx 来搭建一个新的 Nx 单一代码库应用程序。然后,我们为我们的应用程序创建了两个不同的门户,一个是访客门户,另一个是管理员门户。我们学习了如何在访客门户中使用 NgRx 库来维护和管理应用程序的状态。最后,我们看到了如何在管理员门户中使用图表库来显示每个 POI 的统计信息。
在下一章中,我们将使用 Angular CLI 使用 Angular CDK 构建一个 UI 组件库。
练习问题
让我们看看几个练习问题:
-
哪个 npm 包创建一个 Nx 单一代码库应用程序?
-
Angular CLI 和 Nx CLI 之间的区别是什么?
-
我们如何在单一代码库的库中启用 NgRx?
-
我们如何从存储中选择数据?
-
我们如何在 NgRx 中与 HTTP 交互?
-
我们在哪里修改 NgRx 存储的状态?
-
根状态和功能状态之间的区别是什么?
-
我们可以使用哪个 npm 包在 Angular 应用程序中使用 Google Maps?
-
我们如何手动订阅 NgRx 选择器?
-
我们使用哪个组件在 Google 地图上显示附加信息?
进一步阅读
这里有一些链接,可以帮助我们巩固本章所学的内容:
-
Nx:
nx.dev -
NgRx:
ngrx.io -
NgRx 存储指南:
ngrx.io/guide/store -
Angular Material 卡组件:
material.angular.io/components/card/overview -
Angular Google Maps:
github.com/angular/components/tree/main/src/google-maps -
ng2-charts:valor-software.com/ng2-charts -
create-nx-workspace:www.npmjs.com/package/create-nx-workspace
第九章:使用 Angular CLI 和 Angular CDK 构建组件 UI 库
一个 Angular 应用程序由 Angular 组件组成,这些组件组织成模块。当组件需要在模块之间共享相似的外观或行为时,我们将提取其功能到可重用组件中,并将它们分组到一个共享模块中。可重用组件可能从具有许多控件(如表单)的复杂 UI 结构到单个原生 HTML 元素(如按钮)不等。
组件 UI 库是一组可重用组件的集合,可以在特定应用领域之外使用。一个使用 monorepo 架构构建的大型企业应用程序可以在其所有应用程序中使用这些组件。组织外部的项目也可以使用相同的组件库作为外部依赖项。
Angular CLI 包含创建 Angular 库所需的所有工具。Angular 组件开发工具包(CDK)提供了一系列用于创建可访问性和高性能 UI 组件的功能。在本章中,我们将结合Bulma,一个现代 CSS 框架,从头开始创建一个简单的组件 UI 库。
在本章中,我们将更详细地介绍以下主题:
-
使用 Angular CLI 创建库
-
构建可拖拽的卡片列表
-
与剪贴板交互
-
将 Angular 库发布到
npm -
使用组件作为Angular 元素
必要的背景理论和上下文
Angular CDK 包含了一系列常见的交互和行为,我们可以将其应用于 Angular 组件。它是 Angular Material 库的核心,但也可以与 Angular 应用程序中的任何 CSS 框架一起使用。Angular CDK 可以从@angular/cdk npm 包中获取。
Angular CLI 支持开箱即用创建 Angular 库。Angular 库的功能只能在 Angular 应用程序中使用,并且与特定业务逻辑解耦。如果我们想在非 Angular 应用程序中使用 Angular 库,我们必须将其转换为 Angular 元素。
自定义元素是一个网络标准,允许创建独立于任何 JavaScript 框架的 HTML 元素。它通过声明一个自定义 HTML 标签并将其与一个 JavaScript 类关联来实现。浏览器可以识别 HTML 标签并执行类内部定义的 JavaScript 代码。
Angular 元素是使用@angular/elements库将 Angular 组件转换为自定义元素。将 Angular 组件打包为自定义元素将 Angular 框架与元素的 DOM 连接起来,通过数据绑定、组件生命周期和变更检测功能来丰富它。
项目概述
在这个项目中,我们将为我们的 Angular 项目构建一个组件 UI 库。最初,我们将使用 Angular CLI 为我们的库搭建一个新的 Angular 工作空间。然后,我们将使用 Angular CDK 和 Bulma CSS 框架来创建以下组件:
-
一份我们可以使用拖放功能重新排列的卡片列表
-
一个按钮,允许我们将任意内容复制到剪贴板
我们将学习如何将库部署到包注册库,如npm。最后,我们将使用ngx-build-plus库将我们的一个组件转换为 Angular 元素,以便与非 Angular 应用程序共享。以下图表提供了项目的架构概述:

图 9.1 – 项目架构
构建时间:1½小时
入门
完成此项目所需的以下先决条件和软件工具:
-
Angular CLI:一个用于 Angular 的 CLI,您可以在
angular.io/cli找到。 -
GitHub 材料:本章的相关代码可以在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter09文件夹中找到。
使用 Angular CLI 创建库
在我们能够开始使用 Angular CLI 工作与 Angular 库之前,我们需要创建一个 Angular CLI 工作区。这个 Angular CLI 工作区将包含我们的 Angular 库以及一个用于测试库的 Angular 应用程序。
使用以下命令来生成一个新的 Angular CLI 工作区:
ng new my-components --defaults
上述命令将创建一个新的 Angular CLI 工作区,其中包含一个名为my-components的 Angular 应用程序。导航到my-components文件夹,并执行以下命令以生成一个新的 Angular 库:
ng generate library ui-controls
上述命令将在工作区的projects文件夹内创建一个ui-controls库。它将包含与创建 Angular 应用程序时类似的文件和文件夹,包括以下内容:
-
src\lib: 这包含库的源代码,例如模块、组件和服务。 -
src\public-api.ts: 这导出我们从库中想要在其他 Angular 应用程序中公开的工件。 -
ng-package.json: 这包含了一个配置文件,用于 Angular CLI 在底层构建库时使用的ng-packagr库。 -
tsconfig.lib.json: 我们库的 TypeScript 配置文件,它还包含几个 Angular 编译器选项。 -
tsconfig.lib.prod.json: 当我们在生产模式下构建库时使用的 TypeScript 配置文件。
默认情况下,Angular CLI 会为我们生成一个模块、一个组件和一个服务,存放在src\lib文件夹中。它还会将它们导出,以便任何将使用该库的 Angular 应用程序都可以使用。这里有一个例子:
`public-api.ts`
/*
* Public API Surface of ui-controls
*/
export * from './lib/ui-controls.service';
export * from './lib/ui-controls.component';
export * from './lib/ui-controls.module';
现在我们已经设置了 Angular CLI 工作区,我们可以继续安装 Bulma 和 Angular CDK 库,如下所示:
-
执行以下命令来安装 Angular CDK:
npm install @angular/cdk -
运行以下命令来安装 Bulma CSS 框架:
npm install bulma -
打开
angular.json配置文件,并将 Bulma 库的 CSS 样式表文件添加到build架构条目的styles部分中,如下所示:"options": { "outputPath": "dist/my-components", "index": "src/index.html", "main": "src/main.ts", "polyfills": [ "zone.js" ], "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.css", **"****./node_modules/bulma/css/bulma.css"** ], "scripts": [] } -
打开
projects\ui-controls文件夹中的package.json文件,并相应地进行修改:{ "name": "ui-controls", "version": "0.0.1", "peerDependencies": { "@angular/common": "¹⁶.0.0", "@angular/core": "¹⁶.0.0", **"@angular/cdk"****:****"¹⁶.0.3"****,** **"bulma"****:****"⁰.9.4"** }, "dependencies": { "tslib": "².3.0" }, "sideEffects": false }
我们将 Angular CDK 和 Bulma 库添加到 peerDependencies 部分以确保任何消费应用程序都有特定版本的包来运行我们的库。
如果您跟随此项目,每个包的版本号可能会有所不同。为确保您拥有正确的版本,请从工作区根目录的 package.json 文件中复制它们。
我们现在已经完成了 UI 组件库的基本设置。我们还配置了 Angular CLI 工作区中附带的应用程序,以预览和测试库。在下一节中,我们将构建库的第一个组件——一个可重新排序的卡片列表。
构建可拖动卡片列表
我们 UI 库的第一个组件将是一个 Bulma 卡片元素的列表。每个卡片将显示一个标题、一个描述和一个锚点链接元素。我们还将能够使用 Angular CDK 拖动卡片并改变卡片列表的顺序。构建我们的组件将包括以下任务:
-
显示卡片数据
-
添加拖放功能
在下一节中,我们将首先了解如何在卡片列表上显示数据。
显示卡片数据
我们的 Angular 应用程序应将卡片列表作为输入属性传递给组件以显示它们。让我们看看如何创建一个可拖动的卡片组件,如下所示:
-
执行以下 Angular CLI 命令来创建一个 Angular 组件:
ng generate component card-list --project=ui-controls --export上述命令将在 Angular CLI 工作区的
ui-controls项目中创建一个card-list组件。--export选项还将从UiControlsModule中导出组件。UiControlsModule类已经从public-api.ts文件中导出。因此,当我们的 Angular 应用程序导入UiControlsModule时,它也将拥有我们的组件。 -
使用 Angular CLI 的
generate命令创建一个用于卡片数据结构的接口,如下所示:ng generate interface card --project=ui-controls -
上述命令将在我们的工作区
ui-controls项目中创建一个card.ts文件。 -
打开
card.ts文件,并将以下属性添加到Card接口中:export interface Card { **title****:** **string****;** **description****:** **string****;** **link****:** **string****;** } -
打开
public-api.ts文件,并添加以下export语句,以便将组件和接口提供给库消费者:export * from './lib/card-list/card-list.component'; export * from './lib/card'; -
打开
card-list.component.ts文件,并使用@Input装饰器定义一个input属性,如下所示:import { Component, **Input** } from '@angular/core'; **import** **{** **Card** **}** **from****'****../card'****;** @Component({ selector: 'lib-card-list', templateUrl: './card-list.component.html', styleUrls: ['./card-list.component.css'] }) export class CardListComponent { **@Input****()** **cards****:** **Card****[] = [];** }cards属性将在稍后由 Angular 应用程序设置,以显示我们想要显示的卡片数据。 -
打开
card-list.component.html文件,并用以下 HTML 模板替换其内容:<div> <div class="card m-4" *ngFor="let card of cards"> <header class="card-header"> <p class="card-header-title">{{card.title}}</p> </header> <div class="card-content"> <div class="content">{{card.description}}</div> </div> <footer class="card-footer"> <a [href]="card.link" class= "card-footer-item">View on Wikipedia</a> </footer> </div> </div>之前使用的模板使用了 Bulma 的
card组件,并通过遍历cards组件属性来显示每一个,使用*ngFor指令。 -
打开
card-list.component.css文件,并添加以下 CSS 样式::host > div { display: grid; grid-auto-flow: column; overflow: auto; } .card { width: 200px; }在前面的样式表中,我们使用
:host选择器来定位组件的host元素中的div元素,并应用grid样式以在单行中显示所有卡片。 -
打开
ui-controls.module.ts文件,并将CommonModule添加到@NgModule装饰器的imports数组中,如下所示:**import** **{** **CommonModule** **}** **from****'****@angular/common'****;** import { NgModule } from '@angular/core'; import { UiControlsComponent } from './ui-controls.component'; import { CardListComponent } from './card-list/card-list.component'; @NgModule({ declarations: [ UiControlsComponent, CardListComponent ], imports: [ **CommonModule** ], exports: [ UiControlsComponent, CardListComponent ] }) export class UiControlsModule { }CommonModule类对于卡片列表组件模板中的*ngFor指令是必需的。
我们的可组件已经准备好接受并显示以卡片列表形式呈现的数据。让我们看看如何从 Angular 应用程序中消费它,如下所示:
-
首先,执行以下命令来构建组件 UI 库:
ng build ui-controlsAngular CLI 将开始构建库,一旦你在终端上看到以下输出,它就已经完成:
![包含表格的图片 自动生成的描述]()
图 9.2 – 图书馆构建输出
-
打开
app.module.ts文件,并将UiControlsModule类添加到@NgModule装饰器的imports数组中,如下所示:import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; **import** **{** **UiControlsModule** **}** **from****'ui-controls'****;** @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, **UiControlsModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }我们从
ui-controls命名空间导入UiControlsModule,这是库的名称,而不是从我们的工作区中的完整绝对路径导入。 -
打开
app.component.ts文件,并声明一个Card[]类型的component属性,如下所示:import { Component } from '@angular/core'; **import** **{** **Card** **}** **from****'ui-controls'****;** **import** **{ assassins }** **from****'****./assassins'****;** @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'my-components'; **cards****:** **Card****[] = assassins;** }我们使用来自
assassins.ts文件中的示例数据初始化cards组件属性,该文件可以在 入门 部分的 GitHub 仓库中找到。 -
打开
app.component.html文件,并用以下 HTML 模板替换其内容:<div class="container is-fluid"> <h1 class="title">Assassins Creed Series</h1> <lib-card-list [cards]="cards"></lib-card-list> </div> -
要预览应用程序,请运行
ng serve并在浏览器中打开http://localhost:4200。你应该会看到如下内容:

图 9.3 – 卡片列表组件
卡片列表组件显示消费者应用程序通过 cards 输入属性传递的数据。在下一节中,我们将进一步扩展我们的组件,使卡片能够在列表中改变它们的位置。
添加拖放功能
卡片列表组件的一个特性是,我们将能够通过拖放卡片到列表中来改变卡片的位置。应该使用输出属性绑定将卡片列表的顺序返回给消费者应用程序。
Angular CDK 包含一个拖放模块,我们可以用它来实现这个目的。为此,请按照以下步骤操作:
-
打开
ui-controls.module.ts文件,并从@angular/cdk/drag-drop命名空间导入DragDropModule,如下所示:import { DragDropModule } from '@angular/cdk/drag-drop'; -
将
DragDropModule类添加到@NgModule装饰器的imports数组中,如下所示:@NgModule({ declarations: [ UiControlsComponent, CardListComponent ], imports: [ CommonModule, **DragDropModule** ], exports: [ UiControlsComponent, CardListComponent ] }) -
打开
card-list.component.html文件,并按如下方式修改模板:<div **cdkDropListOrientation****=****"horizontal"****cdkDropList** **(****cdkDropListDropped****)=****"sortCards($event)"**> <div **cdkDrag** class="card m-4" *ngFor="let card of cards"> <header class="card-header"> <p class="card-header-title">{{card.title}}</p> </header> <div class="card-content"> <div class="content">{{card.description}}</div> </div> <footer class="card-footer"> <a [href]="card.link" class="card-footer-item">View on Wikipedia</a> </footer> </div> </div>首先,我们将
cdkDrag指令添加到每个要拖动的卡片元素上,以便可以通过拖动来移动它。然后,我们将cdkDropList指令添加到容器元素上,将其标记为拖放列表。在 Angular CDK 中,拖放列表表示其内容可以通过拖放操作重新排序。我们将拖放方向设置为horizontal,因为我们的卡片列表以单行渲染,并且我们还绑定了一个sortCards组件方法到拖放列表的cdkDropListDropped事件。 -
打开
card-list.component.ts文件并相应地修改import语句:import { Component, Input, **Output****,** **EventEmitter** } from '@angular/core'; import { Card } from '../card'; **import** **{** **CdkDragDrop****, moveItemInArray }** **from****'@angular/cdk/drag-drop'****;** -
使用
@Output装饰器创建一个输出属性,并在sortCards组件方法中使用它来向组件的消费者发出重新排序的列表,如下所示:export class CardListComponent { @Input() cards: Card[] = []; **@Output****() cardChange =** **new****EventEmitter****<****Card****[]>();** **sortCards****(****event****:** **CdkDragDrop****<****string****[]>):** **void** **{** **moveItemInArray****(****this****.****cards****, event.****previousIndex****, event.****currentIndex****);** **this****.****cardChange****.****emit****(****this****.****cards****);** **}** } moveItemInArray built-in method of DragDropModule to change the order of the cards property. We pass the event parameter to the moveItemInArray method containing the previous and current index of the moved card. We also use the emit method of the cardChange property to propagate the change back to the Angular application.
卡片列表组件现在已经获得了拖放超级能力。让我们试一试,如下所示:
-
打开
app.component.html文件并为<lib-card-list>组件的cardChange事件添加事件绑定,如下所示:<div class="container is-fluid"> <h1 class="title">Assassins Creed Series</h1> <lib-card-list [cards]="cards" **(****cardChange****)=****"onCardChange($event)"**> </lib-card-list> </div> -
打开
app.component.ts文件并创建一个onCardChange方法来记录新的卡片列表,如下所示:onCardChange(cards: Card[]) { console.log(cards); } -
运行以下命令来构建库:
ng build ui-controls -
执行 Angular CLI 的
serve命令以启动您的应用程序,如下所示:ng serve -
尝试拖放列表中的某些卡片,并注意浏览器和实际应用程序的 控制台 窗口中的输出。
我们 UI 库的第一个组件现在已经包含了所有功能,使其成为一个拖放列表。它可以以 Bulma 卡片格式显示从我们的 Angular 应用程序传递过来的列表。它还可以使用 Angular CDK 拖放模块改变列表中每个项目的顺序,并将更改回传到我们的应用程序。
在以下部分,我们将创建我们库的第二个组件,用于将数据复制到剪贴板。
与剪贴板交互
Angular CDK 库包含一组 Angular 实体,我们可以使用它们与系统剪贴板进行交互。具体来说,它包括一个用于复制数据到剪贴板的指令和一个事件绑定,当内容被复制时执行额外操作。让我们看看我们如何将两者集成到我们的组件库中,如下所示:
-
执行以下 Angular CLI 命令在库中创建一个新的 Angular 组件:
ng generate component copy-button --project=ui-controls --export -
从
public-api.ts文件中导出新生成的组件,如下所示:export * from './lib/copy-button/copy-button.component'; -
打开
ui-controls.module.ts文件并从@angular/cdk/clipboard命名空间导入ClipboardModule,如下所示:import { ClipboardModule } from '@angular/cdk/clipboard'; -
将
ClipboardModule类添加到@NgModule装饰器的imports数组中,如下所示:@NgModule({ declarations: [ UiControlsComponent, CardListComponent, CopyButtonComponent ], imports: [ CommonModule, DragDropModule, **ClipboardModule** ], exports: [ UiControlsComponent, CardListComponent, CopyButtonComponent ] }) -
打开
copy-button.component.ts文件并声明以下组件属性:import { Component, **EventEmitter****,** **Input****,** **Output** } from '@angular/core'; @Component({ selector: 'lib-copy-button', templateUrl: './copy-button.component.html', styleUrls: ['./copy-button.component.css'] }) export class CopyButtonComponent { **@Input****() data =** **''****;** **@Output****() copied =** **new****EventEmitter****<****void****>();** }data属性将用于设置剪贴板数据,当数据成功复制到剪贴板时,将触发copied事件。 -
创建一个组件方法来触发一个
copied输出事件,如下所示:onCopy() { this.copied.next(); } -
打开
copy-button.component.html文件并用以下 HTML 模板替换其内容:<button class="button is-light is-primary" [cdkCopyToClipboard]="data" (cdkCopyToClipboardCopied)="onCopy()"> Copy </button>在前面的模板中,我们使用了一个 Bulma
button组件并将其与两个 Angular CDK 绑定关联。cdkCopyToClipboard属性绑定表示当按钮被点击时,data组件属性将被复制到剪贴板。cdkCopyToClipboardCopied事件绑定将在数据成功复制到剪贴板后立即调用onCopy组件方法。
现在我们已经设置了组件,让我们来看看如何在 Angular 应用程序中使用它,如下所示:
-
打开
app.component.html文件并添加一个包含一个<input>元素和<lib-copy-button>组件的<div>元素,如下所示:<div class="container is-fluid"> <h1 class="title">Assassins Creed Series</h1> <lib-card-list [cards]="cards" (cardChange)="onCardChange($event)"></lib-card-list> **<****h1****class****=****"title mt-5"****>****Clipboard interaction****</****h1****>** **<****div****class****=****"field has-addons"****>** **<****div****class****=****"control"****>** **<****input****class****=****"input"****type****=****"text"** **[(****ngModel****)]=****"title"****>** **</****div****>** **<****div****class****=****"control"****>** **<****lib-copy-button** **[****data****]=****"title"** **(****copied****)=****"log()"****></****lib-copy-button****>** **</****div****>** **</****div****>** </div>在前面的模板中,我们使用
ngModel指令将组件的title属性绑定到<input>元素,并将其绑定到<lib-copy-button>组件的data属性以复制<input>元素的内容到剪贴板。我们还绑定了copied事件到log组件方法。 -
打开
app.component.ts文件并为显示当数据复制到剪贴板时的信息消息创建一个log方法,如下所示:log() { alert(this.title + ' copied to the clipboard'); } -
打开
app.module.ts文件并导入FormsModule,如下所示:import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; **import** **{** **FormsModule** **}** **from****'@angular/forms'****;** import { AppComponent } from './app.component'; import { UiControlsModule } from 'ui-controls'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, UiControlsModule, **FormsModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }FormsModule类是@angular/formsnpm 包的一部分,当我们在应用程序中使用ngModel时是必需的。 -
执行以下命令以构建库,以便我们的应用程序可以识别新的组件:
ng build ui-controls -
使用
ng serve运行应用程序,你应该得到以下输出:

图 9.4 – 剪贴板交互
- 在文本框中输入值
my awesome library并点击复制按钮。你应该得到以下输出:

图 9.5 – 警告信息
我们已经成功创建了一个可以附加到 Angular 应用程序并用于直接与剪贴板交互的按钮!
Angular CDK 包含了许多其他组件和行为,我们可以在 Angular 应用程序中使用。当与高度可定制的 CSS 框架如 Bulma 结合使用时,它可以创建引人注目且独特的界面。在您的 Angular 项目中尝试它们,并构建一个具有丰富组件集的库。在下一节中,我们将学习如何将库发布到npm 包注册库。
将 Angular 库发布到 npm
我们已经看到了如何在同一存储库或组织中的 Angular 应用中构建 Angular 库并使用它。然而,有些情况下,您可能希望通过公共包注册表(如 npm)使您的库对您的基础设施之外的 Angular 项目可用。一个常见的例子是,当您想使您的库开源,以便开发社区的成员可以从中受益时。让我们看看如何按照以下步骤发布我们的ui-controls库到 npm:
-
如果您没有 npm 账户,请导航到
www.npmjs.com/signup创建一个。 -
打开 Angular CLI 工作区中
projects\ui-controls文件夹中存在的package.json文件,并将version属性的值设置为1.0.0。在您的库中遵循语义化版本控制是一种良好的做法,并首次发布为版本1.0.0。Angular 也遵循语义化版本控制,您可以在
semver.org了解更多相关信息。 -
打开一个终端窗口,并运行以下 Angular CLI 命令来构建您的库:
ng build ui-controls -
导航到 Angular CLI 生成的我们库的最终包所在的
dist文件夹,如下面的代码片段所示:cd dist\ui-controls -
在终端中执行以下
npm命令以登录到 npm 注册表:npm login -
在您成功通过 npm 认证后,运行以下命令来发布您的库:
npm publish
运行前面的命令将抛出一个错误,因为 npm 包注册表中已经包含了一个ui-controls包。如果您想预览前面命令的结果,请确保您更改了库的package.json文件中的name字段。
干得好!您的库现在已在公共 npm 注册表中,其他开发者可以在他们的 Angular 应用中使用它。
总是记得在发布库之前更改库的package.json文件中的version数字。否则,npm 注册表将抛出一个错误,指出您试图发布的版本已存在。
在下一节中,我们将学习如何使用我们的库在非 Angular 应用中使用 Angular 元素。
使用组件作为 Angular 元素
我们已经学习了如何使用 Angular CLI 创建 Angular 库。我们还看到了如何将我们的库发布到 npm 注册表,以便其他 Angular 项目可以使用它并从中受益。在本节中,我们将更进一步,学习如何构建我们的 Angular 库以便在非 Angular 环境中使用。
正如我们已经指出的,Angular 框架在许多方面都是一个跨平台的 JavaScript 框架。它可以在服务器上使用Angular Universal运行,在移动平台上运行,也可以在原生桌面环境中运行。除了这些平台之外,它甚至可以在没有使用 Angular 构建的 Web 应用中使用 Angular 元素运行。
让我们看看如何将我们的剪贴板组件转换为 Angular 元素,如下所示:
-
执行以下 Angular CLI 命令以在我们的工作区中生成一个新的 Angular 应用程序:
ng generate application ui-elements --defaults之前的命令将在
projects文件夹中使用默认选项生成ui-elementsAngular 应用程序。Angular CLI 目前不支持在 Angular 库中直接使用 Angular 元素。因此,我们需要创建一个 Angular 应用程序,其唯一目的就是将我们的组件导出为 Angular 元素。
-
导航到
projects\ui-elements文件夹并运行以下命令以安装@angular/elements包:npm install @angular/elements -
打开
ui-elements应用程序的app.module.ts文件并相应地进行修改:import { Injector, NgModule } from '@angular/core'; import { createCustomElement } from '@angular/elements'; import { BrowserModule } from '@angular/platform-browser'; import { UiControlsModule, CopyButtonComponent } from 'ui-controls'; @NgModule({ imports: [ BrowserModule, UiControlsModule ], providers: [] }) export class AppModule { } -
在
AppModule类中添加一个constructor并按照以下方式注入Injector服务:constructor(private injector: Injector) {} -
实现一个
ngDoBootstrap方法来为CopyButtonComponent类创建自定义元素,如下所示:ngDoBootstrap() { const el = createCustomElement(CopyButtonComponent, { injector: this.injector }); customElements.define('copy-button', el); }ngDoBootstrap方法用于挂钩 Angular 应用的手动引导过程。我们使用@angular/elementsnpm 包中的createCustomElement方法创建一个自定义元素,传递组件的类和注入器。最后,我们使用customElements对象的define方法声明自定义元素,传递我们想要使用的 HTML 选择器和自定义元素作为参数。
现在我们已经将将 Angular 组件转换为 Angular 元素的全部操作实施到位,是时候构建它以便我们可以在 Web 应用程序中使用它了。
构建 Angular 元素与构建标准的 Angular 应用程序不同。当我们构建 Angular 应用程序时,Angular CLI 会生成包含应用程序源代码、Angular 框架和任何第三方库的不同 JavaScript 包。在 Angular 元素场景中,我们只想生成包含我们的组件的单个包文件。为此,我们将使用 ngx-build-plus 库,它可以生成单个包,等等。让我们看看如何安装它并在我们的应用程序中使用它,如下所示:
-
执行以下 Angular CLI 命令以安装
ngx-build-plus包:ng add ngx-build-plus --project=ui-elements之前的命令将修改 Angular CLI 工作区的
angular.json文件以使用ngx-build-plus库构建ui-elements应用程序。 -
运行以下 Angular CLI 命令来构建应用程序:
ng build ui-elements --single-bundle之前的命令将构建
ui-elements应用程序并生成包含所有应用程序代码的单个包。 -
将
dist\ui-elements文件夹复制到您硬盘上的另一个位置,并使用您的编辑器打开index.html文件。 -
从
<head>元素中删除<base>标签,并使用 内容分发网络(CDN)添加 Bulma CSS 压缩文件,如下所示:<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css"> -
在
<body>元素中将<app-root>选择器替换为以下 HTML 片段:<div class="container is-fluid"> <h1 class="title">My Angular Element</h1> <copy-button></copy-button> </div> <div> element styled with Bulma CSS classes and the selector of the Angular element that we defined in AppModule. -
在
<div>元素之后插入以下 JavaScript 代码:<script> const el = document.getElementsByTagName('copy-button')[0]; el.setAttribute('data', 'Some data'); el.addEventListener('copied', () => alert('Copied to clipboard')); </script>在前面的脚本中,我们使用纯 JavaScript 与 Angular 元素后面的组件进行通信。首先,我们查询全局
document对象以获取 Angular 元素的引用。然后,我们使用元素的setAttribute方法设置data输入属性。最后,我们通过使用addEventListener方法附加事件监听器来监听copied输出事件。 -
使用 Web 服务器提供
ui-elements文件夹,并使用您的浏览器打开index.html文件。你应该看到以下输出:![图 9.6 – Angular 元素]()
图 9.6 – Angular 元素
如果你不想安装单独的 Web 服务器,你可以使用 Live Server VSCode 扩展。
-
点击 复制 按钮,你应该看到以下提示对话框:

图 9.7 – 提示对话框
我们已经成功地将我们的 UI 组件库中的 Angular 组件用作与 Angular 无关的 Web 应用程序中的原生 HTML 元素!自定义元素的外观和行为与其 Angular 对应物相同。唯一的区别是我们如何使用纯 JavaScript 在我们的 Web 应用程序中设置和配置自定义元素。
摘要
在这个项目中,我们构建了一个组件 UI 库,我们可以在我们的 Angular 应用程序中使用它。最初,我们学习了如何使用 Angular CLI 创建 Angular 库。我们搭建了一个新的 Angular CLI 工作区,其中包含我们的 Angular 库,以及一个用于测试它的 Angular 应用程序。
我们随后使用 Angular CDK 和 Bulma CSS 框架来构建我们库的 UI 组件。我们创建了一个可以重新排序的卡片列表,使用拖放功能和用于复制内容到剪贴板的按钮。
我们还看到了如何在 npm 注册表中发布我们的库,以便在其他 Angular 项目中使用。最后,我们使用 Angular elements 将其转换为自定义元素,以便分发到非 Angular 应用程序。
在下一项目,即本书的最后一个项目中,我们将学习如何自定义 Angular CLI 以创建我们的生成方案。
练习题
让我们看看几个练习题:
-
我们如何使用 Angular CLI 生成新的 Angular 库?
-
我们如何使我们的库的 Angular 艺术品公开?
-
我们使用哪个 CSS 选择器来定位 Angular 组件的
host元素? -
我们如何在 Angular CDK 中标记一个元素为可拖动?
-
我们使用什么方法重新排序可拖动项目列表?
-
哪个 Angular CDK 指令负责将数据传递到剪贴板?
-
我们如何使用
ngx-build-plus库创建单个包? -
我们如何将数据传递到 Angular 元素和从 Angular 元素中?
进一步阅读
这里有一些链接,可以建立在我们在本章中学到的知识之上:
-
Angular 库概述:
angular.io/guide/libraries -
创建 Angular 库:
angular.io/guide/creating-libraries -
Bulma CSS:
bulma.io -
Angular CDK:
material.angular.io/cdk/categories -
Angular 元素概述:
angular.io/guide/elements -
ngx-build-plus:www.npmjs.com/package/ngx-build-plus
加入我们的 Discord 社区
加入我们的社区 Discord 空间,与作者和其他读者进行讨论:

第十章:使用 Schematics 定制 Angular CLI 命令
Angular CLI 是一个强大的工具,也是处理 Angular 应用程序的默认解决方案。它消除了开发者大部分的样板代码和配置,使他们能够专注于有趣的事情,即构建出色的 Angular 应用程序。除了增强 Angular 开发体验外,它还可以轻松地根据每个开发者的需求进行定制。
Angular CLI 包含了一组用于构建、打包和测试 Angular 应用的有用命令。它还提供了一组称为 schematics 的特殊命令,这些命令可以生成各种 Angular 艺术品,如组件、模块和服务。Schematics 提供了一个公共 API,开发者可以使用它来创建自己的 Angular CLI 命令或扩展现有的命令。
在本章中,我们将介绍以下关于原理图(schematics)的细节:
-
安装 Schematics CLI
-
创建 Tailwind CSS 组件
-
创建 HTTP 服务
重要的背景理论和上下文
Angular schematics 是可以通过 npm 安装的库。它们在各种情况下使用,包括创建具有标准用户界面的组件或在一个组织内部强制执行约定和编码规范。一个 schematic 可以作为一个独立的工具使用,也可以作为现有 Angular 库的配套工具。
Angular schematics 被打包成集合,并位于 @schematics/angular npm 包中。当我们使用 Angular CLI 运行 ng add 或 ng build 命令时,它会运行该包中的相应 schematic。目前,Angular CLI 支持以下类型的 schematics:
-
添加:使用
ng add命令在 Angular CLI 工作区中安装 Angular 库。 -
更新:使用
ng update命令更新 Angular 库。 -
生成:使用
ng generate命令在 Angular CLI 工作区中生成 Angular 艺术品。
在这个项目中,我们将专注于生成 schematics,但所有其他命令都适用相同的规则。
项目概述
在这个项目中,我们将学习如何使用 Schematics API 来构建自定义 Angular CLI 生成 schematic,用于创建组件、服务和指令。首先,我们将构建一个用于创建使用 Tailwind CSS 框架的模板的 Angular 组件的 schematic。然后,我们将创建一个用于生成默认注入内置 HTTP 客户端并为 CRUD 操作中的每个 HTTP 请求创建一个方法的 Angular 服务的 schematic。以下图表描述了项目的架构概述:

图 10.1 – 项目架构
构建时间:1 小时
入门
完成此项目需要以下先决条件和软件工具:
-
Angular CLI:Angular 的命令行界面,您可以在
angular.io/cli找到。 -
GitHub 材料:本章的代码可以在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter10文件夹中找到。
安装 Schematics CLI
Schematics CLI 是一个命令行界面,我们可以用它来与 Schematics API 交互。要安装它,运行以下npm命令:
npm install -g @angular-devkit/schematics-cli
上述命令将在我们的系统上全局安装@angular-devkit/schematics-cli npm 包。然后我们可以使用schematics可执行文件为方案创建一个新的集合:
schematics blank my-schematics
上述命令将生成一个名为my-schematics的方案项目。它默认在src文件夹中包含一个同名的方案。方案包括以下文件:
-
collection.json:一个 JSON 方案,描述属于my-schematics集合的方案。 -
my-schematics\index.ts:方案的主入口点。 -
my-schematics\index_spec.ts:方案主入口点的单元测试文件。
该集合的 JSON 方案文件为每个与该集合关联的方案包含一个条目:
`collection.json`
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"my-schematics": {
"description": "A blank schematic.",
"factory": "./my-schematics/index#mySchematics"
}
}
}
集合中的每个方案都包含一个简短描述,如description属性所示,以及一个指向方案主入口点的factory属性,使用特殊语法。它包含文件名./my-schematics/index,后面跟着#字符,以及该文件导出的函数名称,命名为mySchematics。
方案的主入口点包含一个默认导出的规则工厂方法,它返回一个Rule对象,如index.ts文件中所述:
export function mySchematics(_options: any): Rule {
return (tree: Tree, _context: SchematicContext) => {
return tree;
};
}
方案不会直接与文件系统交互。相反,它创建一个由Tree对象表示的虚拟文件系统。虚拟文件系统包含一个暂存区域,其中所有方案中的转换都会发生。这个区域旨在确保任何无效的转换都不会传播到实际的文件系统。一旦方案有效可以执行,虚拟文件系统将应用更改到真实文件系统。方案的所有转换都在SchematicContext对象中操作。
在以下部分,我们将学习如何使用 Schematics CLI 并创建一个组件生成方案。
创建 Tailwind CSS 组件
Tailwind 是一个非常流行的 CSS 框架,它强制执行以实用程序为核心的原则。它包含可以在 Angular 应用程序中使用以创建易于组合的用户界面的类和样式。
我们将使用 Angular CLI 的 Schematics API 来构建一个用于 Angular 组件的生成方案。该方案将生成一个带有 Tailwind 容器布局的新 Angular 组件。
我们将要构建的方案不需要默认安装 Tailwind CSS。然而,我们将使用该方案的程序确实需要它。
让我们看看如何实现这一点:
-
执行以下命令以向我们的集合添加一个新的原理图:
schematics blank tailwind-container上述命令将更新
collection.json文件,包含一个新的tailwind-container原理图条目。它还会在我们的工作区src文件夹中创建一个tailwind-container文件夹。 -
在
tailwind-container文件夹内创建一个schema.json文件,并添加以下内容:{ "$schema": "http://json-schema.org/schema", "$id": "TailwindContainerSchema", "title": "My Tailwind Container Schema", "type": "object", "properties": { "name": { "description": "The name of the component.", "type": "string" }, "path": { "type": "string", "format": "path", "description": "The path to create the component.", "visible": false } }, "required": ["name"] }每个原理图都可以有一个 JSON 模式文件,该文件定义了在运行原理图时可用选项。由于我们想要创建一个组件生成原理图,我们需要为我们的组件添加一个
name和path属性。每个属性都关联着元数据,例如
type和description。在调用原理图时,组件名称是必需的,这由required数组属性所示。 -
打开
collection.json文件,并按以下方式设置tailwind-container原理图的属性:"tailwind-container": { "description": "**Generate a Tailwind container component.**", "factory": "./tailwind-container/index#tailwindContainer", **"schema"****:****"./tailwind-container/schema.json"** }在前面的文件中,我们为我们的原理图设置了一个适当的描述。我们还添加了
schema属性,它指向我们在上一步创建的schema.json文件的绝对路径。 -
在
tailwind-container文件夹内创建一个schema.ts文件,并添加以下内容:export interface Schema { name: string; path: string; }前面的文件定义了
Schema接口,将映射属性与schema.json文件中描述的属性相对应。
我们现在已经创建了所有我们将用于创建原理图的基础基础设施。让我们看看如何编写实际运行原理图的代码:
-
在
tailwind-container文件夹内创建一个名为files的文件夹。 -
在
files文件夹内创建一个名为__name@dasherize__.component.html.template的文件,并添加以下内容:<div class="container mx-auto"></div>前面的文件表示我们的原理图将生成的组件模板。
__name前缀将被我们作为选项传递给原理图的组件名称所替换。@dasherize__语法表示如果以驼峰式输入,名称将通过连字符分隔并转换为小写。 -
创建一个名为
__name@dasherize__.component.ts.template的文件,并添加以下内容:import { Component } from '@angular/core'; @Component({ selector: 'my-<%= dasherize(name) %>', templateUrl: './<%= dasherize(name) %>.component.html' }) export class My<%= classify(name) %>Component {}前面的文件包含将要生成的组件的 TypeScript 类。
@Component装饰器的selector和templateUrl属性是通过dasherize方法和组件的name构建的。类名包含一个不同的方法classify,它接受组件的name作为参数并将其转换为标题格式。 -
打开
tailwind-container文件夹内的index.ts文件,将选项类型设置为Schema,并移除return语句。生成的文件应该是以下内容:import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; import { Schema } from './schema'; export function tailwindContainer(_options: Schema): Rule { return (_tree: Tree, _context: SchematicContext) => { }; } -
修改文件顶部的
import语句如下:**import** **{ normalize, strings }** **from****'@angular-devkit/core'****;** import { **apply, applyTemplates, chain, mergeWith, move**, Rule, SchematicContext, Tree, **url** } from '@angular-devkit/schematics'; import { Schema } from './schema'; -
将以下代码插入到
tailwindContainer函数中:_options.path = _options.path ?? normalize('src/app/' + _options.name as string); const templateSource = apply(url('./files'), [ applyTemplates({ classify: strings.classify, dasherize: strings.dasherize, name: _options.name }), move(normalize(_options.path as string)) ]);在前面的代码中,首先,我们设置组件的
path属性,以防在 schematic 中未传递。默认情况下,我们在src\app文件夹内创建一个与组件同名的文件夹。然后,我们使用apply方法从files文件夹中读取模板文件,并使用applyTemplates函数传递dasherize、classify和name属性。最后,我们调用move方法在提供的路径中创建生成的组件文件。 -
将以下语句添加到工厂函数的末尾:
return chain([ mergeWith(templateSource) ]); chain method to execute our schematic, passing the result of the mergeWith function, which uses the templateSource variable we created in the previous step.
现在,我们可以继续测试我们的新组件 schematic:
-
执行以下
npm命令来构建 schematic:npm run build上述命令将调用 TypeScript 编译器并将 TypeScript 源文件转换为 JavaScript。它将生成与 TypeScript 文件夹并排的 JavaScript 输出文件。
-
运行以下命令将 schematics 库安装到我们的全局 npm 缓存中:
npm link上述命令将允许我们安装 schematic 而不查询公共 npm 注册表。
-
在您选择的文件夹外部的工作空间中执行以下 Angular CLI 命令以使用默认选项生成新的 Angular 应用程序:
ng new my-app --defaults -
导航到
my-app文件夹并运行以下命令来安装我们的 schematics:npm link my-schematics之前的
npm命令将在当前的 Angular CLI 工作空间中安装my-schematics库。npm link命令类似于运行npm install my-schematics,但它从我们的机器的全局 npm 缓存中下载 npm 包,并且不会将其添加到package.json文件中。 -
使用 Angular CLI 的
generate命令创建一个dashboard组件:ng generate my-schematics:tailwind-container --name=dashboard在上述命令中,我们通过传递我们的集合名称
my-schematics,后跟冒号分隔的具体 schematic 名称tailwind-container,来使用我们的自定义 schematic。我们还使用 schematic 的--name选项为我们的组件传递一个名称。 -
我们可以通过观察终端中的输出或使用 VS Code 打开我们的组件来验证我们的 schematic 是否正确工作:

图 10.2 – 生成 Angular 组件
我们已成功创建了一个新的 schematic,可以根据我们的需求创建自定义 Angular 组件。我们构建的 schematic 从头开始生成新的 Angular 组件。Angular CLI 非常灵活,我们可以挂钩到内置 Angular schematics 的执行并相应地修改它们。
在下一节中,我们将通过构建 Angular HTTP 服务的示意图来调查这一点。
创建 HTTP 服务
我们将为我们的 schematics 库创建一个 schematic,用于构建 Angular 服务。它将生成一个导入内置 HTTP 客户端的服务。它还将包含 CRUD 操作中涉及的每个 HTTP 请求的一个方法。
我们将要构建的生成原理图将不会独立存在。相反,我们将它与 Angular CLI 为服务提供的现有生成原理图相结合。因此,我们不需要单独的 JSON 模式。
让我们开始创建原理图:
-
执行以下命令以将新的原理图添加到我们的集合中:
schematics blank crud-service -
运行以下命令以安装
@schematics/angularnpm 包:npm install @schematics/angular -
打开
collection.json文件并修改crud-service原理图:"crud-service": { "description": "**Generate a CRUD HTTP service.**", "factory": "./crud-service/index#crudService", **"schema"****:****"../node_modules/@schematics/angular/service/schema.json"** }我们为原理图设置了一个简短描述,并添加了一个
schema属性,指向 Angular 服务的原始schema.json文件。 -
在工作区中的
crud-service文件夹内创建一个名为files的文件夹。 -
在
files文件夹内创建一个名为__name@dasherize__.service.ts.template的文件,并添加以下代码:import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class <%= classify(name) %>Service { constructor(private http: HttpClient) { } }前面的文件是原理图将生成的 Angular 服务文件的模板。它默认将
HttpClient服务注入到类的constructor中。 -
定义一个表示我们想要与之通信的 API URL 的服务属性:
apiUrl = '/api'; -
为每个 CRUD 操作的 HTTP 请求添加以下方法:
create(obj) { return this.http.post(this.apiUrl, obj); } read() { return this.http.get(this.apiUrl); } update(obj) { return this.http.put(this.apiUrl, obj); } delete(id) { return this.http.delete(this.apiUrl + id); }
在事先创建所有方法可以消除大部分样板代码。使用原理图的开发者只需修改这些方法并为每个方法添加实际实现。
我们几乎完成了原理图,除了创建将调用服务生成的工厂函数:
-
打开
crud-service文件夹中的index.ts文件,并按以下方式修改import语句:**import** **{ normalize, strings }** **from****'@angular-devkit/core'****;** import { **apply, applyTemplates, chain, externalSchematic,** **MergeStrategy****, mergeWith, move**, Rule, SchematicContext, Tree, **url** } from '@angular-devkit/schematics'; -
将
tree参数重命名并从return语句中删除,因为我们不会使用它。生成的工厂函数应如下所示:export function crudService(_options: any): Rule { return (_tree: Tree, _context: SchematicContext) => {}; } -
将以下片段添加到
crudService函数中:const templateSource = apply(url('./files'), [ applyTemplates({ ..._options, classify: strings.classify, dasherize: strings.dasherize }), move(normalize(_options.path ?? normalize('src/app/'))) ]); main differences are that the default path is the src\app folder and that we pass all available options using the _options parameter to the schematic.在事先知道将用于生成 Angular 服务的选项是不可能的。因此,我们使用展开运算符将所有可用选项传递给
templateSource方法。这也是为什么_options参数是any类型的原因。 -
将以下
return语句添加到函数的末尾:return chain([ externalSchematic('@schematics/angular', 'service', _options), mergeWith(templateSource, MergeStrategy.Overwrite) ]);在前面的语句中,我们使用
externalSchematic方法调用用于创建 Angular 服务的内置生成原理图。然后,我们将执行该原理图的结果与我们的templateSource变量合并。我们还使用MergeStrategy.Overwrite定义了合并操作的策略,以便我们的原理图所做的任何更改都将覆盖默认值。
我们创建 CRUD 服务的原理图现在已完成。让我们在我们的示例应用程序中使用它:
-
执行以下命令以构建原理图库:
npm run build我们不需要再次链接原理图库。我们的应用程序将在我们为原理图进行新的构建时自动更新。
-
导航到我们的应用程序所在的
my-app文件夹。 -
执行以下命令以使用我们的新原理图生成 Angular 服务:
ng generate my-schematics:crud-service --name=customers我们使用 Angular CLI 的
generate命令,再次传递我们脚手架集合的名称,但这次目标是crud-service脚手架。 -
新的 Angular 服务是在
src\app文件夹中创建的,如终端窗口中的输出所示:

图 10.3 – 生成 Angular 服务
注意,脚手架已经为我们自动生成了一个单元测试文件。这是如何实现的?回想一下,我们已经将我们的脚手架与 Angular CLI 内置的生成脚手架合并了。所以,默认脚手架所做的任何事情,都会直接反映在自定义脚手架的执行上。
我们刚刚为我们脚手架集合添加了一个新的有用命令。我们可以生成一个与 HTTP 端点交互的 Angular 服务。此外,我们还添加了与端点通信所需的基本方法。
摘要
在这个项目中,我们使用了 Angular CLI 的 Schematics API 来创建满足我们需求的自定义脚手架。我们构建了一个用于生成包含 Tailwind CSS 样式的 Angular 组件的脚手架。我们还构建了另一个脚手架,用于创建一个与内置 HTTP 客户端交互的 Angular 服务。该服务包括与 HTTP CRUD 应用程序一起工作的所有必要工件。
Angular CLI 是一个灵活且可扩展的工具,它极大地提升了开发体验。每个开发者的想象力是限制他们在工具链中使用这种资产所能做到的事情的唯一因素。CLI 和 Angular 框架允许开发者创建出色的 Web 应用程序。
正如我们在整本书中学到的,Angular 框架在 Web 开发者世界中的普及程度非常高,以至于今天可以轻松地将其与任何技术集成,并创建快速且可扩展的 Angular 应用程序。因此,我们鼓励你获取 Angular 的最新版本,并立即创建令人惊叹的应用程序。
练习
使用 Schematics CLI 创建一个用于生成 Angular 指令的 Angular 脚手架。该指令应将 ElementRef 和 Renderer2 服务从 @angular/core npm 包注入到 TypeScript 类的构造函数中。
你应该遵循我们在 创建 Tailwind CSS 组件 部分中为组件脚手架所采取的相同方法。
你可以在 GitHub 仓库中该章节的 exercise 分支的 Chapter10 文件夹中找到解决方案。
进一步阅读
















浙公网安备 33010602011771号