Angular-和-BootStrap-Web-开发第三版-全-

Angular 和 BootStrap Web 开发第三版(全)

原文:zh.annas-archive.org/md5/C3E0BC11B26050B30F3DD95AAA2C59BD

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

近年来,现代 Web 应用程序开发已经发展了很多。这些应用程序采用了移动优先的方法进行设计和开发。Angular 是每个开发人员梦寐以求的框架,用于快速原型设计,以部署复杂的企业应用程序。本书将指导您使用 Bootstrap CSS 框架来快速启动 Angular 应用程序开发。本书的每一章都被构建为让您充分了解 Angular 框架的特定主题和方面,以及其他相关的库和框架。这些方面对于设计和开发强大的 Angular 应用程序至关重要。通过逐步的方法,您将学习以下各个阶段:TypeScript、Bootstrap 框架、Angular 路由、组件、模板、Angular Material、依赖注入等等。

到本书结束时,您将能够使用各种 CSS 框架(包括 Bootstrap、Material Design 和 NG Bootstrap)创建现代、响应式、跨平台的 Angular 应用程序。

这本书适合谁

本书经过精心思考,其内容涵盖了从适合绝对初学者到一些最复杂用例的主题。详细的解释、逐步的实际用例示例和本书的流畅易懂使其成为您收藏的必备品。

本书将使有少量或没有编程背景的开发人员受益,同时也将使经验丰富的开发人员受益。

本书涵盖的内容

第一章,快速入门,通过一个快速入门章节开始您的旅程,向您展示可用的可能性,并激发您的创造力。

第二章,ECMAScript 和 TypeScript 速成课,审查了 TypeScript 语言,从语言的基础到高级方面,这些方面在编写 Angular 应用程序时至关重要。

第三章,Bootstrap - 网格布局和组件,介绍了令人惊叹的 Bootstrap CSS 框架,并解释了您如何使用该超级库提供的一些组件和实用程序。

第四章《路由》使你能够通过学习有关路由的所有知识来熟悉 Angular 框架。从定义简单的路由路径到复杂的路由守卫等等,你将掌握构建应用程序坚固路由系统的知识。

第五章《Flex 布局- Angular 的响应式布局引擎》涵盖了替代布局和一个名为 Flex 布局的网格设计库,并解释了 Flex 布局如何为你的 Angular 应用程序提供强大的布局。

第六章《构建 Angular 组件》涵盖了 Angular 组件,这是现代渐进式 Web 应用程序的主要构建模块。你将学会构建多个组件,并将它们整合在一起构建视图和功能。

第七章《模板、指令和管道》介绍了 Angular 模板引擎、指令和管道。你将探索内置的指令、管道和模板。

第八章《使用 NG Bootstrap 工作》介绍了 NG Bootstrap,这是另一个你可以考虑在项目中使用的超强大框架。

第九章《使用 Angular Material 工作》解释了我们如何使用 Angular Material 提供的组件、指令和实用工具来开发我们的 Angular 应用程序。

第十章《使用表单》介绍了任何动态应用程序的核心。你将了解构建表单的不同方法,探索基于模板的表单、响应式表单等等。

第十一章《依赖注入和服务》涵盖了依赖注入、服务以及幕后的设计哲学。

第十二章《集成后端数据服务》是整个章节中你学到的所有代码、知识和实际示例的结合点。你将把端到端的屏幕,从 UI 到组件,再到服务和模型等等连接起来。你将学到一切你需要的知识,以便吸收 Angular 的所有方面。

第十三章,单元测试,解释了测试是现代软件开发中最重要的方面之一。本章将教你如何测试 Angular 应用程序。

第十四章,高级 Angular 主题,讨论了用户认证和强大的用户管理系统,还涵盖了将应用程序与 Google Firebase 和 Auth0 集成。

第十五章,部署 Angular 应用程序,探讨了如何部署您的 Angular 应用程序并使其达到生产就绪状态。

充分利用本书

为了充分利用本书中学到的信息,建议您快速复习编程基础知识,包括类、对象、循环和变量。这可以用您选择的任何语言来完成。Angular 应用程序不限于任何特定操作系统,所需的只是一个体面的代码编辑器和一个浏览器。在整本书中,我们使用了 Visual Studio Code 编辑器,这是一个开源编辑器,可以免费下载。

下载示例代码文件

您可以从您的帐户在www.packt.com下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support注册,直接将文件发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载”

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

文件下载后,请确保使用最新版本的解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Web-Development-with-Angular-and-Bootstrap-Third-Edition。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有来自丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:"截图显示了编辑过的 angular.json 文件。"

代码块设置如下:

"styles": [
   "styles.css",
    "./node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": [
    "../node_modules/jquery/dist/jquery.min.js",
    "./node_modules/bootstrap/dist/js/bootstrap.min.js"
]  

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { JacketListComponent } from '../../projects/jackets/src/app/jacket-list/jacket-list.component';
import { VendorsComponent } from '../../projects/vendors/src/lib/vendors.component';

任何命令行输入或输出都以如下方式书写:

ng new realtycarousel

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。例如:"我们已添加了菜单链接,添加新列表。"

警告或重要说明会以这种方式出现。提示和技巧会以这种方式出现。

第一章:快速开始

你准备好迈向 Angular 的精通了吗?我猜你是的,有了这本书和你的决心,你一定会成功的。你购买了这本书,我不仅要感谢你,还要在这里向你做出承诺。事实上,是两个承诺。第一个是,如果你认真阅读材料,应用你在学习过程中获得的知识,并且在这些页面中和我一起构建示例应用程序,你就会在迈向 Angular 的精通之路上取得很大进展。

如果你和我一样,你的书架上堆满了成百上千本技术书籍,而你已经读过大部分。有些书开始的速度极其缓慢,深陷于理论和历史细枝末节,而其他书则开始得太快,让读者不知所措,想知道自己是不是太笨,无法理解材料。事实上,介绍读者可能全新的材料,而不让他们在阅读新获得的 400 多页技术书时打瞌睡,是一件棘手的事情。所以,我尊敬的初学者 Angular 大师,这就是我对你的第二个承诺。我承诺尽我所能找到技术和实际之间的平衡,让这本书在向你介绍新材料的同时,尽可能地有趣。

承诺已经说得很清楚了,让我们一起开始 Angular 精通之旅,快速浏览一下我们将在这个简洁但非常重要的第一章中涵盖的内容。

我们将迅速设置你的开发环境,并构建我们的第一个 Angular 应用程序,以便立即获得一些成就感。在编写它时,我们会略过细节,但在那之后,我们将更详细地介绍一些关键的 Angular 基础知识,然后结束本章。这些最基本的知识是你应该熟悉的第一件事情,因为在学习更高级的内容时,我们会一遍又一遍地使用它们。

在我们涵盖了这些基础知识之后,我们将从 Angular 语言转换,并且我们将看一下我们将在本书的其余部分一起构建的完整应用程序。作为一个奖励(在本书中有一些奖励,我希望能给您带来很多价值),我们还将涉及设计原则、线框图和一种很少使用的设计策略,称为纸质原型设计——其重点是可用性测试。纸质原型设计大约自 1985 年左右开始流行,并且在大约 2008 年左右被精益 UX 设计所取代。然而,我总是惊讶于我的许多客户甚至从未听说过纸质原型设计,但当他们尝试时,他们发现它给他们带来的价值,我也感到很高兴。

我们将在本章末尾对纸质原型设计进行高层次的介绍,紧接着线框图部分,这是讨论纸质原型设计的最合逻辑的地方。我们还将涉及一些 UX 设计原则,但不涉及精益 UX 设计过程本身,因为那会让我们偏离本书的重点。然而,如果您对精益 UX 设计过程感兴趣,这是一个很好的起点:www.interaction-design.org/literature/article/a-simple-introduction-to-lean-ux

好的,我尊敬的初学者 Angular 大师,你准备好开始了吗?太好了!让我们开始吧!

本章我们将涵盖的主题有:

  • Angular 的发展

  • Angular 的构建模块

  • 设置您的开发环境

  • 编写您的第一个 Angular 应用程序

  • Angular 基础知识

  • 我们的示例项目

  • 纸质原型设计的过程

Angular 的发展

Angular 是一个基于前端 JavaScript 的 Web 应用程序框架,为您提供了构建强大的单页应用程序(SPA)所需的一切,包括厨房水槽。我们将一起构建的应用程序是一个 SPA,并且我们将在此过程中讨论 SPA 策略。

虽然 Angular 不是第一个基于 JavaScript 的前端 Web 应用程序框架,但它很可能是其中最强大的一个。这可能是因为 Angular 专注于 SPA,因为构建 SPA 应用程序比在您的网页上提供双向数据绑定要复杂得多。

Angular 最初发布于 2010 年晚秋。自那时以来,已经出现了数十个竞争库和框架,包括一些也具有大规模采用和大规模生产实施的库,如 Ember.js、Backbone.js 和 React.js。尽管 Angular 可能具有最高的学习曲线(我们将看到为什么会这样),但它仍然是其中最强大的一个。

乍一看,Angular 的命名和版本控制可能会令人困惑。这有几个原因,如下:

  • Angular 的 1.x 版本:基本上,任何在 Angular 2 之前发布的版本通常被称为 AngularJS。

  • AngularJS 不再处于积极开发模式。它已被置于长期支持模式下。

  • Angular 框架正在积极开发,因此开发人员在讨论它们时需要明确指出他们所指的是两个 Angular 框架中的哪一个。幸运的是,它们分别有两个完全专门的网站:angularjs.org/angular.io。Angular 团队采用了语义化版本控制,从 2.0.0 版本开始。您可以在这里阅读更多关于语义化版本控制的信息:semver.org

  • Angular 2 是对 Angular 1.x(即 AngularJS)的完全重写,因此与 AngularJS 不兼容。虽然 Angular 4 并不是对 Angular 2 的完全重写,但它的核心库有一些变化,需要团队将其主要版本号从 2 增加到 4。版本 3 被完全跳过。

  • 从 Angular 2 开始的所有发布通常被称为 Angular 2+,或者简单地称为 Angular。

  • 由于采用了语义化版本控制,Angular 团队从未发布过 Angular 3,因此直接从 Angular 2 到 Angular 4。具体来说,路由器包的版本存在不一致,已经分发为版本 3.3.0。我们将在第四章中详细介绍 Angular 中的路由。不要让这使您感到困惑。只需知道从未有过 Angular 3。没什么大不了的。在 Windows 操作系统世界中,也从未有过 Windows 9。这些事情都会发生。

阅读完上述列表后,您可以看到为什么在 Angular 周围往往会有一些混淆。然而,只要记住以下两点,就会变得非常简单:

  • 您真的应该只使用 Angular,而不是 AngularJS(除非您有一个非常好的理由)。

  • 除了没有 Angular 3 之外,每年将有两个主要版本发布;它们应该在编号方案上是连续的(即 8、9 等),并且预计它们将向后兼容——至少在相同的主要版本号内(根据语义版本控制的精神)。

您可以在这里查看官方的 Angular 发布时间表:github.com/angular/angular/blob/master/docs/RELEASE_SCHEDULE.md。由于 Angular 是对 AngularJS 平台的完全重写,与 AngularJS 相去甚远,我们将完全跳过 AngularJS,首先看一下 Angular 的构建块——组件。跟上我吗?好的,让我们快速前进。

Angular 的构建块

添加新功能是发布新框架的事情,但幸运的是,基本的底层架构并不经常改变。当它改变时,通常不是完全的彻底改变。除了完全不同于其前身的 Angular 2.0 之外,到目前为止,所有主要版本发布基本上包含相同的架构。

现在让我们来看看框架的核心架构组件。

组件

组件就像小部件一样,负责在屏幕上的视图区域显示自己以及它们消耗和/或操作的数据。Angular 应用程序就像一个组件树,Angular 提供了组件之间双向通信的机制——从父级到子级和从子级到父级。

模板

组件依赖于它们的模板来呈现它们的数据。模板是您定义组件外观的地方,您可以添加样式来装饰您喜欢的任何方式。组件可以包含其模板(即 HTML)和其样式(即 CSS),直接在自身内部,或者引用模板和样式文件在自身外部。归根结底,世界上最花哨的前端框架产生 HTML、CSS 和 JavaScript,因为这三样是浏览器唯一理解的东西。

指令

在您为组件创建的模板中,Angular 使您能够使用称为指令的强大构造来更改 DOM。有用于控制屏幕上的渲染方式(即组件视图)的指令,例如重复 HTML 片段,根据条件逻辑显示内容,隐藏或显示内容,过滤数据数组等等。

模块

Angular 是模块化的。也就是说,它的功能被封装在称为 NgModule 的模块中,并且它们本身就是库。模块非常适合以有组织的方式将代码组合在一起。例如,有用于帮助处理表单、路由和与 RESTful API 通信的模块。许多第三方库被打包为 NgModule,因此您可以将它们整合到您的 Angular 应用程序中。其中两个例子是 Material Design 和 AngularFire - 我们将在后面的章节中查看这两个库。

服务

服务实际上并不是 Angular 的一个特定部分,而是一个非常普遍的概念,代表着应用程序组件可能需要消耗的封装功能、函数和特性。诸如日志记录、数据检索或几乎任何计算或查找服务等功能可以被编写为服务 - 这些服务可以存在于您的应用程序中,也可以存在于外部。您可以将服务视为提供某种服务(例如查找两个邮政编码之间的距离)并且做得很好的高度专业化的类。与组件一样,不仅有大量的第三方服务可以在您的 Angular 应用程序中使用,而且您还可以创建自己的自定义服务。我们将在第十二章中学习如何做到这一点,集成后端数据服务

依赖注入

依赖注入DI)或控制反转IoC)是一种非常有用和常见的软件设计模式。这种模式用于将对象注入到依赖于它们的对象中。依赖于其他对象的对象可以直接使用它,而不需要担心它在哪里加载,或者如何实例化它 - 你只需在需要时使用它,就好像它在你需要它的时候就出现了。服务非常适合注入到我们的应用程序中。我们将学习如何在 Angular 中使用 DI,以及如何使用 Angular 的命令行界面CLI)来生成我们自己设计的可注入服务。

在我们继续设置开发环境之前,这里有一些关于 Angular 的有趣事实:

  • AngularJS 是使用 JavaScript 构建的,而 Angular 是使用 TypeScript 构建的。虽然在编写 Angular 应用程序时这增加了一定程度的抽象,但使用 TypeScript 在构建更大的应用程序和更大的团队时提供了一些重要的优势-我们很快就会谈到这些。

  • AngularJS 基于控制器,而 Angular 是基于组件的。您将在第六章中学习有关组件的所有必要知识,构建 Angular 组件

  • 单页应用程序以难以实现搜索引擎优化SEO)而臭名昭著,但 Angular 对 SEO 友好。

  • 使用 Angular 也可以构建原生移动应用程序。

  • 使用 Angular 也可以构建跨平台的桌面应用程序。

  • Angular 也可以在服务器上运行,使用 Angular Universal。

您必须承认,这是一个相当令人印象深刻和令人兴奋的清单。这些事情以及更多其他事情使学习 Angular 成为一项值得的努力,市场正在寻求 Angular 的专业知识。

设置您的开发环境

要开始使用 Angular,您需要安装Angular CLI;要安装它,您首先需要安装 Node.js 和npmnode 包管理器)。如果您已经安装了 Node.js 和 npm,太好了!如果没有,不用担心-它们很容易安装,我将在书的后面附录 A“使用 Angular 进行 Web 开发的工具链”中带您完成安装过程。在附录 A 中,我还将带您安装 Angular CLI 以及如何使用它构建 Angular 应用程序。为了简洁起见,从现在开始我将简称 Angular CLI 工具为 CLI。

如果您不确定是否已安装 NodeJS 和 npm,您可以通过在命令行上分别输入$ node -v$ npm -v来快速检查。同样,您可以在命令行上输入$ ng -v来查看是否已安装 CLI。如果您收到版本号,那么您已安装了该特定工具(如我所示的下面的截图)。

注意:不要在命令开头输入$$表示命令提示符,您要输入的命令的入口点。基于 Unix 的操作系统,如 macOS 和 Linux 系统,通常使用$%作为命令提示符,具体取决于所使用的 shell,或者系统上的配置文件中指定的任何自定义设置。Windows 操作系统通常使用大于号>作为命令提示符。

如果其中任何命令无法识别,请快速跳转到附录 A,安装工具,然后立即回到这里。我会等着你。

我们还需要一个代码编辑器。今天有许多代码编辑器可用,包括一些免费的。虽然任何代码编辑器都可以,但我建议您在编写本书时至少使用 Visual Studio Code 进行 Angular 开发。原因是 Visual Studio Code 是免费的,跨平台的,是一个优秀的代码编辑器。这也是我在写这本书时使用的代码编辑器,所以当我建议使用某个扩展时,您可以轻松安装相同的扩展。

上述内容就是本章的全部内容。当我们开始构建示例项目时,需要我们有一个本地数据库,您还需要安装 MongoDB。MongoDB,也称为 Mongo,是一个很棒的免费跨平台 NoSQL 数据库。我会在附录 B,MongoDB中带您完成 Mongo 的安装过程。

此外,还有其他软件需要安装,例如 Chrome 扩展程序,我会在适当的时候告诉您它们是什么以及在哪里找到它们。现在,让我们开始编写一些 Angular 代码。

编写您的第一个 Angular 应用程序

当您开始尝试 Angular 代码时,作为您掌握这个强大的框架,通常有两种选择。第一种是使用在线代码编辑器,如 JSFiddle、Plunker、StackBlitz 等。在附录 C 中,使用 StackBlitz,您将学习如何基本使用 StackBlitz,以便您可以不时地使用它来测试一些快速代码,而无需在开发环境中需要测试项目。您可以在 StackBlitz 网站上访问:stackblitz.com

第二种方法是使用您自己的本地开发环境——因为我们已经在前一节中设置了它,您可以创建一个项目,其唯一目的是运行一些快速示例代码,如果您宁愿使用本地开发环境而不是在线代码编辑器。我的目标是向您展示您有选择的余地——学习 Angular 并不只有一种方法来尝试一些代码。

当您使用在线代码编辑器(如 StackBlitz)时,您唯一需要安装的软件是浏览器——没有任何其他工具。虽然这使事情变得非常容易,但代价是您在所能做的事情上受到极大限制。话虽如此,我鼓励您尝试在线代码编辑器,但在本书中我们将只使用我们的开发环境。所以,让我们做到这一点,并在短短几分钟内一起创建一个小应用程序——我们将构建一个待办事项列表应用程序。

使用您的开发环境

从现在开始,我们将使用我们的终端、CLI 和 Visual Studio Code。前往code.visualstudio.com,在那里您可以下载适用于您选择的操作系统的 Visual Studio Code 安装包。

您的文件位置

在设置本地环境时,您当然可以将目录和文件放在任何您喜欢的地方。如果您有一个存放 Web 应用项目的文件夹,请立即转到该文件夹。如果您没有专门的项目存放位置,现在是养成有条理习惯的好时机。例如,在我的电脑上,我有一个名为dev的文件夹,用于我所做的任何开发。在我的dev文件夹中,我有一个名为playground的文件夹,其中有一个我正在学习或玩耍的每种技术的子文件夹。我喜欢在编写代码时使用 Mac,因此我存放 Angular play stuff的完整路径是/Users/akii/dev/playground/angular(如前几页终端屏幕截图底部所示)。同一屏幕截图还显示了我在写作时安装的 Node.js、npm 和 CLI 的版本。如果这样的目录结构适合您,请尽管使用。如果您已经有组织工作的方式,请使用它。重要的是要非常有纪律性和一致性地组织您的开发环境。

生成我们的待办事项列表应用程序

现在我们需要的安装已经完成 - 这意味着我们可以使用 CLI 工具 - 转到您的终端并在命令提示符处键入以下内容$ ng new to-dolist --style=scss --routing,然后按Enter

ng命令运行 CLI 工具,其new命令指示它创建一个新的 Angular 应用程序。在这种情况下,应用程序的名称是to-dolist。您会注意到还有两个命令行标志,这是new命令的特殊选项。style 标志告诉 CLI 我们想要使用scss,而不是css,routing 标志告诉 CLI 我们希望它默认集成和设置路由。在本书中,我们将使用 SASS,而不是 CSS,并且 SCSS 是 Sass 文件的文件扩展名。作为提醒,我们将在第三章中进行 Sass 的速成课程,Bootstrap - 网格布局和组件

第一次使用 CLI 创建您的 Angular 应用程序时,它将花费 45 秒到一分钟多的时间为您创建项目。这是因为它需要为您下载和安装各种东西,然后再创建项目的文件夹结构。但是,创建后续的 Angular 应用程序时,CLI 不会花费太长时间。

提供我们的待办事项应用程序

一旦 CLI 完成创建应用程序,您可以通过转到项目目录($ cd to-dolist)并发出$ ng serve命令来运行它。这将使 CLI 运行您的应用程序。CLI 的内置 Web 服务器默认情况下将在 localhost 端口4200上监听。顺便说一句,CLI 的 Web 服务器会监视您的项目文件,当它注意到文件中的更改时,它会重新加载应用程序 - 您无需停止服务器并再次发出服务器命令。这在开发过程中进行大量更改和调整时非常方便。接下来,打开浏览器并访问http://localhost:4200,您应该会看到类似以下内容的东西,这证明了 CLI 正在正确工作:

现在 CLI 已为您创建了待办事项列表应用程序,请在 Visual Studio Code 中打开该文件夹(注意:为简洁起见,我将把 Visual Studio Code 称为 IDE)。您应该在 IDE 的左侧面板中看到待办事项列表项目的文件夹结构,类似于以下内容(除了待办事项文件夹,您目前还没有;我们将在即将到来的组件子部分中介绍如何使用 CLI 生成它)。

以下是 IDE 中 to-do 列表项目的屏幕截图(app.component.ts文件已打开):

在开发 Angular 应用程序时,您将花费大部分时间在src | app文件夹中工作。

Angular 基础知识

组件是 Angular 的基本构建块。实际上,您可以将 Angular Web 应用程序视为一个组件树。当您使用 CLI 为 Angular 应用程序生成外壳时,CLI 还会为您自动生成一个组件。文件名为 app.component.ts,位于src/app文件夹中。应用程序组件是 Angular 应用程序的引导方式,意味着它是加载的第一个组件,所有其他组件都被拉入其中。这也意味着组件可以嵌套。之前的屏幕截图显示了我们的项目目录结构,src/app文件夹已展开,并且app.component.ts在 IDE 的文件编辑器窗口中打开。.ts文件扩展名表示它是一个 TypeScript 文件。有趣的是,当您编写 Angular 应用程序时,您使用的是 TypeScript 而不是 JavaScript。实际上,Angular 团队使用 TypeScript 编写 Angular!

在以下组件部分之后,您将找到我们 Angular 应用程序的完整代码清单。有六个文件需要编辑。其中三个已经在您使用 CLI 生成的应用程序中可用。另外三个将在您使用 CLI 生成待办事项组件后在项目中可用,这是您比较项目结构和之前屏幕截图时目前缺少的目录。您将在以下组件部分中了解如何做到这一点,这就是为什么完整的代码清单被插入在后面。不要担心 - 跟着走,相信自己可以掌握 Angular,一切都会好起来的。如果你不相信我,就躺在地板上,慢慢地喃喃地说这些话,“这也会过去”,三次。

组件

这一部分是关于 Angular 组件的高层次概述 - 对 Angular 组件是什么的足够覆盖。《第六章》《构建 Angular 组件》完全专门讨论了 Angular 组件,我们将深入研究它们。可以将本节视为窥探组件幕后的一点,当我们讨论组件时,我们将拉开窗帘,好好看看“奥兹国的组件巫师”。请记住,在《奥兹国的巫师》故事中,多萝西和小伙伴们都害怕巫师,但当他最终在窗帘后面显露出来时,他们很快就不再害怕了。

正如前面提到的,你可以将组件视为 Angular 的基本构建块,将你的 Angular 应用程序视为嵌套组件树。按钮、进度条、输入字段、整个表格、高级的东西如轮播图,甚至自定义视频播放器 - 这些都是组件。你网页上的组件可以相互通信,Angular 有一些规则和协议来指导它们如何进行通信。在本书结束时,你将对组件的方方面面非常熟悉。你必须熟悉,因为这就是 Angular 大师的方式!

当你编写一个组件文件时,就像下面的代码一样,它有三个主要部分。第一部分是导入部分。中间部分是组件装饰器,你可以在这里指定组件的模板文件(定义组件的外观)和组件的样式文件(用于为组件设置样式)。

注意:由于我们使用了style=scss标志,我们得到的文件是 SCSS,而不是传统的 CSS 类型文件。导出部分是组件文件中的最后一部分,是组件所有逻辑的放置位置。组件的 TypeScript 文件中可以放入比下面代码片段中显示的更多内容,我们将在《第六章》《构建 Angular 组件》中看到。

import { Component } from '@angular/core'; 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'app';
}

CLI 在创建我们的应用程序时默认为我们创建了应用程序组件,但是我们如何创建自己的组件呢?生成新组件的最简单方法是使用 CLI 并发出以下命令:$ ng generate component name-of-component。因此,要生成一个名为to-doitem的新组件,我们将在命令提示符中键入$ ng generate component to-doitem。请记住要在src | app文件夹内执行此操作。CLI 将生成此组件并将其插入到自己的文件夹中,新创建的文件夹的名称将与组件相同。

在这个文件夹中,您将看到四个新文件,它们的名称都以to-doitem.component开头,因为我们的组件名称是todoitem,嗯,它是一个组件。我们将在后面讨论以spec.ts结尾的文件用于什么,但您可能已经猜到其他三个文件的用途。让我们验证您可能已经在想的内容;组件文件本身的确是名为todoitem.component.ts的文件。该文件包含对其他两个文件的引用:todoitem.component.html,它是组件的模板(HTML 代码,用于定义其标记结构),以及todoitem.component.scss文件,它将保存组件的样式。此外,CLI 修改了一个名为app.module.ts的现有文件。我们将在稍后更详细地讨论这个文件,但现在,您需要知道的是,该文件充当了应用程序组件的注册表。

您可能会想,“这是很多文件。它们都是必需的吗?”对此的简短回答是否定的。在第五章中,Flex-Layout – Angular 的响应式布局引擎,我们将看到如何消除.html文件和.scss文件,只需将所有组件的内容(HTML 和样式)放入组件文件中。然而,Angular 团队提供了将所有这些内容分开的机制的原因是为了使应用程序的代码整洁有序。稍后您可以感谢他们。

在使用 CLI 生成组件时的一个很好的快捷语法是键入$ ng g c name-of-component,其中g是生成的缩写,c是组件的缩写。

除了从头开始创建自己的组件,我们将在第五章中深入研究,Flex-Layout – Angular 的响应式布局引擎

待办事项列表应用程序的代码清单

现在您已经生成了待办事项组件,您在todo文件夹内有四个新文件。您将编辑其中三个文件,使其看起来像下面的代码清单。您还需要编辑项目中已经存在的三个文件,(在这里我们将打开窗帘,见到巫师),我们还可以将其他库和框架的组件集成到我们的应用程序中。我们将在第六章中看看如何使用 NG Bootstrap 进行此操作,以及在第七章中使用 Angular Material,模板、指令和管道。Angular 的组件不少,随着时间的推移可用的数量只会增加。

每当我学习新技术并跟着书籍、博客文章或其他内容时,我都会手动输入所有内容,即使文件可以下载。是的,手动输入可能是一个乏味的过程,但它会激发您的大脑,并且材料和概念开始被吸收。简单地下载文件并将内容剪切粘贴到您的应用程序中并不会产生同样的效果。我会让您决定您想要走哪条路。如果您选择下载代码,本书开头有相应的说明:

  • todo.component.html(在src | app | todo文件夹内)的代码清单如下所示:
<div class="container dark">
    <div class="column">
    <p>Add a todo item</p>
    </div>
    <div class="column">
    <p>Todo list ({{ itemCount }} items)</p>
    </div>
    </div>
    <div class="container light">
    <div class="column">
    <p class="form-caption">Enter an item to add to your todo list</p>
    <form>
    <input type="text" class="regular" name="item" placeholder="Todo item ..." 
      [(ngModel)]="todoItemText">
    <input type="submit" class="submit" value="Add todo" (click)="addTodoItem()">
    </form>
    </div>
    <div class="column">
    <p class="todolist-container" *ngFor="let todoItem of todoItems">
    {{ todoItem }}
    </p>
    </div>
    </div>
  • todo.component.ts(在src | app | todo文件夹内)的代码清单如下所示:
import { Component, OnInit } from '@angular/core';
@Component({
    selector: 'app-todo',
    templateUrl: './todo.component.html',
    styleUrls: ['./todo.component.scss']
})
export class TodoComponent implements OnInit {
itemCount: number;
todoItemText: string;
todoItems = [];
ngOnInit() {
this.itemCount = this.todoItems.length;
}
addTodoItem() {
this.todoItems.push(this.todoItemText);
this.todoItemText = '';
this.itemCount = this.todoItems.length;
}
}
  • todo.component.scss(在src | app | todo文件夹内)的代码清单如下所示:
.container {
    display: grid;
    grid-template-columns: 50% auto;
    }
    .column {
    padding: .4em 1.3em;
    }
    .dark {
    background: #2F4F4F;
    }
    .light {
    background: #8FBC8F;
    }
    input.regular {
    border: 0;
    padding: 1em;
    width: 80%;
    margin-bottom: 2em;
    }
    input.submit {
    border: 0;
    display: block;
    padding: 1em 3em;
    background: #eee;
    color: #333;
    margin-bottom: 1em;
    cursor: pointer;
    }
    .todolist-container {
    background: rgb(52, 138, 71);
    padding: .6em;
    font-weight: bold;
    cursor: pointer;
    }
    .form-caption {
    }
  • 以下是app.component.html(在src | app文件夹内)的代码清单。第一章,快速入门:待办事项列表(快速示例应用):
<br> <br>
<app-todo></app-todo>
<router-outlet></router-outlet>
  • app.module.ts(在src | app文件夹内)的代码清单如下所示:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { TodoComponent } from './todo/todo.component';
@NgModule({
declarations: [
AppComponent,
TodoComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
  • styles.scss(在src文件夹内)的代码清单如下所示:
/* You can add global styles to this file, and also import other style files */
body {
font-family: Arial, Helvetica, sans-serif;
color: #eee;
background: #869bbd;
padding: 4em;
}
a {
color: #fff;
text-decoration: none;
}
ul {
list-style-type: none;
margin: 0 0 2em 0;
padding: 0;
}
ul li {
display: inline;
margin-right: 25px;
}
ul li a {
font-size: 1.5em;
}

太棒了!现在您已经把所有的代码放在了正确的位置。您还记得如何运行您的 Angular 应用程序吗?在命令提示符处输入$ ng serve,一旦出现编译成功的消息,打开浏览器并转到http://localhost:4200。应用程序是否正常工作?如果是,恭喜您建立了您的第一个 Angular 应用程序!如果不是,请检查拼写错误。

玩一下您的新应用程序。我们还没有花时间添加编辑待办事项或删除它们的功能,但您可以通过点击浏览器的刷新按钮来清除它。

为什么刷新页面后会清空数据?这是因为我们使用的是单页应用,没有将输入的数据持久化到数据库中。当我们构建更大的应用程序时,我们一定会确保在本章末尾向您介绍的应用程序中添加持久化数据的能力。

插值

插值是从组件类中的变量获取值并在组件模板中呈现的方法。如果您还记得,组件的逻辑放在组件类的导出部分。这也是您想要使用插值的变量所在的地方,以便在模板中呈现它们的值(即在网页上呈现)。假设您有一个名为items的变量,其值目前为4。要在模板中呈现该值,您可以使用一对双大括号,变量位于其中。变量和组件逻辑都写在类内部。

别担心——在整本书中,我们会看到很多使用插值的代码片段,但现在,您可以看到这个示例代码,展示了它的作用。目前,这段代码是无意义的,是硬编码的,但它确实演示了插值。

第一个屏幕截图是组件文件(home.component.ts);变量在第 10 行声明:

第二个屏幕截图是组件的模板文件(home.component.html)。注意第 6 行的一对双大括号:

最后一个屏幕截图显示了呈现的值,这里是4。这就是插值的基础。在整本书中,随着我们在我们的注释相册上的工作,我们会看到更高级的用法:

模板化和样式

组件部分的最后几段中,我们已经提到了有关模板和样式的内容。现在我们有一个小项目可用——我们用 CLI 创建的一个——我们可以看看这在代码中是什么样子。在 IDE 中打开您的 Angular 项目,并打开app.component.ts文件。这个应用组件文件的第 5 和第 6 行包含了它关联模板(.html文件)和样式文件(.scss)的引用。以下是我 IDE 中打开项目的屏幕截图,app.component.ts文件已打开:

属性绑定

在 Angular 中,我们可以进行两种数据绑定,即单向和双向。内插类似于单向数据绑定。这是因为在这两种情况下,数据都是从组件类流向组件模板,而不是相反。属性绑定是数据绑定,因为数据被绑定到属性。

也可以进行双向属性绑定,意思是不仅可以将组件属性的值绑定到模板,而且模板也可以改变组件属性的值。这在 Angular 中通过ngModel实现。不用担心这个,我们稍后会看到双向属性绑定。只需知道在 Angular 中,单向和双向属性绑定都是可能的。

实现单向属性绑定非常简单。您只需要在组件模板中的 HTML 属性周围加上方括号,并将变量分配给它。要看一下单向属性绑定在代码中是什么样子的快速示例,请查看接下来的三张屏幕截图。

第一张屏幕截图是组件文件(home.component.ts);变量txtPlaceholder在第 11 行声明:

下一张屏幕截图是组件的模板文件(home.component.html)。在第 14 行,您可以看到输入元素的占位符属性周围有方括号:

最后一张屏幕截图是应用程序在浏览器中运行的情况。您可以看到文本“在此输入您的待办事项”被插入为文本框的占位文本,通过单向属性绑定:

事件绑定

在 Angular 中,事件绑定简单地意味着在组件内的元素上注册一个事件,当该事件发生时,将触发调用一个函数。换句话说,一个事件将调用一个函数。你可以让 Angular 监听大量的事件,比如按钮被点击,鼠标悬停在图像上,或者当用户在文本框中按下键时,还有很多其他事件。当然,你可以编写任何你能想到的函数来实现其他功能,比如调用 web 服务,改变背景页面的颜色,计算 Pi 的值到 1000 位小数,或者几乎任何你想要的其他功能。但是,我们如何在我们的 Angular 应用程序中设置事件绑定,以便将我们感兴趣的事件,绑定到我们感兴趣的元素,运行我们想要的函数呢?幸运的是,Angular 团队为我们做到了这一点。

假设我们想通过点击或悬停鼠标等事件进行一些用户交互 - 我们可以使用事件绑定来映射功能。

现在,这个例子并不是很有趣,但我们有我们的待办事项列表应用程序,可以查看我们已经编写的代码。如果你已经输入了代码清单。

我们的示例项目

学习一门新的编程语言,或者学习一个新的框架,是一个动手实验和重复的问题。即使是《生活大爆炸》中的谢尔顿博士也不能只靠读一本关于 Angular 的书就学会它。然而,只是跟随随机的代码示例并不是一件有趣的事情,而且到最后,你实际上并没有任何可以使用的东西。因此,我们在学习 Angular 的过程中采取的方法是构建一个完整的网络应用程序,这样做既有趣又实用,因为你可以部署并自己使用它。

注释照片相册

我们将一起构建的应用程序是基于我推出的一个在线服务之一,名为 Vizcaro。Vizcaro 是一个照片分享服务,但与其分享单个照片不同,你分享相册(一组照片)。此外,照片和相册将被注释,因此你可以为它们添加标题和说明。我们的版本不会拥有我在线服务提供的所有功能,但它将有足够的部分,使它成为一个很好的网络应用程序,以便学习本书中的材料。

设计原则

通常有两种设计类型:设计用户界面(GUI)的方式,以及设计软件组件(API 接口、服务、组件等)的方式。在本书中,我们将涵盖许多代码设计原则。Angular 是一个设计非常出色的软件,这对我们来说非常好,因为它为我们提供了一个完美的机会,在学习 Angular 本身的同时讨论软件设计,以及在构建我们的应用程序时。在本书的剩余部分中,我们还将涵盖用户界面设计原则,特别是在使用线框来指导我们构建模板时。

一般来说,当讨论用户界面设计时,会使用 UX 设计这个术语。从维基百科借来 UX 设计的定义:

“UX 设计是通过改善产品的可用性、可访问性和提供的互动乐趣来增强用户对产品的满意度的过程。”

这是一个很好的定义,适用于不仅仅是软件产品。

线框

线框从 80 年代初就开始存在。它们的重点,至少最初,是桌面应用程序中屏幕的功能(请记住,当时还没有网络应用程序),以及其一般布局。它们并不是用来展示最终设计的样子,包括字体选择、颜色和屏幕上控件的其他属性。实质上,它们是“纸上原型”。相反,纸上原型是使用线框的过程。值得注意的是,名词“线框”和“模型”可以互换使用,它们是同一回事。我将在本章末尾简要介绍纸上原型的过程。

线框工具

正如你可能已经猜到的,或者已经知道的,有几种工具可用于创建线框图,用于布局你的应用程序,比如 Balsamiq Mockups,Mockflow 和 Visio。对于我的 Web 应用程序,以及本书中,我更喜欢使用 Balsamiq Mockups。你最终使用哪种工具,或者已经在使用哪种工具,都无关紧要。事实上,即使你的线框图是用笔在你最喜欢的快餐餐厅餐巾纸的背面手绘的,我也觉得很酷。说真的,重要的是在编写一行代码之前养成创建线框图的习惯。为什么?因为这是明智的做法,可以节省大量时间。此外,这给了你一个完美的机会来真正思考你将要构建的东西。而且,这是你可以向用户展示的东西,以便在编写一行代码之前获得他们对可用性的反馈。还有更多好处;它让你对如何为应用程序设计数据模型以及可能使用的服务的 API 有了一些想法。你会在没有商业计划的情况下开始一项业务吗?你会在没有蓝图的情况下建造你的梦想之家吗?构建 Web 应用程序不应该有任何不同,使用线框图规范页面。永远。明白了吗?

我们注释的相册的线框图

我们将使用 10 个线框图来构建我们的应用程序,每个屏幕都有一个。以下是它们的列表,每个屏幕截图前都有简短的描述。

主页

每个 Web 应用程序都需要某种起始页面。它通常有许多名称,通常是这些之一:主页,登陆页面,索引页面或闪屏页面。我们的将是直接的。没有 Flash 动画或彩虹色的背景;它将是一个简单的页面,让用户知道网站的功能,并希望在五到七秒内完成。如果不能,你可能会永远失去访问者。

仪表板

大多数网络应用程序没有仪表板页面,但那些有的通常会提供用户拥有的东西摘要,他们上次登录的时间,以及公司希望引起用户注意的任何通知。如果您使用在线银行业务,很可能您的银行在线银行网络应用程序有一个仪表板页面 - 它可能是账户列表(支票、储蓄、信用卡、汽车贷款等),以及您在这些账户上的余额。

我们将构建一个用户将用来创建相册的应用程序,因此我们的仪表板将包含我们上传的照片数量,相册数量,上次登录时间等:

图片上传

由于我们的应用程序应该让用户创建相册,我们最好让他们有办法上传照片!是的 - 我们将专门为上传一张照片而设立一个整个网页,因为我们将在上传后使用同一个页面来预览它 - 并且撤消上传。您会惊讶地知道,有一个著名的照片分享网站直到您转到照片列表才会显示您刚刚上传的内容!立即确认您打算上传的照片实际上就是已上传的照片:

照片准备

上传照片是我们注释相册应用程序的第一步。我们将为准备照片专门设置另一个网页。这是我们将允许用户调整图像大小并对其进行注释(给它一个名称和标题)的地方。查看相册时,照片的标题将显示出来:

创建相册

在用户可以在他们的相册中添加照片之前,他们必须能够创建相册。以下网页将用于此目的:

照片列表

您总是需要考虑可用性以及如何设计尽可能直观的用户界面。此页面将显示用户上传的所有照片列表。此外,他们可以在同一页面上编辑任何照片的名称和标题。您的用户需要跳转的页面越少,他们就会越开心:

相册列表

这个页面为相册做了前一页为照片所做的事情——提供了用户创建的所有相册的列表,并提供了一种直观的方式来编辑它们的名称和描述(而不是照片的标题),而无需转到另一个网页:

工作台

工作台是用户可以将照片拖放到相册的地方。这将是我们让用户直观地将特定照片与特定相册关联的方式。顺便说一句,我们的拖放功能不仅直观而且功能强大,还会为用户增添一些乐趣。从心理上讲,用户希望在网站上“玩耍”。拖放——虽然不是令人惊叹的体验——比从照片下拉菜单中选择照片,然后从相册下拉菜单中选择相册,最后点击“连接”或“关联”按钮更有趣。前一种方法会让用户满意,而后一种方法会让他们发脾气,然后离开网站,再也不回来了。

相册查看器

归根结底,用户希望以一种引人入胜的方式查看他们的相册。拖放的东西很有趣,但他们不是为了这个而来。他们来这里是为了看到儿子生日派对的照片,女儿高中毕业的照片,或者他们梦想家园的照片。这对他们来说是一个重要的页面;这是他们使用我们网站的工作将为他们付出的地方。让我们不要让他们失望:

这就结束了我们在本书剩余部分将要构建的带注释的相册、线框和本章计划涵盖的材料的介绍。然而,我想快速讨论一下纸面原型,作为本章的结束,并将其与我们的 Angular 应用程序的规划联系起来。

纸面原型

正如本章开头提到的,纸面原型设计是一个过程。我们还提到纸面原型设计的核心重点是可用性测试。我们没有提到的是,纸面原型设计应该成为你的开发团队使用的软件开发方法论的一部分——无论是瀑布模型还是某种敏捷形式。具体来说,纸面原型设计应该在需求文档交付给项目经理后立即进行。这就是纸面原型设计是什么以及它在哪里适用的高层视图。

让我们现在来看一下在较低层面上的流程机制,也就是开发团队与即将开发的应用程序的用户之间的互动。

纸面原型设计的机制或流程是首先创建线框图并打印出来(我知道,会有更多的树被砍伐,全球变暖会变得更加严重,但纸面原型设计很重要)。一旦纸质版本摆在你面前,你的老板、客户或一群预期用户(比如焦点小组),你或其他人会像点击鼠标一样使用纸面原型,就好像它已经完成并投入生产一样。你会要求他们假装它是实际的完成应用程序。起初听起来很傻,但人类有着令人难以置信的想象力,只需很少的努力,他们就会像使用真正的东西一样使用它!这不是催眠,而是一种非常神奇的事情开始发生。除了在一开始解释你要求他们做什么以及为什么要求他们这样做时,他们会开始自发地大声谈论他们正在采取的行动,或者正在考虑采取的行动,比如,“好的,现在我需要填写这个表格并提交”,或者“按钮在哪里可以撤销我刚刚做的事情。我犯了一个错误”。通过这种练习,你从人们那里得到的最好的信息是他们提出了如何改进某些东西的建议,比如“如果我能轻松地导航回到我...”你能想象编写网页然后意识到被要求进行的更改具有深远影响并且需要耗费大量时间吗?这经常发生。

你意识到通过这样做你得到了什么了吗?你有了测试用户,而且你还没有写一行代码!这是一个非常有力的练习!试一试,然后给我发电子邮件分享你的故事。

所以,当我向客户解释这一点时——不是作为用户,而是如何向他们的用户和/或客户展示纸质原型制作过程时,我通常会被问到,但这是纸张。我们怎么改变屏幕呢?我会尽我所能地回答——通过向他们展示一个例子。我通常会带着一套线框的样本。不仅是为了演示纸质原型制作过程,也是为了展示一个良好的线框的例子。我把登录屏幕放在我们坐着的桌子上,然后让他们用手指作为鼠标指针登录,并在他们想象中的键盘上输入。在他们轻笑并和我一起假装在线框下输入他们的用户名和密码后,他们点击登录按钮,然后我成为了电脑——我拿起登录线框,放下仪表盘线框。他们通常停止轻笑,看着仪表盘页面,停顿几秒钟,然后看着我点头说,这很酷。我明白了

总结

这一章涵盖了各种各样的主题,我知道。这是不可避免的,因为没有一个最佳的起点适用于所有读者。例如,一些读者知道线框是什么,并且已经使用了多年,而其他读者可能刚刚听说过这个术语,甚至连那都不知道。这是本书的第三版,但它与前两版有很大不同,即使它基本上是相同的,这并不意味着读者已经阅读了前两版。你可以把这一章看作是一种漏斗——一个足够宽的漏斗,将各种经验水平和不同知识的读者引入学习 Angular 和本书涵盖的其他共生技术的共同轨道。从第二章开始,ECMAScript 和 TypeScript Crash Course,漏斗方法结束了。其余的章节将更加专注于手头的主题。所以,感谢你和我一起坚持下来。不过,我希望在翻阅这前几十页时,有一些事情是值得的,即使你对 Angular 并不完全陌生。

回顾一下,我们涵盖了 Angular 的演变,包括其语义版本和发布计划。虽然 NodeJS、npm 和 CLI 的安装在附录 A 中有介绍,但本章是引导该讨论的基础,并且我们使用 CLI 来构建我们的第一个 Angular 应用程序和一个待办事项列表应用程序。我们将应用程序命名为待办事项列表,因为我们是开发人员,而不是营销人员(眨眼)。我们还介绍了如何使用 StackBlitz 来构建相同的 Angular 应用程序,而不依赖于我们的本地开发环境。然后,我们介绍了 Angular 的第一个非常基本的构建模块,你需要很好地了解它们,因为它们将在你构建任何 Angular 应用程序时一再使用。具体来说,这些是模板化、属性绑定、事件绑定和类绑定。最后,我们介绍了我们将在整本书中一起构建的注释相册应用程序,并在此过程中介绍了 UX 设计原则、线框图和纸质原型。哇!天哪!

在下一章中,我们将首先了解 JavaScript 和 TypeScript 之间的关系。然后,正如其名称所示,我们将对 TypeScript 进行快速入门,并介绍它相对于 JavaScript 的优势。

第二章:ECMAScript 和 TypeScript 速成课程

第一章,快速入门,是一个杂乱的主题混合,可能看起来有点松散,但是以这种方式呈现是为了铺设与前端 Web 应用程序开发相关的一大片材料和主题,显然是为了开始你成为 Angular 大师的冒险。从这一点开始,每一章都将尽可能保持专注,并且专门致力于特定的覆盖领域。随着你逐渐深入各章,你会发现它们变得越来越技术化。这是一个自然的进步,不必害怕,因为你可能还记得,我在第一章,快速入门中对你做出的承诺之一是不要陷入技术细节以至于达到收益递减的地步。换句话说,不会有任何对我们目的没有任何价值的深奥技术的废话。相反,我们会变得尽可能技术化——不多,也不少。此外,材料将以一种引人入胜的方式呈现,你将以最小的努力获得最大的保留可能性。

不过,请不要认为这意味着你不必努力。和生活中的任何其他事情一样,你想要变得更优秀,就需要付出更多的努力。种瓜得瓜,种豆得豆。话虽如此,本章和下一章将是逐渐升级到接下来的技术深入探讨的过渡阶段——有点像在我们开始在整本书中运用 Angular 技术之前的热身。

本章将涵盖以下主题:

  • JavaScript 和 TypeScript 之间的关系。

  • TypeScript 的速成课程

(快速)路线图

本章是关于 TypeScript 的速成课程,旨在为已经熟悉 JavaScript 的开发人员快速过渡到 TypeScript 提供帮助。

正如在第一章中提到的,TypeScript 是我们在本书中处理 Angular 特定事务时将使用的语言,因此本章将作为您准备好使用 Angular 进行 Web 开发的程序化部分。您可以将第三章《Bootstrap-网格布局和组件》视为本章的表兄弟,因为它的目标类似,但是针对的是呈现方面(即网页布局),而不是程序化方面。第二章《ECMAScript 和 TypeScript 速成课程》和第三章《Bootstrap-网格布局和组件》将一起完成构建客户端 Web 应用程序的先决条件-无论客户端 Web 应用程序框架如何,但也特别适用于基于 Angular 的应用程序。从第五章《Flex-layout-Angular 的强大响应式布局引擎》开始,几乎都将是以 Angular 为中心。简而言之,这是本章和下一章的路线图。让我们开始热身我们的技术肌肉!

JavaScript 和 TypeScript 之间的关系

JavaScript 和 TypeScript 是密不可分的。因此,虽然本章涵盖了 ECMAScript 和 TypeScript 两种技术,但它们足够相似,以至于本章可以同时涵盖两者。它们彼此有多相似?嗯,大部分时间,您可以将 TypeScript 视为 JavaScript 的超集。它们之间关系最有用的描述是:TypeScript 是一种带有许多强大功能和可选类型的严格类型语言,通过其转译器,它变成了普通的 JavaScript。这对开发人员来说非常重要,并带来了几个优势;这足够引人注目,以至于谷歌的 Angular 团队决定从 JavaScript 转换到 TypeScript 来开发 Angular 本身。我们将很快介绍转译器是什么,以及使用 TypeScript 的优势是什么。

JavaScript 的一系列幸运事件

在我们深入技术部分和代码之前,值得快速看一下 JavaScript 的演变以及导致需要像 TypeScript 这样的语言的一些驱动因素。此外,就像 Angular 的命名混乱在开发社区中引起了一些困惑,自从 20 多年前诞生以来,JavaScript 的版本过去更加混乱,因此我想尝试澄清一些关于 JavaScript 版本命名的混乱。更重要的是,我想讨论一下我所说的 JavaScript 的一系列幸运事件。这将有助于为我们在本书的其余部分一起涵盖的大部分内容设定节奏。

我必须承认,我喜欢使用 JavaScript。我一直喜欢这种语言,不是因为语言本身,而是因为它让我们能够使网络生动起来,而无需其他插件,如 Flash 或 Shockwave。然而,近年来,我喜欢使用这种语言的原因还有一些额外的原因,而我喜欢 JavaScript 的确切原因正是我即将介绍的一系列幸运事件。话虽如此,我在行业中有一些朋友持相反的观点,他们认为 JavaScript 是一种“玩具语言”,并且更倾向于使用 Java 和 C#等语言,尽量避免使用 JavaScript,直到他们不得不勉强为客户端编写一些代码。这些老手对 JavaScript 的使用通常不会超出使用 jQuery 库将点击事件绑定到函数调用(以提交表单数据到他们的 Java 或 C# API)。当然,大约十年前,由于 JavaScript 只能在客户端(即浏览器)上运行,没有那么多的库,也没有真正高性能的运行时,因此 JavaScript 并不像 Java 或 C#那样强大。所有这些都将因为一系列幸运事件而发生改变,具体来说,有三个。让我们快速回顾一下。

Chromium 项目

第一个是谷歌的 Chromium 项目。2008 年 9 月,谷歌发布了 Chrome V8,这是一个高性能的 JavaScript 引擎。谷歌的 Chrome V8 极大地提高了 JavaScript 代码的执行方式。这是一个非常成功的项目,它使其他技术得以实现,包括真正立即和永远改变了 JavaScript 未来的技术:Node.js(简称 Node)。

JavaScript 框架

JavaScript 的统治地位作为最重要的 Web 应用程序编程语言,甚至可能是移动应用程序和甚至桌面应用程序的事件系列中的第二个事件,是 JavaScript 框架的爆炸式增长。自 2010 年以来,开发世界对创建基于 JavaScript 的框架的渴望已经变得非常疯狂,不仅用于客户端 Web 应用程序开发(如 Ember,Knockout 和 React),还用于服务器端库(再次感谢 Node),用于创建原生移动应用程序的框架(如 Ionic,React Native 和 Native Script),以及用于开发桌面应用程序的框架(如 Meteor 和 Electron)。我在这段话中没有提到 Angular,因为我们已经知道 Angular 可以用于构建跨平台应用程序,涵盖了浏览器、桌面和原生移动应用程序的所有三个领域。

ECMAScript 2015

幸运事件系列中的第三个事件是 ECMAScript 2015 的发布。ECMAScript 是 JavaScript 标准的官方名称。尽管主要版本发布数量增加,但 JavaScript 语言多年来基本上没有发生变化。这是由于影响力玩家之间的分歧(最好不要提),导致语言的发展分裂和停滞不前。

总之,这是对 JavaScript 当前状态及其生态系统的一个快速概述。JavaScript 的生态系统非常庞大,需要写好几本书来覆盖它。例如,我们甚至没有提到可视化 JavaScript 库。JavaScript 有数百甚至数千个可用于项目的库,我们甚至无法开始覆盖。但是,JavaScript 生态系统中有一部分我们一定会涵盖:单元测试。您可能知道单元测试的重要性,并且可能已经使用诸如 JUnit、NUnit、RSpec 等框架为服务器端代码编写了单元测试,具体取决于您使用的编程语言。但是,客户端上的单元测试同样重要,大多数开发人员并没有进行单元测试,即使他们可能已经为服务器端编写了单元测试脚本。在第十三章中,单元测试,您将学习如何为客户端编写单元测试,特别是如何编写它们来测试您的 Angular 应用程序。我们将一起介绍的两个框架是 Jasmine(一种流行的单元测试框架)和 Karma(一个具有用于测试框架的插件的测试运行器,如 Jasmine)。

有了本章的技术前言,让我们戴上潜水装备,潜入 TypeScript 的海洋吧!

TypeScript 速成课程

TypeScript 对开发人员比 JavaScript 具有许多优势,包括以下内容:

  • 纯粹的面向对象

  • 可选的静态类型

  • 类型推断

  • 访问 ECMAScript 功能

  • 转译

  • IntelliSense 的强大工具支持

  • 您可以构建 Angular 应用程序!

转译与编译

开发人员通常可以在编程的上下文中定义编译是什么。定义可能是这样的:编译是将源代码通过另一个称为编译器的程序转换为机器可读代码的过程。这个结果代码通常被称为汇编代码,它是一组本机于机器 CPU 的机器指令,代码是要在其上运行的机器。

另一方面,转译是将用一种语言编写的源代码转换为另一种(或目标)语言的等效代码的过程。虽然这个定义对于讨论来说已经足够好了,但为了完全准确,我们还必须注意到源语言和目标语言实际上可能是同一语言的不同版本(或发布)。对于我们的转译需求,我们将使用 TypeScript 的转译器 tsc,它与 TypeScript 捆绑在一起。我们关心转译的原因是因为我们将在构建 Angular 应用时使用 TypeScript 编写代码。然而,Web 浏览器只有 JavaScript 引擎/解释器,因此我们需要一种将其转译为 JavaScript 的方法。

letconst关键字是在 ES6 中引入的。为了讨论它们是什么以及它们如何工作,让我们回顾一下var关键字的作用。在 ES6 之前,初始化变量的方式是使用var关键字。关于var需要记住的两件事是:

  • 当您使用var在函数体外定义变量时,它将成为全局作用域。这意味着 JavaScript 文件中的所有其他函数都可以访问它。虽然这有时可能很方便,但也可能很危险,因为值可能会被意图之外的函数无意中更改。当多个函数引用相同的变量名时,这种情况是可能的。

  • 当您使用var在函数内部定义变量时,它将成为局部作用域。与全局作用域变量相反,局部作用域变量只能在创建它们的函数内部访问。这是真实的,无论块作用域如何,因为使用var关键字声明的 JavaScript 变量作用域限定在最近的父函数内。

当然,您仍然可以使用var来声明和定义变量,因为该关键字尚未被弃用。只是现在您对初始化代码的行为有了更明确的控制,并且使用letconst后代码的可读性得到了改善,因为在查看 JavaScript 代码时意图是清晰的。

let关键字创建了块作用域的局部变量,并且其名称来源于其他具有类似构造的语言,比如 Lisp、Clojure、Scala 和 F#。在这些语言中,使用let声明的变量可以被赋值,但不能被改变。然而,在 ES6 中,使用let赋值的变量可以被改变;即使如此,无论它们是否被改变,该变量都是一个本地块作用域变量。

如果你觉得这有点令人困惑,你并不孤单。要牢固理解变量作用域的微妙之处并不是仅仅通过阅读就能学会的事情。编程就像学数学(或者学大多数事情一样):你做得越多,就会变得越好。话虽如此,将所有这些都归纳到你的脑海中的一种方法是看待varlet之间的这个主要区别:由于在一个函数内可以有多个块,甚至是嵌套块(或子块),使用let关键字定义的变量只能在其定义的块内访问,以及从该块的嵌套块中访问。相比之下,使用var定义的变量的作用域是整个封闭函数。记住这个主要区别,你就掌握了。

让我们看一些代码来理解let关键字的影响,然后我们可以继续讨论const关键字:

let x = 5;
 if (x === 5) {
     let x = 10;
     console.log(x); // this line will output 10 to your console
     // note, x was allowed to be changed
 }
 console.log(x);
 // this line will output 5 to your console
 // because the change to x was made within a block

常量

const关键字创建一个常量。你会很高兴知道,因为你已经经历了理解let关键字的痛苦,理解const关键字的作用将会非常简单。准备好了吗?在这里……constlet在它们的作用域工作方式上是相同的。letconst之间唯一的区别是你不能重新声明一个常量,它的值也不能被改变。就是这样。让我们继续讨论一些数据类型。

数据类型

每种编程语言都有数据类型。它们只在可用类型的数量以及类型变量可以保存的值(以及数字类型的值范围)方面有所不同。我不会在本书中探讨强类型与静态类型与弱类型语言之间的哲学辩论(通常称为静态与动态类型)—但由于本章专门讨论 JavaScript 和 TypeScript,我需要简要谈一下它们的类型。

JavaScript 是一种弱类型语言——也就是说它是一种动态语言,而不是静态语言。这意味着 JavaScript 中的变量不绑定到任何特定类型,而是它们的值与类型相关联。变量可以被分配和重新分配给所有可用类型的值。虽然方便,但由于没有编译器检查值是否符合类型引用,因此很难找到错误。这是因为当你使用varletconst声明变量时,你没有指定关联的类型。

相比之下,TypeScript 是可选的静态类型。这里的关键词是可选。TypeScript 是一种静态类型语言,但它不强制你明确注释你的变量所需的类型。这是因为 TypeScript 具有所谓的类型推断,也就是说 TypeScript 运行时将在运行时推断变量的数据类型。这是 TypeScript 的默认行为。现在,这就是可选部分的地方……如果你想严格地给变量加上类型,从而将数据类型绑定到变量而不是与变量的值相关,你必须在变量声明中明确添加类型注释。

这是代码中的情况:

var num: number = 12345; // this is a statically typed variable
var num = 12345; // this is a dynamically typed variable

前两行都是有效的 TypeScript 语法,但它们之间有所不同:

  • 第一行是静态类型变量,使用num关键字进行注释,由 TypeScript 转译器检查,任何问题都将由它报告

  • 第二行,变量声明以 JavaScript 的方式进行(也就是说,没有静态类型的注释),不受检查,任何问题只能在运行时发现。

ES6 有七种数据类型,其中六种被称为原始数据类型,一种被称为引用数据类型(只是称为Object)。JavaScript 标准库中还有几种内置数据类型,但由于这不是 JavaScript 的全面覆盖,我们只会在这里涵盖其中一些:你可能会在你的 Angular 开发中使用的那些。

以下是提供的原始数据类型列表:

  • 未定义

  • 布尔值

  • 数字

  • 字符串

  • 符号

以下是提供的内置数据类型:

  • 日期

  • 数组

  • 地图

  • 集合

对象

仅具有原始数据类型和内置复杂数据类型并不足以编写表达性软件,试图模拟现实世界或虚构世界(在游戏中)。解决方案是拥有具有创建自定义对象构造的编程语言。幸运的是,JavaScript,因此 TypeScript,是一种允许创建自定义对象的编程语言。JavaScript 中的对象是一组映射键和值的集合,其中键可以是字符串或符号。这类似于许多其他编程语言的情况,例如 Python 的字典和 Ruby 的哈希。不要仅仅为了技术而变得太技术化(这是我讨厌的事情,也可能是你讨厌的事情),但 JavaScript 不是经典面向对象的语言。相反,JavaScript 使用原型继承来创建其他对象,而不是从类定义创建对象的实例。换句话说,JavaScript 没有类的概念。JavaScript 有原型。JavaScript 中的对象直接从其他对象继承。实际上,当您在 JavaScript 中使用大括号创建一个空对象时,这实际上是使用内置对象的create方法的语法糖。在 JavaScript 中有几种可用的方法可以创建一个空对象。我们不会在这里涵盖它们所有,但我们将涵盖两种多年来在 JavaScript 中可用的方法,并在 ES6 中提供给我们的方法:

  • 使用Object构造函数:var myObject = new Object();

  • 使用大括号语法:var myObject = {};

  • 使用 ES6 类语法(我们将在接下来的部分介绍语法)

前两种方法创建一个空对象。如果您想在 JavaScript 中轻松创建一个空对象,第二种方法显然是最简单的。然而,第三种方法,ES6 类语法,是我们将在本书中使用的方法。

JSON

JSON 是 JavaScript 对象表示法的缩写,不是一种数据类型,而是结构化数据。JSON 被用作轻量级数据交换格式,并被许多编程语言使用。我们不仅将在稍后更详细地介绍这一点,而且我们将广泛使用这种格式在我们的 Angular 应用程序和我们为其构建的后端 Web 服务之间传递数据。就像编程语言有数据类型一样,数据交换格式通常也有。以下是 JSON 允许表示的数据类型:

  • 字符串

  • 数字

  • 对象

  • 数组

  • 布尔

您可能已经注意到 JavaScript 和 JSON 数据类型之间有很大的重叠。这并非偶然,因为 JSON 是 JavaScript 对象表示法,因此是模仿 JavaScript 的数据类型而建模的。以下是一个包含三个人的姓名和年龄的 JSON 数据的示例(每个都是 JavaScript 对象):

{
   “people”: [
     {“name”:”Peter”, “age”:40},
     {“name”:”Paul”, “age”:42},
     {“name”:”Mary”, “age”:38}
   ]
 }

在前面的 JSON 示例中,我有people作为键,其值是一个包含三个people对象的数组。并没有硬性规定说你必须给嵌套结构命名,但这样做会更易读。在这个简单的例子中,你可以省略键而不会丢失数据,就像下一个 JSON 示例所示的那样:

[
   {“name”:”Peter”, “age”:40},
   {“name”:”Paul”, “age”:42},
   {“name”:”Mary”, “age”:38}
 ]

然而,在第一个例子中,我们有people键,不仅更容易阅读,而且在代码中更容易处理。当我们在第十二章中为我们的应用编写 RESTful web 服务 API 时,我们将采取第一种方法,为我们的数据集提供键。

关于数据交换格式,这里有一个有趣的注释。虽然有一些可供选择的格式,比如 XML 和 SOAP(简单对象访问协议),但在开发 Web 服务时,JSON 是最受欢迎的格式,它受到 JavaScript 的启发。

如果没有 JavaScript,我们会在哪里呢?我不敢想象。

JavaScript 运行环境

本章的其余部分有许多代码片段,所以如果您想在阅读本章的过程中尝试材料,最好启动 JavaScript 运行环境。除非您使用 JavaScript IDE,比如 JetBrains 的 WebStorm,否则您有几种选择可以用来测试 JavaScript 代码。以下是其中的三种选择:

  • 你可以使用在线的 JavaScript 控制台,比如es6console.com/

  • 你可以在终端中使用 Node(附录 A 向你展示了如何安装 Node)。

  • 你可以在浏览器的开发者工具中使用控制台。例如,我主要使用 Chrome 进行开发,而 Google 有出色的开发者工具。

任何这些选择都可以很好地工作。我更喜欢在 Node 终端中快速地做一些小事情,这也是我用来测试我为本章编写的代码的工具。

数组

数组是对象集合的一部分,被称为索引集合。如果你写过一定量的 JavaScript,你一定使用过数组。数组对象可以保存任何有效的 JavaScript 数据类型,并且可以通过调用它们的内置方法(如pushsplice)来增长和缩小。你可以使用indexOf方法来搜索数组中值的存在,使用length属性来获取数组的长度,等等。创建一个空数组的 JavaScript 语法如下:

var myDreamCars = [];

然后你可以使用数组的内置push方法来向数组中添加一个项,就像这样:

myDreamCars.push("Porsche");

或者,你可以一次性地创建数组,就像这样:

var myDreamCars = ["Porsche", "Mercedes", "Ferrari", "Lamborghini"];

indexOf方法非常方便,我们肯定会在后面用到它。在继续讲解TypedArrays之前,让我们快速介绍一下这个方法。当你需要找到数组中特定项的位置,或者判断它是否存在于数组中时,你可以使用indexOf方法。假设我们想要查找 Mercedes 在数组中的位置。我们可以这样搜索:

var indexOfMercedes = myDreamCars.indexOf("Mercedes");

给定我们的myDreamCars数组,indexOf函数会返回 1。这是因为 JavaScript 中的数组从 0 开始索引,而 Mercedes 在我们的数组中是第二个位置。如果我们要查找的东西不在数组中会怎样呢?让我们看看当我们查找 Corvette 时会发生什么:

var indexOfMercedes = myDreamCars.indexOf("Corvette");

当执行上述行时,indexOf函数会返回-1,这表示我们搜索的项没有找到。

TypedArray

TypedArray在 ES6 中使用,尽管它具有与普通 JavaScript 数组对象相同的一些方法,但它与您可能期望的有很大不同。事实上,TypedArray根本不是数组。如果您尝试将TypedArray传递给Array.isArray(),您会发现返回的值是false。那么它们是什么呢?TypedArray为我们提供了对底层二进制数据缓冲区的视图,并使我们能够访问和操作数据。我们不会在本书中涵盖TypedArray,因为我们不会使用它们,它是一种高级数据类型和机制,但我提到它的原因是让您知道它的存在。在我们继续之前,让我至少解释一下它的创建动机和您可能希望考虑可能使用它的用例。TypedArray随着 ES6 的出现而出现,因为 Web 应用程序变得越来越先进,客户端机器现在有如此多的可用性能,编写一个处理和操作音频和视频的客户端应用程序是一个好主意。为了做到这一点,您需要一种机制来使您的 JavaScript 代码能够读取和写入这些原始二进制流的数据。

您可能希望构建的两个立即可以使用TypedArray的示例是:

  • 视频编辑(您希望删除不需要的镜头段)

  • 对音频进行采样(更改声音字节的频率,也许创建原始样本的 11 个版本以创建一个音阶,以便能够从原始单个样本中演奏旋律)

再次,这是 JavaScript 发展的一个例子。

地图

地图是一种数据结构,它在 ES6 中引入了 JavaScript。地图用于将值映射到值。此外,它们允许使用任意值作为键,这意味着您可以使用整数作为键,或字符串,甚至对象;但是,不允许使用符号作为键。还有一些方便的方法可以在地图上执行操作,并且您还可以对地图进行迭代。让我们来看一些创建地图的代码,并探索一些常见的内置函数。首先让我们创建我们的地图。我们将创建一个用于将学习曲线映射到编程语言的地图,使用new关键字:

var mapLangCurve = new Map();

现在让我们使用地图的设置功能向其添加一些条目:

mapLangCurve.set('Python', 'low');
mapLangCurve.set('Java', 'medium');
mapLangCurve.set('C++', 'high');

虽然你可以一次在Map中添加键值对,就像我们刚刚做的那样,但set方法是可链接的,所以我们可以使用这种语法来完成完全相同的事情,这样可以节省一些输入:

var mapLangCurve = new Map().
set('Python', 'low').
set('Java', 'medium').
set('C++', 'high');

或者,作为声明和初始化我们的语言学习曲线Map的第三种方法,我们可以将一个包含两个元素数组的数组传递给 Map 的构造函数。假设我们的两个元素数组设置如下:

var arrLangCurve = [['Python', 'low'], 
['Java', 'medium'], 
['C++', 'high']]; 

然后我们可以像这样将其传递给构造函数:

var mapLangCurve = new Map(arrLangCurve);

这三种创建Map的方法都会产生完全相同的结果。好了,让我们继续快速看一下可以在Map上执行的一些常见操作。我们可以使用size属性获取 Map 的大小:

var langCurveSize = mapLangCurve.size;

我们可以使用get函数检索键的值:

var javaLearningCurve = mapLangCurve.get('Java');

我们可以使用has函数检查Map中键的存在:

var blnCurveExists = mapLangCurve.has('C++'); 

我们可以使用delete函数删除一个键及其值:

mapLangCurve.delete('Python');

我们可以使用clear函数一次性清除一个集合中的所有项。

如果你在 JavaScript 环境中跟着做,现在不要尝试这样做,因为我们需要一些数据来迭代:

mapLangCurve.clear();

在 JavaScript 中,你可以很容易地使用for构造迭代Map,但我们需要知道我们想要迭代什么。我们想要获取我们地图的键吗?还是它的值?或者我们想要获取两者。这是如何迭代我们地图的键:

 for (let key of mapLangCurve.keys()) {
    console.log(key);
} 

这是如何迭代我们地图的值:

 for (let value of mapLangCurve.values()) {
    console.log(value);
} 

这是如何迭代我们地图的键和值:

for (let item of mapLangCurve.entries()) {
    console.log(item[0], item[1]);
} 

大多数情况下,你可能希望访问你的地图的键和值,所以你应该使用地图的entries函数。

在本章的稍后部分,我们将看一下 ES6 给我们的一个构造,叫做解构,它使我们能够直接访问键和值。

WeakMap

WeakMap是一个有趣的生物,它不是从Map继承而来,尽管它们都是键值对的集合,并且共享一些相同的功能。

MapWeakMap之间最重要的区别是可以用作它们的键的数据类型。通过Map,我们已经看到可以使用各种数据类型作为其键,包括对象。但是,WeakMap只能将对象作为其键。这是有意设计的,并且使WeakMap特别有用,如果只关心在键尚未被垃圾回收时访问键的值。让这一点沉淀片刻。我知道这听起来像一个奇怪的用例,但是如果考虑到WeakMap可以帮助减轻 JavaScript 程序中的内存泄漏,可能足够考虑如何在代码中使用WeakMap

数据结构名称中的Weak部分来自于WeakMap弱引用其键对象。这意味着它们有可能被垃圾回收。这一事实导致了我们的下一个观点。由于我们的键可能在任何时候被垃圾回收而无需我们的参与,因此将它们列举出来是没有意义的,因此它们不是,这意味着我们无法遍历集合。

如果需要遍历集合中键或值的列表,则应使用Map。相反,如果不需要遍历Map,只打算将其用作查找表,则可以考虑使用WeakMap

我们将在下一节学习Set

Set

Set是一个唯一值的集合,并且可以按照其元素添加到其中的顺序进行迭代。Set可以包含同类数据,但是每个数据(即元素)都需要是唯一的。如果尝试将现有元素添加到集合中,对集合不会产生影响。

集合具有许多与映射相同的功能。让我们创建一个Set对象,并快速浏览一些常用的函数。

创建一个集合,我们使用new关键字调用它的构造函数:

var setCelestialObjects = new Set();

让我们向我们的Set添加一些元素(即天体):

setCelestialObjects.add('Earth');
setCelestialObjects.add('Moon');
setCelestialObjects.add('Solar System');setCelestialObjects.add('Milky Way');
setCelestialObjects.add('Andromeda');
setCelestialObjects.add(['Aries', 'Cassiopeia', 'Orion']);
setCelestialObjects.add(7);

好吧,数字7并不完全是天体,但我想向您展示您可以将不同类型的元素添加到同一个Set中。与我们的星座数组一样:我们可以向我们的Set添加数组和任何类型的对象。

我们可以使用size属性获取我们的Set的大小:

var sizeCelestialObjects = setCelestialObjects.size;

现在不要这样做,但是您可以使用其clear函数清除Set

setCelestialObjects.clear();

我们可以通过将其值传递到集合的delete函数中来从我们的Set中删除一个元素:

setCelestialObjects.delete('Andromeda');

您可以使用for循环来迭代Set,就像我们用于Map一样:

for (let element of setCelestialObjects) {
    console.log(element);
}

如果您想对Set中的每个元素执行操作,可以使用 set 的forEach函数,该函数以回调作为其参数。例如,如果您的Set中有一组整数,并且想要对它们进行平方运算,可以这样实现:

var setIntegers = new Set();
setIntegers.add(1);
setIntegers.add(7);
setIntegers.add(11);
setIntegers.forEach(function(value) {
    console.log(Math.pow(value, 2));
});

上述代码不会更改我们Set中的元素,而是将平方值打印到控制台。我们无法轻松地就地更改我们的元素,但是我们可以创建一个新的Set并将我们的平方元素存储在其中,就像这样:

var setSquaredIntegers = new Set();
setIntegers.forEach(function(value) {
    setSquaredIntegers.add(Math.pow(value, 2));
});

我们可以使用has函数来检查我们的Set中是否存在元素:

var blnElementExists = setCelestialObjects.has('Moon');

如果您还记得我们讨论过的MapMap对象有以下三个函数:keys,values 和 entries。Set也有这三个函数,但它们的返回值是非常不同的。当您在Set上调用这些内置函数时,将会得到一个SetIterator对象。

在本书中,我们不会使用SetIterator,但是就像我给您介绍TypedArray的用例一样,我想给您介绍SetIterator的用例。Map对象和Set对象是不同的数据结构,您可以以不同的方式迭代这些结构。如果使用迭代器,可以构建一个可以以相同方式迭代这两种类型数据结构的函数。换句话说,您可以将对象传递到迭代集合的函数中,而不必担心它们的类型。

WeakSet

WeakSet是一组弱引用对象,每个对象必须是唯一的;不允许添加重复的对象。回想一下我们在WeakMap上的讨论,由于它们的键只能是对象,因此它们的键可能会在我们脚下被垃圾回收。因此,就像WeakMap一样,对于迭代,WeakSet也是如此:我们无法对集合进行迭代。

WeakSet有非常少量的内置函数,即 add,delete 和 has。WeakSet还有一个length属性,类似于数组,而不是Mapsize属性。

让我们快速看一下创建WeakSet对象的语法,以及它的属性和函数。

我们可以使用其构造函数创建一个空的WeakSet对象:

var myWeakSet = new WeakSet();

让我们创建三个空对象添加到我们的WeakSet中,然后使用WeakSet对象的add函数将它们添加到其中,然后使用其length属性获取它包含的对象数量:

var objA = {};
var objB = {};
var objC = {};
myWeakSet.add(objA); 
myWeakSet.add(objB); 
myWeakSet.add(objC);  
var lengthMyWeakSet = myWeakSet.length; // lengthMyWeakSet will be set to 3

您可能会问,等一下。您说对象必须都是唯一的,任何重复的对象都不会被插入到 WeakSet对象中。所有对象都是空的;它们不是相同的吗?是的,当尝试插入操作时,重复的对象将被拒绝。然而,虽然我们的三个对象都具有相同的值(即它们都是空的),但它们实际上是三个独立且不同的对象。

在 JavaScript 中,与大多数其他面向对象的语言一样,决定对象是否与另一个对象相同的是对象引用(即底层内存地址),而不是其内容。

以下是您可以比较两个引用同一对象的对象变量的方法:

var blnSameObject = Object.is(objA, objB);

objAobjB各自引用空对象,但这是两个不同的对象;因此,blnSameObject将被设置为false

如果我们做了以下操作,由于objBobjC变量指向内存中的同一个对象,试图将objC添加到myWeakSet的行将不会对myWeakSet产生影响,因为底层对象已经包含在WeakSet对象中:

var objA = {};
var objB = {};
var objC = objB; // objB and objC now both point to the same object in memory
myWeakSet.add(objA); 
myWeakSet.add(objB); 
myWeakSet.add(objC);  
var lengthMyWeakSet = myWeakSet.length; // lengthMyWeakSet will be set to 2

几页前,我们介绍了在 JavaScript 中创建对象的三种不同方法。我还提到我们将在后面介绍如何使用 ES6 类语法创建对象。此外,我还提到 JavaScript 没有类的概念,但我们在本节中涵盖了类。让我们澄清这一切,看看如何在 JavaScript 中创建类,以及如何从这些类创建对象。

在 ES6 之前的 JavaScript 版本中,不存在类的概念。相反,每当创建对象时,JavaScript 运行时会直接从其他对象继承,而不是从类继承(请记住,JavaScript 不是经典的面向对象语言;它使用原型继承)。这并不是说 JavaScript不好,但它确实与众不同。为了将经典面向对象的风格和语义带入,ES6 给我们带来了类的概念。类是对象的蓝图,当我们从这个蓝图或模板创建一个对象时,它被称为实例化。我们使用类来实例化(使之存在)一个或多个对象。让我们来看一下语法。

让我们创建一个Car类,并为其提供一个构造函数、三个属性和三个方法:

class Car {
  constructor(make, model) {
    this.make = make;
    this.model = model;
    this.speed = 0;
  }
  get speed() {
    return this._speed;
  }
  set speed(value) {
    this._speed = value;
  }
  speedUp() {
    this.speed += 10;
  }
  slowDown() {
    this.speed -= 10;
  }
}

我故意在这里使用了术语方法,而以前我总是把它们称为函数。因为我们现在讨论的是类和对象,在经典的面向对象术语中,方法函数更好。

你需要记住任何面向对象语言的两种关系,如下:

  • 对象是它们类的实例

  • 对象封装了操作数据的数据和方法

数据代表对象在任何时刻的状态,方法代表对象具有的行为。就是这样。

好了,回到我们的类示例。我们的Car类有一个构造函数,它接受两个参数:汽车的制造商和型号。它还有三个实例变量:makemodelspeed。此外,它有两个方法,speedUpslowDown。最后,speed实例变量实际上是一个属性;这是因为它有一个关联的 getter 和 setter。

需要注意的是,我们类中的 setter 和 getter 在属性名称前面有一个下划线,而关联的实例变量没有。这很重要,因为如果没有下划线,JavaScript 运行时在实例化您的Car对象时会抛出异常(即RangeError: Maximum call stack size exceeded)。

太好了!那么,我们如何创建它的实例(即Car对象),以及如何调用它的方法和读取它的属性?以下是代码来帮助回答这些问题。

我们创建我们的Car对象就像创建任何其他对象一样,通过调用它的构造函数:

var myG6 = new Car('Pontiac', 'G6');

让我们读一下我们车的当前速度:

myG6.speed; // this returns 0, which is the value it was initialized to

哦,天啊!零英里每小时?这是不可接受的。让我们踩油门!看这个:

myG6.speedUp(); // this increases our speed by 10 mph
myG6.speedUp(); // this increases our speed by another 10 mph
myG6.speedUp(); // this increases our speed yet again, by 10 mph
myG6.speedUp(); // this increases our speed, one last time, by ... you guessed it, 10 mph

我们的速度有多快?我们最好检查一下:

myG6.speed; // this now returns 40 

该死!我们刚刚进入了一个学校区,必须将最高速度降到 20 英里/小时:

myG6.slowDown(); // this decreases our speed by 10 mph
myG6.slowDown(); // this decreases our speed by another 10 mph

让我们再次检查我们的速度:

myG6.speed; // this now returns 20

呼!好了,目前我们的速度还可以。

总结一下这一部分,在 JavaScript 中有几件事情要记住:

  • 与 Java 或 Python 不同,JavaScript 中的类只能有一个构造函数。不支持构造函数重载。

  • 你可以在你的类中使用 super 调用(用于调用层次结构中更高级别的类的构造函数),但必须在使用this引用之前调用它,就像我们将makemodel参数分配给它们各自的实例变量时一样。

接口

到目前为止,我们一直在看一些 JavaScript 的新添加,这些对我们来说是可用的。对于我们来说,接口部分是 TypeScript 特有的东西,因为 JavaScript 没有接口的概念。

接口就像是类的合同,并提供了类必须遵循的一组规则。让我们从构建Car类转换到构建Animal类,并且在此过程中,让我们的类实现一个我们称之为Species的接口:

class Animal implements Species {
}  
interface Species {
  name: string;
  isExtinct: boolean;
} 

我们的Animal类是空的。它甚至没有构造函数或任何实例变量,但对我们来说这并不是问题,因为它仍然可以满足我们演示如何使用接口的目的。

稍微看一下Species接口。你会注意到一些事情:

  • 它有两个公共属性。TypeScript 有访问修饰符,就像 Java 和 C#一样,我们将在后面的章节中使用它们时介绍。现在,你只需要知道属性上缺少访问修饰符意味着属性是公共的。这很重要,因为由于接口描述了实现它的类的公共接口,其属性必须是公共的。

  • 第二件你会注意到的事情是我们在输入属性。我们声明name属性的类型为字符串,isExtinct属性的类型为布尔值。这是 TypeScript 的一个主要优势,正如我们之前学到的那样,也是 TypeScript 得名的原因(即,一个有类型的 JavaScript)。

我们将在本书的后面看到访问修饰符的作用。有三种:

  • 公共的: 这是默认的修饰符,意味着属性或函数对所有其他代码可见

  • 私有: 类的属性和函数标记为私有的可供其声明的类的成员函数使用

  • 受保护的: 这与私有相同,但类成员也对从其声明的类继承的任何类可见

我们将类与接口结合的方式是在类定义中使用implements关键字,就像我们在这个例子中所做的那样。一旦我们这样做,类必须遵守接口的合同。

那么现在呢?如果Animal类没有实现Species接口规定的两个属性,那么 TypeScript 在转译过程中会抛出错误。

我们还可以通过在属性或函数的末尾添加问号来描述一个可选的接口。我们的接口中没有列出函数,但我们也绝对可以有函数。

如果我们的接口是一个可选的合同,它会是这样的:

interface Species {
 name?: string;
  isExtinct?: boolean;
} 

继承

我们提到,标有受保护访问修饰符的类成员也对任何从它们声明的类继承的类可见,因此我们最好快速讨论一下继承。

继承并不意味着我们创建的类会变得独立富裕;那是一种完全不同的继承。我们谈论的继承类型可能不那么令人兴奋,但对于我们的 JavaScript 代码来说,它更有用。

一个类可以从另一个类继承。为了做到这一点,我们在类定义中使用extends关键字。让我们再次转换一下,这次是从AnimalEmployee(尽管我在一些客户地点的一些浴室和厨房看到过一些动物,我可以告诉你一些员工也可以是动物)。让我们来看代码:

class Employee {
  constructor(name) {
    this.name = name;
    this.title = "an employee";
  }
  annouceThyself() {
    return `Hi. My name is ${this.name} and I am ${this.title}.`;
  }
}
class Manager extends Employee {
  constructor(name) {
    super(name);
    this.title = "a manager";
  }
}

让我们创建一个Employee对象,并让员工宣布自己:

var emp = new Employee("Goofy");
console.log(emp.annouceThyself()); 
//  Hi. My name is Goofy and I am an employee.

让我们创建一个Manager对象,并让经理宣布自己:

var mgr = new Manager("Mickey");
console.log(mgr.annouceThyself()); 
//  Hi. My name is Mickey and I am a manager.

这里发生了什么:

  1. 我们创建了一个Employee类。

  2. 我们创建了一个从Employee继承的Manager类。

  3. Manager类除了构造函数之外,没有任何属性或函数。但是,它从Employee类继承了属性(nametitle)和方法(annouceThyself)。

  4. Manager类中的构造函数调用Employee类中的构造函数,传递经理的名字。

  5. 经理的构造函数重新分配了title属性的值。

这是相当简单的,但这里有两个要点要记住:

  • 继承类从它继承的类中获取所有类成员。

  • 一个构造函数可以调用它的父类的构造函数,并且如果父类有父类,等等,这种情况可以一直延续下去。

解构

解构是一个非常酷和非常有用的构造,我们将在本书中多次使用它,而且在你完成本书后,你在你的 Angular 项目中也无法离开它。简而言之,解构是一个 JavaScript 表达式,它使我们能够轻松地从对象和集合中提取数据。

当我们看Map对象时,我提到我们会看一个解构的例子。在这里。

假设我们有以下对象:

const author = {
  firstName: 'Aki',
  lastName: 'Iskandar',
  topics: ['Angular', 'JavaScript', 'TypeScript', 'Bootstrap', 'Node'],
  cities: ['Calgary', 'Cleveland'],
  publisher: 'Packt'
}

如果我们想提取firstNamelastNamepublisher,我们知道如何以传统的方式做到这一点(即在 ES6 之前):

const firstName = author.firstName;
const lastName = author.lastName;
const publisher = author.publisher;

好吧,解构(尽管它的语法看起来有点奇怪)通过以下语法节省了我们大量的按键次数,给我们提供了相同的结果(提取数据的新变量):

const {firstName, lastName, publisher} = author;

我们可以很容易地看到,它通过将一个变量写入控制台来完成了它的工作:

console.log(publisher);  //  Packt

这非常方便,当我们一起编写应用程序时,我们将充分利用它。

模板字符串

模板字符串是用反引号括起来的字符串(即`)。

注意:反引号字符通常与波浪号(即~)在键盘上的同一个键上,并且紧挨着数字 1 键的左边。

JavaScript 总是允许我们使用双引号和单引号来创建字符串,那么为什么要使用第三种类型的字符串创建字符呢?嗯,事实证明,鉴于前端框架的大量使用,有一个共同的需求需要做三件事:

  • 字符串插值

  • 多行字符串

  • 标记模板

for-of 循环

JavaScript 为我们带来了forEach构造,用于循环遍历集合。这是一个很好的内置方法,但你无法跳出这个循环。我们还有for-in循环,对于具有字符串键的对象来说非常好,但在迭代数组时有一些缺点。

进入新的for-of循环。它适用于对象、数组和映射,并且你可以跳出它。以下是语法,与for-in循环的语法相同,只是将in更改为of

let myArray = 5, 10, 15, 20, 25;
  for (var item of myArray) {
  console.log(item);
}

装饰器

装饰器也是 TypeScript 的一部分。TypeScript 中的装饰器装饰函数和类,就像它们在其他一些语言中一样,比如 Python 和 Java。

我们不会花太多时间在这里,因为我们不会为我们要一起构建的应用程序编写自己的装饰器,但由于 Angular 大量使用装饰器,我想至少让你了解它们的用途。我们还将看一个快速的例子,如何创建一个装饰器以及如何使用它,但我们会快速地飞过它。

装饰器是一种通过用装饰器注释类来为函数或类(通常是类)添加功能的方法。装饰器只是一个函数,尽管乍一看它的语法看起来有些奇怪。让我们看一些代码:

function iq(target) {
  Object.defineProperty(target.prototype, 'iq', {value: () => "Genius"})
}
@iq
class Person {
}
let readerOfMyBook = new Person();
console.log(readerOfMyBook.iq());  // prints out "Genius" to the console

这是中级到高级水平的 TypeScript,整个章节都可以写关于装饰器的内容。我们没有时间在这里详细介绍它们,但要记住的是,它们只是简单地为函数或类添加功能的函数,你只需要用装饰器的名称来注释函数或类(即@NameOfDecorator)。

Promises

当我们在[第十二章 集成后端数据服务中使用它们时,我们将更详细地介绍 promises,所以我会推迟代码。原因是展示一个真正好的实际例子需要相当多的代码,因为需要调用异步代码。所以,我承诺在书中稍后会有真实世界的 promises。但是,我们至少可以看一下定义,这样你就知道它们是什么。

当你调用一个可能需要很长时间才能返回结果或完成任务的函数时,而且你不想延迟程序的执行,你可以异步调用该函数。这意味着你的代码在异步调用前一行后会继续执行到下一行。如果你不异步调用它,你的程序的执行将停止并等待你最后调用的函数从它正在做的事情中返回,比如从数据库中读取一堆记录。

有几种不同的方法可以异步调用函数。异步调用函数最常见的方式是使用回调函数。回调函数是你传递给异步调用的函数的函数,这样它就可以在完成工作后调用该函数。这就是回调函数得名的原因;你调用的函数在完成时会回调你。

Promises 是我们可以用来异步编程的另一种机制。虽然 Promises 使事情变得更加可控,但在 JavaScript 中编写良好的异步代码通常仍然非常困难。因为这个事实,人们开始编写 JavaScript 库来尝试使异步代码更容易编写。有几种库可供选择。一个拯救了我的理智的库叫做 Async。

话虽如此,我仍然没有给你一个 Promise 的定义,所以在这里:Promise 是一个代理尚未知的值;它就像一个值的占位符,最终将从异步调用的函数中返回。这种构造允许异步函数立即返回一个值,就像它是一个同步方法一样。返回的初始值是一个 Promise,一旦调用的函数完成了它的工作,Promise 最终将被返回的值替换。

我知道这可能需要花费一些时间来理解,但当我们在第十二章中编写我们的代码,集成后端数据服务,你将理解 Promise。这是一个承诺,双关语。

模块

在 ES6 之前,JavaScript 没有模块的概念。模块是简单的代码文件,可以加载到其他代码中,以便加载的模块中的函数对导入模块的代码可用。模块可以加载模块。模块导致模块化的代码,这是一件好事。与其在一个文件中编写庞大的代码块,不如将其分割成逻辑单元,并使该代码存在于多个文件中。这导致了代码重用,命名空间和可维护性。

虽然 JavaScript 没有模块,但我们仍然能够在一定程度上实现相同的功能。我们可以在网页中调用函数之前使用脚本标签加载脚本文件。但是,对于在服务器端或网页之外的其他环境中运行的 JavaScript,没有模块,编写非单片应用程序变得困难。

让我们继续进行代码。

假设我们有一个名为alphafunctions.js的文件,其中包含以下代码:

function alpha1() {
  console.log("Alpha 1 was called");
}
function alpha2() {
  console.log("Alpha 2 was called");
}
export {alpha1, alpha2};

export关键字用于标记哪些函数可以被导出,因此可以被其他模块导入。

现在假设我们有这个文件main.js,其中包含以下代码:

import {alpha1, alpha2} from ./alphafunctions;  
alpha1();  //  "Alpha 1 was called" is written to the console

默认导出

假设我们总是希望将我们的alpha1函数导入到其他模块中,或者至少更频繁地这样做。我们可以在keyword函数之前添加关键字export default。因此,当我们导入它时,我们不再需要在函数名称周围使用大括号。让我们在代码中看到这一点。

查看alphafunctions.js

export default function alpha1() {
  console.log("Alpha 1 was called");
}
function alpha2() {
  console.log("Alpha 2 was called");
}
export {alpha1, alpha2};

查看main.js

import alpha1, {alpha2} from ./alphafunctions;  

虽然这并不是一个惊人的差异,但“默认导出”这个术语在对话和博客文章的代码片段中经常出现,因此我想确保我们至少快速看一下,这样你就明白为什么有时会有大括号,而有时没有。当你使用 JavaScript 库时,你也会在文档和代码示例中看到这一点。所以,现在你知道了。

总结

在本章中,我们涵盖了 JavaScript 的一些历史,特别是围绕 JavaScript 生态系统的一系列幸运事件,这些事件巩固了该语言作为近代最重要的编程语言。现在我们不仅可以编写在浏览器中执行的客户端代码,还可以编写在服务器上运行的 JavaScript 代码。如果这还不足以成为使用更多 JavaScript 的有力理由,你还可以将 JavaScript 用于原生移动开发,以及创建桌面应用程序。这真是一个令人兴奋的故事!

然后,我们简要地查看了 ES6 发布时添加到 JavaScript 中的一些内容。这些新增内容相当重要,特别是因为 JavaScript 在十多年来基本保持不变,因此这些新增内容真正加强了语言。我们还列举了 TypeScript 带来的一些好处。请记住,你可以将 TypeScript 视为 JavaScript 的超集,并且可以将其定义为 ES6 加上可选类型。

微软将 TypeScript 贡献给 JavaScript 开发人员是该公司长期以来对开源世界做出的最重要的贡献之一。Angular 本身就是用 TypeScript 编写的,因为 TypeScript 相对于纯 JavaScript 具有优势,所以在构建 Angular 应用程序时,最好使用 TypeScript 进行编写。我们记得 JavaScript 是唯一可以在浏览器中执行的语言,但幸运的是,TypeScript 附带了一个转译器,可以将我们的 TypeScript 代码转换为纯 JavaScript 代码。

如路线图中所述,第三章《Bootstrap - 响应式网格布局和组件》,我们有一个类似的目标。在本章中,我们将对 SASS 进行快速介绍,这是我们将用来为我们的 Angular 组件设计样式的工具,而不是使用 CSS。我们还将涵盖足够的 Bootstrap 知识,让您能够舒适地使用这个古老的 CSS 框架来布局我们将一起构建的网络应用程序 ListingCarousel 的网页。您将获得足够的知识,立即将这些技能应用于您目前可能拥有或将来可能开始的几乎任何网络应用项目中。

第三章:Bootstrap - 网格布局和组件

嘿——您已经到达第三章,Bootstrap – 网格布局和组件。太棒了!接下来的议程是倒一杯您最喜欢的饮料,因为这是一个重要的章节,您会希望保持清醒。然而,这并不全是工作,因为这一章是我们一起开始构建示例应用程序的地方,我们应该能够在其中玩得开心。在这个过程中,我们还将涵盖各个领域的相当多的材料。以下是我们将要涵盖的内容列表:

  • 我们将正式介绍我们的示例应用程序,名为 Listing Carousel,并提出一些建议,说明您可以如何将此应用程序转化为您可能更喜欢的其他事情——可以是在本书旁边(而不是构建 Listing Carousel),或者在完成本书后,如果您首先与我一起构建 Listing Carousel。

  • 我们将介绍我们在整本书中如何逐步构建我们的应用程序的计划,并且您还将看到我们有一些备选技术可供选择,用于构建 Listing Carousel,或者您可能会在本书结束时受到启发,选择构建自己喜欢的应用程序。

  • 我们还将研究 Sass,这是一种使编写项目中的 CSS 变得更容易、更有组织的技术。

  • 我们肯定会研究 Bootstrap——它的两个主要部分:响应式网格和一些组件。

  • 在第一章,快速入门中,我们偷偷看了一些将构成我们示例应用程序的线框图。好吧,这一章是我们将编写 HTML 代码,利用 Bootstrap,将线框图变为现实的地方。

  • 作为额外材料,我们还将研究软件项目从构思到实现的过程,使用一个真实的案例研究,即 Listing Carousel。这包括项目阶段,如分析、需求收集、用例图、线框图和实施。

在本章结束时,我们的网页将是硬编码的,不会有任何 Angular 代码。随着我们在书中的进展,我们将逐渐将它们转变为一个完全成熟的 Angular 应用程序,通过添加路由、Angular 组件、模板、数据等。

关于本章不包括的内容

本章涵盖了很多内容,包括 Sass 和 Bootstrap 的响应式网格以及其中的一些组件。然而,本章并不全面涵盖您需要了解的这些内容。原因很简单——不仅有专门讲解 Bootstrap 的书籍,而且 Bootstrap 的官方网站是查找 Bootstrap 文档的理想地方。重复他们的文档不是这本书页面的好用处,也不是您的时间和辛苦赚来的钱的好用处。相反,更明智的做法是以实际的方式介绍 Bootstrap 的网格和组件——比如在本书中一起构建一个应用程序,并在需要时参考官方文档。

顺便说一句,同样的情况也适用于[第五章],Flex-Layout – Angular's Responsive Layout Engine,[第八章],使用 NG Bootstrap,和[第九章],使用 Angular Material,因为每种技术都有它们自己的官方文档。

我的工作是做以下事情:

  • 向您介绍这些技术(并指向它们的官方文档)

  • 演示它们如何以实际、有趣和引人入胜的方式应用

  • 鼓励您通读整本书,这样您就可以成为一个 Angular 网页开发大师

顺便说一句,Angular 当然也有自己的官方文档,但其中包含的内容太多,甚至开始都可能让人望而却步。根据我的经验,学习新技术的更有趣的方法是通过教程,而这本书正是这样一本全面的教程,通过构建一个应用程序来学习,每章节中都有额外的解释和一些额外的材料。如果我做得好的话,您应该能够使用 Angular 构建几乎任何您可能需要(或想要)构建的应用程序。这就是目标。

现在让我们来看看“列表轮播”,这是我们将一起构建的示例应用。

我们的示例应用

“列表轮播”是我们这本书的示例应用,它是一个真实的在线应用,为房地产经纪人(即专业的房地产销售人员)提供了一个机会,以引人入胜和信息丰富的方式与他们在社交媒体上的联系人分享他们的房源。我的一家公司拥有并运营它。

我选择这个应用的原因并不是让你偷我的代码,然后试图和我竞争(这样做完全不酷,也不推荐),而是因为通过一些调整,你可以把这个应用变成你自己的在线服务。例如,你可以很容易地把这个应用变成一个分类广告应用(比如 Craigslist 或 Kijiji),只需添加搜索功能,或者通过添加搜索功能,甚至可以变成一个约会/婚介网站,再加一点代码。或者,如果你喜欢美食,可以把它变成一个餐厅网站。让餐厅注册并在轮播中列出他们的菜单——每张幻灯片上放一道菜或开胃菜,然后餐厅老板可以与他们的社交媒体圈分享他们的菜单。或者,如果你喜欢有新的方式分享相册,你可以把这个应用变成类似的东西。我以前想过一个主意,就是创建一个人们可以展示他们的作品集的网站(比如艺术家、建筑师和摄影师)——随意去建立类似的东西并且发挥它。选择真的是无限的。重点是,我想为这本书想出一个有趣的应用——一个能给你一些动力来完成整本书的应用。为什么?很简单——因为我知道如果你只是读它,这本书对你来说就不会像它本来可以给你的那样有价值。所以,承诺和我一起深入代码并构建一些你会喜欢的东西。谁知道,也许你会想出一个有利可图的在线业务的好主意!我的目标是让你在阅读这本书的时间投资变得有意义,如果我成功了,你就可以给这本书一个五星好评(眨眼)。这听起来不错吗?

游戏计划

我们有一个逐步的游戏计划,以 Listing Carousel 作为我们一起讨论这本书材料的焦点。虽然这本书并没有明确地分成部分(也就是说,章节的分组),但我们现在可以通过将我们需要为构建应用程序做的工作分成三个主要阶段来松散地将它们分组。跟着我一起做,这一切都会有意义——给我们一个将材料(也就是书的章节)与我们将一起构建的应用程序相结合的方法,同时给我们一个我们的目标。

在开始驾驶之前知道你要去哪里,随时能够认清自己的位置是很好的。像这样拥有一份路线图/游戏计划会让整个过程更加愉快,从而最大程度地增加你会完整地阅读这本书的机会,而不仅仅是偶尔查找一些内容。这本书的设计并不像一本食谱书,而是旨在教你如何烹饪。你将通过实践学会烹饪(这是一种双关语),通过准备一道适当复杂的菜肴来要求一定水平的知识和技能来正确烹饪。这本书有四个主要的好处:

  • 它为你提供了所有的食材,甚至是你准备这道菜所需的替代食材(也就是选择)。

  • 它为你提供了厨师所需的知识、教授了烹饪餐点所需的过程和技能。

  • 它以一种系统的方式做这些事情,让你尽可能有效和高效地学习。

  • 餐点的选择是代表你可能需要烹饪的大多数菜肴复杂性的一道菜。换句话说,如果你学会了如何烹饪这道菜(也就是我们的示例应用程序),你应该有信心能够准备任何你被要求准备的菜肴。

撇开烹饪类比不谈,这本书的承诺是教会你如何通过一种方法论的过程来使用 Angular 构建一个实际的应用程序。毕竟,如果你想一想,这就是你买这本书的原因,不是吗?否则,你可以试图在这里那里搜索,希望最终能把一切拼凑在一起。那既不有趣,也不是学习 Angular 的聪明方式。当你想学习新东西,或者把基本技能提升到更高水平——无论是什么,不仅仅是 Angular——你都需要有一个明确的目标,并制定一个到达目标的路线图/计划。好吧,我们知道我们的目标,那就是构建列表轮播、学习 Angular,以及一大堆其他好东西。所以现在让我们来看看我们的计划。

在构建我们的应用程序的第一阶段,我们需要决定要构建什么,它将具有什么功能,以及它将是什么样子。一旦我们确定了所有范围和线框图,下一步就是为我们的应用程序建立骨架。到这个阶段结束时,我们的应用程序很可能只是硬编码的——只是一些 HTML 和 CSS。我们唯一的组件将是我们从 Bootstrap 组件库中选择使用的组件。你可以把这看作是我们的应用程序有了皮肤和骨骼,但还没有内在或跳动的心脏。

在构建我们的应用程序的第二阶段,嗯,你猜对了,我们将开始给我们的皮肤和骨骼应用程序添加一些内在!具体来说,它将是 Angular 的内在!请记住,在第一阶段,我们根本没有使用 Angular——一点也没有——这是有意的。虽然我们的应用程序在第二阶段结束时肯定会比第一阶段更有活力,但它的行为会更像一个机器人——非常机械化。如果你记得电影《绿野仙踪》,它会像铁皮人一样——非常活跃,但没有心脏。这个第二阶段(也就是给我们的应用程序一些内在)将包括第四章,“路由”,第七章,“模板,指令和管道”,以及第六章,“构建 Angular 组件”。

最后,在构建应用的第 3 阶段,我们将最终给我们的铁皮人一个心脏!是的!好了,是什么让我们的应用有了心脏?答案是数据和 API!数据就像是我们应用的血液,而 API 就像是心脏——接收数据并推送数据。我打赌你从来没有以这种方式思考过 Web 应用,从现在开始,你将无法以其他方式思考它们(微笑)。这第三阶段将包括第十章,使用表单,第十二章,集成后端数据服务,第十一章,依赖注入和服务,以及第十四章,高级 Angular 主题

第十三章,单元测试,和第十五章,部署 Angular 应用程序,实际上并不是任何阶段的一部分,但它们发挥着非常重要的支持作用。在这两章中,我们将学习如何测试我们为应用程序编写的代码,以及如何以几种不同的方式部署它。

这就是我们对我们的整体规划的大致了解。让我们再仔细看一下我们构建应用程序第一阶段的五步游戏计划,然后我们将进入我们的第一个技术主题,SASS。

  • 步骤 1:在本章中,我们将研究 Bootstrap 的响应式网格,以及几个 Bootstrap 的组件:

  • 我将解释 Bootstrap 的网格如何工作,并如何帮助我们布置网页。

  • 在构建页面时,我将介绍我们将在页面上使用的 Bootstrap 组件,并且我们将使用我们的线框图来指导我们,结合 Bootstrap 的网格。

  • 步骤 2:在第五章中,Flex-Layout – Angular's Powerful Responsive Layout Engine,我们将用 Flex-layout 替换 Bootstrap 的网格系统。我们只会在一些网页上这样做,其他网页仍然会使用 Bootstrap 的网格。我们这样做有两个原因:

  • 向您展示总是有备选方案可用,并且您通常可以混合和匹配这些备选方案。

  • 能够混合和匹配为我们提供了一条路径,可以在不需要一次完成所有工作的情况下用另一种技术替换另一种技术。我们不想完全重建所有东西,我们只想重新做一些最初构建的东西的一部分,以学习如何应用特定的替代技术。

  • 步骤 3:在第六章中,构建 Angular 组件,我们将看看如何构建自己的组件以在网页中使用。由于在创建我们自己的组件时,我们控制 HTML 和 CSS,我们可以利用 Bootstrap 的组件来创建我们自己的 Angular 组件的外观和感觉。注意:第七章,模板、指令和管道,也是其中的一部分,因为这两章是相关的。

  • 步骤 4:在第八章中,使用 NG Bootstrap,我们将发现有现成的Angular-ready Bootstrap 组件。就像我们将为我们的一些页面用 Flex-layout 替换 Bootstrap 的网格一样,我们也将用 NG Bootstrap 项目的组件做同样的事情——即用 NG Bootstrap 项目的组件替换一些 Bootstrap 组件。我们这样做的动机是意识到有许多不同的第三方组件可以方便地用于我们的 Angular 应用程序,包括基于 Bootstrap 组件的组件。

  • 步骤 5:在第九章中,使用 Angular Material,我们将再次替换一些 Bootstrap 组件,但这一次,它们与 Bootstrap 组件没有任何关系。Angular Material 项目有精美设计的组件,专门为在 Angular 应用程序中使用而设计,我们将学习如何将其中一些组件整合到我们的应用程序中。

同样,这里需要注意的重要事情是,我们在布置网页和选择使用的组件方面有技术选择,包括创建我们自己的自定义组件,当构建我们的 Angular 应用程序时。此外,正如你将在第十二章中看到的,集成后端数据服务,你在服务器端和数据库技术栈方面几乎有无限选择。而且,在第十四章中,高级 Angular 主题,我们将探索一些第三方身份验证 API,我们可能希望利用它们来代替从头开始编写我们自己的应用程序。

是的!我们有很多有趣的东西要一起在这本书中讨论。也就是说,让我们先专注于首要的事情,开始利用本章提供的好东西:Sass,Bootstrap,软件项目的典型演变(即,从构思到实现),并使用 Bootstrap 构建我们的列表轮播页面(即,构建应用程序的第一阶段)。我将在第七章的开头为构建应用程序的第二阶段提供类似的计划,模板,指令和管道,并在第十二章的开头为构建应用程序的第三阶段提供最终计划,集成后端数据服务

我知道要涵盖的内容很多,但回顾我们的计划是我们要做的重要事情——知道我们在哪里,我们要去哪里总是有帮助的。现在,让我们加快速度,在我们讨论 Bootstrap 之前迅速过一遍 Sass 速成课程。

Sass 速成课程

对于大多数技术来说,包括本书中提到的所有技术,如 ES6,Bootstrap,Node 和 MongoDB,都可以写成整本书。Sass 也不例外。这个速成课程的目标不是让你成为 Sass 专家,也不是为了重复 Sass 的官方文档。由于空间限制,速成课程的目标只是向你介绍 Sass,并激励你自己进一步探索它,无论是在你完成这本书之后,还是与之并行进行,因为 Sass 是一个非常酷的技术。

Bootstrap 团队已经采用了 Sass 来进行 Bootstrap 项目的开发,而其他技术(比如 Compass)也是基于 Sass 构建的。严格来说,你不必知道如何使用 Sass 来编写 Angular 应用程序,甚至不需要通过本书来学习,但这是一项值得学习的技术,所以我鼓励你自己更仔细地了解一下。现在让我们一起来学习一些 Sass 的基础知识。

什么是 Sass?

  • Sass 是 Syntactically Awesome StyleSheets 的缩写,但当然,Sass 比缩写更有趣味!Sass 是 CSS 的扩展,为我们编写 Web 应用程序的 CSS 提供了额外的功能和灵活性。编译后,Sass 会为我们生成格式良好的 CSS。具体来说,Sass 为 CSS 带来的附加功能包括嵌套规则、变量、条件逻辑、数学运算、混合等。此外,Sass 还使得在 Web 项目中更容易维护和组织样式表。在这个速成课程中,我们将学习这些内容。

  • Sass 与所有版本的 CSS 兼容,不仅仅是 CSS3 和 CSS4。

  • 由于 Angular CLI 的存在,Sass 可以很好地融入我们的 Angular 应用程序中,因为 CLI 默认为我们编译组件中的 Sass。

  • Sass 的官方网站可以在这里找到:sass-lang.com/

Compass 框架

Compass 是一个构建在 Sass 之上的 CSS 编写框架,提供了一些不错的附加功能,并且还会将你的 Sass 编译成 CSS。如果你在非 Angular 项目上工作,Compass 是一个选择(记住,Angular CLI 会为我们的 Angular 项目编译 Sass 为 CSS)。我们不会在本书中涵盖 Compass,但我至少想让你注意到这项技术,因为我知道作为 Web 开发人员,你不仅会使用 Angular 这项技术。然而,作为 Web 开发人员,我们无法避免使用 CSS!

这里的要点是,你可以简单地在 Angular 项目中使用 Angular CLI 来使用 Sass,但是对于非 Angular 项目,尤其是如果你的项目倾向于使用大量 CSS,可以考虑使用 Compass。

大公司使用 Compass。我知道其中两家,并且我每天都在使用它们的在线服务,它们分别是 LinkedIn(www.linkedin.com/),这是世界上最大的面向就业的社交网络服务,以及 Heroku(www.heroku.com),这是一个非常受欢迎的云应用平台。

您可以在官方网站compass-style.org/上了解有关 Compass 的所有信息。另一个提供有关 Sass 和 Compass 教程的不错的在线参考资料名为The Sass Way,网址为www.thesassway.com/

两种 SASS 样式

Sass 有两种语法风格:旧语法依赖缩进,而新语法使用大括号而不是缩进。两种语法风格之间的另一个区别是,旧风格不需要在行末加上分号,而新风格需要。这两种风格的文件扩展名也不同——旧风格的文件扩展名是.sass,而当前风格的文件扩展名是.scss

现在让我们快速看一下每种 CSS 语法的一个示例。第一个代码块是旧风格(.sass),第二个代码块以新的语法风格(.scss)产生相同的效果。我们将在整本书中都使用新风格。

这里给出的示例代码是用于编写.sass语法的:

$blue: #0000ff
$margin: 20px

.content-navigation
  border-color: $blue
  color: lighten($blue, 10%)
 .border padding: $margin / 2 margin: $margin / 2 border-color: $blue  

这里给出的示例代码是用于编写.scss语法的:

$blue: #0000ff;
$margin: 16px;

.content-navigation {
 border-color: $blue;
 color: lighten($blue, 10%);
}

.border {
 padding: $margin / 2; 
 margin: $margin / 2; 
 border-color: $blue;
}

两种语法风格之间的主要区别在于,旧风格旨在简洁,而新风格旨在更符合传统 CSS 语法的开发人员的习惯。

在前面的代码块中,您可能已经注意到了$blue$margin。这些不是 CSS 项目,而是变量的示例。您可能还注意到了除法运算符。变量和数学计算只是您的 Sass 代码中可能包含的一些内容。我们将在接下来的部分中看到这些以及更多 Sass 功能。

无论您使用哪种语法——旧的还是新的——编译结果都是相同的。如果您将前面的任何一个代码块运行到在线编译器,比如Sass Meister*(我马上也会提到这个工具),生成的 CSS 将是以下内容:

.content-navigation {
  border-color: #0000ff;
  color: #3333ff;
}

.border {
  padding: 10px;
  margin: 10px;
  border-color: #0000ff;
}

安装 Ruby

Sass 是用 Ruby 编写的,因此我们需要在计算机上安装 Ruby。要下载并安装最新版本,请转到 Ruby 的官方网站:www.ruby-lang.org/en/downloads/。要查看您的计算机上是否已安装 Ruby,请在命令行或终端上运行此命令:$ ruby -v

如果 Ruby 已安装,输出将显示版本号。例如,当我执行$ ruby -v时,我的终端输出是ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-darwin16]。从 2.3 版本开始的任何版本都足够满足我们的需求。

安装 Sass

安装了 Ruby 后,安装 Sass 就非常简单。请前往sass-lang.com/install并按照说明操作。

就像您可以通过在终端或命令行中运行$ ruby -v来获取 Ruby 的版本一样,您也可以使用 Sass 执行相同的操作。在终端或命令行中执行以下命令$ sass -v,以查看您系统上的 Sass 版本。我的系统输出如下:

Sass 3.5.5 (Bleeding Edge).

Sass 的在线工具

有几个在线工具可以用来将 Sass 文件编译成 CSS 文件。我喜欢的一个工具叫做Sass Meister,您可以在这里访问:www.sassmeister.com

我喜欢它,因为它非常易于使用,并且在 Sass 语法出现问题时提供了不错的帮助。它还支持旧样式和新样式的 Sass 语法,并允许您从几种不同的 Sass 编译器中进行选择。您可以在窗格顶部的 Options 菜单选项下找到这些设置。

要使用该工具,只需在左窗格中编写您的 Sass 代码,编译后的 CSS 将显示在右窗格中。注意所选的选项,确保激活的选项是您想要的。

Sass 的离线工具

就像在线工具一样,我们有几个选项可以选择使用离线工具将 Sass 文件编译成 CSS 文件。我使用 Koala,因为它易于使用,跨平台,并且免费。您可以从项目网站免费下载 Koala:koala-app.com/

Koala 让您不仅可以处理 Sass,还可以用它来编译和/或压缩 Less、JavaScript、CoffeeScript,甚至可以与 Compass 框架一起使用。

学习如何使用 Koala 的最佳方法是阅读官方文档,可以在github.com/oklai/koala/wiki#docs找到。但是,如果您只是暂时使用 Koala 来编译您的 Sass 文件,让我在这里快速概述一下步骤,以免您需要在书本和在线文档之间来回跳转。

您需要做的就是使用您选择的任何文本编辑器(如 Sublime Text 或 Visual Studio Code)创建一个 Web 项目,并在项目的根文件夹中创建一个 CSS 文件夹和一个 Sass 文件夹。当然,您不需要一个完成的项目,您只需要非常基本的文件夹结构。创建项目结构后,您可以打开 Koala 开始使用它为您编译 Sass 文件。以下是基本步骤:

  1. 创建一个空的项目结构,至少包括以下内容:
  • 根文件夹中有一个空的index.html页面

  • 在根文件夹中有一个 CSS 文件夹,其中有一个空的styles.css文件

  • 在根文件夹中有一个 Sass 文件夹,其中有一个空的 style .scss文件

  1. 打开 Koala 应用程序。

  2. 单击左上角的大加号(+),导航到您项目的根文件夹,并选择它。此时,Koala 将找到您的styles.scssstyles.css文件。

  3. 在 Koala 的右侧窗格中右键单击styles.scss文件,选择设置输出路径,然后在文件资源管理器中导航到并选择您的styles.css文件

遵循上述步骤就是您需要做的一切,以便设置 Koala 来为您编译 Sass 文件。编译的输出将被插入到您的styles.css文件中。

Sass 功能和语法

现在让我们来看看一些 Sass 的功能,这些功能在您的应用程序中最有可能使用。我们不会在我们的示例应用程序中使用所有这些功能,但我想向您展示一些 Sass 提供的很酷的东西。

嵌套

使用 Sass,您可以将 CSS 规则嵌套在彼此之内。Sass 不仅更易于阅读,而且有助于避免大量重复的 CSS 选择器。这对于高度嵌套的 CSS 规则尤其有效。让我们看一个简单的例子:

/* Here is some basic Sass that uses nesting */

#outer-frame p {
  color: #ccc;
   width: 90%;
  .danger-box {
    background-color: #ff0000;
    color: #fff;
  }
} 

前面的 Sass 代码将被编译,生成相应的 CSS 代码:

/* This is the CSS that the above Sass is compiled to */

#outer-frame p {
 color: #ccc;
 width: 90%;
}
#outer-frame p .danger-box {
 background-color: #ff0000;
 color: #fff;
}

变量

Sass 变量就像你期望的那样:它们存储您想要在整个样式表中重用的信息。这样可以节省时间和烦人的错误。就像其他语言中的全局变量一样,您只需要在一个地方定义它们,所以如果它们需要更改,您只需要在一个地方更改变量,而不是更改 CSS 样式中的所有出现。

您几乎可以存储任何东西。这是一个示例,我们在其中存储了字体信息和字体颜色:

/* Here is the Sass code defining the variables */

$font-stack: Helvetica, sans-serif;
$primary-color: #333;
body {
 font: 100% $font-stack;
 color: $primary-color;
}

前面的 Sass 代码将被编译,生成相应的 CSS 代码:

/* Here is the CSS that the above Sass is compiled to */

body {
  font: Helvetica, sans-serif;
  color: #ccc;
}

数学运算

由于 Sass 编译为 CSS,您可以让它为您执行数学计算,而不是自己执行。您还可以让数学运行在变量上,而不是像下面的例子中的硬编码数字一样。当然,这样做非常方便:

/* Here is Sass that has some math in it */

.main-container { width: 100%; }
article {
 float: right;
 width: 700px / 960px * 100%;
}

前面的 Sass 代码将被编译,生成相应的 CSS 代码:

/* Here is the CSS that the above Sass is compiled to */

.main-container {
 width: 100%;
}
article {
 float: right;
 width: 72.91667%;
}

导入

Sass 使您能够使用@import指令将一个样式表导入到另一个样式表中。这就像它听起来的那样,非常简单。让我们来看一个例子。在以下三个代码列表中,第一个是基本样式(base.scss)表,适用于整个站点,第二个是用于报告页面(reports.scss)的样式表。第三个是在 Sass 编译期间报告样式表导入基本样式表时得到的结果 CSS 样式表。请注意,在 Sass 中使用@import指令时不需要文件扩展名:

/* base.scss */
body {
 margin: 10px;
 padding: 10px;
 font: Helvetica, sans-serif;
 color: #333;
 background-color: #eee;
}

/* reports.scss */
@import 'base';
p {
 margin: 5px;
 padding: 5px;
 color: #0000CD;
 background-color: #EEE8AA;
}

前面的 Sass 代码将被编译,生成相应的 CSS 代码:

body {
 margin: 10px;
 padding: 10px;
 font: Helvetica, sans-serif;
 color: #333;
 background-color: #eee;
}
p {
 margin: 5px;
 padding: 5px;
 color: #0000CD;
 background-color: #EEE8AA;
}

扩展

使用@extend让您可以从一个选择器向另一个选择器共享一组 CSS 属性。一个选择器可以使用@extend Sass 指令从另一个选择器继承。以下示例显示了一组三个相关样式(活动的,非活动的和终止的)的常见样式属性。

%common-status-styles {
 width: 200px;
 height: 75px;
 padding: 10px;
 color: #333;
}

.active {
 @extend %common-status-styles;
 background-color: green;
 border-color: #001a00;
}

.inactive {
 @extend %common-status-styles;
 background-color: yellow;
 border-color: #999900;
}

.terminated {
 @extend %common-status-styles;
 background-color: pink;
 border-color: #ff5a77;
}

当前面的 Sass 代码被编译时,它会变成以下 CSS:

.active, .inactive, .terminated {
 width: 200px;
 height: 75px;
 padding: 10px;
 color: #333;
}

.active {
 background-color: green;
 border-color: #001a00;
}

.inactive {
 background-color: yellow;
 border-color: #999900;
}

.terminated {
 background-color: pink;
 border-color: #ff5a77;
}

混合

混合器就像命名模板。它们是 Sass 允许您将 CSS 或 Sass 声明(即 CSS 样式)分组并为其命名的方式。这样,您可以根据需要在其他 CSS 类中包含这些声明,而无需复制和粘贴-这样做会在以后需要更改时造成一些混乱。在某种意义上,它们也像变量,因为您只需要在一个地方更改某些内容(即在混合器本身),但它们比变量更强大,这就是为什么我提到它们像模板。实际上,混合器甚至可以使用变量进行参数化。让我们看一个例子,前面的描述应该清楚地说明了我所说的混合器就像模板的意思。

这是我喜欢在我的网站中使用的下拉菜单的样式示例。我们将参数化宽度,以便我们可以创建不同大小的下拉菜单。请注意使用@mixin指令:

@mixin custom-dropdown($dropdown-width) {
 -webkit-appearance: button;
 -webkit-border-radius: 2px;
 -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
 -webkit-padding-end: 20px;
 -webkit-padding-start: 2px;
 -webkit-user-select: none;
 background-image: url(https://www.maxfusioncloud.com/static/img/15xvbd5.png), 
   -webkit-linear-gradient(#FAFAFA, #F4F4F4 40%, #E5E5E5);
 background-position: 97% center;
 background-repeat: no-repeat;
 border: 1px solid #AAA;
 color: #555;
 font-size: 10pt;
 margin: 0px;
 overflow: hidden;
 padding: 5px 12px 6px 6px;
 text-overflow: ellipsis;
 white-space: nowrap;
 width: $dropdown-width;
}

以下是我们如何使用混合器(请注意使用@include指令):

.small-dropdown { @include custom-dropdown(75px); }
.medium-dropdown { @include custom-dropdown(115px); }
.large-dropdown { @include custom-dropdown(155px); }

这将编译为以下 CSS:

.small-dropdown {
 -webkit-appearance: button;
 -webkit-border-radius: 2px;
 -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
 -webkit-padding-end: 20px;
 -webkit-padding-start: 2px;
 -webkit-user-select: none;
 background-image: url(https://www.maxfusioncloud.com/static/img/15xvbd5.png), 
    -webkit-linear-gradient(#FAFAFA, #F4F4F4 40%, #E5E5E5);
 background-position: 97% center;
 background-repeat: no-repeat;
 border: 1px solid #AAA;
 color: #555;
 font-size: 10pt;
 margin: 0px;
 overflow: hidden;
 padding: 5px 12px 6px 6px;
 text-overflow: ellipsis;
 white-space: nowrap;
 width: 75px;
}

.medium-dropdown {
 -webkit-appearance: button;
 -webkit-border-radius: 2px;
 -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
 -webkit-padding-end: 20px;
 -webkit-padding-start: 2px;
 -webkit-user-select: none;
 background-image: 
   url(https://www.maxfusioncloud.com/static/img/15xvbd5.png), 
   -webkit-linear-gradient(#FAFAFA, #F4F4F4 40%, #E5E5E5);
 background-position: 97% center;
 background-repeat: no-repeat;
 border: 1px solid #AAA;
 color: #555;
 font-size: 10pt;
 margin: 0px;
 overflow: hidden;
 padding: 5px 12px 6px 6px;
 text-overflow: ellipsis;
 white-space: nowrap;
 width: 115px;
}

.large-dropdown {
 -webkit-appearance: button;
 -webkit-border-radius: 2px;
 -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
 -webkit-padding-end: 20px;
 -webkit-padding-start: 2px;
 -webkit-user-select: none;
 background-image: 
   url(https://www.maxfusioncloud.com/static/img/15xvbd5.png), 
   -webkit-linear-gradient(#FAFAFA, #F4F4F4 40%, #E5E5E5);
 background-position: 97% center;
 background-repeat: no-repeat;
 border: 1px solid #AAA;
 color: #555;
 font-size: 10pt;
 margin: 0px;
 overflow: hidden;
 padding: 5px 12px 6px 6px;
 text-overflow: ellipsis;
 white-space: nowrap;
 width: 155px;
}

到目前为止,您可以看到 Sass 有多酷,它可以为您节省多少时间,以及您如何使用它来避免代码重复和避免犯愚蠢的剪切和粘贴错误。

如果所有这些还不够酷的话,Sass 还通过其内置函数为您提供了很多强大的功能。有很多内置函数,这就是为什么 Sass 如此强大和实用。您可以在这里查看它们:sass-lang.com/documentation/Sass/Script/Functions.html。我们只会涵盖一个,只是为了向您展示如何在接下来的内置函数部分中使用函数的示例。

占位符

占位符旨在与@extend指令一起使用。使用占位符的规则集,但不使用@extend指令,将不会呈现为 CSS。使用占位符的一个有效用例是,如果您正在编写一个用于代码重用的 Sass 库。您可以编写一个包含占位符的 Sass 文件,该文件旨在包含在您或其他人编写的另一个 Sass 文件中。如果 Sass 文件中的规则集导入了作为库的其他 Sass 文件,并且规则集扩展了您的库中的占位符,则在编译 Sass 文件时,它将呈现为 CSS 文件。如果没有规则集扩展占位符,则占位符将不会呈现/打印到 CSS 文件中。

让我们来看一个例子。注意,占位符前缀为百分号(%):

%warning-placeholder {
 color: red;
 font-weight: bold;
 font-size: 1.5em;
}

.warning {
  @extend %warning-placeholder;
}

Sass 代码编译为以下 CSS:

.warning {
  color: red;
  font-weight: bold;
  font-size: 1.5em;
}

内置函数

当我们介绍 Sass 的扩展功能时,每个类的边框颜色比其对应的背景颜色暗 20%。为了找到比另一个颜色暗 20%的颜色,你需要进行一些繁琐的数学运算,如果以后决定更改百分比,就需要进行更多繁琐的数学运算。幸运的是,Sass 为我们提供了内置函数,可以做各种事情,包括变暗和变亮颜色等等。

现在,让我们重新审视之前在扩展部分看到的 Sass 代码,并且这次使用变量和内置的变暗函数来更灵活地编写它,以便让 Sass 为我们做数学运算。这样,如果以后选择更改百分比,就会变得很容易。以下 Sass 代码的编译输出将与之前扩展部分的编译输出完全相同,因此我们不会在这里重复。

/* Example of using variables and a built-in function */

$active-color: green;
$active-border-color: darken($active-color,20%);
$inactive-color: yellow;
$inactive-border-color: darken($inactive-color,20%);
$terminated-color: pink;
$terminated-border-color: darken($terminated-color,20%);

%common-status-styles {
 width: 200px;
 height: 75px;
 padding: 10px;
 color: #333;
}

.active {
 @extend %common-status-styles;
 background-color: $active-color;
 border-color: $active-border-color;
}

.inactive {
 @extend %common-status-styles;
 background-color: $inactive-color;
 border-color: $inactive-border-color;
}

.terminated {
 @extend %common-status-styles;
 background-color: $terminated-color;
 border-color: $terminated-border-color;
} 

自定义函数

Sass 通过使用其现成的内置函数为我们提供了很大的能力,但有时,没有什么能替代自定义函数——为手头的项目做出你想要的功能。Sass 团队的成员知道这一点,因此为我们提供了一种在 Sass 文件中添加自定义函数的方法。

为了结束这个 Sass 速成课程,让我们快速看一下如何创建自定义函数。我们的函数将根据两个参数计算宽度百分比,目标宽度的列数和我们拥有的总列数。

在这个简短的例子中,你会注意到我们做了以下几点:

  • 使用变量

  • 进行一些简单的数学运算

  • 使用内置的 Sass 函数(即百分比)

  • 引入两个新的 Sass 命令:@function@return:

@function column-width-percentage($cols, $total-cols) { 
 @return percentage($cols/$total-cols); 
}

.col-1 { 
 width: column-width-percentage(4, 12); 
}

.col-5 { 
 width: column-width-percentage(5, 12); 
}

这将编译为以下 CSS:

.col-1 {
 width: 33.33333%;
}

.col-5 {
 width: 41.66667%;
}

我希望你能在网页开发中找到 Sass 的用武之地。现在看起来可能有点多余,但当你花些时间去尝试时,我相信你会发现聪明的方法来利用 Sass 帮助你更好地组织你的 CSS,并帮助减少代码重复的问题。

现在让我们转变一下方向,快速看一下 Bootstrap。

Bootstrap 速成课程

在本节中,我们将重点介绍 Bootstrap,特别是其响应式网格和组件。我们将只介绍足够多的 Bootstrap 网格,让您对如何使用它有一个坚实的开始。我们还将只介绍 Bootstrap 的五个组件,让您开始。Bootstrap 拥有远远超过五个组件,而且您可以以许多方式自定义每一个组件。然而,这是 Bootstrap 的速成课程,而不是详尽的手册——要试图详尽地介绍 Bootstrap,需要的是一本全面的手册。Bootstrap 是一个庞大的库,有大量的选项供您使用,因此重要的是向您展示基础知识,并告诉您在哪里获取有关 Bootstrap 的更多信息,而不是试图详尽地介绍它。好消息是,这个 Bootstrap 的速成课程是让您快速上手的最快方法。

采取这种方法的原因如下:

  • 我们不会在示例应用程序中使用所有的 Bootstrap 组件

  • 我们的示例应用程序也将使用 ng-bootstrap 组件和 Angular Material 组件进行制作(我们将在后面的章节中介绍:第八章,使用 NG Bootstrap,和第九章,使用 Angular Material

  • 对我们来说,Bootstrap 最重要的部分将是 Bootstrap 的网格——我们将对网格进行比我们将要查看的五个组件更详细的介绍

然而,与 Sass 速成课程不同,我们将看到如何实际使用 Bootstrap,因为我们将直接在本章中的主页布局中使用它。

什么是 Bootstrap?

Bootstrap 是一个用于构建响应式网站的 CSS 框架,重点是移动优先。虽然还有其他前端呈现框架,但 Bootstrap 仍然是这个领域的霸主,不仅因为它拥有最多的关注度,而且它可能拥有最多的运行次数。我所说的运行次数是指它在网站中被使用的次数,因此它比其他 CSS 框架更加经受考验。Bootstrap 的领先关注度(即流行程度)主要是由于以下三个原因:

  • 它是其类别中最早的框架之一(因此竞争几乎不存在)

  • 它得到了世界顶级社交网站(即 Twitter)的支持

  • 该项目自 2011 年 8 月开始存在,因此非常成熟。

另外,正如我们将在第八章中看到的,使用 NG Bootstrap,ng-bootstrap 项目的目标是使用 Bootstrap 4 创建 Angular 小部件,这充分说明了 Angular 社区对 Bootstrap 的看法。

第三版保持了 Angular 和 Bootstrap 之间的关系之所以如此真实的原因是因为它们各自在各自的领域都是领导者,并且它们之间是共生兼容和互补的。事实上,这两个框架就足以构建强大的 Web 应用程序的前端部分——只需选择任何你喜欢的后端构建,因为如今所有的后端框架都可以生成和消费 JSON,包括仍在运行 COBOL 程序的大型机。这是因为 JSON 已经成为通过消息集成系统的最流行方式。

动机

如果你曾经尝试过在不使用框架的情况下构建一个在不同视口大小(即,形态因素/屏幕大小)上运行良好的网站,那么你很容易就能理解使用 Bootstrap 的动机——从头开始构建这样的东西既乏味又困难。移动计算确实加剧了对类似 Bootstrap 的需求,而它的出现是不可避免的。虽然对于几乎任何框架都可以说同样的话,即你可能不应该花时间重新发明轮子,除非你有极好的理由这样做,但可以说(对于绝大多数网站,甚至 Web 应用程序)前端比后端更加重要。在过去几年里,客户端已经成为了新宠。我并不是在暗示后端不重要——事实恰恰相反,i,集成后端数据服务,完全致力于构建后端。然而,我建议的是,当移动计算出现时,我们已经有了足够多的后端技术和大量的框架可供选择,但缺乏前端框架。

我将在结束这个动机部分时添加的最后一条评论是,在商业世界中一举两得可以为公司带来竞争优势(即,市场速度)和/或财务优势(即,成本节约)—因此在软件开发中也不例外。如果你可以构建一次东西,在这种情况下是一系列网页,并且在移动和桌面上使用相同的客户端代码,而不是构建两套一切(甚至考虑到平板电脑,甚至三套),你应该意识到节约了时间和金钱。这就是承诺—不幸的是,它并不总是兑现。然而,在这些领域获得一些优势肯定比没有获得任何优势要好。

Bootstrap 在我们示例应用程序中的作用

对于我们的示例应用程序,Bootstrap 将仅用于两个目的:

  • 使用其响应式网格布局网页

  • 利用一些现成的组件快速构建样式良好的 UI

安装 Bootstrap

出于学习目的,安装 Bootstrap 与我们将在 Angular 应用程序中安装 ng-bootstrap 是不同的。本章重点介绍 Bootstrap 的网格系统以及一些组件,因此我们将通过暂时不创建 Angular 应用程序或完全不使用 Angular 来保持简单。在本章结束时,我们将只有我们的皮肤和骨头应用程序(如前所述),然后将其转换为一个完整的 Angular 应用程序。

让我们从将 Bootstrap 集成到我们的 HTML 中的最小且最快的方式开始。要使用 Bootstrap 提供的所有功能,我们只需要添加一个样式表和三个 JavaScript 文件的资源链接。

以下是创建演示如何将 Bootstrap 引入的空 HTML 页面的 HTML 代码:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 3 - Bootstrap: Responsive Grid Layout & 
       Components</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/
         bootstrap/4.0.0/css/bootstrap.min.css" crossorigin="anonymous”>
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js” 
         crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/
         1.12.9/umd/popper.min.js" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/
         js/bootstrap.min.js” crossorigin="anonymous"></script>
  </head>
<body>
This page is intentionally blank. It's sole purpose is to show the HTML code that needs to be added to integrate Bootstrap.
</body>
</html>

根据先前 HTML 代码中链接文件的顺序,以下是一个 CSS 文件和三个 JavaScript 文件的用途:

  • bootstrap.min.css文件是 Bootstrap 的压缩样式表,其中定义了所有默认样式

  • jquery-3.2.1.slim.min.js文件是包含 jQuery 库的压缩 JavaScript 文件,并且被引用,因为 Bootstrap 本身依赖于 jQuery

  • popper.min.js文件是另一个名为 Popper 的第三方库的压缩 JavaScript 文件,并且被引用,因为 Bootstrap 利用其中的功能来实现其工具提示组件

  • 最后,bootstrap.min.js文件是 Bootstrap 本身的压缩 JavaScript 文件,用于各种组件,如模态和下拉组件,这些组件需要 JavaScript 来运行

您还会注意到这些链接是指向 CDN(即内容传送网络)的资源。虽然有其他安装 Bootstrap 在我们网站上的方法,但使用 CDN 的优点有三个:

  • 我们不需要在我们的网页项目中下载和包含文件

  • 客户加载我们的网页的时间被最小化,因为这些资源可能在他们访问我们之前的其他网站时已经被下载到他们的浏览器中

  • 服务器经过优化,用于传送这些资产(使用缓存和其他托管策略)

当我们在本章后面看 Navs 和 Navbar 组件时,我们将考虑在我们的主页上添加导航栏。

Bootstrap 的响应式网格系统

从我的角度来看,特别是作为一个专注于 Angular 的 Web 开发人员,Bootstrap 提供的最重要的东西是响应式网格。原因是有许多来自许多不同库的 Web / HTML 组件可供选择进行 Web 开发(例如 NG Bootstrap 和 Angular Material,我们将在后面的章节中介绍),因此,我们并不仅仅局限于只使用 Bootstrap 的组件。然而,无论您最终使用哪些组件,或者如果您创建自己的组件(正如我们将在第六章中学习的那样,构建 Angular 组件),Bootstrap 网格仍然可以用于构建响应式布局,并极大地简化我们创建设计良好的 Web 应用程序的繁重任务。

Bootstrap 的网格使我们能够轻松地为各种视口(即屏幕)大小布局我们的页面。我们只需使用特殊的 Bootstrap 类来指示在我们的页面上为我们的应用程序可能运行的不同视口大小定位事物。

如果你曾经想知道 Bootstrap 是否是建立在其他东西之上的,答案是,毫不奇怪地,“是”。库和框架经常相互依赖。这就是开源世界中现代软件的构建方式。毕竟,当我们已经有完全可靠的轮子可供使用时,为什么要重新发明轮子呢?我们已经从之前关于安装 Bootstrap 的部分中看到,Bootstrap 依赖于 jQuery 和 Popper。Bootstrap 的响应式网格系统是建立在 CSS3 中引入的 CSS Flexbox 之上的。

在 CSS4 中有一个更新的网格系统,称为 CSS Grid,但 Bootstrap 4 没有使用它。Bootstrap 使用 CSS Flexbox。这并不意味着 Bootstrap 落后于时代,因为更新并不一定意味着更好。使用 CSS Grid 可以使一些事情变得更容易,而使用 CSS Flexbox 可以使其他事情变得更容易。

稍后,当我们讨论 Bootstrap 预定义的用于在网格内垂直和水平对齐事物的类时,如果你熟悉 CSS Flexbox,这些类名可能会让你感到熟悉。这是因为 Bootstrap 在内部使用 CSS Flexbox,并且类名是受其类名启发而来的。

网格本身有三个主要部分(容器、行和列),每个部分都是在 Bootstrap 的 CSS 文件中定义的类,这就是为什么需要在我们的页面中引用它。

这就是俗语变得有点混乱的地方,所以让我快速解释一下。正如你所知,HTML 中没有名为“容器”、“行”或“列”的元素。但是,在 HTML 中我们有 div 元素,在 Bootstrap 中,我们用一个类来装饰它,特别是用容器、行或列的类。但是当我们谈论 Bootstrap 时,假装这些类型的 HTML 元素会更容易。让我澄清一下,因为从现在开始我将把 Bootstrap 行称为行元素,把 Bootstrap 列称为列元素。我的意思是:

  • 说“容器元素”比说“具有容器类的 div 元素”更容易(在代码中,这就是“容器元素”的样子:<div class="container">

  • 说“行元素”比说“具有行类的 div 元素”更容易(在代码中,这就是“行元素”的样子:<div class="row">

  • 更容易说“列元素”,而不是不得不说“具有列类的 div 元素”(在代码中,这就是“列元素”的样子:<div class="col">

好了,希望这样说得通。

容器

容器是网格中的根元素,或顶层元素。它包含一个或多个行,这些行必须嵌套在容器内,而行又可以包含零个或多个列。要创建一个 Bootstrap 网格,我们首先创建一个容器,为此,我们只需创建一组 HTML div元素,并将容器类分配给第一个div元素。

在代码中看起来是这样的:

<div class="container">
</div>

哈!你明白我为什么提到前面关于容器元素的东西了吗?这是一个混乱的方式来尝试解释它。所以,现在让我们用我们的新术语来重新表达一下。

要创建一个 Bootstrap 网格,首先添加一个容器元素,就像这样:

<div class="container">
</div>

啊,这样说和读起来容易多了!好了,回到我们正常的节目安排…

你可以有两种类型的容器,它们的类名使它们彼此区分开来:

<!-- fixed-width container centered in the middle the viewport 
--> <div class="container"></div> 

<!-- full-width container that spans the entire viewport width (no margins) 
--> <div class="container-fluid"></div>

行元素必须嵌套在容器元素中。(哈!我喜欢这个元素的东西。试着在写作中解释它而不做类似的事情!)Bootstrap 网格必须至少包含一行,并且可以包含所需的行数。

在前一个容器代码的基础上,以下是具有两行的网格代码的样子:

<div class="container"> 
  <div class="row"> 
  </div> 
  <div class="row"> 
  </div> 
</div> 

一行不一定要包含一列——例如,你可能只是想在网格中的两行之间留出空白,但它最多可以包含 12 列。

然而,重要的是要注意,一行中的列数与嵌套列元素的数量不成比例(我们将在下一节中看一下 Bootstrap 对列的概念)。这是因为一行中的总列数与该行中的列元素数量是独立的。

让我通过在上一行代码中添加三个示例来澄清这个概念。我将解释class="col-4"class="col-6",以及一般来说,class="col-x"(其中 x 是从 1 到 12 的整数)的含义,紧接着是以下三个网格示例。

在第一个示例中,网格有两行,每行都有三列等宽:

<div class="container"> 
  <div class="row"> 
    <div class="col-4"> 
    </div>
    <div class="col-4"> 
    </div> 
    <div class="col-4"> 
    </div> 
  </div> 
  <div class="row"> 
    <div class="col-4"> 
    </div>
    <div class="col-4"> 
    </div> 
    <div class="col-4"> 
    </div> 
  </div> 
</div>

在第二个示例中,网格只有一行,有两列等宽:

<div class="container"> 
  <div class="row"> 
    <div class="col-6"> 
    </div>
    <div class="col-6"> 
    </div> 
  </div> 
</div>

在第三个例子中,网格也只有一行,有两列,但它们的宽度不相等。事实上,第一列只占总宽度的 25%,第二列占了剩下的 75%。

<div class="container"> 
  <div class="row"> 
    <div class="col-3"> 
    </div>
    <div class="col-9"> 
    </div> 
  </div> 
</div>

好了,现在我们已经看过三个网格示例,我们可以讨论一下 "col-x" 类名到底是什么意思。网格允许每行最多有 12 列,你在行中嵌入的每个列元素可以跨越 1 到 12 列——这就是 x 代表的意思。举个例子,如果我们在行中有一个列元素,并且希望它跨越可用的 12 列中的 8 列,我们的类名将是col-8,我们的列元素将如下所示:<div class="col-8">。关键是我们行中的列的总数(也就是我们类名中 x 的总和)不应该超过 12。但是,它可以少于 12。

此外,你的网格中的每一行可以有不同数量的列,每个列的宽度也可以不同。在讨论一些有趣的方法之前,让我们快速看一个例子,你可以通过向行元素添加预定义的 Bootstrap 类来对齐行内的列。

<div class="container"> 
  <div class="row"> 
    <div class="col-10"> 
    </div>
    <div class="col-2"> 
    </div> 
  </div> 
  <div class="row"> 
    <div class="col-4"> 
    </div>
    <div class="col-3"> 
    </div>
    <div class="col-5"> 
    </div> 
  </div> 
</div>

在前面的代码中的网格有两行,第一行有两列宽度不等,第二行有三列宽度不等。

每当你有一个网格时,你需要关心的是如何在其中对齐。Bootstrap 有预定义的类,可以用于行元素,以便对其中的列元素进行对齐。

以下是其中一些类:

  • justify-content-center(居中对齐列)

  • justify-content-start(左对齐列)

  • justify-content-end(右对齐列)

  • justify-around-end(均匀间隔列)

  • justify-between-end(将所有可用空间放在两列之间)

这些类的有趣之处在于它们影响封装行中列的水平对齐,只有当所有列元素跨越的列数总和少于 12 时,你才能看到它们的效果。这正是为什么允许少于 12 列的跨度的原因。

这是一个包含少于 12 列跨度的行元素的例子:

<div class="container"> 
  <div class="row justify-around-end"> 
    <div class="col-4"> 
    </div>
    <div class="col-4"> 
    </div> 
  </div> 
</div>

在先前的示例中,我们有一个包含两列的一行网格。然而,由于跨度的列少于 12 列,将应用水平对齐(由于justify-around-end类),这将产生可见效果,即将列居中,并在列周围插入可用的未使用空间(在这种情况下为行宽的三分之一)。这将使列两侧出现边距,它们之间的边距加倍。

其他提到的类别具有不同的水平对齐效果,与它们旁边的项目描述不同。我鼓励你尝试使用这些类别来熟悉它们。

列元素必须嵌套在行元素中,就像之前的示例所示。我们已经看到了多少列元素可以放入一行中,这取决于它们各自的列宽度。

网格中的列基本上是网格中的单元格,是您的内容(即文本、图像等)要插入的位置。如果您有一个包含六行四列元素的网格,您有 24 个单元格可供放置您的内容。

就像你可以使用行元素上的特殊类别来对齐列元素一样,你也可以使用列元素上的特殊类别来对齐列元素内的内容。

以下是您可以在列元素上使用的一些类别,以便对齐其中的内容:

  • align-self-start将强制特定单元格的内容位于单元格顶部

  • align-self-end将强制特定单元格的内容位于单元格底部

  • align-self-center将强制特定单元格的内容位于单元格的垂直中心

不同的视口大小

关于 Bootstrap 的网格,我想要讨论的最后一件事可能是最重要的。什么使得网格具有响应性?也就是说,网格如何适应不同的视口尺寸?这个问题有两个方面的答案。首先,大多数 HTML 布局(甚至是那些根本没有设计为响应式的普通布局)在不同尺寸的屏幕上查看时都有一定的灵活性。然而,虽然标准网页的布局在平板电脑和普通 19 英寸显示器上的渲染可能仍然可以接受,但对于一个在平板上看起来不错,但目前正在普通手机上查看的网站来说,情况往往会变得混乱,甚至根本无法使用,比如 iPhone 7 或类似尺寸的 Android 设备。这就是我们需要一些设计干预的地方,也是 Bootstrap 网格适应设备视口尺寸的第二种方式,即对类和列的类名进行特殊调整。

你会记得我们一直在为列元素使用的类名具有以下一般形式:

<div class="col-x">

为了使网格具有响应性,Bootstrap 包含了让我们通过在colx之间的类名中添加一个符号来调整类的能力(即从 1 到 12 的整数)。

例如,以下是带有其中一个这些符号的列元素类的样子(实际上,它不是一个符号,而是一个新的类名,但是为了解释起见,你可以把它看作是一个符号):

<div class="col-sm-4">

我将解释一下col-sm-4中的sm是什么意思,但实际上,在实践中,你会在列元素上看到不止一个类名。例如,以下是列元素上可能的一组类名:

 <div class="col-xs-12 col-sm-4 col-md-3" >

好的,让我们解释一下这组类是用来做什么的。为了做到这一点,让我首先列出可用的符号及其含义:

视口尺寸 超小 超大
网格断点 <576px >=576px >=768px >=992px >=1200px
最大容器宽度 540px 720px 960px 1140px
符号 xs sm md lg xl
典型设备 iPhone、iPod、Android 手机 iPad 1、iPad 2、iPad Mini 旧显示器(低分辨率,800x600)、一些旧 Android 平板 普通现代显示器、大多数现代 Android 平板 高分辨率现代显示器、iPad 3、iPad 4、Android 平板
类前缀 .col-xs- .col-xs- .col-md- .col-lg- .col-xl-

在上表中,从底部的第三行开始,我列出了对你可用的五个符号。在倒数第二行,我列出了符号和网格断点适用的典型目标设备。我将在一会儿讨论网格断点,但我只想说我列出的这些目标设备是经验法则——它们并非一成不变。例如,Android 平板电脑在五个视口大小列中出现了三次。这是因为有许多 Android 平板电脑制造商,甚至更多尺寸的显示器(即视口)可供选择。笔记本电脑也是如此。然而,基于苹果产品的视口大小是众所周知的,数量较少——这就是我按名称列出它们的原因。可以说,通过查看典型设备的行,你可以相当清楚地了解你可能想要使用哪个列类。

掌握了视口大小和之前的表格的知识,现在让我们解密这个列元素和类别的含义:

<div class="col-xs-12 col-sm-4 col-md-3" >

这个列元素包含一组三个类,每个类基本上指示浏览器如何根据视口大小呈现列和其内容。从技术上讲,视口大小是显示器的最大尺寸(以像素为单位)。以分辨率设置为 1600 x 900 的 13 英寸笔记本电脑显示器为例,其视口大小为 1600 像素宽,900 像素高。然而,在实际情况下,视口大小是浏览器窗口的尺寸,而不是笔记本电脑显示器本身的尺寸。这在我们谈论响应式网页设计时是一个重要的区别,因为在使用台式机或笔记本电脑时,人们可以调整他们的浏览器大小——这会强制网页重新呈现——因此,这确实是 Bootstrap 的视角和我们的目的所在的视口大小。

回到解密上一列元素的过程,参考上一个视口大小表,并提到浏览器调整大小如何决定我们作为开发者关心的视口大小,我们现在可以解密这三个类别指示浏览器要做什么:

  • col-xs-12:这告诉浏览器,当视口宽度小于 576 像素时,该列应跨越所有 12 列。换句话说,该列应占据整个行的可用宽度。

  • col-sm-4:这告诉浏览器,当视口宽度在 576 到 767 像素之间时,该列应占用 12 个可用列中的四列。换句话说,该列应占据行宽的 1/3。

  • col-md-3:这告诉浏览器,当视口宽度为 768 像素或更多时,该列应占用 12 个可用列中的三列。换句话说,该列应占据行宽的 1/4。

我们本可以通过添加带有类前缀.col-lg-.col-xl-的类来控制视口宽度为 992 像素或更多时列的呈现,但在我们刚刚看到的例子中,我们似乎并不在乎——也就是说,无论视口有多宽(甚至是 2400 像素!),我们的列宽都会按比例占据行宽的 25%。

各位先生女士,这就是你如何设计一个网页,同时保持网格单元格中内容在成千上万个视口尺寸上的呈现方式。通过利用 Bootstrap 的网格,我们不再需要编写多个版本的网页来让它们在不同尺寸的显示器上显示我们想要的样子。相当酷,不是吗?

Bootstrap 组件

正如本章开头提到的,我不想在讲解组件时简单地重复 Bootstrap 的文档。相反,我将简要讨论我们将使用的 Bootstrap 的五个组件,展示它们的一些基本代码,并指向 Bootstrap 官方文档,让你可以了解更多关于这些组件的选项,其中有很多——远远超出了这本书的覆盖范围。

按钮组件

按钮无处不在——不,我指的不是你最喜欢的衬衫上的按钮。如果你曾经乘坐电梯(嘿,有些人绝对拒绝进入电梯),你肯定会看到按钮,并按下其中一个按钮会把你带到你想去的地方。电视遥控器也是一样的——但是它不是把你带到另一个地方(至少目前还没有,但也许在未来,你们永远不知道),它是把你的思绪从舒适的客厅带到另一个地方。这些按钮执行功能性、有意义的任务。网页上的按钮呢?可以说,它们也传输东西——比如,当你点击表单上的提交按钮时,它传输信息。但也许按钮同样重要的功能是帮助使你的网页更具吸引力和直观。幸运的是,Bootstrap 让我们可以轻松地向我们的网页添加漂亮的样式按钮——比浏览器在添加按钮元素时呈现的默认灰色按钮精致 100 倍。

让我们看看一些这些类,同时探索一些 Bootstrap 预定义的按钮类(即样式)。

无需任何调整,我们可以通过给按钮元素分配两个类来轻松插入一个漂亮的样式按钮,就像这样:

<button type="button" class="btn btn-primary">Click me</button>

那个按钮是蓝色的,但是我们可以通过其他类来访问其他默认颜色:

  • btn-secondary: 浅炭灰色,白色字体

  • btn-success: 浅绿色,白色字体

  • btn-danger: 红色,白色字体

  • btn-warning: 金黄色,黑色字体

  • btn-info: 蓝绿色,白色字体

  • btn-light: 浅灰色,黑色字体

  • btn-dark: 几乎是黑色,白色字体

还有一个将按钮变成链接的类:btn-link

如果你更喜欢白色一些,或者颜色更轻一些,Bootstrap 有一组与前面的类匹配的类,称为轮廓按钮。颜色和类名是相同的,唯一的区别是在btnsecondary, success, danger等之间加上outline这个词。按钮是透明的,除了轮廓或边框,当然,按钮上的文本的字体颜色也是不同的。

这些类名看起来是这样的:

  • btn-outline-secondary: 浅炭灰色轮廓,字体颜色相同

  • btn-outline-success: 浅绿色轮廓,字体颜色相同

  • btn-outline-danger: 红色轮廓,字体颜色相同

  • btn-outline-warning:金菊色轮廓,字体颜色相同

  • btn-outline-info:青色轮廓,字体颜色相同

  • btn-outline-light:浅灰色轮廓,字体颜色相同

  • btn-outline-dark:几乎黑色轮廓,字体颜色相同

所有这些按钮都有默认的高度和字体大小。但是,正如您可能已经猜到的那样,Bootstrap 有一种方法可以通过分别添加.btn-lg.btn-sm类来使默认按钮变大或变小。以下是它的样子:

  • <button type="button" class="btn btn-primary btn-lg">我很大</button>

  • <button type="button" class="btn btn-primary btn-sm">我很小</button>

您可以在这里阅读有关 Bootstrap 按钮的所有信息:getbootstrap.com/docs/4.0/components/buttons/

警报组件

当用户在网页上执行操作,例如在其用户资料中更新其电话号码时,让他们知道更新是否成功或不成功总是很好。有时这些用户反馈消息被称为“闪现消息”(因为它们通常只出现片刻,然后消失,以免使屏幕混乱)。Bootstrap 称它们为“警报”,通过向div元素添加预定义的警报类和 role 属性来创建它们。

在大多数情况下,它们的着色和命名方案与按钮组件相当一致。以下是可用的警报:

  • <div class="alert alert-primary" role="alert">这是一个主要警报</div>

  • <div class="alert alert-secondary" role="alert">这是一个次要警报</div>

  • <div class="alert alert-success" role="alert">这是一个成功警报</div>

  • <div class="alert alert-danger" role="alert">这是一个危险警报</div>

  • <div class="alert alert-warning" role="alert">这是一个警告警报</div>

  • <div class="alert alert-info" role="alert">这是一个信息警报</div>

  • <div class="alert alert-light" role="alert">这是一个浅色警报</div>

  • <div class="alert alert-dark" role="alert">这是一个黑暗警报</div>

Bootstrap 的警报不仅外观漂亮,而且非常整洁。您可以在其中嵌入链接(毕竟它只是 HTML),甚至插入一个可选的关闭按钮。警报组件是 Bootstrap 依赖 jQuery 库的一个很好的例子,因为它需要用于关闭警报组件。

警报值得学习,这样您就可以在应用程序中利用它们。这是 Bootstrap 关于其警报组件的文档链接:getbootstrap.com/docs/4.0/components/alerts/

导航栏组件

导航栏组件非常丰富 - 您可以做很多事情 - 但本质上,它是 Bootstrap 为您提供一个漂亮样式的网页顶部导航栏的方式。丰富性来自于可以使用的一些子组件。这些包括以下内容:

  • .navbar-brand 用于您公司、产品或项目名称

  • .navbar-nav 用于全高度和轻量级导航(包括对下拉菜单的支持)

  • .navbar-toggler 用于与我们的折叠插件和其他导航切换行为一起使用

  • .form-inline 用于任何表单控件和操作

  • .navbar-text 用于添加垂直居中的文本字符串

  • .collapse.navbar-collapse 用于通过父断点对navbar内容进行分组和隐藏

在这里展示所有这些项目的示例将成本过高,而受益甚微。与其在这里这样做,不如在本章后面向您展示如何使用 Bootstrap 来构建我们示例应用程序的导航菜单。代码可以在本章末尾的代码列表中找到。接下来的页面中的第一个线框显示了一个标志占位符、一个菜单以及登录和立即尝试按钮。线框代表我们打算构建的页面的草稿。我们的导航栏看起来可能会有所不同,但将包含线框上显示的所有部分。

有关 Bootstrap 的 Navs 和 Navbar 组件的更多文档可以在这里找到:getbootstrap.com/docs/4.0/components/navs/getbootstrap.com/docs/4.0/components/navbar/

模态组件

模态组件是吸引用户注意力的好方法,可以用它们来创建灯箱、用户通知等。我喜欢用它们来弹出表单,让用户直接从列出这些项目的页面上添加和编辑项目。这样,所有项目列表的功能(即查看、添加、编辑和删除)都在一个页面上完成。以这种方式使用模态组件会导致直观的清晰设计。

与导航栏组件一样,这里展示示例并不是展示模态框的最佳方式。我将通过代码(在适当的时候引用代码清单)来向你展示我们将如何创建下面线框中显示的模态表单。当你看到线框时,你会发现我在页面中非常慷慨地使用了模态框。我甚至用它们来实现网站的登录和注册功能。

这里有几个关于 Bootstrap 模态组件的演示,你可以在这里查看:getbootstrap.com/docs/4.0/components/modal/

我们只涵盖了 Bootstrap 提供的四个常用组件,但这已经足够让我们一窥预定义组件的功能。还有许多其他可以使用的组件,你可以在官方 Bootstrap 网站上找到它们:getbootstrap.com/docs/4.0/components/

同样,我们没有涵盖所有 Bootstrap 的组件,因为官方文档已经完成了这项工作,并且做得很好。此外,我们将在后面的章节中使用 NG Bootstrap 组件、Angular Material 组件和我们将一起创建的自定义组件。

清单轮播 - 正式介绍

软件项目的演变是一件非常有趣的事情,它遵循一系列非常合乎逻辑的阶段。以下是我们将涵盖的阶段 - 这对于任何软件项目都是真实的:

  1. 创意生成/概念。

  2. 分析/可行性研究:对产品概念进行可行性研究的目的是审查项目的投资回报率(即投资回报率)。换句话说,这个项目是否值得公司投入资源(时间、金钱、人才等)?

  3. 需求收集。

  4. 使用案例。

  5. 线框。

  6. 实施。

有了这些软件项目阶段的概述,让我们来看一个使用清单轮播的真实例子。

创意生成/概念

软件项目的想法可以来自任何地方,任何时间,但在绝大多数情况下,这些想法都是受到解决组织在其生命周期中不可避免地遇到的问题的启发。主要的问题类别有解决低效和通过创建一个比竞争对手更好(即与众不同)的竞争产品在市场上创造机会。换句话说,软件项目通常可以被视为是一个高效的举措或竞争优势的举措。解决这两种类型的问题是每个不断发展的组织在其存在的某个时刻,或者在其整个存在期间都会遇到的需求。

那么,Listing Carousel 是如何构想出来的呢?作为前房地产销售员,转行成为 IT 专业人员,我很容易想到一种更好的方式,让房地产经纪人能够更好地向他们的社交媒体圈子传播他们的新房源,并以比目前其他主要选项更具信息性的方式展示他们的房源。虽然房地产经纪人可以通过多种方式推广他们的房源,但我发现他们缺乏两个基本的东西:

  • 他们的房源可以轻松地在他们的社交媒体圈子(即 Instagram 和 Facebook)中传播

  • 以一种稍微更具吸引力的方式呈现房产,同时更好地描述房产

所以,我的问题是我必须创建一个与其他软件服务明显不同的软件产品。解决办法是考虑之前列出的两个产品差异化因素,并假设我可以获得所需的技术来实现它。因此,对于 Listing Carousel 来说,可以说这个软件项目是作为竞争优势的举措构想出来的。

太好了!我有一个潜在的软件项目要做!接下来呢?嗯,正如本节开头提到的,下一个阶段是进行可行性研究。回想一下,对产品概念进行可行性研究的目的是审查项目的投资回报率,并进行研究,看所需技术是否已经可获得,或者是否可以创建?让我们简要地看一下接下来的内容。

分析 - 可行性研究

项目分析的这个阶段将决定是否继续进行。进行可行性研究的正确方法是准备一个商业计划,并向投资者展示。为什么?公司经理编写商业计划并向投资者(或公司的副总裁、总裁或首席执行官——对于内部软件项目)展示的原因是因为他们需要一份可以与投资者分享以衡量项目兴趣的文件。如果投资者有兴趣进行投资,那么这意味着该项目具有价值。

这个完美的文件是正式的商业计划,因为它包含了投资者想要看到的所有重要信息的摘要,即:

  • 市场分析:市场是否有空间容纳另一个类似产品?市场潜力是多少?

  • 竞争分析:我们的产品/服务将如何不同?我们将在成本、质量还是功能上竞争?

  • 所需资源:项目需要什么人员?需要多少人时来构建并推向市场?

  • 预算:项目总共需要多少资金预算(IT、销售、运营成本等)?

  • 财务预测:在接下来的 12 个月、两年、三年和五年内可以预期的收入是多少?何时达到盈亏平衡点?

  • 退出策略:我们要经营公司多久?如何收回我们的投资?

你可能会问我是否真的为一个规模相当小的软件项目准备了详细的商业计划。答案是——当然!为什么?简而言之,我需要看看实施该项目是否值得我的时间和金钱。具体来说,我花了必要的时间准备商业计划,原因如下:

  • 市场分析:无论你觉得一个想法有多好,你都需要尽职调查,以合理确定市场对你即将进入的产品或服务是否有需求。如果有市场空间,那么你就有潜在的机会。在我的情况下,我相信 Listing Carousel 有市场空间,并且它在竞争激烈的市场中具有足够的差异化,可以给我带来竞争优势。

  • 开发成本和时间:时间和金钱都是宝贵的资源——开发软件产品或服务都需要。你在一个项目上投资的每一美元意味着你不能在另一个项目上投资。你的时间也是如此。你在做某事上花费的每个小时意味着你放弃了做其他事情的时间。所以,明智地选择你投入资源的地方!在我的情况下,我有一些钱用于一个有趣的项目——所以金钱部分已经得到解决。时间呢?这对我来说是一个困难的决定。虽然我实际上没有时间,但我喜欢这个项目,我有一些朋友是房地产经纪人——所以我决定,去他妈的,让我们试试吧。所以,我知道我需要投入多少钱,大致需要投入多少时间。

  • 预期收入:仅仅因为我要投入的必要资源(即时间和金钱)对我来说是可以接受的,并不意味着一切已成定局。下一步是进行一些计算,看看我是否会随着时间获利,以及能赚多少。如果投资回报率足够高,那就可以继续进行。在我的情况下,投资回报率实际上并不如我希望的那么好,事实上几乎为零!换句话说,我可能只能勉强保本。然而,你也必须听从直觉,而我的直觉告诉我,也许我最终能够出售软件服务,这将使项目变得有价值。在撰写本文时,我尚未出售 Listing Carousel,但它开始有了一点点利润。

  • 退出策略:在着手建立任何业务之前——我把 Listing Carousel 视为一个独立的业务——你必须考虑一个退出策略。什么是退出策略?它基本上定义了你如何摆脱经营和/或服务公司的义务。公司不会自己运行,所以除非你想永远与公司结婚,你需要从一开始就有一个退出策略。我无法在这本书中再多花时间详细阐述这一点,但可以说的是,我构建了公司的结构,使我的退出策略早已考虑在内。

需求收集

软件项目的这个阶段构成了项目计划的基础,项目经理使用该计划来确保项目按计划和预算进行。需求通常是从最终客户(内部或外部)那里收集的,但如果您正在构建市场上尚不存在的新产品,需求也可以来自想法板。

例如,对于列表走马灯,我向一些房地产经纪朋友请教,告诉他们我想要构建什么,以及我想要使它与他们已经在使用的东西不同。以下是我们提出的部分需求清单:

  • 能够创建走马灯式照片查看器(每个列表/属性一个,其中可以包含任意数量的照片)

  • 用户有能力上传照片

  • 能够为每张照片做注释(即,在照片底部添加说明)

  • 能够翻转照片,显示照片上所示内容的详细描述

  • 用户有能力将照片链接到走马灯

  • 用户有能力在走马灯中订购/重新订购照片

  • 用户有能力在 Facebook 上发布列表的走马灯

  • 用户有能力在 Instagram 上发布列表的走马灯

  • 用户有能力在他们可以访问的任何网站上手动放置一个魔术链接,以在模态灯塔中打开列表的走马灯

  • 在列表的走马灯自动滚动显示照片时播放背景音乐的能力

  • 每个走马灯都将被分配一个唯一的短链接,以便用户可以通过电子邮件或短信发送给任何他们喜欢的人

我们不会将所有这些功能都构建到我们的示例应用程序中,因为书中没有足够的空间来这样做,但我们将构建重要的功能。我们将省略的两个功能是魔术链接和播放音乐背景音轨。我不会做任何承诺,但我可能会在不久的将来在我的博客AngularMotion.io上发布有关如何构建魔术链接的博文。

线框图

这个阶段是规划和布局应用程序的外观和感觉的阶段。

以下是我们将要构建的页面的 12 个线框图(注意:有几个线框图太长,无法作为一个屏幕截图,比如欢迎页面,因此它们有多个屏幕截图)。

我们将在接下来的章节中实施其中一些线框图,并学习如何实施一些布局和组件。

实施

这就是关键所在。我们将使用刚刚审查过的线框图编写一些网页代码,以帮助我们进行指导。我们还需要一个网络服务器,这样我们就可以在构建页面时在浏览器中提供我们的新页面。

安装我们的临时网络服务器

我们将在第十二章中使用 Node 的内置网络服务器,集成后端数据服务。但是,由于在达到那一点之前我们还有一段时间,我们需要一个简单的临时解决方案。

我们以前没有讨论过浏览器,因为没有必要这样做,但现在有必要了。虽然使用哪种浏览器查看 Angular 应用程序并不重要,但在我们一起阅读本书时,使用相同的浏览器会更容易,尽管不是必需的。我在开发 Web 应用程序时首选的浏览器是 Chrome。与大多数浏览器一样,Chrome 有许多其他开发人员创建的扩展,可以提供从订阅通知到调试工具等各种功能。您可以从这里下载适合您选择操作系统的 Chrome:www.google.com/chrome/。您可以在 Chrome Web Store 中搜索并安装 Chrome 的扩展:chrome.google.com/webstore/category/extensions。在本书中,我们将使用 Chrome,特别是它的一些扩展,来完成一些任务。

首要任务是安装一个 Chrome 扩展,它将帮助我们为我们的应用程序构建页面。它被称为Web Server for Chrome,您可以在 Chrome Web Store 中搜索并安装它。我没有直接包含链接,因为 URL 非常长。

此扩展允许您选择文件所在的文件夹以及要监听的端口。您还可以选择其他选项。默认情况下启用的一个常见选项是自动显示index.html文件。例如,假设您为端口号输入8887,您将指向http://127.0.0.1:8887,并且您指定的文件夹中的index.html页面将自动在浏览器中提供。一旦您配置了这两个设置,您就可以查看我们创建的页面。

欢迎页面

我们将使用 Bootstrap 组件和网格布局来实现的第一个线框是欢迎首页。

看一下下面的线框截图。我们有一个包含应用程序 logo 占位符、导航菜单以及右侧的登录和立即尝试按钮的页眉部分。然后是一个展示应用程序标题的巨幕页眉。接下来,我们的内容部分被划分,以便我们可以将内容添加到页面上:

我们现在将继续实现上述线框截图的代码实现。我们首先将实现页眉部分,并使用<nav>标签来对所有页眉部分的代码进行分类,包括 logo、菜单和操作按钮:

<nav  class="navbar navbar-default navbar-fixed-top">
 <div  class="container">
 <div  class="navbar-header">
 <button  type="button"  class="navbar-toggle"               data-toggle="collapse" data-target="#myNavbar">
 <span  class="icon-bar"></span>
 <span  class="icon-bar"></span>
 <span  class="icon-bar"></span>             </button>
 <a  class="navbar-brand"  href="#myPage">Logo</a>
 </div>
 <div  class="collapse navbar-collapse"  id="myNavbar">
 <ul  class="nav navbar-nav mr-auto">
 <li><a  href="#features">Features</a></li>
 <li><a  href="#pricing">Pricing</a></li>
 <li><a  href="#about">About</a></li>
 </ul>
 </div>
 <div  class="collapse navbar-collapse ">
 <ul  class="nav navbar-nav navbar-right">
 <li><a  href="#features">Login</a></li>
 <li><a  href="#pricing">Try Now</a></li>
 </ul>
 </div>
 </div> </nav>

在上面的代码中,我们正在实现一个nav标签元素,并使用 Bootstrap 导航栏类,navbar-defaultnavbar-fixed-topnavbar-brandnavbar-collapse等。这些类具有默认功能,几乎涵盖了导航部分的所有方面。在上面的代码中值得注意的一点是navbar-collapse类,它有助于自动呈现各种设备屏幕分辨率。我们还添加了一些菜单链接,如功能、定价和关于。我们还添加了我们的操作项目,登录和立即尝试。

在浏览器中启动页面,我们应该看到如下截图所示的输出:

接下来,我们需要为内容部分制作布局。巨幕和内容部分。我们将使用jumbotron类与div部分,对于内容部分,我们将使用 Bootstrap 网格列类,rowcol-sm-8col-sm-4

<div  class="jumbotron text-center">
  <h1>The Smart way to showcase your listings</h1>  <p>Simple, beautiful and wonderful app</p>  </div> <!-- Container (About Section) -->
<div  id="about"  class="container-fluid">
 <div  class="row">
 <div  class="col-sm-8">
 <h2>Annotate your prices</h2><br>
 <h4>Some pictures aren't 1000 words and sometimes pictures 
     don't do something justice</h4><br>
 </div>

 <div  class="col-sm-4">
 <span  class="glyphicon glyphicon-signal logo"></span>
 </div>
 </div> </div> <div  class="container-fluid bg-grey">
 <div  class="row">
 <div  class="col-sm-4">
 <span  class="glyphicon glyphicon-globe logo slideanim"></span>
 </div>
 <div  class="col-sm-8">
 <h2>Our Values</h2><br>
 <h4><strong>MISSION:</strong> Our mission lorem ipsum dolor sit amet, 
    consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore 
    et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud 
    exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</h4> <br> <p><strong>VISION:</strong> Our vision Lorem ipsum dolor sit amet, 
    consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore 
    et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud 
    exercitation ullamco laboris nisi ut aliquip ex ea commodo 
    consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p> </div> </div> </div>

现在,让我们分析上面的代码,以了解一些重要的要点。我们正在利用强大的 Bootstrap 网格工具来创建我们的应用程序布局,使用列类来创建在各种屏幕分辨率上呈现的布局。在浏览器中运行应用程序,我们应该能看到如下截图所示的输出:

到目前为止,干得不错,伙计们。我们刚刚使用 Bootstrap 布局组件创建了我们的第一个欢迎页面布局。我们将继续使用相同的组件,并构建更多的线框,以使您感到舒适。在下一节中,我们将学习如何使用 Bootstrap 模态组件创建注册和登录界面。

注册

接下来,我们将使用 Bootstrap 的模态组件来实现我们的注册和登录页面。

让我们来看一下下面的线框。这是一个简单的模态窗口,带有一些表单字段输入元素:

让我们继续实现代码。以下是创建模态窗口的示例代码:

<div  class="modal fade"  id="signup-modal"  tabindex="-1"  role="dialog"      aria-labelledby="myModalLabel"  aria-hidden="true"  style="display: 
        none;">
  <div  class="modal-dialog">
  <div  class="loginmodal-container">
  <h1>Signup Account</h1><br>
  <form>
  <input  type="text"  name="firstname"  placeholder="Firstname">
  <input  type="text"  name="lastname"  placeholder="Last Name">
  <input  type="text"  name="brokrage"  placeholder="Brokrage">
  <input  type="text"  name="user"  placeholder="Username">
  <input  type="password"  name="pass"  placeholder="Password">
  <input  type="submit"  name="login"  class="login 
                loginmodal-submit" value="Sign Up">
  </form>  <div  class="login-help">
  <a  href="#">Register</a> - <a  href="#">Forgot Password</a>
  </div>
  </div>
  </div>
  </div>

在上面的代码中,我们使用了 Bootstrap 的模态组件和模态类 modal 和 modal-dialog。在模态对话框内容中,我们使用输入表单元素——名字、姓氏、经纪人、用户和密码创建了注册表单。在浏览器中启动页面,我们应该能看到如下截图所示的输出:

这是构建我们应用程序的一个很好的开端。在下一节中,我们将使用相同的 Bootstrap 模态组件来构建我们的登录页面。

登录

在前一节中,我们已经学习了如何在模态窗口内创建注册表单。在本节中,我们将学习如何在模态窗口内创建登录界面。方法和原则与我们创建注册页面的方式完全相同。

让我们来看一下下面的登录线框,我们马上就要实现它:

是时候行动起来了。我们将首先创建一个模态窗口,然后可以绑定一个点击事件来打开对话框窗口:

<div  class="modal fade"  id="login-modal"  tabindex="-1"  role="dialog"     aria-labelledby="myModalLabel"  aria-hidden="true"  style="display: 
     none;">
 <div  class="modal-dialog">
 <div  class="loginmodal-container">
 <h1>Login to Your Account</h1><br>
 <form>
  <input  type="text"  name="user"  placeholder="Username">
  <input  type="password"  name="pass"  placeholder="Password">
  <input  type="submit"  name="login"  class="login 
                   loginmodal-submit"  value="Login">

  </form>
             <div  class="login-help">
 <a  href="#">Register</a> - <a  href="#">Forgot Password</a>
 </div>
 </div>
 </div> </div>

在上面的代码中,我们实现了一个带有另一个表单的模态窗口,这次是用于登录功能,包括一些表单元素——用户名和密码以及一个提交按钮。在浏览器中启动页面,我们应该能看到以下输出:

我们的应用程序现在几乎成形了。我相信你和我一样兴奋。让我们继续实现列表页面。

列表

在之前的章节中,我们使用 Bootstrap 组件创建了我们的主页、注册和登录页面。在本节中,我们将创建我们的列表页面。看一下以下线框图。我们需要循环遍历我们的列表,并显示一个网格部分,在那里我们将显示到目前为止所有的列表。简单吗?当然:

我们需要使用 Bootstrap 的高级布局和网格组件来创建上述布局。看一下以下示例代码。我们可以以多种方式实现上述布局。我们可以使用 Bootstrap 网格列来设计布局,或者我们可以使用表格元素来设计结构。在这个例子中,我将向您展示如何使用表格元素来做到这一点,网格结构留给您作业:

<div  class="container-fluid">
 <table  class="table table-hover shopping-cart-wrap">
 <thead  class="text-muted">
 <tr>
 <th  scope="col"  width="150">Thumbnail</th>
 <th  scope="col">Caption</th>
 <th  scope="col">Property</th>
 <th  scope="col"  width="200"  class="text-right">Action</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td>
 <figure  class="media">
 <div  class="img-wrap"><img  src=
 "https://via.placeholder.com/150"  class="img-thumbnail img-sm"></div>
 </figure>         </td>
 <td>  Master Bedroom  </td>
 <td>  789 Charelston Rd  </td>
 <td  class="text-right">  <a  title=""            href=""  class="btn btn-outline-success"  ata-toggle="tooltip"            data-original-title="Save to Wishlist">            <i  class="fa fa-heart"></i></a>  <a  href=""               class="btn btn-outline-danger">Remove</a>            </td>
  </tr>
  <tr>
  <td>          <figure  class="media">
 <div  class="img-wrap"><img  src=
 "https://via.placeholder.com/150"                class="img-thumbnail img-sm"></div>
 </figure>         </td>
 <td>  Kitchen  </td>
 <td>  789 Charelston Rd  </td>
 <td  class="text-right">         <a  title=""  href=""  class="btn btn-outline-success"           data-toggle="tooltip"  data-original-title="Save to Wishlist">           <i  class="fa fa-heart"></i></a>         <a  href=""  class="btn btn-outline-danger btn-round">Remove</a>            </td> </tr>
 <tr>
 <td>
 <figure  class="media">
 <div  class="img-wrap"><img  src= 
          "https://via.placeholder.com/150"            class="img-thumbnail img-sm"></div>
 </figure>         </td>
 <td>  Den  </td>
 <td>  789 Charelston Rd  </td>
 <td  class="text-right">         <a  title=""  href=""  class="btn btn-outline-success"            data-toggle="tooltip" data-original-title="Save to Wishlist">            <i  class="fa fa-heart"></i></a>         <a  href=""  class="btn btn-outline-danger btn-round">Remove</a>
 </td> </tr> </tbody> </table> </div>  <!-- card.// -->

在上面的代码中,我们使用container-fluid类创建了一个容器,在容器内部,我们创建了一个表格和行结构来显示我们的列表。在更实际的情况下,数据将始终来自后端 API 或服务。对于我们的示例和学习目的,我们在这里存根化了数据。在浏览器中启动页面,我们应该看到如下截图所示的输出:

如果您看到前面截图中显示的输出,请给自己一个鼓励。我们在学习中取得了很大的进步。到目前为止,我们已经使用各种不同的 Bootstrap 组件和网格布局创建了四个页面。

在下一节中,我们将探索应用程序的一些其他线框图,我会留给你练习。大多数线框图将使用相同的组件、布局和网格布局。

创建列表

在本节中,我将与您分享创建列表页面的线框图。可以使用 Bootstrap 组件和布局轻松创建创建列表页面。相反,我们将在下一章中学习如何使用 Flex-layout 来实现这一点。以下是您参考的线框图:

在下一节中,我们将看到编辑列表页面的设计和线框图细节。

编辑列表

在本节中,我们将学习编辑列表屏幕的设计和线框图。如果您仔细观察,编辑列表页面与创建列表页面类似,只是数据在加载时被填充。

创建列表屏幕一样,我们将在下一章中使用 Flex-layout 设计编辑列表页面。

线框图集合

在本节中,我们将看到其他页面的设计线框图,这些页面将在接下来的章节中创建。

以下是列表预览页面的设计线框图:

以下是物业详情的设计线框图。如果您注意到,我们将使用相同的 Bootstrap 模态窗口组件。当我们打开模态窗口时,我们应该看到物业详情:

现在,我们将学习如何为照片页面设计线框图。如果您仔细观察,布局结构看起来与列表页面相似。我们将不得不使用常见库创建可重用的设计,这些设计可以在各种页面和模板中重复使用:

接下来是上传照片页面。我们将再次创建一个模态窗口组件,并通过它提供文件上传选项,以便我们可以轻松上传照片:

现在,让我们继续编辑照片线框图。我们再次利用 Bootstrap 的模态窗口组件来设计我们的编辑照片页面。我们将使用 Angular 的数据绑定来绑定模态窗口中的数据:

最后,我们将探索照片预览页面。我们可以使用模态窗口 Bootstrap 组件显示照片。我们将关闭常见的操作按钮以关闭或编辑模态窗口:

在本章中,我们做了相当多的工作,学习了 Bootstrap 网格和布局组件。作为实际学习示例的一部分,我们创建了一些页面并设计了我们将在应用程序中使用的线框图。

总结

这一章充满了各种好东西。你现在应该了解我们将要构建的示例应用程序,我们构建阶段的高层游戏计划,以及我们将采取的第一阶段构建的五个步骤过程。

然后,我们讨论了 Sass 是什么,以及它的一些功能如何帮助我们创建应用程序的 CSS。我们将研究一些工具,帮助你在编写应用程序的 Sass 时。接下来,我们了解了 Bootstrap 是什么,以及如何将其集成到你的应用程序中。我们学习了 Bootstrap 的网格是什么,以及如何使用它的基础知识,以及一些 Bootstrap 的组件以及如何使用它们。

最后,我们研究了软件项目的演变,从构思到实施。在这里,我们涵盖了不同类型的分析,需求的收集以及一些用例。我们还涵盖了线框图,详细介绍了每个线框图的目标,以及基本的设计原则(在描述线框图时提到)。

到目前为止,在本书中,除了我们在第一章中构建的快速待办事项应用程序外,我们甚至还没有接触过 Angular。这将会改变——从下一章开始,第四章,路由。在这一章中,我们将使用 CLI 创建应用程序的 Angular 外壳(就像我们在书的开头所做的那样)。但是,我们将为其添加路由。我将在我们逐步进行的过程中解释什么是路由,以及如何为我们的应用程序配置路由。

所以,在你翻页之前,给自己一个鼓励,伸展一下,也许倒一杯你最喜欢的饮料。干得好,我的 Angular 初学者同伴们。通过了这前三章,你现在已经准备好迎接 Angular 了!

第四章:路由

上一章是一个庞然大物,但它是必要的,因为它需要给你提供两种你可能会使用的技术的速成课程,或者应该考虑在你的网页开发项目中使用的技术(无论你的项目是否利用 Angular)。另外,第三章《Bootstrap - 网格布局和组件》也有助于为本书的其余部分铺平道路。

与之相比,本章要小得多,但它标志着我们进入 Angular 的真正开端。从这一点开始,每一章,甚至第十二章《集成后端数据服务》,其中主要关注在 Node 和 MongoDB 中构建后端服务,都包含 Angular 的内容(具体来说,如何使用 Angular 的 HTTP 客户端以及如何将代码封装在 Angular 服务中)。

关于本章的另一个注意事项是,大多数关于 Angular 的书籍在介绍 Angular 模板和组件之前并不介绍路由,这是可以接受的;但我们不会遵循这种方法。尽管路由和组件模板是密不可分的,这也是大多数书籍在介绍组件之后再讨论路由的原因,但理解组件并不是理解路由的先决条件。

更一般地说,大多数编程书籍都试图提前呈现所有的材料和概念,然后在以后的某个时候回过头来尝试以某种方式实现它们。这种方法的问题之一是,它违反了我们大脑在吸收和内化新信息时的工作方式。通常最好立即以小的增量步骤使用新信息。

本书的重点是尽可能实用,尽快实现,并以最大程度地保留和理解新材料的方式。因此,为了实现这一点,我们将在整本书中一起构建我们的示例应用程序,学习我们需要的主题,而不是在之前。这意味着我们经常会实现尚未完全解释的内容。它们将在实现它们时向您解释,或者在之后立即解释——当您的大脑处于最佳状态,并寻找模式以促进理解时。

所以,不要担心头等跳进去——通常这是最好的方式。我是你的向导,我会一直陪伴你到书的最后。

在本章中,我们将一起学习以下内容:

  • 为 Angular 应用程序定义路由是什么

  • 使用 CLI 创建应用程序的外壳以及它的前几个组件

  • 为我们的应用程序配置路由

  • 研究路由重定向、参数化路由和路由守卫

  • 完成我们应用程序的路由配置。

  • 研究路由策略

有很多内容要涵盖(即使是像这样的小章节),所以让我们开始吧!

什么是 Angular 中的路由?

在 Angular 中,路由简单地将请求的 URL 映射到一个组件。这往往会让从另一个具有路由的技术(特别是不是单页面应用程序框架的技术)转向 Angular 的人感到困惑。让我稍微解释一下。

Angular 应用程序只有一个页面(因此,术语单页面应用程序),我们将在创建 Angular 应用程序时看到。Angular 组件有模板,这些模板是用于设计结构和布局的标准 HTML 元素。正如我们将在第六章中看到的 构建 Angular 组件,它们也有样式。

正如书的第一章中提到的,Angular 应用程序可以被看作是组件树。这意味着组件可以包含其他组件,并且这种组件的嵌套可以根据应用程序的需要继续进行。

因此,尽管组件有模板(注意:一些 web 框架将 web 页面称为模板),Angular 的路由将 URL 路径映射到组件,而不是 web 页面或模板。当请求的 URL 渲染为组件的模板时(我们马上就会看到这是如何发生的),不仅会渲染该组件的模板,还会渲染所有嵌套组件的模板。由 Angular 路由映射到的顶级组件可能包含其他子组件,这些子组件又可以包含其他子组件,依此类推。这就是组件树的含义。

在大多数情况下,Angular 应用程序中的数据是从父组件流向其直接子组件的。它不会从父组件流向其孙子组件。此外,数据也不会向上流动。这是一个单向流动-从父级到子级。我说“在大多数情况下”,因为有一些技术和库可以改变部分行为-例如,组件可以通过中介相互通信,我们将在本书后面讨论。但是,按设计,没有外部干预,数据是从父级到子级流动的。

随着我们在本书中的进展,您将熟悉所有这些。您现在唯一需要理解的是,要理解路由,URL 被映射到组件而不是页面,因为 Angular 应用程序只有一个页面。Angular 应用程序中唯一的页面是index.html页面,位于 app 目录中。在[第六章]中,我们将看到我们的默认组件如何加载到index.html页面中。现在,让我们回到路由。

使用 CLI 创建应用程序的外壳

这就是一切的开始。我们现在已经到达了使用 CLI 创建应用程序的起点以及我们需要连接到路由配置的第一批组件的点。我们已经学习了如何安装 CLI,甚至一起创建了我们的第一个 Angular 应用程序-尽管我们的待办事项应用程序很小,只是为了让我们入门-在[第一章]中。

如果您还没有安装 CLI,那么现在肯定要安装了。一旦您完成了这个步骤(希望您已经完成了),启动 CLI,让我们开始吧!

首要任务是在您的计算机上创建一个目录,您将在其中放置所有的 Angular 项目。不要为我们的示例应用程序创建一个目录,因为 CLI 会为您完成这项工作。只需在文件系统上创建一个文件夹,并从命令行(如果您的操作系统是 Windows)或终端(如果您的操作系统是 Mac 或 Linux)中导航到该文件夹。为了简洁起见,从现在开始,我将称其为您的终端,文件夹为目录。

接下来,我们将使用 CLI 来创建我们应用程序的骨架(即根目录),以及 CLI 为我们创建的所有必需的 Angular 应用程序所需的文件和子目录。输入以下命令:

ng new realtycarousel 

注意:这将需要大约一分钟的时间来完成。

如果你看到最后一行输出为 Project realtycarousel successfully created.,那么现在你应该有一个名为realtycarousel的目录,其中包含我们应用程序的所有文件。

上述命令的输出如下截图所示:

现在让我们测试一下是否可以运行它。使用cd命令导航到你的realtycarousel目录:

cd realtycarousel

接下来,使用 CLI 的服务器命令启动我们的 Angular 应用程序:

ng serve  

你应该在终端看到一堆行输出。如果其中一行类似于*** NG Live Development* Server is listening on localhost:4200, open your browser on http://localhost:4200/ **,并且最后一行是webpack: Compiled successfully,那么你应该打开浏览器并将其指向http://localhost:4200

如果你看到一个带有 Angular 标志的页面,这意味着一切都设置正确了。你现在有一个空的 Angular 应用程序。

你可以按下Ctrl + C来停止 CLI 的开发服务器。

接下来,让我们添加几个组件,我们将在路由配置中引用它们。同样,现在不要担心组件。我们将在第六章 构建 Angular 组件 和 第七章 模板、指令和管道 中深入研究它们。

依次运行以下 CLI 命令列表:

ng g c home
ng g c signup ng g c login
ng g c logout
ng g c account
ng g c listings
ng g c createListing
ng g c editListing
ng g c previewListing
ng g c photos
ng g c uploadPhoto
ng g c editPhoto
ng g c previewPhoto
ng g c pageNotFound

第一个命令的输出如下截图所示:

当我们创建所有其他组件时,我们应该看到类似的输出。

我们现在有了我们需要的第一组组件。虽然它们的模板现在是空的,但这已经足够让我们为我们的应用程序配置路由了。

由于我们将在应用程序中使用 Bootstrap 进行一些操作,例如其导航栏和响应式网格,我们需要安装 Bootstrap 以及其依赖项。在第三章中,Bootstrap - 网格布局和组件,我们只是在index.html页面的头部引用了一些 CDN URL,以便能够使用 Bootstrap。但是,我们现在将以不同的方式安装 Bootstrap - 我们将使用npm

您需要在系统上安装 Node.js 才能使用node package managernpm)。

要安装 Bootstrap、jQuery 和 Popper,请在终端中运行以下命令:

npm install bootstrap@4 jquery popper --save

我们已经安装了库,现在是时候在我们的配置文件中包含它们,以便它们在整个应用程序中可用。

打开angular.json文件,并在相应的部分中包含样式表和 JavaScript 文件,如下面的代码片段所示:

"styles": [
    "styles.css",
    "./node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": [
    "../node_modules/jquery/dist/jquery.min.js",
    "./node_modules/bootstrap/dist/js/bootstrap.min.js"
] 

屏幕截图显示了编辑后的angular.json文件:

一切准备就绪!

现在我们已经拥有了我们需要为应用程序设置路由的核心文件。我们还确保安装了 Bootstrap,因为我们将在本章中为我们的应用程序创建导航栏。此外,我们的导航链接将包含 Angular 用于路由的特殊标签,这也是我们此时需要安装 Bootstrap 的另一个原因。

让我们再次使用我们的 IDE(最好使用 Visual Studio Code,但您可以使用您喜欢的任何 IDE)打开我们的项目,这样我们就可以查看项目结构。此外,在下一节“完成我们的路由配置”中,我们将对一些文件进行更改以进行设置,因此您需要一种方便打开和编辑这些文件的方式。

现在在您的 IDE 中打开项目后,导航到app目录,该目录位于src目录内。作为 Angular 开发人员,我们将在app目录中度过绝大部分时间。在app目录中,您会找到许多以app开头的文件。这些文件组成了我们应用程序中的根组件(即应用程序组件),当我们来到第六章 构建 Angular 组件时,我们将会检查这些文件的每个文件的作用,您将会非常熟悉 Angular 组件。您将在app目录中看到许多子目录,每个子目录都是我们刚刚创建的组件,比如 about、account、home 等。

请记住,Angular 应用程序的编写语言是 TypeScript,这就是.ts文件扩展名的含义。让我们开始为我们的应用程序配置路由。

首先要了解的是基本概念

在这一部分,我们将在开始为我们的 Angular 应用程序添加路由之前,快速了解一些基本概念的概述。在基本概念中,我们将学习Base HrefRouterLinkRouterLinkActive,这些是我们在使用 Angular 路由时需要在模板中实现的内容。

Base Href

为了在应用程序内部组合链接,每个 Angular 应用程序都应该在父级别定义base href

打开由 Angular CLI 生成的应用程序,并查看index.html文件。我们将看到基本href定义为/,这将解析为根或顶级层次结构。

以下截图显示了由 Angular CLI 生成的默认基本href配置:

RouterLink 和 RouterLinkActive

在第七章中,模板、指令和管道,我们将详细了解组件、指令和模板。现在,只需了解,就像 HTML5 中的锚元素和href属性一样,Angular 提供了一种绑定链接和 URL 资源的方式:

<nav>
 <a routerLink="/home" routerLinkActive="active">Home</a>
 <a routerLink="/listings" routerLinkActive="active">Listings</a>
</nav>

在上述代码中,我们添加了两个链接。请注意,我们已经在链接中添加了routerLink属性,这将帮助我们分别绑定/home/listings的值。

还要注意,我们已经添加了routerLinkActive属性,并将值分配为active。每当用户点击链接时,Angular 路由将知道并使其处于活动状态。有些人称之为魔术!

为我们的应用程序配置路由

是时候为我们的应用程序添加 Angular 路由了。

我们有两种实现路由的选项:

  • 我们可以使用 Angular CLI 在项目创建期间添加路由

  • 或者我们可以手动添加 Angular 路由到我们的应用程序中

首先,让我们探索简单的方法,使用 Angular CLI 添加路由。

Angular CLI 为我们提供了一种简单的方法来为我们的 Angular 应用程序添加路由功能。在生成新项目时,Angular CLI 将提示我们选择是否要为我们的应用程序添加路由。

以下截图显示了在 CLI 中显示添加 Angular 路由选项:

当我们选择在我们的应用程序中添加路由选项时,我们使用 Angular CLI 创建文件,导入所需的模块,并创建路由规则集。

现在,让我们手动为我们的项目添加路由。让我们看看如何在我们的应用程序中配置路由。

为了配置我们的路由,我们需要按照以下步骤进行:

  1. 打开app.module.ts文件

  2. 在文件顶部的import部分添加以下import语句:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

RouterModule包含路由服务和路由指令。

Routes模块定义了路由类型(记住,TypeScript 为 JavaScript 添加了变量类型)。

  1. app-routing.module.ts文件中编写一些路由和规则集:
const appRoutes: Routes = [
  { path: ‘home’, component: HomeComponent },
  ...
  { path: ‘’, redirectTo: ‘/home’, pathMatch: ‘full’ },
  { path: ‘**’, component: PageNotFoundComponent  }
];

这段代码只显示了三个映射:

  • HomeComponent的映射

  • 重定向的映射

  • 通配符或catch-all的 URL 请求的映射

第一个映射对象是最简单的情况。URL 路径(即域名后面的部分)映射到一个组件,没有任何参数(注意路由可以被参数化,我们很快会在参数化路由部分看到)。这个路由的作用是指示 Angular 在请求的 URL 路径以 home 结尾时呈现HomeComponent模板。

第二个映射对象是如何将一个路径重定向到另一个 URL 和路由的示例。这通常被称为路由重定向。在我们的情况下,路径是一个空字符串,这意味着当仅在浏览器位置栏中输入域名时,Angular 的路由机制将重定向请求(即更改 URL 中的路径)到/home。由于有一个处理/home的映射对象,它将被触发,从而呈现HomeComponent模板。这是网站的常见做法——输入域名通常会将用户带到主页或索引网页。在我们的情况下,由于我们正在构建 SPA(这就是 Angular web 应用程序),没有主页,而是一个主页组件,这意味着主页组件的模板被呈现以模拟主页。

第三个映射对象是通配符匹配的一个示例,并且放置在最后一个映射对象。当 Angular 的路由机制解析请求的 URL 时,它会从上到下将其与映射对象进行比较。如果 URL 不匹配任何映射规则集,将触发最后一个映射对象。对于我们的应用程序来说,这意味着如果没有匹配项,将呈现PageNotFoundComponent模板。

  1. 现在是时候导入我们的appRoutes了;这是我们告诉 Angular 我们的路由的方式。appRoutes是一个包含我们路由映射的常量,让我们接着创建它:
imports: [
 BrowserModule,
 RouterModule.forRoot(appRoutes)
]
  1. 最后,我们需要将app-routing.module.ts文件导入到app.module.ts中。

app-routing.module.ts文件的完整代码清单在本章后面的完成我们的路由配置部分中。

我们已经将路由直接添加到app.module.ts文件中。将路由配置文件分离出来是一个很好的做法。更好的做法是,在创建项目时始终使用 Angular CLI 直接添加路由。

就是这样;我们已经在我们的项目中实现了路由。在下一节中,我们将详细了解如何添加更多路由,向我们的路由添加参数,并创建子路由。

参数化路由

参数化路由是具有变量值作为 URL 路径一部分的路由。例如,一个常见的例子是当我们通过 ID 引用某些内容时,如下所示:

  • /listing/23(在我们的房地产网站上显示属性#23)

  • /listing/55(在我们的房地产网站上显示属性#55)

  • /listing/721(在我们的房地产网站上显示属性#721)

显然,必须配置数百个路由不仅会很繁琐、低效和容易出错,而且这些路由的维护(即删除路由和添加新路由,因为属性列表的库存发生了变化)将会很麻烦。

幸运的是,Angular 允许参数化路由,可以解决这些问题。

看一下以下代码片段中更新的路由:

const routes: Routes = [
{ path: 'home'},
{ path: 'listings/:id', component: ListingDetailsComponent },
{ path: ‘’, redirectTo: ‘/home’, pathMatch: ‘full’ },
{ path: ‘**’, component: PageNotFoundComponent  } ];

仔细看,在前面的路由中,我们添加了一个捕获列表id的路由,并且我们还将其映射到ListingDetailsComponent组件。

换句话说,我们还可以说我们已经为列表创建了一个通用模板,并且根据运行时传递的动态值,组件将显示相应的数据。

那很容易。如果我们有一个涉及创建子路由的更复杂的场景呢?继续阅读。

子路由

到目前为止,我们创建的路由都是非常简单和直接的用例。在复杂的应用程序中,我们将需要使用深度链接,这指的是在许多级别下追踪链接。

让我们看一些例子:

  • /home/listings(显示家中的列表)

  • /listing/55/details(显示列表#55 的详细信息)

  • /listing/721/facilities(显示列表#721 的设施)

这就是子路由对我们非常有用的地方。

在以下示例中,我们在 home 路由路径内创建了一个子路由:

const routes: Routes = [
{ path: 'home',
 component: HomeComponent,
 children: [
 { path: 'listings',
    component: ListingsComponent}
 ]
},
{path: 'listings/:id', component: ListingDetailsComponent },
{path: '', redirectTo: '/home', pathMatch: 'full'}
];

在前面的代码中,我们为home路径定义了children,再次指定了pathcomponent,这将对应于子路由路径。

好的,很好。这是好东西。

如果我们想在用户访问特定路由之前添加一些验证呢?就像俱乐部外面的保镖一样?那个保镖就叫做路由守卫。

路由守卫

与大多数 Web 应用程序一样,有一些资源(即页面/组件模板)是每个人都可以访问的(例如欢迎页面定价页面关于我们页面和其他信息页面),还有一些资源只能被授权用户访问(例如仪表板页面和帐户页面)。这就是路由守卫的作用,它是 Angular 防止未经授权用户访问应用程序受保护部分的方式。当有人尝试访问保留给授权用户的 URL 时,他通常会被重定向到应用程序的公共主页。

在传统的 Web 应用程序中,检查和验证是在服务器端代码中实现的,实际上没有选项可以在客户端验证用户是否可以访问页面。但是使用 Angular 路由守卫,我们可以在客户端实现检查,甚至不需要访问后端服务。

以下是我们可以在应用程序中使用的各种类型的守卫,以增强授权安全性的各种类型的守卫:

  • CanActivate:帮助检查路由是否可以被激活

  • CanActivateChild:帮助检查路由是否可以访问子路由

  • CanDeactivate:帮助检查路由是否可以被停用

  • Resolve:帮助在激活任何路由之前检索路由数据

  • CanLoad:验证用户是否可以激活正在进行懒加载的模块

在我们开始实际操作之前,我想给你快速概述一下 Angular 路由守卫,比如在哪里使用它们,如何使用它们,返回类型是什么,等等。路由守卫总是作为服务注入的(即,我们有@injectable并且需要注入它)。守卫总是返回一个布尔值,truefalse。我们可以让我们的路由守卫返回可观察对象或承诺,内部将其解析为布尔值。

我们将继续在上一节中创建的示例上继续工作和扩展。我们将添加一个新组件并将其命名为CRUD。作为用户,当您尝试访问crud路由时,我们将检查路由返回true时。我们将允许用户导航并查看模板;否则,应用程序将抛出错误提示。

让我们直接进入代码,实现路由守卫。就像我们学习如何生成组件或服务一样,我们可以使用ng命令生成路由守卫。在终端中运行以下命令:

ng generate g activateAdmin

我们刚刚生成了一个名为activateAdmin的新路由守卫。上述命令的输出显示在这里:

让我们看看 Angular CLI 生成的文件。在编辑器中打开activate-admin.guard.ts文件。看一下文件中生成的默认代码:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class ActivateAdminGuard implements CanActivate {
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> 
     | boolean {
    return true;
  }
}

前几行只是从 Angular 路由器中导入所需的CanActivateActivatedRouteSnapShotRouterStateSnapshot模块。接下来,我们知道由于路由守卫是可注入的,通过使用@injectable

装饰器,我们正在告知 Angular 将其注入到根内。我们正在创建一个名为ActivatedAdminGuard的类,其中已经创建了一个名为canActivate的方法。请注意,该方法必须返回一个布尔值,要么是true要么是false。我们已经创建了我们的路由守卫,现在让我们在app-routing.module.ts文件中创建一个路由。

看一下app-routing.module.ts文件的更新代码:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { CrudComponent } from './crud/crud.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import {ActivateAdminGuard } from './activate-admin.guard';

const routes: Routes = [
    { path: 'login', component: LoginComponent },
    { path: 'register', component: RegisterComponent },
    { path: 'crud', component: CrudComponent, canActivate:[ActivateAdminGuard] }

    ];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

请注意,在路由中,我们已经添加了canActivate接口,并且对于我们的crud路径,当我们尝试启动crud路由时,由于canActivate方法返回true,用户将能够看到组件模板。

现在,继续将值设置为false,看看会发生什么。

如果你看到应用程序的路由返回到base href,不要感到惊讶。

完成我们的路由配置

如前几节所承诺的,我将分享整个AppModule的源代码,包括路由配置。以下代码可能看起来很长或令人害怕,但相信我,它实际上非常简单和直接。

在学习本章的过程中,我们生成了许多组件并创建了它们的路由路径。我们只是导入这些组件并用它们的路径更新appRoutes。就是这样。我保证。

这是app.module.ts文件的完整清单:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { SignupComponent } from './signup/signup.component';
import { LoginComponent } from './login/login.component';
import { ListingsComponent } from './listings/listings.component';
import {ListingDetailsComponent } from './listing-deatails/listing-details.component';
import { EditListingComponent } from './edit-listing/edit-listing.component';
import { PreviewListingComponent } from './preview-listing/preview-listing.component';
import { PhotosComponent } from './photos/photos.component';
import { UploadPhotoComponent } from './upload-photo/upload-photo.component';
import { EditPhotoComponent } from './edit-photo/edit-photo.component';
import { PreviewPhotoComponent } from './preview-photo/preview-photo.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { FeaturesComponent } from './features/features.component';
import { PricingComponent } from './pricing/pricing.component';
import { AboutComponent } from './about/about.component';
import { SupportComponent } from './support/support.component';
import { AccountComponent } from './account/account.component';
import { LogoutComponent } from './logout/logout.component';

const appRoutes: Routes = [
 { path: 'home', component: HomeComponent },
 { path: '', redirectTo: '/home', pathMatch: 'full' },
 { path: 'signup', component: SignupComponent },
 { path: 'login', component: LoginComponent },
 { path: 'logout', component: LogoutComponent },
 { path: 'account', component: AccountComponent },
 { path: 'features', component: FeaturesComponent },
 { path: 'pricing', component: PricingComponent },
 { path: 'about', component: AboutComponent },
 { path: 'support', component: SupportComponent },
 { path: 'listings', component: ListingsComponent },
 { path: 'listing/:id', component: ListingDetailsComponent },
 { path: 'listing/edit', component: EditListingComponent },
 { path: 'listing/preview', component: PreviewListingComponent },
 { path: 'photos', component: PhotosComponent },
 { path: 'photo/upload', component: UploadPhotoComponent },
 { path: 'photo/edit', component: EditPhotoComponent },
 { path: 'photo/preview', component: PreviewPhotoComponent },
 { path: '**', component: PageNotFoundComponent }
];
@NgModule({
 declarations: [
 AppComponent,
 HomeComponent,
 SignupComponent,
 LoginComponent,
 ListingsComponent,
 CreateListingComponent,
 EditListingComponent,
 PreviewListingComponent,
 PhotosComponent,
 UploadPhotoComponent,
 EditPhotoComponent,
 PreviewPhotoComponent,
 PageNotFoundComponent,
 FeaturesComponent,
 PricingComponent,
 AboutComponent,
 SupportComponent,
 AccountComponent,
 LogoutComponent
 ],
imports: [
 BrowserModule,
 RouterModule.forRoot(appRoutes)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

我们刚刚创建了我们的路由,但我们需要通过创建一些链接来更新我们的模板文件,这些链接将具有前面定义的路由的路径。

任何应用程序中最重要的一点就是一个设计良好的菜单,它有助于引导用户并增加良好的用户体验。

使用 Bootstrap nav组件,我们将在下一节为我们的应用程序设计一个菜单。

Bootstrap 导航栏和路由链接指令

在我们结束本章之前,让我们回顾一下并为我们的应用程序创建 Bootstrap 导航栏。如果你还记得上一章,第三章,Bootstrap - 网格布局和组件,我曾提到我们将在本章中涵盖 Bootstrap 导航组件。之所以这样做是因为我们将使用路由指令将我们的导航栏与我们的路由绑定在一起,所以最好的地方就是在本章中进行覆盖,因为它属于路由的范畴。

在上一节中,我让你手动在浏览器栏中输入路由路径 URL 以查看路由是否正常工作,本节中,我们将把所有路由 URL 添加到 Bootstrap navbar组件中,这样用户就可以直接点击导航,而不是手动输入。

在本章的开头,我们简要提到了routerLinkrouterLinkActive。现在是时候看到它们的实际效果了。

让我们看一下app.component.html文件,这是我们应用程序组件的模板。如果你熟悉 ASP.NET 中的主页面的概念,或者 Rails 中的布局页面,那么你可以将应用程序组件模板视为 Angular 应用程序的等价物。这是因为应用程序组件是将形成我们的应用程序的组件树中的顶级组件。我提出主布局的概念的原因是,无论 HTML 被插入到其中,服务器都会通过在布局页面中呈现调用页面来保留它。虽然这在 Angular 中并不是发生的事情,因为它不是服务器端技术,但在概念上是正确的。

我的意思是,无论我们将什么 HTML 插入到应用程序组件的模板中,当其他组件在其中呈现时,它通常仍然可见。这使得应用程序组件模板成为保存我们的导航栏的理想位置,因为无论选择哪个组件模板来由我们的路由规则集呈现给用户请求的给定 URL,它都将始终可见。

这是我们的app.component.html文件的代码清单:

<div>
 <nav class="navbar navbar-expand-lg navbar-light bg-light">
 <a class="navbar-brand" href="/">LISTCARO</a>
 <button class="navbar-toggler" type="button" data-toggle="collapse" 
   data-target="#navbarSupportedContent" 
   aria-controls="navbarSupportedContent" aria-expanded="false" 
   aria-label="Toggle navigation">
 <span class="navbar-toggler-icon"></span>
 </button>
 <div class="collapse navbar-collapse" id="navbarSupportedContent">
 <ul class="navbar-nav mr-auto">
 <li routerLinkActive="active" class="nav-item"> 
 <a routerLink="/" class="nav-link">Home</a>
 </li>
 <li routerLinkActive="active" class="nav-item"> 
 <a routerLink="photos" class="nav-link">Photos</a>
 </li> 
 <li routerLinkActive="active" class="nav-item"> 
 <a routerLink="listings" class="nav-link">Listings</a>
 </li> 
 <li routerLinkActive="active" class="nav-item"> 
 <a routerLink="features" class="nav-link">Features</a>
 </li>
 <li routerLinkActive="active" class="nav-item"> 
 <a routerLink="pricing" class="nav-link">Pricing</a>
 </li>
 <li routerLinkActive="active" class="nav-item"> 
 <a routerLink="about" class="nav-link">About</a>
 </li>
 <li routerLinkActive="active" class="nav-item"> 
 <a routerLink="support" class="nav-link">Support</a>
 </li>
 <li class="nav-item dropdown">
 <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" 
   role="button" data-toggle="dropdown" aria-haspopup="true" 
   aria-expanded="false">
 User name
 </a>
 <div class="dropdown-menu" aria-labelledby="navbarDropdown">
 <a routerLink="account" class="dropdown-item">Account</a>
 <div class="dropdown-divider"></div>
 <a routerLink="logout" class="dropdown-item">Log out</a>
 </div>
 </li>
 </ul>
 <form class="form-inline my-2 my-lg-0">
 <button class="btn btn-outline-success my-2 my-sm-0" type="submit">
   Log In</button>
 <button class="btn btn-outline-success my-2 my-sm-0" type="submit">
   Try Now</button>
 </form>
 </div>
 </nav>
 <br />
 <router-outlet></router-outlet>
</div>

深呼吸,让我们分析前面的代码行。我们正在使用 Angular 指令和属性以及 Bootstrap 内置类。所以让我们开始:

  • 我们正在创建一个菜单navbar元素<nav>,在 Bootstrap 中提供,并分配内置的navbar类,navbar-expand-lg navbar-light bg-light

  • 我们还使用navbar-brand类创建了应用程序的标志的元素和占位符。

  • 使用navbar-nav类,我们正在定义一组链接。

  • 我们正在使用锚标签<a>添加一些链接,并分配nav-link类,这将形成菜单部分的链接。

  • 我们还使用dropdown-menu类创建了一个下拉菜单,并使用dropdown-item向菜单添加项目。

  • 对于 Angular 指令和属性,我们正在使用routerLinkrouterLinkActive,如首先要做的事情-基本概念部分所述,routerLink属性用于绑定链接的 URL 资源。

  • 为了突出显示活动链接,我们正在使用routerLinkActive属性。您会注意到,对于所有链接,我们已经将属性值分配为active。Angular 在运行时将检测到链接被点击并将其突出显示。

太棒了,到目前为止做得很好。我们已经为我们的应用程序实现了一个nav菜单。我们离看到我们的应用程序运行只有一步之遥。

指定渲染组件模板的位置

我们需要告诉 Angular 我们希望在哪里显示映射组件的组件模板,以符合我们的路由规则集。对于我们的应用程序,我们希望路由器调用的组件在我们的导航栏下呈现。

Angular 有一个指令可以做到这一点,<router-outlet>,它在RouterModule中定义。

在我们添加用于创建 Bootstrap 导航栏的 HTML 下面,添加以下一行 HTML:

<router-outlet></router-outlet>

这就是告诉 Angular 路由服务调用的组件应该呈现在哪里所需的一切。

运行我们的应用程序

既然我们已经完成了为我们的应用程序配置路由,让我们快速试一下。

您还记得如何构建和启动我们的 Angular 应用程序吗?对了!使用 CLI 并像这样发出serve命令:

ng serve

确保在执行此操作时,您位于应用程序的根文件夹中。

一次性启动应用程序并在浏览器中打开 localhost 的快捷方式是使用ng server命令与open选项,就像这样:

ng serve --open

您应该看到的是浏览器地址栏中的 URL 指向http://localhost:4200/home,这是 Angular 路由在起作用。ng serve命令与open选项一起发出了http://localhost:4200的 URL,但这触发了路由重定向到/home。很酷,对吧?

当我们运行应用程序时,我们应该看到以下截图中显示的输出:

在下一节中,我们将学习一些我们可以在应用程序中实现的路由策略。

路由策略

Angular 中有两种客户端路由策略:

  • HashLocationStrategy(通常用于客户端目的,如锚标签)

  • PathLocationStrategy(这是默认值)

要启用HashLocationStrategy,在app.module.ts文件中,我们有RouterModule.forRoot(appRoutes),在forRoot方法的第二个参数中添加{ useHash: true }。应该是这样的:

RouterModule.forRoot(appRoutes, { useHash: true })

使用HashLocationStrategy的 URL 在其路径中有一个井号(#)。以下是一个例子:

madeuplistofpeople.com/superheros#cloudman

前面的 URL 表示对服务器的madeuplistofpeople.com/superheros的 get 请求。

从井号(#)开始的所有内容都不是请求的一部分,因为浏览器只会发送井号左边的所有内容到服务器。

URL 的#cloudman部分仅由客户端使用,通常情况下,浏览器会自动滚动到页面上的锚标签(在本例中,滚动到具有name属性为cloudman的锚标签)。

HashLocationStrategy策略的一个用途是使用井号来存储应用程序状态,这对于实现 SPA 的客户端路由非常方便。

例如,考虑以下 URL:

这种 URL 模式非常适合 SPA,因为发送到服务器的唯一请求是madeuplistofpeople.com,基本上就是一个页面。客户端将以其编程的任何方式处理不同的哈希片段(即从井号到右侧井号的末尾)。

总结一下,PathLocationStrategy的一个重要概念是 Angular 利用了一个名为 pushstate 的 HTML5 历史 API。我们可以使用 pushstate API 更改 URL,同时抑制浏览器发送新请求(即更改后的 URL)到服务器的传统默认操作。这使得可以实现客户端路由,而无需使用井号(#)。这就是为什么它是 Angular 中默认的客户端路由策略的原因。

然而,也有一个缺点。如果浏览器刷新,将向服务器发出请求,服务器将用返回的内容重置您的应用程序。换句话说,除非您实施了本地存储策略,否则您的应用程序将丢失其状态。

摘要

这是一个相当简短的章节,但我们仍然涵盖了很多内容。在本章中,我们为我们的应用程序创建了骨架,包括创建我们的路由映射到的组件。然后,我们逐步配置了我们应用程序的路由。这包括导入两个必需的模块(即 RoutingModule 和 Routes),编写路由规则集的映射对象形式,并指定路由组件的呈现位置。

我们还将 Bootstrap 安装并集成到我们的应用程序中,并在根组件的模板中创建了我们的 Bootstrap 导航栏。然后,我们看了一下如何让 Angular 意识到已安装的节点包,特别是 Bootstrap 和 jQuery,因为这是我们安装 Bootstrap 及其依赖项(即 jQuery 和 Popper)的方式。

尽管在本章中我们没有使用参数化路由和路由守卫,但我们在这里提到它们,因为我们将在本书的后面部分使用它们——在第十二章 集成后端数据服务 和 *第十四章 高级 Angular 主题,并且根据本书的精神,在我们需要它们的时候讨论它们,而不是提前,我们将推迟它们的演示直到适当的时间。

最后,我们看了一下 Angular 让我们可以选择的两种客户端路由策略。

在本章中,我们一再提到了“组件”这个词,因为路由将 URL 路径映射到组件。我们甚至使用 CLI 创建了几个组件,但我们没有花时间去理解组件。这完全没关系,因为正如我们所提到的,你不需要理解组件就能理解路由。现在我们已经掌握了路由,我们将在接下来的章节中看看组件。但在我们开始之前,还有另一个简短的章节[第五章],Flex-layout – Angular 的响应式布局引擎,我们将快速介绍一下。这是一个有点奇怪的章节,因为 Flex-layout 是 Bootstrap 响应式网格的替代方案,因此完全不需要构建 Angular 应用程序。然而,我认为这可能会引起你的兴趣。说到这里,让我们把注意力转向 Flex-layout。

第五章:Flex-Layout - Angular 的响应式布局引擎

Flex-Layout 是一个基于 TypeScript 的 Angular 布局引擎。它是在 Angular 项目中布置组件的替代方法,而不是使用 Bootstrap 的网格。Flex-Layout 起源于 AngularJS Material,这是一个由谷歌团队创建的 UI 组件框架,由 Thomas Burleson 领导,他是 Angular 大会上的知名演讲者。我还没有机会参加 Angular 大会,比如 ng-conf 或 AngularMix,但我会的。也许我会在那里见到你!全球范围内有许多关于 Angular 的会议,所以你知道你在明智地学习一项需求量很高且将会持续存在的技术。我想我还没有对你说过这个,所以我现在会说。恭喜!恭喜你选择了这样一个伟大的技术来在你的项目中使用,甚至可能作为构建你职业生涯的基石技术。

当我发现可以改变我为客户和自己创建软件的方式的技术时,我忍不住感到兴奋,现在我可以和你分享我的兴奋!所以,请原谅我稍微偏离了手头的材料。

好的,现在让我们来看看这一章我们将要涵盖的内容。

  • 为什么这一章被包括在书中

  • 我们组件布局的四种可用技术

  • 为什么 FlexBox CSS 可能是最佳选择

  • Flex-Layout 是什么,为什么你应该考虑使用它?

  • 整合 Flex-Layout

  • Flex-Layout API

  • 在使用 Flex-Layout 时的设计策略

  • 将我们的线框和组件与本书的章节和主题相关联

  • 实现我们选择的线框

为什么这一章被包括在书中

这是一个非常简短的章节。事实上,这可能是本书中最短的章节。然而,我想包括它是为了给你提供选择,特别是在拥有替代技术来替代 Bootstrap 方面。在合理范围内,你拥有的选择越多,你就越好。此外,一些开发者喜欢使用 Bootstrap,而另一些则不喜欢。我怀疑这是因为 Bootstrap 的布局系统是一个网格。我不知道有多少开发者喜欢被限制在这样的东西里。不要误会,我并不是在抨击 Bootstrap(Bootstrap 是一项很棒的技术,甚至在本书的标题中都有它的名字!),但 Flex-Layout 确实感觉更加灵活。一些开发者更愿意使用类似 Flex-Layout 这样的东西的另一个原因是它更加友好。例如,你使用专门的元素,而不是使用带有特殊属性的 DIV 元素。有时这被称为采用声明性方法,有时对开发者来说更自然。这可能现在对你来说有些难以理解,但在本章结束时你会明白的。

我们组件布局的四种可用技术

作为网页开发者,除非你有幸在团队中有一个网页设计师,否则我们必须花时间来布局页面上的组件。

顺便说一句,让我们为我们未来的讨论确定一些术语。在前几章中,我已经交替使用了组件页面这两个术语,但现在是时候更加精确了。你知道,Angular 应用默认是单页应用,因此只有一个页面。我在书中已经多次提到,Angular 应用就像一个组件树,一切都始于根组件。组件是可组合的,也就是说一个组件可以由其他组件组成。这会导致什么结果呢?嗯,我们需要一个网页来渲染我们的根组件,从那一刻起,我们的根组件引入其他组件,这些组件又引入其他组件。最终的结果是,我们的组件递归地渲染自己,以产生我们有多个页面的错觉。当然,我们并没有多个页面。我们只有一个网页,我们的应用程序的架构方式是每个页面都有一个主要的包含组件。这意味着当你看到我提到页面时,实际上是指该页面上的主要组件,而不是组件

回顾一下我们在第四章 路由中编写的代码,现在应该开始对你有意义了。具体来说,给定的 URL 映射到一个组件。对于不是单页应用的传统 Web 应用程序,URL 映射到视图或“页面”。好的,让我们把注意力转回到布局策略的考虑和可用选项。

在我们的应用程序中布置组件包括以下四个必要条件:

  • 在容器中布置我们的组件(即父组件和子组件)

  • 调整我们的组件大小

  • 将我们的组件相对放置在一起

  • 组件的样式

我并不自诩是样式或 CSS 方面的专家。我几乎无法搭配我穿的衣服。虽然我们在第三章中看到了一些 CSS,Bootstrap - 网格布局和组件,在我们的 SASS 速成课程中(在接下来的章节中我们肯定会看到更多的 CSS),但这不是一本关于设计和样式的书。Packt Publishing 出版了一些关于 CSS 的优秀书籍。在本章中,我们只关注在容器中布局我们的组件。为此,我们有四种可以选择的技术:表格、浮动和清除、FlexBox CSS 和 CSS Grid。

是的,当然,Flex-Layout 也是我们的选择,因为我们选择了 Angular(微笑)。然而,我列出的四种布局技术适用于网页开发一般情况——无论是前端框架、库,还是普通的 HTML 和 CSS。正如我们在第三章中所看到的,Bootstrap - 网格布局和组件,Bootstrap 是一个建立在 FlexBox CSS 之上的 CSS 框架,因此也适用于网页开发一般情况。

回到我们对布局技术的讨论,让我们对比一下通常可用于网页开发的四种技术,看看是否有一个明显的赢家。从那里,我们将继续本章的细节,看看 Flex-Layout 是什么,以及为什么我们应该使用它。

表格

每个网页开发者(2000 年之前出生)都听说过并可能使用过TABLE标签。这是从哪里来的?嗯,很久以前,在一个遥远的星球上,一群外星程序员发明了 HTML 表格标签。这些外星人很快厌倦了使用这种布局技术,所以他们禁止了它的使用,并放逐了所有教授表格标签的网页开发书籍。与此同时,在地球上的某个地方,大约在 1994 年,一位对布局问题感到沮丧的网页开发者被一本看起来像技术书籍的东西砸到了头上。它的标记似乎是某种形式的象形文字,对年轻的技术人员来说都是无法理解的,除了那个熟悉的标记语言。第一章的标题只是<TABLE>

开玩笑的是,虽然表格在网页开发的早期阶段非常有帮助,但现在它们是一种古老的布局技术,经常受到指责。以下是一些表格不再是布局页面元素的默认方法的原因:

  • 它们往往会在我们的网页和组件中混乱标记

  • 它们是维护的噩梦,因为使用表格移动东西非常乏味

  • 它们是刚性的——比网格更加刚性,以至于我们有时不得不诉诸于嵌套表格,这当然加剧了前两个要点

然而,尽管存在这些负面因素,使用表格仍然是一个有效的选择,这就是为什么我在这里将其列为主要的四个选项之一。

使用浮动和清除进行定位

CSS 有一些非常酷的功能。我最喜欢的是其中一些处理定位的声明。具体来说,我指的是两个 CSS 声明,即浮动和清除。这些声明可以应用于块级元素,如<div>,以及内联元素,如<img>。块级元素是占据父元素空间的元素,而内联元素乐意分享它们所在父元素的水平空间。

浮动元素(如<div>)的概念是,它放弃了占据整个水平线的需求。简而言之,它将其空间折叠为仅消耗所需的空间,而不是贪婪地利用水平空间,其他元素现在可以驻留在其旁边,而不是被推到下面。当被浮动的元素不占据整个空间时,旁边浮动的元素在水平空间不足时会换行到下一行。话虽如此,您可以开始看到如何通过使用 CSS 浮动声明来浮动元素来实现一定程度的响应式设计。

清除的目的是控制浮动的效果。当您在元素上使用 CSS 声明清除时,基本上是在指示该元素不要浮动到更高的水平空间上,即使有空间可以浮动。请记住,浮动元素意味着元素将占据它可以占据的最高垂直空间,前提是有空间,并且它的相邻元素也已经被浮动(特别是对于希望独占整个水平空间的块级元素)。当没有足够的空间时,它会换行到下一个可用的位置,如果有足够的空间,它会浮动到其他元素的旁边。唯一的例外是,如果您在其样式或类中应用了清除声明,它将始终表现为换行,即使上方有空间。我们对此了解吗?很好。

通过浮动清除定位元素确实有效,您可以使用它们创建一些相当复杂的布局。但随着视口尺寸变小,它们的效果可能并不总是您想要看到的。在响应式布局的世界中,尽可能多地控制布局至关重要,而仅限于浮动和清除通常会使布局重新排列成为一项挑战,尤其是在各种视口尺寸下,至少与下面两个选项给予您的精度一样多。另一件需要习惯的事情是,浮动元素需要根据您是将元素向左还是向右浮动来重新排列页面上的元素列表。

我在浮动清除上花了更多时间的原因是,有太多开发人员没有花时间让它深入人心。这里的要点是,您可以仅使用这种布局技术走得很远,根据项目的性质和要求,这可能是医生开的处方。当然,关于浮动清除的设计策略还有更多要说,但那是另一本书。像往常一样,我建议尝试使用这种布局技术/策略。

FlexBox CSS

FlexBox CSS 是一个随着 CSS3 而出现的布局技术。这是一个非常强大的东西,这也是为什么其他框架,比如 Bootstrap 和 Flex-Layout,都是建立在它之上的。但 FlexBox CSS 最好的地方在于,它几乎被所有通用的浏览器所理解。使用 FlexBox,我们既可以获得巨大的浏览器覆盖范围,又可以为应用程序提供令人钦佩的布局灵活性。

我不会再多说 FlexBox CSS,因为很可能你不会直接使用它。我可以假设这样做的原因有三个:

  • Bootstrap 是建立在 FlexBox CSS 之上的,你可能更有可能使用 Bootstrap 网格而不是直接使用 FlexBox CSS

  • 对于 Flex-Layout 也是一样的,因为它基本上是在 FlexBox CSS 的基础上包装了一个很好的 API,使其更容易使用

CSS Grid

CSS Grid FlexBox CSS 是一个随着 CSS4 而出现的布局技术。它也是一个非常强大的东西,它使一些事情比使用 FlexBox CSS 更容易,但与此同时,有些事情比使用 FlexBox CSS 更难实现。作为 CSS 世界相对较新的补充,它并没有被广泛整合到通常使用的浏览器中。

为什么 FlexBox CSS 可能是最佳选择

在阅读了前面几段的内容后,谁是赢家对你来说应该不会有什么意外。显然是 FlexBox CSS。让我们用一个因素列表来总结选择布局选项时应该考虑的因素:

  • 浏览器覆盖范围:作为开发者,我们非常关心我们的 Web 应用的覆盖范围。

  • 易用性:我知道这有点牵强,因为 Bootstrap 的网格和 Flex-Layout 都是建立在它之上的,使其更容易使用。但一旦你掌握了 FlexBox CSS,大多数布局要求都可以比较容易地处理。

  • 易于维护:这个因素是从前一个要点中得出的。但大多数开发者感到惊讶的是,在典型应用的生命周期中,开发者参与其中的时间有 20%是在构建它,而 80%的时间是在维护它,所以最后一个要点不能过分强调。

同样,我们不认为 Bootstrap 和 Flex-Layout 是布局技术,因为它们是在基础布局技术之上的工具/框架。

什么是 Flex-Layout,为什么应该使用它?

我们已经讨论了为什么对于我们来说,布局组件的最佳选项是 FlexBox CSS,但这是关于 Flex-Layout 的一章,所以我现在需要向你介绍它。所以让我们现在做到这一点,然后我将列出一些原因,为什么你应该考虑使用它,而不是直接使用 FlexBox CSS(再次强调,因为 Flex-Layout 是建立在 FlexBox CSS 之上的)。

Flex-Layout 的主页可以在这里找到:www.github.com/angular/flex-layout

以下是一些关于 Flex-Layout 的要点:

  • 它是一个独立的库。

  • 它是 Angular 原生的(并且是 TypeScript 实现)。

  • 它与 CLI 集成。

  • 它有静态 API,用于容器,以及其他静态 API,用于容器子元素。这些 API 具有以下特点:

  • 它们是声明性的

  • 它们支持数据绑定和变化检测

  • 它们是在 HTML 中使用的指令

  • 对于我们来说,没有 CSS 需要编写,因为它会动态地为我们注入

与 FlexBox CSS 相比,使用它的一些优势,以及从前面的要点中可以得出以下结论:

  • 你不必是 CSS 专家(事实上,正如你很快会看到的,我们甚至不会使用 CSS 样式表)

  • 它完美适配 Angular(事实上,它是 Angular 原生的)

  • 有 API 可以帮助开发人员更快地开发应用程序

另一个需要知道的好处是,由于 Flex-Layout 是一个独立的(即自包含的)库,它可以与或无需 Angular Material 一起使用。我们将在第九章中查看 Angular Material,那里我们将使用它的一些组件。同样,这些组件可以用作 ng-Bootstrap 的替代品,或与 ng-Bootstrap 一起使用。我们将在第八章中查看 ng-Bootstrap,使用 NG Bootstrap

我在前面的要点列表中提到了 Flex-Layout 具有静态 API。我没有提到的是它还有响应式 API。我们将在接下来的章节中介绍 Flex-Layout 的静态 API,但我把它的响应式 API 留给你阅读(我在该章节的末尾包含了 Flex-Layout 文档的链接)。

然而,我想简要谈一下响应式 API。响应式 API 是为了让您创建自适应的 UX(即,为不同的视口大小创建略有不同的布局)。为了做到这一点,您还需要利用 MediaQueries,而不仅仅是 FlexBox CSS。是的,这是一章关于 Flex-Layout,那么为什么我要提到您需要结合 FlexBox CSS 利用 MediaQueries 呢?我提到这一点是为了指出 Flex-Layout 团队在这个领域(即,响应式 UX,而不仅仅是布局)已经为我们做好了准备。他们通过为静态 API 提供扩展来将 MediaQueries 抽象化,这意味着我们不必手工编写繁琐的规则集——因为他们在静态 API 上创建了扩展,我们可以利用在那里学到的知识并将扩展应用于在我们的 HTML 中创建自适应的 UX。这真的非常聪明!

集成 Flex-Layout

Flex-Layout 库作为一个自包含的模块,所以我们只需要在一个地方导入它。与上一章的路由集成更加直接。

现在让我们将 Flex-Layout 添加到我们的项目中。我们需要做的第一件事是安装该库。在您的终端中,导航到我们在第四章中开始创建的realtycarousel应用程序的根文件夹,并输入以下内容:

 npm install --save @angular/flex-layout

这将安装该库,这样我们就可以在任何一个 Angular 应用程序中导入它。

注意:如果您的 CLI 输出警告,比如类似于"``@angular/flex-layout@5.0.0-beta.14需要@angular/cdk@⁵.0.0的对等依赖,但没有安装。您必须自己安装对等依赖"(这就是发生在我身上的事情),只需像其他任何东西一样安装即可,如下所示:

npm install --save @angular/cdk@⁵.0.0

接下来,我们需要将其导入到我们的RealtyCarousel应用程序中。为此,我们需要向应用程序的主模块添加一些内容。在 IDE 中打开您的RealtyCarousel项目,然后从src/app目录中打开app.module.ts文件。在文件顶部的其他导入语句中,添加以下导入语句:

  import { FlexLayoutModule } from '@angular/flex-layout';  

(在我们为RouterModule添加的import语句的下面就可以了。)

我们还需要在@NgModule部分的导入数组中包含FlexLayoutModule,就像这样:(就在RouterModule.forRoot(appRoutes)语句下面,我们为RouterModule添加的那样。)

到此为止。我们现在可以利用 Flex-Layout 的功能。几乎我们在 Flex-Layout 中做的任何其他事情都是在我们的 HTML 中完成的。

让我们接下来看一下 Flex-Layout API,这是我们将在页面中利用 Flex-Layout 的方式(即组件模板)。

Flex-Layout API

与 FlexBox CSS 相比,Flex-Layout 更容易使用的原因是它具有抽象出 CSS 的 API。我们仍然需要 CSS(记住,浏览器只能理解 HTML、JavaScript 和 CSS),但我所说的 CSS 将被抽象化是指当我们的应用程序被转译时,Angular Flex-Layout 会为我们注入 CSS。正如我所提到的,Flex-Layout 甚至没有 CSS 样式表,我们也不需要编写任何 CSS。

以下是 Flex-Layout API 的表格,详细说明了它们的用途,以及一个快速的语法示例:

类型 API 用于 示例
静态(对于容器) fxLayout 定义流的方向(即 flex-direction)。 <div fxLayout="row" fxLayout.xs="column"> </div>
静态(对于容器) fxLayoutAlign 定义元素的对齐方式。 <div fxLayoutAlign="start stretch"> </div>
静态(对于容器) fxLayoutWrap 定义元素是否应该换行。 <div fxLayoutWrap> </div>
静态(对于容器) fxLayoutGap 设置元素之间的间距。 <div fxLayoutGap="15px"> </div>
静态(对于子元素) fxFlex 指定在其容器流布局中调整宿主元素的大小。 <div fxFlex="1 2 calc(15em + 20px)"> </div>
静态(对于子元素) fxFlexOrder 定义 FlexBox 项目的顺序。 <div fxFlexOrder="2"> </div>
静态(对于子元素) fxFlexOffset 在其容器流布局中偏移 FlexBox 项目。 <div fxFlexOffset="20px"> </div>
静态(对于子元素) fxFlexAlign 类似于fxLayoutAlign,但适用于特定的 FlexBox 项目(而不是全部)。 <div fxFlexAlign="center"> </div>
静态(对于子元素) fxFlexFill 将元素的尺寸最大化到其父容器的尺寸。 <div fxFlexFill> </div>

这些 API 有选项和默认值。例如,fxLayout API 默认为行,但也有列,以及行反转和列反转。

另外,在fxLayout API 的示例中,.xs与 Bootstrap 网格有类似的概念,它提供了一种允许不同视口尺寸的方式。因此,在前面表格中的第一个示例中,常规视口的布局将使元素在行内从左到右流动,而对于小视口,元素将堆叠在单列中。

在前面表格中的示例中,还有一个有趣的地方是在fxFlex API 中进行了计算。这有点像我们在第三章的 SASS 快速入门中所看到的,Bootstrap - 网格布局和组件,尽管 SASS 是由 Ruby 编译的,而 Flex-Layout 是由 TypeScript 编译的。

我不会在这里列举所有的选项,因为你购买这本书不是为了阅读文档,就像我写这本书不只是为了复制文档一样。当然,我会指引你去查找 Flex-Layout 的文档。你可以在他们的官方网站找到:github.com/angular/flex-layout/wiki/API-Documentation

幸运的是,Flex-Layout 团队在文档方面做得非常出色。他们的维基还包括了几个实时布局演示,你可以看一看。这是直接链接:tburleson-layouts-demos.firebaseapp.com/#/docs

使用 FlexBox 时的设计策略

由于 Flex-Layout 更多地是一种流动的方式,而不是网格,因此通常更容易考虑应用程序的垂直部分并为它们分配自己的容器。这是因为容器内的部分会随着视口尺寸变小而自动向下包裹。容器内的元素应该被视为属于一起。与 Bootstrap 等网格系统相比,思维方式是不同的;网格中的单元格标记了元素的物理边界。单元格内的元素不会自动换行,因为在设计/布局时,您会将元素插入特定的单元格中。另一种概念化网格和 FlexBox 之间的差异的方法是将网格视为二维的(即行和列 - 就像电子表格一样),将 FlexBox 视为一维的(即它要么水平流动,要么垂直流动)。

一旦您有了垂直容器的想法,您就可以考虑从左到右流动的子容器,然后随着视口尺寸变小,子容器向下包裹 - 当它向下包裹时,所有具有该子容器的元素都会一起移动。请记住,当我提到子容器时,我指的是 FlexBox 容器可以嵌套 - 这就是为什么开发人员可以控制布局的大部分原因。在布局页面时,将流程视为“从外到内”。这意味着您应该将页面分成大的垂直部分 - 例如标题、主体和页脚 - 然后深入到每个部分中添加子容器,这些子容器将从左到右流动。

很难用言语描述“流动”,因此像往常一样,最好的方法是尝试使用您的容器和元素,并研究随着视口尺寸调整它们的流动行为。本章包括三个组件模板(即*页面)的代码清单,以及它们的线框图。您将看到我如何为这些组件模板设计布局。在此过程中,我还会告诉您我为什么做出了一些决定。

将我们的组件与本书的章节和主题相关联

到目前为止,我们还没有讨论我们将在何时何地实施我们的组件。部分原因是直到第四章 路由,我们甚至都没有开始编写任何 Angular 代码,唯一的例外是我们在第一章 快速入门中的待办事项列表迷你应用。然而,现在我们已经开始编写 Angular 代码,现在是时候做了。

开始讨论的一个好地方是选择我们将使用 Flex-Layout 布局的组件模板。由于这本书更多地关注 Bootstrap 而不是 Flex-Layout,我们将使用 Bootstrap 的网格来布局我们应用程序中其余的组件模板,这占了大部分。

我们要做的第一件事是列出我们的线框图,作为参考,它们代表我们应用的页面(即组件模板),我们将选择其中三个,在接下来的部分实现我们选择的线框图中实现它们。然后,我们将看一下接下来的表格,它将向您展示我们将实现哪些组件模板,以及哪些章节,具体来说,我们将把它们与哪些主题配对。

以下是我们从第一章 快速入门中的 13 个线框图的列表:

  • 首页

  • 注册

  • 登录

  • 编辑个人资料(不在书中涵盖范围内)

  • 房产列表(不在书中涵盖范围内)

  • 创建列表

  • 编辑列表

  • 预览列表

  • 房产详情(不在书中涵盖范围内)

  • 照片列表

  • 上传照片/创建卡片

  • 编辑照片(不在书中涵盖范围内)

  • 预览照片

以下是我们将在本书中一起实现的线框图的表格,以及它们关联的章节和主题的列表。您可以将其用作在概念上将我们的应用程序组合在一起的路线图,也就是说,从高层次上,您将知道我们将在哪一章中实现应用程序中组件模板的各个部分:

线框图/组件模板 关联章节 关联主题
首页 3 Bootstrap 网格
注册 3, 8, 10 模态对话框,ng-Bootstrap(输入框),表单
登录 14 认证
创建列表 5, 14 Flex-Layout, 自定义验证
编辑列表 5, 10 Flex-Layout, 表单
预览列表 5, 6, 9 Flex-Layout, 组件,Angular Material(芯片)
照片列表 6, 7 组件,模板
上传照片/创建照片卡 10 表单
预览照片 6, 9 组件,Angular Material(卡片)

上表显示了我们将在我们的线框(即组件模板)中实施的主题。例如,通过查看从顶部开始的第四行,我们可以看到当我们实施我们的创建列表线框(即我们的CreateListingComponent)时,我们将使用本章的 Flex-Layout,以及来自第十四章 高级 Angular 主题的自定义验证。

请记住,每个线框都需要组件——尽管在相关章节列中没有列出第六章 构建 Angular 组件,以及相关主题列中的组件。我之所以对一些线框这样做,比如照片列表和预览照片,是因为我们将会更多地讨论组件,而不是比如注册或编辑列表线框。此外,某些线框将使我们更加关注其他主题。例如,您可以看到对于上传照片线框,我们将更多地关注表单,来自第十章 使用表单

由于我们不会跳来跳去,这意味着在我们阅读本书时,我们将会多次回顾我们的大部分页面(即组件模板),两次、三次,甚至四次。

实施我们选择的线框

我在本章中选择要与您实施的三个线框(即组件模板)如下:

  • 创建列表(包括因为视图中有许多部分和元素)

  • 编辑列表(出于与创建列表相同的原因而包括)

  • 预览列表(包括因为视图中有非常少的元素)

在上述线框的列表中,您可能已经注意到有三个线框被标记为不在书中涵盖范围内。以下是线框排除列表,以及排除原因:

  • 编辑个人资料:这被排除在外,因为它只是另一个编辑表单(与编辑列表屏幕非常相似)

  • 房产列表:这被排除在外,因为它只是另一个列表屏幕(很像照片列表屏幕)

  • 房产详情:这被排除在外,因为从 Angular 的角度来看,这是一个无趣的静态屏幕

  • 编辑照片:这个被排除了,因为这只是另一个编辑表单

但不要担心。我们将在剩下的页面中一起构建的应用程序的所有代码,包括书中不会实现的四个线框的代码,以及非基于 UI 的代码(例如 第十二章 中的基于 Python 的 API,集成后端数据服务,等等),都可以通过下载获得。我已经为你准备好了。

最后一个值得注意的点,然后我们将继续进行一些 Flex-Layout 编码。你可以看出我们的应用程序将需要一些线框被多次重新访问,以便我们可以完成它——也就是说,我们将分阶段构建我们的应用程序,看起来像是一种混乱的来回方式。这不是因为作者疯了——正如他的一些朋友喜欢给你讲述一些强有力的案例,证明恰恰相反——而是出于设计。记住,本书的理念是最大限度地提高你对材料的吸收效果,这样你就可以尽快成为 Angular 大师。在尽可能的范围内,我们将立即实施我们所涵盖的材料,以便它立即有意义,并且牢固。这就是目标,也是为什么我想包括前面的表格(即,将线框与章节和主题相关联)。

我的疯狂通常都是有条不紊的方法(眨眼)。现在让我们把注意力转向本章的三个线框的实现。

创建列表线框

在本节中,我们将汇集所有的知识和理解,学习为创建列表页面创建我们的应用程序页面。看一下下面的线框,我们将使用 Flex-Layout 将其转换为代码:

另一个线框显示,我们将需要一个标题部分和一个两列布局来容纳表单和输入元素。

我们将首先在我们的应用程序中创建一个新组件,并将其命名为“创建列表”。在组件模板文件中,让我们向模板添加以下示例代码:

<h1>Create Listing</h1> <div  fxLayout="row"  fxLayoutAlign="space-between">
 Logo Here  </div> <div  class="bounds">
 <div  class="content"  fxLayout="row"  class="menu">
 <div  fxFlexOrder="1">Manage Listings</div>
 <div  fxFlexOrder="2">Manage Photos</div>
 <div  fxFlexOrder="3">Manage eCard</div>
 <div  fxFlexOrder="4">Business Opportunity</div>
 </div>
 <div  class="content"  fxLayout="row"  fxLayout.xs="column"  
            fxFlexFill  >
 <div  fxFlex="60"  class="sec1"  fxFlex.xs="55">  
        <form  action="/action_page.php">

 <label  for="lprice">Listing Price</label>
 <input  type="text"  id="lprice"  name="lprice"                 placeholder="Listing price">

 <label  for="country">Property Type</label>
 <select  id="country"  name="country">
 <option  value="australia">USA</option>  <option  value="canada">UK</option>
 <option  value="usa">UAE</option>
 </select>

 <label  for="laddress">Street Address</label>
  <input  type="text"  id="laddress"  name="laddress"              placeholder="Street Address">  <label  for="city">City</label>
  <input  type="text"  id="city"  name="city"  placeholder="City">  <label  for="state">State/Province</label>
 <select  id="state"  name="state">
 <option  value="New York">Australia</option>
 <option  value="New Jersey">Canada</option>
 <option  value="Texas">USA</option>
 </select>         <label  for="pcode">Postal Code</label>
 <input  type="text"  id="pcode"  name="pcode"              placeholder="postal code">

 <label  for="sfoot">Square Foot</label>
 <input  type="text"  id="sfoot"  name="sfoot"              placeholder="Square Foot">   <label  for="bedrooms"># Bedrooms</label>
 <input  type="text"  id="bedrooms"  name="bedrooms"              placeholder="Bedrooms">
  <label  for="bathrooms"># Bathrooms</label>
 <input  type="text"  id="bathrooms"  name="bathrooms"              placeholder="bathrooms">  <input  type="submit"  value="Submit">
 </form>
  </div>
  <div  fxFlex="40"  class="sec2"  >  <label  for="ldescription">Listing Description</label>
 <textarea  id="ldescription"  name="ldescription"              placeholder="Listing price"></textarea>
 </div>  </div>  </div>

在上面的代码中,我们使用fxLayout创建了一行,为我们的标志创建了一个占位符。接下来,我们创建了菜单链接,并使用fxFlexOrder对菜单链接进行排序。现在,我们需要创建一个两列布局,所以我们现在在fxLayout行内创建了两个子元素,每个fxFlex分别为 60 和 40。在这两列中,我们将放置我们的表单输入元素,以创建表单,如线框所示。运行应用程序,我们应该看到输出,如下面的截图所示:

现在,是时候进行一些代码操作了。我们将在我们的 Angular 项目中创建一个名为 edit-listing 的新组件,并在组件模板文件中重用相同的代码,以快速准备好编辑列表页面:

我们已经准备好了创建列表页面的布局。如果你仔细看,我们的标签并不完全在输入字段旁边。需要更新什么?没错,我们需要在主列内创建一个子列。通过作业来尝试一下。现在,同样的,我们可以轻松实现我们的编辑列表页面。

编辑列表线框

在上一节中,我们创建了我们的创建列表页面。在本节中,我们将学习为我们的编辑列表页面实现页面布局。看一下我们将要实现的示例。它不是看起来和创建列表页面完全一样吗?没错。

创建编辑列表页面的布局大部分都是相同的,除了在启动编辑页面时加载数据,而在创建屏幕上,最初不会加载任何数据:

<h1>Edit Listing</h1>

<div fxLayout="row" fxLayoutAlign="space-between">
    Logo Here
  </div>

  <div class="bounds">

      <div class="content" 
         fxLayout="row" class="menu">

            <div fxFlexOrder="1">Manage Listings</div>
            <div fxFlexOrder="2">Manage Photos</div>
            <div fxFlexOrder="3">Manage eCard</div>
            <div fxFlexOrder="4">Business Opportunity</div>

      </div>

    <div class="content" 
         fxLayout="row"
         fxLayout.xs="column" 
         fxFlexFill >

        <div fxFlex="60" class="sec1" fxFlex.xs="55">

            <form action="/action_page.php">

              <label for="lprice">Listing Price</label>
              <input type="text" id="lprice" name="lprice" 
                   placeholder="Listing price">

              <label for="country">Property Type</label>
              <select id="country" name="country">
                <option value="australia">USA</option>
                <option value="canada">UK</option>
                <option value="usa">UAE</option>
              </select>

              <label for="laddress">Street Address</label>
              <input type="text" id="laddress" name="laddress" 
                    placeholder="Street Address">

              <label for="city">City</label>
              <input type="text" id="city" name="city" 
                    placeholder="City">

              <label for="state">State/Province</label>
              <select id="state" name="state">
                <option value="New York">Australia</option>
                <option value="New Jersey">Canada</option>
                <option value="Texas">USA</option>
              </select>

              <label for="pcode">Postal Code</label>
              <input type="text" id="pcode" name="pcode" 
                   placeholder="postal code">

              <label for="sfoot">Square Foot</label>
              <input type="text" id="sfoot" name="sfoot" 
                   placeholder="Square Foot">

              <label for="bedrooms"># Bedrooms</label>
              <input type="text" id="bedrooms" name="bedrooms" 
                    placeholder="Bedrooms">

              <label for="bathrooms"># Bathrooms</label>
              <input type="text" id="bathrooms" name="bathrooms" 
                     placeholder="bathrooms">

              <input type="submit" value="Submit">
            </form>
        </div>
        <div fxFlex="40" class="sec2" >

            <label for="ldescription">Listing Description</label>
            <textarea id="ldescription" name="ldescription" 
                 placeholder="Listing price"></textarea>

        </div>

    </div>

在上面的代码中,我们创建了两行,一行用于标题部分,另一行用于内容行。在内容行内,我们使用fxRow创建了两个子列,它们将用表单输入字段元素填充。输出将与创建列表页面完全相同。

总结

本章提供了对令人兴奋的技术的快速介绍。当然,可以专门撰写一本专门介绍 FlexBox CSS 和 Flex-Layout 的小书,所以仅仅在几页中介绍并不能充分展现它应有的价值。如果有一个行业变化迅速,那就是我们的行业,因此应该提到替代技术 - 如果技术足够令人兴奋,甚至可能获得自己的章节 - 无论是哪本技术书籍和哪些技术。这正是 Flex-Layout 和这本书的情况。我希望向你深入介绍 Flex-Layout。

我们从快速回顾四种布局技术的选项开始,解释了为什么 FlexBox CSS 是其中最佳选择。然后我向你介绍了 Flex-Layout,并提出了一些令人信服的理由,说明为什么你应该考虑使用它而不是 FlexBox。接下来,我们看到了如何将 Flex-Layout 集成到我们的 Angular 项目中,并查看了一些其 API。最后,我们回到了我们的线框图(即组件),并将它们与本书中的章节相关联,然后实现了与本章相关的组件。

我希望你喜欢这一章,并且会尽量在你的网页开发项目中尝试使用 Flex-Layout。我预测许多 Angular 开发者将选择 Flex-Layout 作为布局组件的首选工具。对于我的下一个项目,我已经倾向于使用 Flex-Layout 而不是 Bootstrap 的网格来设计所有组件模板。

在下一章中,我们将学习任何 Angular 应用程序的构建块 - 组件。我们将深入学习并使用 Angular 组件创建一些很酷的东西。祝阅读愉快。

第六章:构建 Angular 组件

由于整个 Angular 由几个相互关联的部分组成,几乎不可能选择 Angular 的某一部分比其他部分更重要。删除其中任何一个部分都会使整个系统受损,甚至可能变得无用。话虽如此,如果我必须选择一个真正重要的部分,我会选择组件。组件有几个非常酷的特点,比如当我们构建组件时,我们基本上也在扩展 HTML,因为我们正在创建自定义 HTML 标签。组件是 TypeScript 类,正如我们稍后在本章中将看到的那样,我们将代码链接到自定义 HTML 标签的方式是通过@Component注释。我也会在本章后面解释注释是什么。

在此之后使用的术语简要说明:我使用“部分”一词而不是“组件”一词,以避免混淆,因为“组件”一词是一个多义词-在不同的上下文中有不同的含义。此外,当谈论视图(即屏幕)时,我从经典的 Web 应用程序角度使用“页面”一词,而不是字面意义上的意思。

Angular 应用程序包含一个根组件。但是,在讨论应用程序的屏幕或视图时,有必要提及其他充当该视图的根组件的组件。例如,注册屏幕有一个根组件。

以下是我们将一起涵盖的主题的项目列表:

  • 一个 Angular 应用程序作为组件树

  • @Component注释

  • @Component注释的属性

  • 内容投影

  • 生命周期钩子

  • 组件接口

  • 需要实现与本章相关的三个线框的组件

Angular 应用程序架构-组件树

Angular 应用程序基本上是一个组件树。正如我们在之前的章节中学到的,Angular 是一个单页面应用程序框架,因此有一个单页面来展示其组件树。我们已经看到 Angular 有一个顶层组件,称为根组件,根据我们希望应用程序对用户操作做出的响应,我们让根组件加载其他组件。这些其他组件(暂时称它们为次级根组件)反过来递归地渲染其他组件。我们在第四章中设置路由的方式是将 URL 映射到我们的次级根组件,每个页面一个组件,当用户点击导航(即菜单)链接时,它们就会显示出来。

所有这些都是可能的原因是组件是可组合的。这意味着我们的组件由其他组件组成,因此是嵌套的。我们可以在任意深的组件层次结构中嵌套我们的组件,因此在本节的开头就有了这样的陈述,Angular 应用程序基本上是一个组件树

Angular 框架会递归地加载和渲染我们的组件。

设计 Angular 应用程序

就像大多数工程项目一样,软件项目也需要有一个设计和架构应用程序的过程。开始的典型方式是将你正在构建的东西分解成独立的工作块。在 Angular 的术语中,这意味着我们需要将我们的应用程序分解成独立的组件,每个组件负责某些事情,比如显示计算结果或接受用户输入。

一旦我们有了需要使用的组件列表(无论是第三方组件还是自定义组件),我们需要把它们当作黑匣子——或数学函数。让我解释一下我的意思。

当我说我们需要把组件当作黑匣子对待时,我是在建议我们不应该在这个阶段(即我们只是列举它们时)让我们的思绪被它们的实现所占据。我们将在本章稍后关注构建我们的组件,但现在,把它们当作黑匣子就足够了。

当我说我们需要把组件当作数学函数来对待时,我只是建议我们考虑输出会是什么,以及函数(也就是我们的组件)需要什么输入。组件的输入和输出构成了它们的公共接口。我们稍后会更仔细地研究组件接口。

将你的组件分解为子组件

一个应用程序中的组件数量,甚至每个页面中的组件数量,都各不相同。它可以从几个到几百甚至更多。然而,对于将组件(比如作为特定页面的顶级组件的子组件)分解为子组件,有一个很好的经验法则。如果你记住了组件的可重用性,当你将组件分解为子组件时,你只需要问自己这个问题:“这个组件有两个或更多部分可以在其他地方重用吗?”如果答案是肯定的,你可能会受益于进一步分解。如果答案是否定的,那么你就完成了,不需要再进一步分解组件。

让我们考虑一个简单的例子,只是为了让这个问题不那么抽象。假设你在页面上有一个商品清单,每个商品占据一行,商品就是一个组件。我们还假设每个商品都有一个缩略图,用于显示该商品。如果缩略图可以在其他地方使用,比如在结账页面或商品详细页面,那么这个缩略图应该是它自己的组件,是商品组件的子组件。

从商品清单示例中放大一点,从页面视图开始,你可以采取这种方法来帮助你在规划组件时开始:

  • 你的页面页眉也是一个组件

  • 你可能在页面右侧有一个快速链接部分,这也将是另一个组件

  • 你有你的主要内容部分,占据了大部分屏幕空间,这也将是一个组件

  • 你的页面页脚也是一个组件

从前面的组件中,所有这些组件都可能是可重用的,除了主要内容部分。您可能希望您的页面标题和页面页脚出现在应用程序中的每个页面上,并且您可能希望在各个页面上重新显示快速链接部分。出于这些原因,这些组件可能已经很好了。不需要进一步的拆分。您需要拆分主要内容组件的原因是它不可重用,因为您不太可能拥有相同页面的两个副本!

组件责任

被架构化的 Angular 应用程序将具有不仅可重用而且有明确定义边界的组件。也就是说,它们具有关注点分离。每个组件只做一件事,并且做得很好。这些组件应该相互抽象,它们不应该了解彼此的细节(即实现)。它们应该了解彼此的唯一事情是如何与彼此通信。这是通过它们的公共接口实现的,我们很快会看到这一点。

目前,您需要知道的是,当您计划应用程序的组件时,您应该列出它们的责任。也就是说,写下它们将要做什么。敏锐的读者可能会看到用例图和组件责任列表之间的联系,因为组件是用户将如何与应用程序交互的方式。

注解

注解是 TypeScript 的一个新特性。它们是以@符号为前缀的符号,我们将其添加到我们的代码中(即用于装饰我们的类)。注解可以出现在我们的类声明顶部,或者在我们的函数顶部,甚至在我们的类属性顶部。一般来说,注解的作用是在它们附加的地方(即我们的类、函数或属性)注入样板代码。虽然我们不需要注解,因为我们可以选择自己编写样板代码,但我们最好利用它们,因为样板代码不应该一遍又一遍地编写。此外,通过使用注解而不是手写样板代码,不仅可以消除单调乏味,而且我们不必处理容易出错的代码。我们将在本书的各个章节中看到更多的注解,但让我们专注于本章的@Component@NgModule装饰器。

@Component

虽然注解可以出现在我们的类声明顶部,或者在我们的函数顶部,甚至在我们的类属性顶部,但@Component注解将始终出现在我们组件类声明的顶部。

为了使@Component注解对我们可用,我们必须像这样导入它:

import { Component } from '@angular/core';

让我们仔细看一下那行代码。这是 JavaScript,具体来说是 ES6。如果你还记得第二章中的ECMAScript 和 TypeScript 速成课程,这个语句的大括号部分是 ES6 中称为解构的新构造。此外,没有明确的路径指向@angular/core模块。我们让 CLI 和 TypeScript 编译器来找出模块在哪里,以及如何加载和使其在我们的类中可用。

@Component 装饰器的属性

@Component装饰器为配置我们的组件提供了许多属性。让我们来看看它们。

选择器

selector@Component注解的一个属性,它的值(类型为字符串)是为我们的自定义 HTML 标签命名的。我喜欢汽车,所以这里有一个car组件的示例代码,显示了它的注解、选择器和类名:

@Component({
 selector: 'car'
})
class CarComponent {
}

当 Angular 看到我们的自定义 HTML 标签<car></car>时,它会创建我们的CarComponent的一个实例,并将我们的自定义标签替换为浏览器实际理解的一些 HTML。好的,但是在我们的组件类中,我们在哪里添加东西,使我们的组件不再只是一个幽灵般的光环?下一节就是答案(即template属性)。

模板和模板 URL

我们可怜的小car组件目前还没有可见的主体。这是因为 Angular 需要知道在渲染我们的car组件时要添加什么浏览器友好的 HTML,而我们还没有为 Angular 提供这个。提供的方法是使用template属性(类型为字符串)来保存 Angular 在创建CarComponent类的实例后将为我们渲染的 HTML(每当它看到我们的自定义标签<car></car>时)。让我们通过加强我们之前的@Component注解来纠正这一点:

@Component({
  selector: 'car',
  template: '<h3>What production car has the fastest acceleration 
     time from 0 to 60?</h3><p>Tesla </p>'
})
class CarComponent {
}

如果我们的组件需要大量 HTML 会发生什么?好吧,这就是为什么我们有另一个可以使用的属性,templateUrltemplateUrl属性为我们提供了一种将组件的 HTML 从组件类外部化并放在单独文件中的方法。您的template属性看起来可能是这样的:

template: 'car.html'

styles 和 stylesUrls

styles属性用于您期望的用途-向我们的组件模板添加样式。就像template属性一样,值的类型是字符串。此外,因为在多行上间隔 CSS 最容易阅读,我们将使用反引号字符(在 ES6 中是新的,因此也在 TypeScript 中可用),它使我们能够创建所谓的模板文字。让我们向CarComponent类添加styles参数,看看这可能是什么样子:

@Component({
 selector: 'car',
  template: '<h3>What production car has the fastest acceleration 
     time from 0 to 60?</h3><p>Tesla </p>',
  styles: [`
    .car {
      color: #008000;
      font-weight: bold; 
    }
  `]
})
class CarComponent {
}

这就是styles属性的全部内容。我敢打赌你可以猜到styleUrls属性的作用。是的-它的工作原理就像templateUrl属性一样。它为我们提供了一种将组件的 CSS 从组件类外部化并将其放在外部样式表中的方法。请注意,我提到了文件,即文件的复数形式。styleUrls属性接受字符串数组的值(与templateUrl属性的值的类型为字符串相反)-因此,如果我们想要,我们可以将多个样式表传递给它

因此,通过使用模板,templateUrl,styles 和styleUrls属性的组合,我们可以将 HTML(即我们的组件模板)和我们想要应用于模板的 CSS 封装在我们的组件类中-感谢@Component注释为我们提供的属性。由于selector属性,我们可以在组件的父模板中使用自定义 HTML 标记。您开始对所有这些东西如何组合在一起有了良好的感觉吗?如果没有,别担心-当我们开始实现示例应用程序的视图时,您很快就会明白。

视图封装

视图封装是非常方便和非常酷的东西-就像 Angular 中的大多数东西一样-用于配置我们的 CSS 的范围。

通常,当我们创建(或更改)CSS 类时,样式会应用于整个应用程序,而不限于特定页面、组件等。Angular 通过允许我们将样式封装(即限制或包含)到包含给定样式表/CSS 的组件中,为我们提供了对此的一定程度的控制。这是通过@Component注释的另一个属性encapsulation来实现的。

我们可以将组件样式的封装设置为以下三个可能值之一:

  • ViewEncapsulation.Emulated: 这是默认值,效果是我们的样式将仅限于我们的组件。它们不会影响我们页面上的其他任何东西。但是,我们的组件仍将继承或访问全局可访问的样式。

  • ViewEncapsulation.Native: 这基本上与ViewEncapsulation.Emulated相同,只是我们要求 Angular 阻止或保护我们的组件免受任何全局定义的样式影响。效果是我们的组件将免受未分配给我们@Component注释的stylesstyleUrls属性的任何样式的影响。

  • ViewEncapsulation.None: 这是我们会使用的设置,如果我们不想控制 CSS 隔离的级别。换句话说,如果我们希望让我们组件的 CSS 影响其他页面资产,并且还希望我们的组件继承全局定义的 CSS 规则集,这就是我们会使用的设置。

这很酷,不是吗?多么棒的功能!如果你仔细想想,这是使代码重用成为可能的事情之一,甚至在不同的应用程序之间,而不仅仅是在同一个应用程序中。如果我们想要保证我们的组件在 Angular 应用程序中看起来相同,无论任何给定应用程序的样式如何,我们可以将我们组件的encapsulation属性设置为ViewEncapsulation.Native,然后就可以了。

模块与 NgModule

术语非常重要,因为由于语义的原因很容易混淆事物。当涉及的主题中的语言/术语包含重载词时,这一点尤为真实,就像 Angular 作为主题一样。例如,我们已经看到,我们必须非常明确地说明我们所说的组件页面的含义。同样的事情也适用于模块这个词,所以在继续之前,我想在这一点上澄清一些事情。

正如我们在第二章中所看到的,ECMAScript 和 TypeScript 速成课,模块的概念在 ES6 中是新的。在 JavaScript 中,当我们谈论模块时,通常是指一个代码文件,然后我们可以将其导入到我们执行脚本的上下文中,使其封装的函数对我们的脚本可用。Angular 模块,或NgModule,是由多个文件组成的模块,因此通常被称为包。因为我们像导入 JavaScript 模块一样对待这个NgModule或包,我们经常认为它们是等价的,但它们并不是。

本章重点是组件,但当我们将对后端 API 的调用封装在一个统一的包中时,我们还将在第十一章中看一下如何构建我们自己的NgModules依赖注入和服务

在我们离开关于NgModule的讨论之前,将进一步讨论推迟到以后的章节,我想至少触及一下它的一些参数,因为@NgModule是我提到过的另一个存在的注解。

@NgModule 装饰器的属性

如果您查看我们在第四章中开始构建的示例应用程序中的app.module.ts文件,您会看到在我们的AppModule类上的@NgModule注解中有四个参数。让我们快速看一下这四个参数以及我们用它们做什么:

  • 声明:这是我们列出需要打包在这个NgModule中的组件和指令的地方。

  • 导入:这使得其他模块的导出声明对我们的NgModule可用。

  • 提供者:这是我们列出服务和值的地方,以便它们为依赖注入DI)所知。它们被添加到根作用域,并被注入到其他具有它们作为依赖项的服务或指令中。我们将在第十二章中介绍 DI,集成后端数据服务

  • 引导:这是我们列出我们希望 Angular 在应用程序启动时引导的组件。

在我们的应用程序中只能有一个NgModule,我们在其中使用 Bootstrap 参数,因为引导过程始于只有一个模块。

内容投影

内容投影的概念为组件开发人员提供了一种可以增加其可重用性的机制。特别是,我指的是它们的数据显示方式(即呈现方式)。

这意味着,我们不再试图创建一个组件,为每种可能的方式都有属性,而是可以更改其模板(这几乎是不可能的),以便使用组件的开发人员可以变化这些属性的值,以自定义渲染方式。内容投影提供了一种以更少的仪式实现这一点的方法。

我们使用的机制是一对 ng-content 标签,就像这样:<ng-content></ng-content>

我们将在照片列表页面中实践这一点,但现在让我给你展示一个人为的例子。让我们修改我们的 CarComponent 模板为以下代码片段(添加一对 ng-content 标签):

template: '<h3>What production car has the fastest acceleration time from 0 to 60?</h3><ng-content></ng-content>'

这样做的目的是使 CarComponent 的父组件能够将内容投影到 CarComponent 的模板中,从而根据需要更改模板。假设我们不仅仅想在常规文本中显示汽车制造商,而是想在一组 <p> 标签中显示汽车制造商。

父组件将如下所示:

<car>
    <strong>Tesla</strong>
</car>

而不是如下所示:

<car></car>

再次,这是一个人为的例子。另外,Angular 的整个重点是拥有动态数据,但我们在这里没有做到。例如,我们会将汽车问题和答案数据绑定到组件模板中的元素,而不是将其硬编码(在这种情况下是 哪辆量产汽车的 0 到 60 加速时间最快?特斯拉)。然而,我们简化的硬编码代码以最直接的方式说明了内容投影的概念——即不使数据动态化,而我们将在本书的后面部分做一些动态化。

投影多个部分

可以包含多对 ng-content 标签。然而,由于 Angular 无法确定哪个投影内容已替换了哪组 ng-content 标签,我们需要以某种方式标记 ng-content 标签,以使它们彼此区分开来。

一种简单的方法是通过类名标记或标记ng-content标签,以便预期投影的内容替换所需的一组ng-content标签。我们使用ng-content的名为select的属性来标记标签。让我们扩展我们的虚构CarComponent示例,看看这在具有两对ng-content标签时会是什么样子:

template: '<ng-content select=".question"></ng-content><ng-content select=".answer"></ng-content>'

以下是父组件的样子:

<car>
    <h3 class="question">What production car has the fastest acceleration 
       time from 0 to 60?</h3>
    <span select="answer"><strong>Tesla</strong></span>
</car>

通过使用ng-content标签及其select属性,如果您有多个内容投影目标,您可以创建可由消费者定制的组件。

生命周期钩子

与几乎所有活着的事物一样,从我们太阳系中的恒星到您可能买来装饰餐桌的花朵,Angular 组件也有一个生命周期,它们从诞生到消亡经历的不同阶段或阶段。

我们可以在这些不同的阶段钩入任何我们希望 Angular 为我们运行的代码,因为 Angular 为我们提供了特殊的方法,每个组件生命周期阶段都有一个方法,Angular 会为我们调用。我们所要做的就是提供我们希望 Angular 运行的代码,我们是通过在组件类中添加与生命周期钩子同名的函数来实现的。

组件有一组生命周期钩子,其子组件(即子组件)也有一组生命周期钩子。以下表列出了最常见的生命周期钩子:

生命周期钩子 类型 在...时调用
constructor 组件 Angular 在类上调用new时创建组件。
ngOnInit 组件 组件已完全初始化。
ngOnChanges 组件 输入属性发生变化(每次变化调用一次)。
ngOnDestroy 组件 Angular 即将销毁组件。
ngAfterContentInit 组件的内容投影发生后。
ngAfterContentChecked Angular 在内容上运行其变更检测算法。
ngAfterViewInit 组件的视图已完全初始化。
ngAfterViewChecked Angular 在视图上运行其变更检测算法。

最常见的生命周期钩子

从前面的八个生命周期钩子中,你最有可能只使用其中的三个(在大多数情况下)。所有这三个都属于组件类型的生命周期钩子:

  • ngOnInit:我们的组件初始化逻辑将放在这里。你可能会认为构造函数是添加初始化逻辑的地方,但ngOnInit更可取,因为通过我们的接口(即输入属性)进行的任何数据绑定都已经完成。构造函数阶段并非如此。

  • ngOnChanges:当我们想知道哪些输入属性已经改变,以及它们被改变成了什么,这就是需要查看的地方。

  • ngOnDestroy:这是我们为组件插入清理逻辑的地方(如果我们有任何需要清理的东西 - 否则,我们就不使用它)。

这是一个我们如何钩入ngOnInit生命周期钩子的例子(我们只是向控制台输出一些内容):

class CarComponent {
    ngOnInit()  {
        console.log('An instance of our CarComponent has 
            been fully initialized.');
    }
}

组件接口 - 输入和输出,以及数据流

如果你要在特定屏幕上创建一个组件的图表(即视图/页面),在它们之间画箭头来表示数据流,箭头将从一个组件的输出指向另一个组件的输入。

在代码中,正如我们将在实现中看到的那样,我们绑定输出和输入的方式是在我们的组件模板中(即在 HTML 中)。但是要在 HTML 中进行绑定,我们需要在代码中创建我们的组件,并且我们需要给它们接口。

让我们快速看一个具体的例子,它将展示父组件如何将数据传递给它的子组件。为了演示这一点,让我们首先创建我们的两个组件。

这是我们的DadComponent,它将是父组件:

import {Component } from '@angular/core';
@Component({
    selector: 'dad',
    template: `<h1>Hello. {{message}}.</h1> <br/> 
        <son *ngFor="let name of arrSonNames" 
        [Name]="name">
        </son>
    `,
})
export class DadComponent { 
    message : string = "I'm a Dad";
    arrSonNames = ['Justin','','Brendan'];
}

这是我们的SonComponent,它将是子组件:

import { Component, Input, OnInit } from '@angular/core';
@Component({
    selector: 'son',
    template: `<h2>Hi. I'm a son, and my name is {{_name}}.</h2>`
})
export class SonComponent implements OnInit {
    _name: string;
    constructor() {
        console.log("The son component was just instantiated.");
    }
    ngOnInit(){
        console.log("The son component is now fully initialized.");
    }
    @Input()
    set Name(name : string ) {
        this._name = (name && name.trim()) || "I am a son."; 
    }
    get Name() {
        return this._name;
    }
}

这段代码中发生了很多事情。我不会描述前面代码块中发生了什么。相反,我希望你花几分钟时间研究一下,看看你能否弄清楚发生了什么。你应该从以前的章节中获得足够的信息,再加上一些关于 JavaScript/TypeScript 的基本知识,以及对 getter 和 setter 的理解(因为许多语言都有)。我知道你能做到——试一试。我会给你两个提示:1)@Input()是一个装饰器,在这种情况下,它创建了SonComponent的公共接口;2)DadComponent最终会创建三个SonComponent的实例。其中两个儿子会知道自己的名字,不幸的是,其中一个儿子不会知道自己的名字。他会说什么?知道自己名字的儿子叫什么?你能看出为什么会创建三个儿子吗?你能猜到会写入控制台什么,以及会写入多少次吗?

我们将在我们的实现中看到很多这种模式,所以如果看起来奇怪,或者似乎有点复杂,并且你不能回答我提出的所有问题,不要担心。过一段时间,这些东西应该变得很自然。是的,我将从现在开始解释我们的实现代码——不是详细到极致,但足够让你理解手头的材料。目前,我只是想让你感受一下通过组件接口传递数据是什么样子。

我们三个页面的组件实现

我们现在有足够的知识来实现(即,在代码中创建)我们示例应用程序以下三个页面所需的组件:

  • 预览列表

  • 照片列表

  • 预览照片

为了生成这些组件,我们将利用 Angular CLI 原理图。运行以下命令,我们应该期望自动生成组件和所需的文件:

ng generate component photo-listing
ng generate component preview-listing
ng generate component preview-photo

一旦命令成功运行,我们应该看到如下屏幕截图所示的输出:

在上面的屏幕截图中,我们可以注意到已为组件生成了相应的文件,并且app.module.ts文件已经更新为最新生成的组件。

到目前为止,我们应用程序的最终项目结构如下所示:

摘要

在本章中,我们涵盖了很多内容。您可能并没有完全理解上一节中的一些代码,这没关系,因为当我们一起为示例应用程序实现页面时,您会变得擅长这些内容。由于本章是关于组件的,我只是想向您展示如何设置父组件和子组件的一般结构,以及如何通过子组件的公共接口从父组件传递数据。但是,现在您应该对 Angular 应用程序只是一组组件的树有了相当好的理解。分解组件为子组件的经验法则是什么,注解和装饰器是什么。

我们还研究了@Component注解/装饰器是什么,它的属性是什么,以及如何配置它们。然后,我们转向了@NgModule装饰器是什么,它的一些属性是什么,以及它们的作用是什么。然后,我们研究了内容投影是什么,以及如何使用它允许其他开发人员自定义他们的渲染。

最后,我们学习了什么是生命周期钩子,如何使用它们以及为什么要使用它们。然后,我们转向了组件接口是什么以及如何创建它们。最后,我们研究了我们三个页面(预览列表、照片列表和预览照片)所需的组件的实现。

在下一章,第七章,模板、指令和管道,我们将深入研究组件的模板部分,因为那里是所有数据绑定和渲染发生的地方——将我们的 Angular 应用程序从一堆 0 和 1 带到我们的屏幕上。

Angular 提供了许多工具,以指令和管道的形式,供我们利用,这样我们就可以告诉它如何在画布上绘制。所以,翻过页面,让我们了解如何让 Angular 开始在应用程序画布上放置我们的组件绘制,从而使我们的应用程序生动起来——这就是我们将把我们的组件放置到我们的三个页面(预览列表、照片列表和预览照片)上的地方。

第七章:模板、指令和管道

模板定义了组件在网页上的显示和布局方式。Angular 提供了几个内置指令,让开发人员控制他们的组件的显示方式——从是否显示或隐藏组件,到在页面上多次渲染组件。内置指令还提供了一种将类和样式绑定到组件的机制。

在第六章,构建 Angular 组件中,我们看了组件的结构以及如何将我们的应用程序分解为一棵组件树。

在本章中,您将学习如何控制组件在其父模板中的显示。具体来说,我们将一起讨论以下内容:

  • 模板

  • 指令

  • 管道

模板

在上一章中,我们已经了解了组件模板是什么以及如何创建它们。然而,到目前为止,我们只看到了静态 HTML。在本节中,我想稍微放大一下,和您一起看一些模板语法,这些语法允许我们创建动态 HTML,这当然是 Angular 的主要目标之一。

在 Angular 中,模板语法为我们提供了一种机制,使我们的 HTML 动态化——具体来说,用于数据绑定、属性绑定和事件绑定。在本章中,我们将看看这三种绑定类型。Angular 赋予我们创建生成动态 HTML 模板或操作 DOM 的能力,是通过一组符号。

以下是我们可以使用的六个基本符号:

  • {{ }} 用于字符串插值和单向数据绑定

  • [( )] 用于双向数据绑定

  • # 用于变量声明

  • ( ) 用于事件绑定

  • [ ] 用于属性绑定

  • * 用于前置结构指令,例如ngFor,正如我们将看到的

指令

指令的三种类型是:组件、属性指令和结构指令。然而,我们实际上只会涵盖其中的两种——属性指令和结构指令。原因是我们已经花了整整一章的时间来覆盖第一种指令,也就是组件。没错!组件实际上是隐藏的指令!具体来说(这说明了组件与属性和结构指令的区别),组件是具有模板的指令。当然,这必须意味着属性和结构指令没有模板。

好的,那么指令到底是什么?让我们给术语“指令”一个明确定义,以消除在讨论接下来的两种指令之前可能引起的任何混淆。我们将使用的定义是:Angular 指令是提供特定 DOM 操作的构造。DOM(或 HTML DOM)是文档对象模型的缩写,不是 Angular 的东西,而是浏览器的东西。所有现代浏览器在加载网页时都会创建一个 DOM,这是一个可以被 JavaScript 访问的对象树。没有 DOM,Angular(以及任何其他操作 DOM 的 Web 框架)都不会存在。

正如我们在第六章中所看到的,构建 Angular 组件符合我们对指令的定义,因为它们确实是提供特定 DOM 操作的构造。它们的模板不仅被注入到我们的页面中(替换它们的自定义 HTML 标签),而且它们本身包含数据、属性和事件绑定,进一步操作 DOM。

我们已经以各种方式充分解释了组件,并将在接下来的章节中看到它们在实现我们的线框时的实际应用。

剩下的两种指令类型不会在我们的页面或视图中注入任何 HTML 模板,因为它们没有任何模板。然而,它们会操作 DOM,正如我们之前对指令的定义所要求的那样。现在让我们来看看这两种类型的指令分别是做什么的。

属性指令

属性指令通过改变特定 DOM 元素的外观或行为来操作 DOM。这些类型的指令被括号括起来,是 HTML 元素的属性。括号是符号(我们在本章开头列出的五种符号之一),它们向 Angular 发出信号,告诉它可能需要改变指令所属元素的外观或行为。

最后一句话很啰嗦,让我们看一个你最有可能使用的属性指令的代码示例。我所指的指令名为hidden,它将导致 Angular 要么显示要么隐藏它的元素:

<div [hidden]="usertype != 'admin'">
  This element, and its contents, will be hidden for all users that are not Admins. 
</div>

在前面的代码中,我们隐藏了div元素和所有非管理员用户类型的嵌入式 HTML。在这里,usertypeadmin当然是应用上下文的东西,只是用作示例来说明 Angular 可以做什么。

更一般地说,hidden属性指令与要评估的表达式相关联。表达式必须评估为布尔值(即truefalse)。如果表达式评估为true,Angular 将从视图中隐藏该元素。相反,如果表达式评估为false,Angular 将不做任何改变,并且该元素将在视图中显示。

就像我在之前的章节中所做的那样,我会确保将您指向官方在线文档。正如您现在所知,我不喜欢其他许多 IT 书籍采取的方法,即机械地重复文档。虽然在某种程度上是不可避免的,但有些书籍的大部分页面都是这样。因此,我将继续远离这种陷阱,并将继续以更好的方式添加所有可能的价值。

也就是说,属性指令的官方在线文档可以在angular.io/guide/attribute-directives找到。

结构指令

结构指令通过添加或删除特定的 DOM 元素来操作 DOM。就像我们有语法可以用来向 Angular 发出信号,告诉它我们有一个需要注意的属性指令一样,使用括号符号,我们也有结构指令的等价物。

我们用来向 Angular 发出信号,告诉它我们有一个结构指令需要注意的语法是星号(*)。结构指令以星号为前缀,这向 Angular 发出信号,告诉它可能需要向 DOM 添加或删除元素。正如我在本章开头列举的那样,星号是我们可以在模板语法中使用的符号之一。

NgFor

正如我们看一个属性指令的代码示例,你最有可能使用的,现在让我们来看一个结构指令的代码示例,你可能会经常使用——NgFor

<ul>
 <li *ngFor='let car of [{"make":"Porsche", "model":"Carrera"}, {"make":"Ferrari", "model":"488 Spider"}]'>
   {{ car.make }}: {{ car.model }}
 </li>
</ul>

之前的ngFor代码示例输出如下:

Porsche: Carrera
Ferrari: 488 Spider

在上面的代码中,有几件事我想指出;首先是*ngFor结构指令。让我们用项目符号形式来看一下这些:

  • ngFor接受一个可迭代对象,并循环遍历它,向 DOM 添加元素

  • 指令语法的一般形式是 *ngFor="let <value> of <collection>"

  • NgFor(注意大写 N)指的是定义指令的类

  • ngFor(注意小写 n)既是属性名称,也是NgFor类的一个实例

  • 其余的结构指令遵循与NgFor相同的大小写约定(参见前两个项目符号)。

  • 我们可以嵌套使用ngFor(就像我们可以嵌套使用for each...in 循环一样)

接下来,我提供给ngFor指令的集合并不代表我们通常如何向指令传递数据。我之所以以这种方式编码是为了简洁。我们通常会这样做,即在组件类中定义数据(即我们的集合),并将其分配给一个变量,然后在附加到指令的语句中使用该变量。

访问迭代的索引值

我们经常会对迭代的索引值感兴趣——也许是为了抓取每个第 n 个对象,或者按照 x 的数量分组,或者可能我们想要实现某种自定义分页。无论需要读取迭代的当前索引值是什么,我们都可以使用index关键字将索引设置为表达式中的变量。

以下是一些演示这一点的示例代码:

<ul> 
  <li *ngFor="let car of cars; let i = index">
    Car #{{ i + 1 }}: {{ car.model }}
  </li>
</ul>

在上面的代码示例中,让我们假设汽车集合是在其他地方填充的,比如在组件类中。

此外,Angular 会为我们更新每次迭代的索引值,而我们所要做的就是引用它。

请注意,我们使用 {{ i + 1 }} 来输出汽车编号。这是因为,与大多数数组或可迭代对象一样(在大多数语言中,但肯定在 JavaScript 和 TypeScript 中),索引是从零开始的。另外,请注意,双大括号内的表达式 i + 1 不仅仅是一个变量。在 Angular 中,双大括号内插入的任何内容都会被评估。如果我们愿意,甚至可以在那里插入函数调用。

结构指令的官方在线文档可在 angular.io/guide/structural-directives 上找到。

内置指令

我们有几个内置指令可供我们使用。让我们在接下来的部分中看看这些。

  • NgFor(我们已经涵盖了这个,作为结构指令的第一个示例)

  • NgIf

  • NgSwitchNgCaseNgDefault

  • NgStyle

  • NgClass

  • NgNonBindable

NgIf

当我们想要在 DOM 中显示或移除元素时,我们使用 NgIf 指令。我们向指令传递一个表达式,它必须求值为布尔值。如果求值为 true,元素将在视图上显示。相反,如果表达式求值为 false,元素将从 DOM 中移除。

请注意,我们还可以绑定到 hidden 属性(属性绑定将在下文中描述)来实现相同的视觉效果,但是属性绑定方法和使用 NgIf 指令之间存在区别。区别在于,使用 hidden 的属性绑定只是隐藏元素,而使用 NgIf 指令会从 DOM 中实际移除元素。

以下是代码中 NgIf 的样子(在我们的汽车示例中的上下文中,假设我们有一个 horsepower 属性):

<ul *ngFor="let car of cars">
  <li *ngIf="car.horsepower > 350">
    The {{ car.make }} {{ car.model }} is over 350 HP. 
  </li>
</ul>

在大多数传统编程语言中,当有一系列传统的 ifthenelse 语句中要检查的替代事物时,有时使用 switch 语句(如果语言支持)更有意义。Java、JavaScript 和 TypeScript 是支持这种条件构造的语言的例子(当然还有许多其他语言)。Angular 也给了我们这种能力,所以我们可以更加表达和高效地编写我们的代码。

让我们在下一节中看看在 Angular 中如何实现这一点。

NgSwitch、NgCase 和 NgDefault

在一些编程语言中,比如 Java、JavaScript 和 TypeScript,switch语句不能单独使用。它需要与其他语句和关键字一起使用,即casedefault。Angular 的NgSwitch指令的工作方式完全相同,NgSwitchNgCaseNgDefault一起使用。

让我们通过创建一个包含我们的汽车数据、样式和模板的组件来丰富一下这里稍微大一点的例子,该组件使用NgSwitchNgCaseNgDefault

@Component({
  selector: 'car-hp',
  template: `
    <h3>Cars styled by their HP range</h3>
    <ul *ngFor="let car of cars" [ngSwitch]="car.horsepower"> 
      <li *ngSwitchCase="car.horsepower >= 375" class="super-car">
        {{ car.make }} {{ car.model }} 
      </li>
      <li *ngSwitchCase="car.horsepower >= 200 && car.horsepower 
          < 375" class="sports-car">
        {{ car.make }} {{ car.model }}
      </li>
      <li *ngSwitchDefault class="grandma-car">
        {{ car.make }} {{ car.model }}
      </li>
    </ul>
  `,
  styles: [`
    .super-car {
      color:#fff;
      background-color:#ff0000;
    },
    .sports-car {
      color:#000;
      background-color:#ffa500; 
    },
    .grandma-car {
      color:#000;
      background-color:#ffff00; 
    } 
  `],
  encapsulation: ViewEncapsulation.Native 
})
class CarHorsepowerComponent {
  cars: any[] = [
    {
      "make": "Ferrari",
      "model": "Testerosa",
      "horsepower": 390
    },
    {
      "make": "Buick",
      "model": "Regal",
      "horsepower": 182 
    }, 
    {
      "make": "Porsche",
      "model": "Boxter",
      "horsepower": 320
    }, 
    {
      "make": "Lamborghini",
      "model": "Diablo",
      "horsepower": 485
    }
  ];
}

在前面的代码中,我们构建了一个完整的组件

CarHorsepowerComponent。在父组件模板中,Angular 将用我们在CarHorsepowerComponent中创建的模板替换我们自定义的 HTML 元素<car-hp>的实例(这是因为我们将car-hp分配给了我们的CarHorsepowerComponent类的组件注解的selector属性)。

我们还在组件类中包含了传递给NgFor指令的集合数据,而不是在之前的例子中内联在分配给NgFor指令的表达式中。

这是一个简单的例子,其模板遍历我们的汽车集合,并根据当前汽车的马力应用三种样式之一到汽车的品牌和型号上-这是通过NgSwitchNgCaseNgDefault指令实现的。具体来说,这是结果:

  • 如果汽车的马力等于或大于 375 马力,我们将认为它是一辆超级跑车,并且将汽车的品牌和型号以白色字体呈现在红色背景上

  • 如果汽车的马力等于或大于 200 马力,但小于 375 马力,我们将认为它只是一辆跑车,并且将汽车的品牌和型号以黑色字体呈现在橙色背景上

  • 如果汽车的马力低于 200 马力,这是我们的默认(或通用)情况,我们将认为它是一辆适合祖母开车的汽车,并且将汽车的品牌和型号以黑色字体呈现在黄色背景上-因为大多数祖母都觉得蜜蜂的颜色搭配很吸引人

当然,祖母的评论只是为了娱乐价值,我并不是故意冒犯任何需要花费整整 8 秒,甚至更多时间从 0 到 60 英里/小时加速的人(眨眼)。说实话,我的一辆车(2016 年本田思域)只有 158 马力——相信我,我曾经在上坡路上被一位开英菲尼迪 Q50 的祖母超过。这就是为什么在那可怕的经历之后的几天内,我买了一些更强大的东西(大笑)。

我想在上一个示例中指出的最后一件事是NgSwitch指令的使用方式。您会注意到我以不同的格式编写了它,即[ngSwitch]="car.horsepower",而不是*ngSwitch="car.horsepower"。这是因为在使用结构指令时,Angular 对我们施加了一条规则,即我们不能有多个使用星号符号作为指令名称前缀的结构指令。为了解决这个问题,我们使用了属性绑定符号[ ](一对方括号)。

NgStyle

NgStyle指令用于设置元素的样式属性。让我们重新设计之前的CarHorsepowerComponent示例,该示例用于演示NgSwitchNgCaseNgDefault,以展示如何使用NgStyle更好地实现相同的期望结果(即有条件地设置元素样式):

@Component({
  selector: 'car-hp',
  template: `
    <h3>Cars styled by their HP range</h3>
    <ul *ngFor="let car of cars"> 
      <li [ngStyle]="{ getCarTextStyle(car.horsepower) }" >
        {{ car.make }} {{ car.model }}
      </li> 
    </ul>
  `,
  encapsulation: ViewEncapsulation.Native 
})
class CarHorsepowerComponent {
  getCarTextStyle(horsepower) {
    switch (horsepower) {
      case (horsepower >= 375):
        return 'color:#fff; background-color:#ff0000;';
      case (horsepower >= 200 && horsepower < 375):
        return 'color:#000; background-color:#ffa500;';
      default:
        return 'color:#000; background-color:#ffff00;';
    }
  }
  cars: any[] = [
    {
      "make": "Ferrari",
      "model": "Testerosa",
      "horsepower": 390
    },
    {
      "make": "Buick",
      "model": "Regal",
      "horsepower": 182 
    }, 
    {
      "make": "Porsche",
      "model": "Boxter",
      "horsepower": 320
    }, 
    {
      "make": "Lamborghini",
      "model": "Diablo",
      "horsepower": 485
    }
  ];
}

在我们重新设计原始的CarHorsepowerComponent类时,我们通过将逻辑移入类中的一个函数来简化了组件模板。我们删除了组件注释的样式属性,而是创建了一个函数(即getCarTextStyle)来返回样式文本给调用函数,以便我们可以设置正确的样式。

虽然这是一种更清晰的方法,但我们可以做得更好。由于我们正在为汽车文本设置样式,我们可以完全更改样式类,而不是通过文本传递实际的样式规则集。

在下一节中,关于NgClass,我们将再次重写我们的代码,以了解如何完成这一点。

NgClass

NgClass指令类似于NgStyle指令,但用于设置样式类(从组件注释的样式属性中的 CSS 规则集),而不是通过原始 CSS 规则集设置样式。

以下代码示例是最后三个代码示例中最好的选择,以实现我们想要做的事情:

@Component({
  selector: 'car-hp',
  template: `
    <h3>Cars styled by their HP range</h3>
    <ul *ngFor="let car of cars"> 
      <li [ngClass]=" getCarTextStyle(car.horsepower) " >
        {{ car.make }} {{ car.model }}
      </li> 
    </ul>
  `,
  styles: [`
    .super-car {
      color:#fff;
      background-color:#ff0000;
    },
    .sports-car {
      color:#000;
      background-color:#ffa500; 
    },
    .grandmas-car {
      color:#000;
      background-color:#ffff00; 
    } 
 `], 
 encapsulation: ViewEncapsulation.Native 
})
class CarHorsepowerComponent {
  getCarTextStyle() {
    switch (horsepower) {
      case (horsepower >= 375):
        return 'super-car';
      case (horsepower >= 200 && horsepower < 375):
        return 'sports-car';
      default:
        return 'grandmas-car';
    }
  }
  cars: any[] = [
    {
      "make": "Ferrari",
      "model": "Testerosa",
      "horsepower": 390
    },
    {
      "make": "Buick",
      "model": "Regal",
      "horsepower": 182 
    }, 
    {
      "make": "Porsche",
      "model": "Boxter",
      "horsepower": 320
    }, 
    {
       "make": "Lamborghini",
       "model": "Diablo",
       "horsepower": 485
    }
  ];
}  

在这里,我们保留了组件注释的styles属性,保持了模板的轻量和清晰,我们的函数只返回要分配给我们的NgClass指令的 CSS 类的名称。

NgNonBindable

我们要介绍的最后一个指令是NgNonBindable指令。当我们希望 Angular 忽略模板语法中的特殊符号时,就会使用NgNonBindable。为什么我们要这样做呢?嗯,假设你和我决定创建一个在线的 Angular 教程,而网站本身要使用 Angular 进行编码。如果我们想要将文本{{ my_value }}呈现到视图中,Angular 会尝试在当前范围内查找my_value变量来绑定值,然后插入文本。由于这不是我们希望 Angular 做的事情,我们需要一种方法来指示 Angular,“嘿,顺便说一句,现在不要尝试评估和字符串插值任何东西,只需像对待任何其他普通文本一样呈现这些符号”。

比如,这是一个span元素的样子:

<p>
To have Angular perform one-way binding, and render the value of my_value onto the view, we use the    double curly braces symbol like this: <span ngNonBindable>{{ my_value }}</span>
</p>

请注意NgNonBindable指令在开放的<span>标记中的位置。当 Angular 看到ngNonBindable时,它将忽略双大括号,并且不会单向绑定任何内容。相反,它将让原始文本呈现到视图中。

使用 NgModel 指令进行数据绑定

我们在示例中看到了单向数据绑定的一个例子,该示例演示了如何使用NgFor指令。换句话说,单向数据绑定是使用双大括号符号{{ }}完成的。我们在双大括号中包含的变量(例如示例中的car.makecar.model)是单向绑定的(即从组件类到模板),转换为字符串,并呈现到视图中。它不允许将任何更改绑定回组件类。

为了实现双向数据绑定,从而也允许在视图中绑定对组件类的更改,我们必须使用NgModel指令。

当我们实现我们的线框时,我们将看到这一点,但现在让我向你展示一下它是什么样子的。为了使用NgModel,我们必须首先从forms包中导入一个名为FormsModule的 Angular 模块,就像这样:

import { FormsModule } from '@angular/forms';

然后,要使用这个指令,我们会有类似这样的东西:

<div [(ngModel)]="my_content"></div>

将这段代码放在这里不仅会导致视图模板显示组件类中my_content的值,而且对视图模板中这个div的任何更改都会被绑定回组件类。

事件绑定

在我们实现示例应用程序的线框时,我们将看到很多事件绑定。为了绑定我们感兴趣的元素上要监听的事件,我们将事件名称括在括号中(这是我们在模板语法中可以使用的特殊符号之一)。为此,我们分配一个语句在事件触发时运行。

这是一个 JavaScript 警报的例子,当有人点击<span>元素时将会触发:

<span (click)="alert('This is an example of event binding in Angular');"></span>

在上面的代码中,我们附加了一个click事件,并调用一个带有消息的警报框。

属性绑定

我们在先前的例子中已经看到了属性绑定,但为了完整起见,我在这里很简要地给出另一个例子:

<p class="card-text" [hidden]="true">This text will not show.</p>

在这个先前的例子中,我们将要设置的属性括在方括号中(这是我们在模板语法中可以使用的特殊符号之一)。当然,在这个例子中这并不是很有用,因为我已经将布尔值硬编码为true,而不是使用要求评估的表达式,但这个例子的重点是集中在[hidden]部分。

自定义指令

Angular 是可扩展的。我们不仅可以轻松创建自定义组件(这样我们就不受限于使用第三方提供的现成组件),还可以创建自定义属性指令,这样我们就不受限于 Angular 默认提供的内容。

我会留下一些我们在 Angular 中可以做的自定义事情,比如自定义属性指令、自定义管道(我们将在下一节中看到管道是什么),以及自定义表单验证,直到第十四章,高级 Angular 主题。我们将在第十章,使用表单中看到表单验证。我选择将这本书中涵盖的所有高级内容都放在一个章节中是有充分理由的——让你有时间先消化基础知识。当高级章节出现时,接近书的末尾,你将准备好并更容易吸收那些信息。

管道

管道用于格式化我们模板视图中的数据。管道将接受数据作为输入,并将其转换为我们期望的格式,以便向最终用户显示。我们可以在我们项目中的任何 Angular 模板或视图中使用pipe属性(|)。

在我们开始创建示例之前,让我快速概述一下。假设我们从后端服务获取产品的价格为 100,并根据用户的国家或偏好,我们可能希望以$100 的方式显示价值,如果用户来自美国,或者以 INR 100 的方式显示价值,如果用户来自印度。因此,我们能够在没有任何主要复杂性的情况下转换我们显示价格的方式。这要归功于货币管道运算符。

Angular 提供了许多内置管道,可以直接在我们的模板中使用。此外,我们还可以创建自定义管道来扩展我们应用程序的功能。

以下是 Angular 提供的所有内置管道的列表:

  • 小写管道

  • 大写管道

  • 日期管道

  • 货币管道

  • JSON 管道

  • 百分比管道

  • 小数管道

  • 切片管道

我们将通过一些有趣的实际示例来了解每个可用的内置管道。到目前为止,我们可以利用我们在 Angular 项目中创建的任何现有模板文件。

我们需要一些数据,我们想要使用我们的管道来处理和转换。我将在我们的app.component.ts文件中快速创建一个数据集:

products: any[] = [ {  "code": "p100",
  "name": "Moto",
  "price": 390.56
 }, {  "code": "p200",
  "name": "Micro",
  "price": 240.89
 }, {  "code": "p300",
  "name": "Mini",
  "price": 300.43
 } ];

我们在应用程序组件中创建了一个产品的样本数据集。好了,现在我们可以在我们的app.component.html文件中应用我们的管道了。我们将在模板中保持简单。我们将只创建一个表格并绑定表中的值。如果你今天感觉有点冒险,那就继续使用 Flex-Layout 为我们的应用程序创建一个布局,我们在第五章中学到了Flex-Layout – Angular's Responsive Layout Engine

<h4>Learning Angular Pipes</h4> <table>
  <tr>
  <td>Product Code</td>
  <td>Product Name</td>
  <td>Product Price</td>
  </tr>
  <tr  *ngFor="let product of products">
  <td>{{product.code}}</td>
  <td>{{product.name}}</td>
  <td>{{product.price}}</td>
  </tr>  </table>

在上面的示例代码中,我们创建了一个表格,并使用数据绑定将数据绑定到我们的模板中。现在是时候在我们的模板中使用管道运算符了。要应用任何管道,我们必须在数据中添加管道运算符,如下面的语法所示:

{{ data | <pipe name> }}

我们可以通过应用大写管道轻松地将我们的产品名称转换为大写,如下所示:

<td>{{product.name | uppercase }}</td>

同样地,我们也可以使用小写管道,这将使所有字符变为小写:

<td>{{product.name | lowercase }}</td>

你可能会说那太简单了?确实如此!让我们继续。类似地,我们将使用数字管道操作符来显示或隐藏小数点。

为了显示产品价格,我们想要添加货币;没问题,我们将使用货币管道:

<td>{{product.price | currency }}</td>

在前面的例子中,我们通过添加货币管道来转换了产品价格。剩下的管道操作符就留给你作业了。

当我们使用货币管道时,默认情况下会添加$ currency

我们可以通过给货币管道加参数来自定义它。我们将学习如何向管道操作符传递参数。我们将不得不通过以下方式扩展管道操作符的语法来传递参数:

{{ data | pipe : <parameter1 : parameter2> }}

前面的语法看起来类似于我们学习如何定义管道操作符的方式,只是现在它有两个参数。根据我们的需求,我们可以定义任意数量的参数的管道操作符。在前面的例子中,我们使用了货币操作符,所以让我们传递参数来扩展货币管道操作符:

<td>{{ product.price | currency: 'INR' }}</td>

我们正在向我们的货币管道操作符传递INR参数。现在,货币管道操作符的输出将不再是$,而是如下所示的屏幕截图中显示的内容:

在本节中,我们已经学会了使用内置的管道操作符。现在,我们将学习如何创建我们自己的自定义管道。

自定义管道

Angular 在自定义管道和自定义指令的领域也是可扩展的。然而,我将推迟我们对自定义管道的讨论,直到第十四章,“高级 Angular 主题”。我在这里包含了这一部分作为一个占位符,以及对以后的覆盖的提醒,也是为了完整性。

总结

在本章中,我们放大了组件模板,以及我们用于创建它们的模板语法。我们的模板语法包括符号、指令和管道。

我们已经看到指令只是没有模板的组件,它们有两种主要的类型——属性指令结构指令。无论它们的类型或类别如何,我们都可以通过将它们添加为元素的属性来将指令与 HTML 元素关联(或附加)。

我们已经介绍了我们可以在模板语法中使用的以下特殊符号。我们还介绍了我们可以在模板语法中使用的内置指令。接下来,我们介绍了事件绑定,以及属性绑定,最后,我们介绍了管道,它为我们提供了格式化数据的方式,以便按照我们期望的方式呈现到视图中。

我们知道 Angular 是可扩展的,并且它为我们提供了创建自定义指令和自定义管道的机制,但我们将推迟讨论任何自定义内容到[第十四章]《高级 Angular 主题》。

在下一章,[第八章]《使用 NG Bootstrap 工作》,我们将重新戴上组件帽子,以便探索ng-bootstrap为我们在构建 Angular 应用程序时带来了什么。

第八章:使用 NG Bootstrap

Bootstrap 是最受欢迎的 CSS 框架之一,而 Angular 是最受欢迎的 Web 应用程序框架之一。NG Bootstrap 是一个由 Bootstrap 4 CSS 构建的小部件(即组件)集合。它们专门用于作为 Angular 组件使用,并旨在完全替代由 JavaScript 驱动的 Bootstrap 组件。一些由 JavaScript 驱动的 Bootstrap 组件的示例包括以下内容:

  • 轮播

  • 折叠

  • 模态

  • 弹出框

  • 工具提示

在本章中,我们将继续探讨组件,但将重点放在 ng-bootstrap 上,这是一个第三方 Angular 组件库,而不是 Angular 代码库的一部分。这一章和第九章,使用 Angular Material,都是相对较短的章节,但我想把它们包括在这本书中,原因与我包括第五章,Flex-Layout – Angular's Responsive Layout Engine相同-那就是给你选择的机会。在这一章的背景下,这意味着你可以选择为你的 Angular 应用程序利用的现成组件。

ng-bootstrap 没有官方的缩写,但为了方便起见,在本章中,我将给它一个。我们将把 NG Bootstrap 称为 NGB-事实证明,这也是键盘上有趣的输入(因为字母之间的距离如此接近)。试试看。

就像本书中的其他章节一样,我不会消耗大量页面来简单地重复 NGB 的官方文档,这些文档可以在网上免费获取,只是为了让这本书看起来令人敬畏。我宁愿给你一本 300 到 400 页的书,充满了精心挑选的好东西,让你一直阅读,而不是一本 500-600 页的书,可以用作你辛苦赚来的钱的催眠剂。话虽如此,NGB 的官方在线文档可以在这里找到:

ng-bootstrap.github.io

我想最后快速提一下的是,本章和接下来的一章(第八章,使用 NG Bootstrap)将比本书中的其他章节更加视觉化。这是因为我们现在开始进入我们示例应用程序的实质内容,并且我们将开始在视觉上构建事物。

现在处理完了杂事,接下来我们将一起讨论本章中要涵盖的内容:

  • 集成 NGB

  • NGB 小部件(特别是折叠、模态和轮播)

  • 设计规则是我们应该考虑的要点,以帮助避免过度使用小部件

集成 NGB

NGB 的存在意义是成为 Bootstrap 需要 JavaScript 的组件的完整替代品(例如本章开头列出的组件)。事实上,在官方网站的入门部分的第一页上,他们进一步表示,您不应该使用任何基于 JavaScript 的组件,甚至不应该使用它们的依赖项,如 jQuery 或 Popper.js。这可以在以下网址找到:ng-bootstrap.github.io/#/getting-started

安装 NBG

首先要做的事情是:在我们查看使用 NGB 时需要注意的一个警告之前,让我们将其添加到我们的项目中——我还将向您展示如何解决可能遇到的冲突库(通过展示我的package.json文件)。

使用npm安装 NGB 很简单。但是,与其他模块一样,我们还需要将其导入并在根模块中列出(即app.module.ts)。以下是步骤:

  1. 运行npm installnpm install --save @ng-bootstrap/ng-bootstrap

  2. 将 NGB 导入到我们的根模块中:import {NgbModule} from '@ng-bootstrap/ng-bootstrap';

  3. 在导入数组中列出NgbModule(作为根模块的@NgModule装饰器的参数)如下:NgbModule.forRoot()

如果您创建了一个使用 NGB 的 Angular 模块,那么您也需要将 NGB 导入其中。将 NGB 导入其他模块的语法与刚刚概述的导入到根模块中的语法完全相同,但是将 NGB 模块列为模块的@NgModule装饰器的参数的语法略有不同。它只是在导入数组中列出为NgbModule,而不是NgbModule.forRoot(),因为我们必须在根模块中列出它。

那么,我们要如何查看一些组件,而不会无意中搞乱我们示例应用程序的 NGB 部分呢?只有一种方法——我们要确保我们不直接或间接地将 jQuery 或 Popper.js 加载到我们的示例应用程序中,不使用 Bootstrap 组件(确保您理解 Bootstrap 和 NGB 是两个不同的库)。

让我快速澄清一些事情。我们的示例应用程序中安装了 jQuery 和 Popper.js,您可以通过查看我们的package.json文件来验证这一点。在其中,您将在依赖项部分看到 jQuery 和 Popper.js 的条目。我们不打算卸载这些库。只要我们不通过同时使用 Bootstrap 来加载它们,它们对我们使用 NGB 是无害的。换句话说,NGB 组件和 Bootstrap 组件不应共存于我们的 Angular 应用程序中。我们可以使用其中一个而不会出现问题,但绝不能同时使用两者。这样清楚吗?好的。

如果您尝试从项目中删除 jQuery 和/或 Popper.js,每当运行项目时,您可能会收到几个编译警告。虽然警告可能不会阻止项目运行,但始终努力实现干净的构建。

确保获得干净的构建有时可能会很麻烦,因为您需要注意库的版本。接下来的代码清单是我的package.json文件。当我运行npm install然后npm start时,我一直能够获得干净的安装编译。如果您没有获得干净的编译,您可能想要将您的package.json与我的进行比较,如下所示:

{
  "name": "listcaro",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {
    "ng": "ng",
    "start": "ng serve -o",
    "build": "ng build --prod",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "⁶.0.4",
    "@angular/cdk": "⁶.2.1",
    "@angular/common": "⁶.0.4",
    "@angular/compiler": "⁶.0.4",
    "@angular/core": "⁶.0.4",
    "@angular/flex-layout": "⁶.0.0-beta.16",
    "@angular/forms": "⁶.0.4",
    "@angular/http": "⁶.0.4",
    "@angular/platform-browser": "⁶.0.4",
    "@angular/platform-browser-dynamic": "⁶.0.4",
    "@angular/router": "⁶.0.4",
    "@angular/material": "⁶.2.1",
    "@ng-bootstrap/ng-bootstrap": "².1.0",
    "bootstrap": "⁴.0.0",
    "core-js": "².4.1",
    "jquery": "³.3.1",
    "npm": "⁶.1.0",
    "popper": "¹.0.1",
    "popper.js": "¹.14.3",
    "rxjs": "⁶.0.0",
    "save": "².3.2",
    "zone.js": "⁰.8.26"
  },
  "devDependencies": {
    "typescript": "2.7.2",
    "@angular/cli": "~1.7.4",
    "@angular/compiler-cli": "⁶.0.4",
    "@angular/language-service": "⁵.2.0",
    "@types/jasmine": "~2.8.3",
    "@types/jasminewd2": "~2.0.2",
    "@types/node": "~6.0.60",
    "codelyzer": "⁴.0.1",
    "jasmine-core": "~2.8.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~2.0.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "¹.2.1",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "⁰.2.2",
    "protractor": "~5.1.2",
    "ts-node": "~4.1.0",
    "tslint": "~5.9.1"
  }
}

您可以查看可用的 Angular 模块列表及其最新版本号,您可以使用npm安装,网址是:www.npmjs.com/~angular

为什么使用 NGB?

由于无法使用基于 JavaScript 的组件,也无法直接使用 JavaScript 库(如 jQuery 或 Popper.js),您可能会问,为什么要使用 NGB

这是一个很好的问题。以下是简短的答案,以要点形式:

  • Angular 不依赖于 jQuery。它使用自己的 jQuery 实现,称为 jQLite,这是 jQuery 的子集。

  • 我们不会失去使用任何由 JavaScript 驱动的 Bootstrap 组件的能力(例如模态框或轮播),因为它们在 NGB 中已经重新设计为 Angular。再次强调,NGB 的唯一目的是完全替代任何由 JavaScript 驱动的 Bootstrap 组件。

  • 在构建 Angular 应用程序时的一个经验法则是尽量只使用特定于 Angular 的组件;也就是说,专门为 Angular 制作的组件,比如 NGB 小部件和来自 Angular Material 的组件。当然,这包括创建自定义的 Angular 组件。虽然你可以通过折衷使用非特定于 Angular 的组件来解决问题,但这并不推荐。Angular 功能齐全,正如我们所学到的,它也非常可扩展。很难想象有哪种情况下坚持使用特定于 Angular 的组件、模块、指令、管道、服务等会阻止你做你需要做的事情。

  • NGB 是一个坚实的 Angular 中心组件库,在你不尝试创建被不鼓励的变通方法时运行良好。

为 NGB(和 Angular Material 等)创建我们的游乐场

NGB 只有两个依赖项(Angular 和 Bootstrap CSS),幸运的是,我们的示例应用程序已经有了这两个东西——一个是默认的(因为我们的示例应用程序是一个 Angular 应用程序),另一个是在第三章中安装 Bootstrap 时安装的。然而,我们将向我们的示例应用程序添加一些内容,以便我们可以尝试使用 NGB 组件——一个游乐场视图。

在构建任何技术堆栈的 Web 应用程序时,我长期以来的传统做法,不仅适用于 Angular 应用程序,是添加一个页面作为我可以在当前构建的应用程序的上下文中尝试各种东西的地方。我把它称为游乐场。在我们的情况下,我们的游乐场将是一个组件,其模板将作为我们探索一些 NGB 组件时的实验画布。我们还将把它连接到我们的菜单,以便我们可以轻松访问它。

在本书的其余部分,我们将保留我们的游乐场视图,只会在第十五章中删除它,部署 Angular 应用程序,在那里我们将学习如何部署我们的应用程序,并不希望我们的游乐场随之而去。

所以,现在让我们这样做。自从我们在第四章中创建的示例应用程序中添加组件以来已经过了一段时间,因此我想借此机会列举出使用 playground 作为示例的步骤(在接下来的各自部分中)。请注意,这是手动向我们的项目添加组件的方式,与几章前使用 CLI 为我们添加的方式不同。

创建 playground 目录

我们需要做的第一件事是创建一个目录,用于保存我们 playground 组件所需的文件。我们的每个组件都有自己的目录,并且都是app目录的子目录,而app目录本身是项目根目录中src目录的子目录。

由于我们正在添加一个新组件,我们将遵循我们的惯例并为其创建一个目录。在您的 IDE 中,右键单击app目录,选择“新建文件夹”,输入playground作为名称,这遵循了我们迄今为止使用的惯例。完成后,我们将有一个地方来插入将共同组成我们组件的文件。

创建 playground 组件类

现在我们需要创建我们的 playground 组件类。在您的 IDE 中,右键单击新创建的playground目录,然后选择“新建文件”,输入playground.component.ts作为名称。playground.component.ts文件是我们的component类。在此文件中输入以下代码:

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

@Component({
    selector: 'playground',
    templateUrl: './playground.component.html',
    styleUrls: ['./playground.component.css']
})
export class PlaygroundComponent implements OnInit {

    constructor() { }

    ngOnInit() { }

    pageTitle: string = "Playground";

}

通过查看我们的 playgroundComponent类文件,您会注意到一些事情:

  • 除了从@angular/core模块中导入组件之外,我们还导入了OnInit。这是因为我们给自己一个设置一些变量的地方,如果需要的话,比如用于传递任何子组件。

  • 我们已经为我们的类包含了一个构造函数。无论我们是否使用它,它都为我们提供了一种机制,可以在组件的生命周期中触发一些代码。我们现在不会使用它,但我想向您展示,我们的“组件”函数就像传统的面向对象类一样,因此具有我们可以利用的构造函数。

  • 我们已经设置了组件以使用外部文件作为其模板和样式,而不是内联。因此,下一步是创建这两个文件(请参见以下两个部分)。

  • 我们在类中声明了一个属性(即pageTitle),类型为字符串,并将我们视图的名称分配给它。在下一节中,我们的模板将使用单向绑定语法显示此属性。

创建游乐场模板文件

我们现在需要为我们的游乐场组件创建模板文件,这将是我们组件的视觉界面。在您的 IDE 中,右键单击playground目录,选择新建文件,输入playground.component.htmlplayground.component.html文件是必需的,因为我们已将其作为参数传递给了我们的组件装饰器。在此文件中输入以下代码:

<h3> 
{{ pageTitle }} </h3> <hr>  

目前这个文件中还没有太多内容,但这将是我们添加 NGB 组件以便进行实验的地方。当然,实验是学习任何对您来说可能是新的技术的最佳方式。目前我们的模板只是通过绑定到我们类的pageTitle属性来显示我们的页面名称。

创建游乐场 CSS 文件

我们需要为游乐场组件创建的最后一个文件是用来存放其样式的文件。在您的 IDE 中,右键单击playground目录,选择新建文件,输入playground.component.css作为名称。playground.component.css文件也是必需的,因为我们已将其作为参数传递给了我们的组件装饰器。在此文件中输入以下代码:

/* Nothing here yet. This is a placeholder file that we may use later. */

前面的代码是不言自明的。目前这个文件中还没有任何样式,但为您创建的每个组件至少创建一个 CSS 文件是个好主意。

创建游乐场菜单项

好的。因此,按照前面部分的说明,您现在应该有一个游乐场组件,可以用作几乎任何实验的沙盒。在我们的特定情况下,我们将使用它来实验 NGB 小部件(即组件),但我们还将在第九章 使用 Angular Material期间使用这个沙盒。

在我们继续插入第一个 NGB 小部件之前,我们将会看一下。为我们的游乐场视图创建一个临时菜单链接是个好主意,这样我们就可以很容易地从应用程序内部访问它。现在让我们来做这个。

在您的 IDE 中,打开app.component.html文件。这是在启动过程中为您的 Angular 应用程序加载的主要或起始模板。这也是我们在《第四章》《路由》中创建菜单的地方。在这个文件中,在清单菜单项之后插入以下代码:

<li routerLinkActive="active" class="nav-item"> 
  <a routerLink="playground" class="nav-link">Playground</a> 
</li>

这个小的 HTML 代码片段所做的只是在我们的菜单中添加一个playground导航链接,并指示 Angular 的路由系统在点击时加载游乐场组件(因此加载游乐场模板,然后递归加载任何子组件)。

好的,很好,我们现在已经设置好,准备好看我们的第一个 NGB 小部件了。

NGB 小部件

如前所述,NGB 小部件是第三方 Angular 组件,旨在取代基于 JavaScript 的 Bootstrap CSS 组件。NGB 有许多小部件可用,但在接下来的章节中,我们只会看到其中的三个。

您可以在以下网址找到完整的 NGB 小部件列表以及它们的文档:ng-bootstrap.github.io/#/components/

折叠

折叠组件是一个有用的东西,可以节省屏幕空间。我们使用这个组件的用例是切换说明的显示或隐藏。当其父组件的模板被渲染时,组件的状态最初将被折叠,但用户可以根据需要切换说明的显示和重新折叠它们。

让我们在代码中看一个快速示例,我们可以在我们的游乐场中尝试,在这个示例中,我们可以切换页面上的一部分内容的显示和隐藏,这部分内容将是假设的说明(目前)。

我们需要修改三个文件才能使其工作。其他 NGB 组件的使用(甚至是我们将在下一章中看到的 Angular Material 组件)工作方式类似,因此我将花时间在每个代码清单后解释事情,因为这是我们一起看的第一个第三方组件。在以后看类似的组件时,如果它们与这些组件有实质性的不同,我会给出解释。

我们的父组件

在本章以及《第八章》《使用 NG Bootstrap》中,我们的父组件将始终是我们的游乐场组件。

修改你的 playground 组件模板(即playground.component.html文件),使其看起来如下:

<h3> 
  {{ pageTitle }} 
</h3> 
<hr> 

<ngb-collapse></ngb-collapse>

<br />

This is our page's main content

我们在 playground 模板中唯一添加的新内容是<ngb-collapse></ngb-collapse>,这是我们的自定义指令,将指示 Angular 在那里插入我们子组件的模板。ngb-collapse是我们组件类元数据中的选择器(即我们传递给组件装饰器的对象)。接下来让我们来看看那个文件。

我们的 NGB 折叠组件类

我们已经命名了我们的组件类(利用了 NGB 的collapse组件)NgbCollapseComponent—但这段代码在哪里呢?好吧,我们需要创建一个新目录,并在该目录中创建两个新文件,就像我们创建 playground 组件时所做的那样。是的—我们为我们的 playground 组件创建了三个文件,但是对于NgbCollapseComponent,我们将跳过 CSS 文件。

首先,创建一个名为ngb-collapse的目录。在这个新目录中,创建一个名为ngb-collapse.component.ts的文件,并在其中添加以下代码:

import { Component } from '@angular/core';

@Component({
  selector: 'ngb-collapse',
  templateUrl: './ngb-collapse.component.html'
})
export class NgbCollapseComponent {
  public isCollapsed = true;
}

正如你所看到的,我们没有定义styleUrls数组,这就是为什么我们不需要为它创建一个文件(如果我们想要给这个组件添加样式,我们会命名为ngb-collapse.component.css)。为了实验 NBG 折叠组件,我们只关心创建一个组件类文件和它的模板文件。

我们在组件类文件中感兴趣的另一件事是isCollapsed属性。当然,我们可以随意命名它,但重要的是它被声明并且最初设置为true。我们将通过将其值绑定到模板文件中的ngbCollapse属性来使用这个属性。这样做将导致我们组件模板的一部分被折叠(隐藏)或展开(显示)。请注意,我强调了我们组件中的目标内容将被隐藏或显示,而不是被添加或从 DOM 中移除。如果我们的内容被隐藏(即不可见),它仍然存在于 DOM 中。这是因为 NGB 折叠小部件不作为结构指令。它通过属性绑定实现其隐藏/显示功能。

现在让我们来看第三个文件,我们的组件模板

NgbCollapseComponent类。

我们的 NGB 折叠组件模板

ngb-collapse目录中创建另一个文件,命名为ngb-collapse.component.ts,并在其中添加以下代码:

<p> 
    <button type="button" class="btn btn-outline-primary" (click)="isCollapsed = !isCollapsed"> 
        {{ isCollapsed ? 'Show' : 'Hide' }} Instructions 
    </button> 
</p> 
<div id="collapseExample" [ngbCollapse]="isCollapsed"> 
    <div class="card">
        <div class="card-body">
            These are the hypothetical instructions for something.
        </div>
    </div>
</div>

让我们一起看一下这段代码。我们感兴趣的第一件事是将click事件绑定到表达式上,这个表达式基本上在我们的组件类中定义的isCollapsed变量之间切换truefalse

(click)="isCollapsed = !isCollapsed"  

我们的切换按钮的文本始终设置为两个值中的一个。当显示说明时,按钮文本为“隐藏说明”。当说明被隐藏时,按钮文本为“显示说明”。这当然是我们想要的行为,但乍一看,你可能会认为需要一个if .. else结构才能使其全部工作。令人惊讶的是,多亏了 Angular 的插值模板语法,只需要很少的代码就可以根据我们的isCollapsed变量的值来改变按钮的文本。让我们花点时间来看一下负责确定按钮文本应该是什么的小代码片段,以及它是如何为我们呈现的:

{{ isCollapsed ? 'Show' : 'Hide' }} Instructions

在第七章中,模板、指令和管道,我们看了一下我们可以在模板语法中使用的所有符号,比如插值、双向绑定等等。在这种情况下,为我们工作的符号是插值符号(即一对双大括号)。我之所以称它为神奇,是因为它不仅可以用作字符串插值,而且还足够聪明,可以处理表达式甚至函数调用。因此,我们不仅仅局限于将变量名视为简单的字符串插值。

为了确定我们的按钮文本应该是什么,我们使用 JavaScript 的三元运算符语法根据我们的isCollapsed变量的值渲染(或插值)文本为两个值中的一个,显示或隐藏。当然,无论布尔值是什么,说明文本都将始终被呈现,从而使按钮文本成为“显示说明”或“隐藏说明”。这一切都是简洁而内联完成的。相当酷,不是吗?

导入和声明

如果你尝试运行项目,你会得到一些错误。这是因为我们还没有在app.module.ts文件中为这个组件设置导入和声明。让我们现在来做这个。

在我们为我们的游乐场组件添加的导入行之后添加这个导入行:

import { NgbCollapseComponent } from './ngb-collapse/ngb-collapse.component';

并将NgbCollapseComponent添加到声明数组中。

通过在app.module.ts文件的声明数组中导入前述导入并将我们的组件类添加到其中,我们的项目应该可以构建和运行得很好。

干得好。现在让我们继续进行我们的模态组件。

模态

模态对话框窗口自从桌面 Windows 操作系统的早期时代(互联网之前)就存在了,并且在网站上也变得很受欢迎——特别是自从 jQuery 出现以来。模态窗口用于与用户进行交互,通常是为了从他们那里获取信息。此外,它们通过调暗背景以及禁用模态区域外的任何交互来帮助设计师将用户的注意力集中在应该的地方。我们使用模态窗口的一个用例是显示登录表单。

让我们看一个在我们的播放中可以尝试的快速示例代码,以显示一个模态窗口。由于 NGB 小部件的集成都遵循相同的模式,我不会像折叠 NGB 小部件那样详细介绍它,但我会指出重要的地方。

我们所有的组件都以相同的方式开始。我们需要为我们的组件创建一个文件夹(让我们将其命名为ngb-modal),并且我们需要创建我们的两个文件——一个用于我们的组件类,另一个用于我们的组件模板。让我们分别将它们命名为ngb-modal.component.tsngb-modal.component.html

接下来的部分是我们的 NGB 模态组件的两个代码清单,然后是必要的导入和声明,就像我们为折叠组件所做的那样。

我们的 NGB 模态组件类

在我们的组件类中,我们首先从适当的模块中导入必要的类,然后我们使用@Component装饰器装饰我们的类,这样我们就可以将其链接到模板并设置我们的选择器(即,我们将添加到我们的播放模板中的自定义 HTML 标记)。

接下来,我们添加一个构造函数,这样我们就可以注入NgbModal服务(注意:我们将在第十二章中介绍依赖注入,集成后端数据服务)。

我们的类有一个名为closeResult的变量,它由私有方法getDismissReason填充,描述了用户如何关闭模态对话框。

我们还有一个open方法,负责使模态对话框渲染。正如我们将在下一节的代码清单中看到的(在我们的组件模板中),open方法是由我们的游乐场内的按钮点击触发的。

您会注意到 open 方法接受一个参数(在本例中命名为content)。我们组件的模板将要在模态对话框中显示的内容包裹在它的ng-template标签中,正如您将看到的,这些标签与#content模板变量相关联。如果您还记得第七章中的内容,模板、指令和管道,模板语法中的井号(即#)用于表示一个变量:

import {Component} from '@angular/core';
import {NgbModal, ModalDismissReasons} from '@ng-bootstrap/ng-bootstrap';

@Component({
  selector: 'ngb-test-modal',
  templateUrl: './ngb-modal.component.html'
})
export class NgbModalComponent {
  closeResult: string;

  constructor(private modalService: NgbModal) {}

  open(content) {
    this.modalService.open(content).result.then((result) => {
    this.closeResult = `Closed with: ${result}`;
  }, (reason) => {
    this.closeResult = `Dismissed ${this.getDismissReason(reason)}`;
  });
}

  private getDismissReason(reason: any): string {
    if (reason === ModalDismissReasons.ESC) {
      return 'by pressing ESC';
    } else if (reason === ModalDismissReasons.BACKDROP_CLICK) {
      return 'by clicking on a backdrop';
    } else {
      return `with: ${reason}`;
    }
  } 
} 

现在让我们来看看我们的组件模板,ngb-modal.component.html

我们的 NGB 模态组件模板

我们的组件模板不仅负责为模态对话框中显示的内容提供视图,还将为我们提供用户将使用的视觉元素(在本例中为按钮)来触发模态对话框。

以下 HTML 代码是我们的组件模板,稍后我们将用于我们的登录表单(注意:我们将在第十章中涵盖表单,使用表单):

<ng-template #content let-c="close" let-d="dismiss">
  <div class="modal-header">
    <h4 class="modal-title">Log In</h4>
    <button type="button" class="close" aria-label="Close" (click)="d('Cross click')">
    <span aria-hidden="true">&times;</span>
    </button>
  </div>
  <div class="modal-body">
    <form>
      <div class="form-group">
        <input id="username" class="form-control" placeholder="username" >
        <br>
        <input id="password" type="password" class="form-control" placeholder="password" >
      </div>
    </form>
  </div>
  <div class="modal-footer">
    <button type="button" class="btn btn-outline-dark" (click)="c('Save click')">submit</button>
  </div>
</ng-template>

<button class="btn btn-lg btn-outline-primary" (click)="open(content)">Launch test modal</button>

既然我们已经有了我们的组件类和组件模板,我们必须告诉我们应用程序的根模块关于它们——我们将在下一节中做到这一点。

导入和声明

就像我们的折叠组件一样,如果您在这一点上尝试运行项目,您会得到一些错误——出于同样的原因——因为我们还没有在app.module.ts文件中为这个组件设置导入和声明。你知道该怎么做。

在我们为游乐场和折叠组件添加的导入行之后,添加这个导入行:

import { NgbModalComponent } from './ngb-modal/ngb-modal.component';

并将NgbModalComponent添加到声明数组中。

我知道你已经掌握了这个。让我们通过将另一个 NGB 小部件集成到我们的游乐场视图中来进行更多练习——作为奖励,我们将预览一下 Angular 的HttpClient模块。我们将使用HttpClient模块来获取我们轮播图的图片,并且我们还将在第十一章中使用HttpClient模块来调用我们的 API,依赖注入和服务

所以让我们伸展双腿和双臂,用咖啡杯装满咖啡,然后继续前进到更有趣的组件之一(也将是我们示例应用程序的焦点),NGB 轮播。

轮播

轮播组件最显著的特点是作为一种工具(即小部件或组件)来按照预定顺序显示一系列图像,就像翻阅相册一样。我们的用例将会是这样:让用户有能力翻阅物业的照片。

让我们看一个快速的示例代码,我们可以在我们的游乐场中尝试显示三张图片。我们将从组件类开始,然后转到组件模板。这些代码清单直接来自 NGB 网站上的轮播示例,网址为:ng-bootstrap.github.io/#/components/carousel/examples

我将把类的连接,使用import语句等等留给你作为练习。提示:这与我们之前在游乐场中添加折叠和模态组件时涵盖的过程完全相同(在它们各自的导入和声明部分)。然而,我会在每个代码清单后提到一些事情。

我们的 NGB 轮播组件类

在这一部分,我们将实现ngb-carousel组件类。以下是更新后的组件类。我们将稍后分析代码:

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';

@Component({
  selector: 'ngb-test-carousel', 
  templateUrl: './ngb-carousel.component.html',
  styles: [`
    .carousel {
      width: 500px;
    }
 `]
})
export class NgbCarouselComponent implements OnInit {
  images: Array<string>;

  constructor(private _http: HttpClient) {}

  ngOnInit() {
    this._http.get('https://picsum.photos/list')
    .pipe(map((images: Array<{id: number}>) => this._randomImageUrls(images)))
    .subscribe(images => this.images = images);
  }

  private _randomImageUrls(images: Array<{id: number}>): Array<string> {
    return [1, 2, 3].map(() => {
      const randomId = images[Math.floor(Math.random() * images.length)].id;
      return `https://picsum.photos/900/500?image=${randomId}`;
    });
  }
}

在我们的组件类ngb-carousel.component.ts中有一些事情正在进行。我们从 Angular 的http模块中导入HttpClient类,还从rxjs/operators模块中导入map类。HttpClient类将在第十一章中更仔细地讨论,依赖注入和服务,用于从picsum.photos获取图像对象的 JSON 列表,这是一个免费服务,提供占位图像,就像他们的网站所说的那样,照片的 Lorem Ipsum。map类用于将从HttpClientGET请求返回的许多图像对象中随机映射三个到我们的字符串数组变量images

从 API 中获取图像对象发生在我们的组件初始化时,因为GET请求发生在ngOnInit()组件的生命周期钩子内。

我们的 NGB 轮播组件模板

在本节中,我们将实现我们的ngb-carousel组件模板文件:

<ngb-carousel *ngIf="images" class="carousel">
  <ng-template ngbSlide>
    <img [src]="images[0]" alt="Random first slide">
    <div class="carousel-caption">
      <h3>First slide label</h3>
      <p>Nulla vitae elit libero, a pharetra augue mollis interdum.</p>
    </div>
  </ng-template>
  <ng-template ngbSlide>
    <img [src]="images[1]" alt="Random second slide">
    <div class="carousel-caption">
      <h3>Second slide label</h3>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
    </div>
  </ng-template>
  <ng-template ngbSlide>
    <img [src]="images[2]" alt="Random third slide">
    <div class="carousel-caption">
      <h3>Third slide label</h3>
      <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur.</p>
    </div>
  </ng-template>
</ngb-carousel>

这个模板很简单。除了img HTML 元素的src属性之外,其他都是硬编码的。在 HTML img src属性周围使用方括号是属性绑定的一个例子(正如我们在第七章中学到的,模板、指令和管道)。在这种情况下,轮播中的图片数量已知为三张。在实践中,就像我们在示例应用程序中所做的那样,模板通常会使用*ngFor结构指令来迭代长度可变的项目数组。

通过几个示例了解了如何将 NGB 小部件集成到我们的 playground 中后,现在我们可以在我们的应用程序中实现它们。

将 NGB 集成到我们的示例应用程序中

在前面的NGBwidgets部分,我们介绍了一些 NGB 中可用的组件。当然,你现在知道为什么我不会介绍所有可用的组件了,对吧?如果你说,“是的 Aki,我知道为什么。如果你介绍了所有的组件,基本上就是在重复已经可以在其他地方找到的文档”,那么你是正确的!介绍 16 个组件中的 3 个就足够了,几乎占了 19%(这几乎等同于每五页文档中重复一次!)。

但还有另一个原因。我们只打算实现我们介绍过的三个 NGB 组件中的两个,即模态组件和轮播组件,所以没有必要介绍太多其他的内容。好的,让我们继续把我们新学到的知识付诸实践。

我们在前面的部分学习了如何实现模态、轮播和折叠组件。我们为每个组件创建了选择器。对于模态组件,我们创建了一个名为ngb-test-modal的选择器;对于轮播组件,我们创建了一个名为ngb-test-carousel的选择器;最后,对于折叠组件,我们创建了一个名为ngb-collapse的选择器。现在我们需要在playground.component.html文件中使用这些选择器,以便小部件在页面上可见。

以下是 playground 组件模板文件的更新代码:

<p>
 {{pageTitle}} </p> <app-ngb-collapse></app-ngb-collapse> <app-ngb-modal></app-ngb-modal> <app-ngb-carousel></app-ngb-carousel>

我们使用了每个组件的选择器添加了指令。在命令行中使用ng serve命令运行应用程序,我们应该能看到输出,如下面的截图所示:

我们的应用程序已经集成了小部件,但我们肯定可以在设计上做得更好。在接下来的几节中,我们将学习一些设计原则和最佳实践,这些将在接下来的章节中实施。

UX 设计原则

几乎所有事情都有经验法则,网页设计也不例外。在网页设计中有应该做和不应该做的事情,既然我们现在真的开始深入研究我们的模板,现在是回顾一些这些设计原则的好时机。

可能有几十个设计原则,但我不是一个专家,所以最好是去找一本专注于 UX 和 GUI/界面设计的好书(我知道 Packt 有一些相关的书)。然而,由于我们正在构建一个应用程序,我们的应用程序由几个组件组成,如果我不介绍这三个基本的设计原则,那就不够周全。

我们不仅将在接下来的三个小节中涵盖它们,而且在构建示例应用程序的模板时,我们将遵守它们。我们之所以有 UX 设计原则之类的东西,归根结底就是一件事——我们希望用户能够快乐!

保持简洁

UX 准则 #1:保持简洁。

没有什么比过于繁忙(即混乱)的用户界面更容易让用户头疼。你可能听说过“少即是多”的表达方式,这个表达方式当然也适用于 UX 设计。

人们觉得自己没有时间做任何事情——如果做某事让他们觉得他们在浪费他们宝贵的资源(即时间),他们会比你数到 10 更快地变得不快乐。这如何与第一个 UX 设计原则相关?如果你的页面上有很多东西要看,他们不知道从哪里开始看——如果他们不能很快理解他们所看到的东西,那么你猜对了:他们会变得不快乐。

混乱几乎从来都不是一件好事。想想你的卧室或厨房。当它整洁,每样东西都有一个地方和目的,你可以轻松快速地找到你要找的东西时,你会更快乐吗?还是当你浪费 5 分钟找那个铲子来做早餐,而你几乎没有时间吃时,你会更快乐?答案,我希望是显而易见的。访问网站的用户也是这样想的。

保持功能性

UX 准则 #2:保持功能性。

这个 UX 原则与第一个原则相关,因为它与说我们视图上的几乎所有东西都应该有一个功能是一样的。在屏幕上有成千上万个毫无意义的东西的日子已经过去了。你还记得上世纪 90 年代网站的样子吗?Flash 风靡一时。网页看起来像雪球,或者有着大大的跳动的动画按钮,上面写着“立即点击这里”。这些都不再被容忍。如果你的网页上有这样的东西,很有可能你的访客会尽可能快地离开你的网站。如果屏幕上有东西,它最好有一个目的。

如果你想看一个极端的例子,一个网站关注第一和第二(以及即将到来的第三)UX 设计原则,只需看一下谷歌的主页:www.google.com/

保持明显

UX 原则 #3:保持明显。

没有什么比强迫用户使用大量的脑力、时间和侦探技能来找出他们需要做什么,或者如何在网页应用程序中执行他们想要执行的特定任务更让用户沮丧的了。

您的网页应用程序的用户之所以成为用户,是因为他们需要一个工具来完成某些事情。无论他们想要完成的任务是为了快乐还是工作,都无关紧要。无论他们想要完成什么,他们都不想花费比合理时间更多的时间。如果他们需要花费太多时间来弄清楚事情,猜猜看?是的!他们会变得不快乐!

这第三个 UX 设计原则可能是最难坚持的,但作为应用程序构建者,我们有责任给予它应有的关注。

总结

在本章中,我们探讨了 NG Bootstrap——两个免费提供给我们在 Angular 应用程序中使用的第三方组件库中的第一个。我们将在下一章中探讨第二个,Angular Material。

我们学习了如何安装 NGB,然后在应用程序中创建了一个游乐场,这样我们就有了一个可以玩耍(即实验)这些第三方组件的地方,包括临时通过路由将游乐场与菜单连接起来,以便轻松访问我们的游乐场。虽然我们本可以在集成这些组件到应用程序的预期用途之前创建一个完全独立的项目来玩耍,但通常更方便的是在现有基础设施中创建一个游乐场。当然,当我们部署应用程序时,我们可以轻松地删除游乐场和菜单选项及其相应的路由。

设置好我们的游乐场后,我们开始学习如何集成 NGB 的三个小部件:折叠、模态和轮播。

最后,为了结束本章,因为我们现在处于书中的组件和布局部分(而不是后端数据集成和服务部分),现在是一个很好的时机来介绍一些设计原则。因此,我们简要介绍了三个良好设计的主要原则:保持清晰、功能性和明显性。在本书的其余部分,我们将尽力遵守这些设计原则。

现在,戴上你的组件帽子,翻开书页,让我们来看看 Angular 团队为我们设计的华丽组件。合理地利用 Angular Material 组件,可以提高我们示例应用的可用性和美观度。幸运的是,Angular Material 与 Bootstrap 兼容良好,因此在同一个 Angular 项目中同时使用这两个库并不成问题。

第九章:使用 Angular Material

欢迎来到关于 Angular Material 的章节。我必须说,我印象深刻。统计数据显示,购买技术书籍的大多数人并没有读很远。您已经完成了大部分书籍——干得好,Angular 绝地!

这将是一个简短的章节,原因有几个。首先,这本书主要用于构建应用程序,主要使用 Angular 和 Bootstrap。因此,可以将这一章视为我们的额外奖励。另一个原因是,这一章仅旨在介绍在使用 Angular 时与 Bootstrap 一起使用的另一种用户界面(UI)组件库。应该有一本单独的关于 Angular Material 的书,但这一章将在展示库提供的功能和组件方面涵盖很多内容。

我们将了解导航和菜单组件、布局组件、表单字段元素、按钮、对话框和弹出组件,以及许多有趣的元素,您肯定会喜欢,并可能考虑在下一个项目的框架中使用。

总结一下,本章将涵盖的主题有:

  • 什么是 Angular Material?

  • 安装 Angular Material

  • 组件的类别

好的,让我们直接开始,从描述 Angular Material 开始。

什么是 Angular Material?

Angular Material 是一个丰富的组件集合,可以轻松地插入到 Angular 应用程序中,并且也适用于 Web、移动和桌面应用程序。Material Design 来自谷歌,是 Angular 的制造商,这意味着对组件以及将来推出的新组件进行了大量的本地支持、优化和性能调整。以下列表显示了在我们的应用程序中使用 Material Design 时我们可以获得的一些好处:

  • UI 组件可以立即使用,无需额外的开发工作

  • 我们可以选择性地选择单独使用组件,而不是被迫一次性导入所有模块

  • 组件的渲染非常快

  • 通过双向或单向数据绑定功能,可以轻松地将数据插入组件中,这是 Angular 的一个非常强大的功能

  • 组件在 Web、移动和桌面应用程序中具有相同的外观、感觉和行为,这解决了许多跨浏览器和跨设备的问题

  • 性能经过调整和优化,以便与 Angular 应用程序集成

您可以在官方网站material.angular.com上找到有关 Angular Material 的所有必要文档。

在本章中继续之前,让我们快速生成一个应用程序,在这个应用程序中我们将实现所有的 Angular Material 组件。运行以下ng命令以生成一个名为AngularMaterial的新应用程序:

ng new AngularMaterial

一旦命令成功执行,我们应该看到以下截图中显示的输出:

现在我们的应用程序已经生成,让我们学习如何在项目中安装 Angular Material 库。

安装 Angular Material

到目前为止,您一定有一种强烈的直觉,即当我们想在 Angular 应用程序中安装任何东西时,我们有一个强大的命令行界面CLI)工具。我们将继续使用相同的 CLI,并借助npm来安装 Angular Material。

您也可以选择通过 YARN 命令安装 Angular Material—不同的打包系统,同样的结果。

Angular Material 有一个核心依赖和先决条件,需要安装两个包—CDK 和 Animations。所以,让我们先安装这些,然后再安装 Angular Material:

npm i @angular/cdk --save

npm i @angular/animations --save

npm i @angular/material --save

成功运行上述命令后,我们应该看到以下截图中显示的输出:

打开package.json文件;我们应该看到已安装的包,以及它们旁边列出的相应版本号。如果你看到我们最近安装的三个包,那就意味着我们准备好开始使用 Angular Material 创建一些很棒的 UI 界面了。

一旦我们安装了 Angular Material,我们将需要将所有必需的模块导入到我们的app.module.ts文件中。Material 提供了许多模块,每个模块都有特定的目的。例如,如果我们打算使用 Material 卡片,我们将需要导入MatCardModule。同样,如果我们想在应用程序中使用 Material 芯片,我们需要导入MatChipsModule。虽然我们可以在AppModule中确实只导入所需的模块,但在大多数使用 Material UI 的应用程序中,我们将需要所有模块。现在,让我们快速学习如何一次性导入所有模块。我们可以将所有模块导入到一个通用模块中,然后在app.module.ts文件中使用新创建的通用模块。首先,在我们的项目结构中创建一个文件,并将其命名为material-module.ts,然后我们可以添加以下代码以一次性导入所有模块到这个文件中:

import  {A11yModule}  from  '@angular/cdk/a11y'; import  {DragDropModule}  from  '@angular/cdk/drag-drop'; import  {ScrollingModule}  from  '@angular/cdk/scrolling'; import  {CdkStepperModule}  from  '@angular/cdk/stepper'; import  {CdkTableModule}  from  '@angular/cdk/table'; import  {CdkTreeModule}  from  '@angular/cdk/tree'; import  {NgModule}  from  '@angular/core'; import  {
  MatAutocompleteModule,
  MatBadgeModule,
  MatBottomSheetModule,
  MatButtonModule,
  MatButtonToggleModule,
  MatCardModule,
  MatCheckboxModule,
  MatChipsModule,
  MatDatepickerModule,
  MatDialogModule,
  MatDividerModule,
  MatExpansionModule,
  MatGridListModule,
  MatIconModule,
  MatInputModule,
  MatListModule,
  MatMenuModule,
  MatNativeDateModule,
  MatPaginatorModule,
  MatProgressBarModule,
  MatProgressSpinnerModule,
  MatRadioModule,
  MatRippleModule,
  MatSelectModule,
  MatSidenavModule,
  MatSliderModule,
  MatSlideToggleModule,
  MatSnackBarModule,
  MatSortModule,
  MatStepperModule,
  MatTableModule,
  MatTabsModule,
  MatToolbarModule,
  MatTooltipModule,
  MatTreeModule, }  from  '@angular/material'; @NgModule({
 exports:  [
  A11yModule,
  CdkStepperModule,
  CdkTableModule,
  CdkTreeModule,
  DragDropModule,
  MatAutocompleteModule,
  MatBadgeModule,
  MatBottomSheetModule,
  MatButtonModule,
  MatButtonToggleModule,
  MatCardModule,
  MatCheckboxModule,
  MatChipsModule,
  MatStepperModule,
  MatDatepickerModule,
  MatDialogModule,
  MatDividerModule,
  MatExpansionModule,
  MatGridListModule,  MatIconModule,
  MatInputModule,
 MatListModule,
 MatMenuModule,
 MatNativeDateModule,
 MatPaginatorModule,
 MatProgressBarModule,
 MatProgressSpinnerModule,
 MatRadioModule,
 MatRippleModule,
 MatSelectModule,
 MatSidenavModule,
 MatSliderModule,
 MatSlideToggleModule,
 MatSnackBarModule,
 MatSortModule,
 MatTableModule,
 MatTabsModule,
 MatToolbarModule,
 MatTooltipModule,
 MatTreeModule,
 ScrollingModule, ] }) export  class  MaterialModule  {}

在上述代码中,我们将所有必需的模块导入到文件中。暂时不要担心对先前列出的模块进行分类。当我们学习 Material 提供的组件时,我们会了解这些模块。下一步非常明显——我们需要将这个新创建的模块导入到我们的app.module.ts文件中:

import  {MaterialModule}  from  './material-module';

一旦我们导入了模块,不要忘记将其添加到AppModule的导入中。就这样。我们已经准备好开始学习和实现由 Angular Material 提供的组件了。

你知道吗?谷歌还发布了一个轻量级的基于 CSS 和 JavaScript 的 Lite 库,Material Design Lite,它开始使用组件的方式与任何其他 UI 库一样。然而,可能有一些组件不具有完全支持。在getmdl.io/了解更多信息。

让我们立即开始学习 Angular Material 的组件。

组件类别

作为前端开发人员,你可能已经使用了许多 UI 组件,甚至更好的是,你可能在过去的项目中创建了自己的自定义组件。正如前面提到的,Angular Material 提供了许多组件,可以在我们的应用程序中方便地使用。Angular Material 提供的 UI 组件可以归类为以下类别:

  • 布局

  • 材料卡片

  • 表单控件

  • 导航

  • 按钮和指示器

  • 模态框和弹出窗口

  • 表格

为每个类别生成组件是一个好主意,这样当我们开始实现应用程序时,占位符将可用。这些组件将以清晰的分类方式托管所有组件,并且它们将成为您可以用来参考 Material 库中任何组件实现的一站式组件。

首先,让我们为我们的类别生成组件。依次运行以下ng命令:

ng g component MaterialLayouts
ng g component MaterialCards
ng g component MaterialForm
ng g component MaterialNavigation
ng g component MaterialButtons
ng g component MaterialModals
ng g component MaterialTable

在成功运行命令后,我们应该看到生成的组件已添加到我们的项目结构中,如下截图所示:

很好。我们已经生成了我们的应用程序;我们已经安装了 Angular Material。我们还将所有所需的模块导入到了我们的AppModule文件中,最后,我们为 Material 的 UI 组件中的每个类别生成了组件。在我们开始实现 Material 组件之前,我们需要做的最后一件事是为之前列出的每个类别添加路由。打开app-routing.module.ts文件,导入所有新创建的组件,并将路由添加到文件中:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MaterialFormComponent } from './material-form/material-form.component';
import { MaterialNavigationComponent } from './material-navigation/material-navigation.component';
import { MaterialCardsComponent } from './material-cards/material-cards.component';
import { MaterialLayoutComponent } from './material-layout/material-layout.component';
import { MaterialTableComponent } from './material-table/material-table.component';
import { MaterialModalsComponent } from './material-modals/material-modals.component';
import { MaterialButtonsComponent } from './material-buttons/material-buttons.component';

const routes: Routes = [
 { path: 'material-forms', component: MaterialFormComponent },
 { path: 'material-tables', component: MaterialTableComponent },
 { path: 'material-cards', component: MaterialCardsComponent},
 { path: 'material-layouts', component: MaterialLayoutComponent},
 { path: 'material-modals', component: MaterialModalsComponent },
 { path: 'material-buttons', component: MaterialButtonsComponent },
 { path: 'material-navigation', component: MaterialNavigationComponent }
];

@NgModule({
 imports: [RouterModule.forRoot(routes)],
 exports: [RouterModule]
})
export class AppRoutingModule { }

在上述代码中,我们导入了所有新创建的组件,并为每个组件创建了路由路径。到目前为止,一切都很顺利。现在,大舞台已经准备就绪,可以开始了。让我们先从我们的布局开始。

导航

任何 Web 应用程序最常见和基本的需求之一是导航菜单或工具栏。Angular Material 为我们提供了多种选项,我们可以选择最适合我们应用程序的菜单类型。

使用原理图生成导航组件

我们将从最简单和最快的方式开始,通过使用原理图来将导航添加到我们的应用程序中。没错,离我们的菜单上线只有一步之遥。Angular CLI 提供了原理图,以便获得各种组件。要在我们的应用程序中安装导航菜单,请在 Angular CLI 命令提示符中运行以下命令:

ng generate @angular/material:nav myMenu

在上述命令中,我们使用原理图生成了一个名为myMenu的新菜单组件。在成功运行命令后,我们应该看到以下截图中显示的输出:

使用ng serve命令运行应用程序,我们应该看到以下截图中显示的输出:

这不是一个非常酷的导航菜单吗?它带有一个顶部标题工具栏和一个可折叠的侧边栏菜单。这个组件是由原理图自动生成的。如果你不是自动生成组件的忠实粉丝,没关系,我们开发人员对这些事情可能会挑剔。让我们看看如何创建我们自己的菜单。

自定义 Material 菜单和导航

Angular Material 提供了MatMenuModule模块,其中提供了<mat-menu>MatToolBarModule指令。还提供了<mat-toolbar>,它将用于在我们的应用程序中实现菜单和标题。打开material-navigation.component.html文件并添加以下代码:

<mat-toolbar id="appToolbar" color="primary">
<h1 class="component-title">
 <a class="title-link">Angular Material</a>
 </h1>
 <span class="toolbar-filler"></span>
 <a href="#">Login</a>
 <a href="#">Logout</a>
</mat-toolbar>

在上述代码中,我们使用<mat-toolbar>作为包装器实现了工具栏指令,并使用<h1>添加了一个标题。我们还在标题部分添加了一些链接。使用ng serve运行应用程序,我们应该看到以下截图中显示的输出:

太棒了。让我们再增强一下。我们想要在标题工具栏中添加一个下拉菜单。记得我告诉过你,我们有MatMenuModule模块提供的<mat-menu>指令吗?让我们在上述代码中的标题工具栏中添加菜单指令如下:

<mat-toolbar id="appToolbar" color="primary">
<button md-icon-button (click)="sidenav.toggle()" class="md-icon-button sidenav-toggle-button" [hidden]="sidenav.opened">
<mat-icon aria-label="Menu" class="material-icons">menu</mat-icon>
</button>

<h1 class="component-title">
<a class="title-link">Angular Material</a>
</h1>
<span class="toolbar-filler"></span>

<button mat-button [matMenuTriggerFor]="menu" color="secondary">Menu</button>
<mat-menu #menu="matMenu" >
<button mat-menu-item>Item 1</button>
<button mat-menu-item>Item 2</button>
</mat-menu>

<a href="#">Login</a>
<a href="#">Logout</a>
</mat-toolbar>

请注意,我们使用mat-button属性添加了一个按钮,并绑定了matMenuTriggerFor属性。这将显示使用<mat-menu>指令定义的下拉菜单。现在让我们使用ng serve命令运行应用程序,我们应该看到以下输出:

自定义侧边栏菜单

太棒了。现在我们有了自制菜单可以使用。我知道你想要更多,对吧?你也想要添加一个侧边栏吗?让我们来做吧。为了将侧边栏添加到我们的应用程序中,Angular Material 为我们提供了一个MatSidenavModule模块,其中提供了我们可以在应用程序中使用的<mat-sidenav>指令。因此,让我们继续修改上述代码如下:

<mat-sidenav-container fullscreen>
 <mat-sidenav #sidenav mode="push" class="app-sidenav">
 <mat-toolbar color="primary">
 <span class="toolbar-filler"></span>
 <button md-icon-button (click)="sidenav.toggle()" class="md-icon-button 
   sidenav-toggle-button" [hidden]="!sidenav.opened">
 </button>
 </mat-toolbar>
</mat-sidenav>
<mat-toolbar id="appToolbar" color="primary">
 <button md-icon-button (click)="sidenav.toggle()" class="md-icon-button 
   sidenav-toggle-button" [hidden]="sidenav.opened">
 <mat-icon aria-label="Menu" class="material-icons">menu</mat-icon>
 </button>
 <h1 class="component-title">
 <a class="title-link">Angular Material</a>
 </h1>
 <span class="toolbar-filler"></span>
 <button mat-button [matMenuTriggerFor]="menu" 
   color="secondary">Menu</button>
 <mat-menu #menu="matMenu" >
 <button mat-menu-item>Item 1</button>
 <button mat-menu-item>Item 2</button>
 </mat-menu>
 <a href="#">Login</a>
 <a href="#">Logout</a>
 </mat-toolbar>
</mat-sidenav-container>

不要被代码行数吓到。我们只是做了一些改动,比如添加了<mat-sidenav>指令,它将包含侧边栏的内容。最后,我们将整个内容包装在<mat-sidenav-container>指令内;这很重要,因为侧边栏将覆盖在内容上方。使用ng serve命令运行应用程序,我们应该看到以下截图中显示的输出:

如果你看到了上面截图中显示的输出,给自己一个鼓励。太棒了!你做得非常好。所以,我们已经学会了两种在我们的应用程序中实现导航和菜单的方法。我们可以使用原理图生成导航组件,也可以编写自定义菜单导航组件。无论哪种方式,用户体验UX)都是赢家!

现在我们有了导航菜单组件,让我们学习一下 Angular Material 库的其他组件。

卡片和布局

在这一部分,我们将学习关于 Angular Material 卡片和布局的知识。Angular Material 的基本布局组件是卡片。卡片包装布局组件还可以包括列表、手风琴或展开面板、选项卡、步进器等等。

材料卡片

卡片是用于组合单个主题的数据的文本、图像、链接和操作的内容容器。卡片可以有标题、正文、图像或链接,根据它们的可用性和功能,可以显示给用户。Angular Material 提供了一个名为MatCardModule的模块,其中提供了<mat-card>指令。我们将使用这个来组合我们应用程序的内容。

创建卡片的基本示例如下:

<mat-card class="z-depth" >
 <mat-card-title><a href="" primary >Packt Books</a></mat-card-title>
 <mat-card-subtitle>Family of wonderful Authors and Readers
   </mat-card-subtitle>
 <mat-card-content>
 We are learning to create wonderful cards. Each card has some specific 
  data to be displayed to users.
 </mat-card-content>
<mat-card-actions> <button mat-raised-button>Tweet This</button>
  <button mat-raised-button>Share</button></mat-card-actions>
</mat-card>

在上面的代码中,我们使用了MatCardModule提供的指令。我们将使用<mat-card>作为包装指令,以便将内容分组。通过使用<mat-card-title>指令,我们设置了卡片的标题。我们使用<mat-card-subtitle>指令在<mat-card>指令内设置副标题。在<mat-card-content>内,我们放置所有需要显示给用户的内容。每个卡片可能有我们希望用户执行的操作,例如分享、编辑、批准等。我们可以使用<mat-card-actions>指令显示卡片操作。

使用ng serve命令运行应用程序,我们应该看到以下截图中显示的输出:

请注意,我们在 Angular Material 卡片内添加了一些内容。您是否想知道卡片内可以显示什么样的内容?只要您想,我们都可以使用。我们可以添加链接、图片、列表、手风琴、步进器等。在下一节中,我们将学习如何将列表添加到我们的卡片中。

列表

列表是一组项目的集合。在我们的应用程序中,可以是有序列表,也可以是无序列表。在本节中,我们将学习如何在卡片内添加不同类型的列表。看看下面的示例代码:

<mat-card class="z-depth" >
 <mat-card-title>Material Lists</mat-card-title>
 <mat-card-content>
 <mat-list>
 <mat-list-item> New York City</mat-list-item>
 <mat-list-item> London</mat-list-item>
 <mat-list-item> Dallas</mat-list-item>
</mat-list>
 </mat-card-content>
</mat-card>

在上面的代码中,我们添加了几个城市的列表。我们使用了MatListModule中提供的<mat-list><mat-list-item>指令,以便在卡片内创建和显示城市列表。上面的代码输出如下:

带分隔线的列表

我们还可以很容易地为列表项添加divider类,以便在视觉上将它们分隔成行。我们需要添加<mat-divider>指令以实现该功能。看看下面更新的代码:

<mat-card class="z-depth" >
 <mat-card-title>Material Lists with Divider</mat-card-title>
 <mat-card-content>
<mat-list>
 <mat-list-item> Home </mat-list-item>
 <mat-divider></mat-divider>
 <mat-list-item> About </mat-list-item>
 <mat-divider></mat-divider>
 <mat-list-item> Contact </mat-list-item>
 <mat-divider></mat-divider>
</mat-list>
</mat-card-content>
</mat-card>

导航列表

我们可以扩展列表使其可点击,从而将其转换为导航链接。要使列表项可点击,我们需要使用<mat-nav-list>指令。看看下面的示例代码:

<mat-card class="z-depth" >
 <mat-card-title>Material Navigational Lists</mat-card-title>
 <mat-card-content>
<mat-nav-list>
 <a mat-list-item href="#" *ngFor="let nav of menuLinks"> {{ nav }} </a>
</mat-nav-list>
 </mat-card-content>
</mat-card>

在上面的代码中,我们使用了MatListModule模块中提供的<mat-nav-list><mat-list-item>指令,创建了导航类型的列表和卡片内的列表项。上面的代码输出如下:

手风琴和展开面板

另一个非常酷的 UI 组件是手风琴或展开面板。当我们需要将数据分组在一起时,使用它非常方便。我们需要使用MatExpansionModule模块中提供的<mat-accordion><mat-expansion-panel>来实现我们应用程序中的手风琴功能。看看下面的示例代码:

<mat-card class="z-depth" >
 <mat-card-title>Material Expansion Panels</mat-card-title>
 <mat-card-content>
<mat-accordion>
 <mat-expansion-panel>
 <mat-expansion-panel-header>
 <mat-panel-title>
 Personal Details
 </mat-panel-title>
 </mat-expansion-panel-header>
</mat-expansion-panel>
 <mat-expansion-panel >
 <mat-expansion-panel-header>
 <mat-panel-title>
 Professional Details
 </mat-panel-title>
 <mat-panel-description>
 </mat-panel-description>
 </mat-expansion-panel-header>
 <p>I'm visible because I am open</p>
 </mat-expansion-panel>
</mat-accordion>
 </mat-card-content>
</mat-card>

每个<mat-expansion-panel>都将有一个<mat-expansion-panel-header>,我们可以在其中为展开面板提供标题和描述,并将内容放在<mat-expansion-panel>指令本身内。上面的代码输出如下:

有时我们需要引导用户完成一系列步骤的用例。这就是我们下一个组件发挥作用的地方。它被称为步进器。顾名思义,这将用于设计水平或垂直的步骤,并将一系列步骤分组,用户可以导航到这些步骤。

步进器

与我们在手风琴和展开面板部分学到的类似,我们需要添加一个包装器和一个<mat-horizontal-stepper>指令,在其中,我们将创建<mat-step>指令。对于我们想要添加的每个步骤,我们需要为我们的应用程序创建一个新的<mat-step>指令。我们也可以创建一个垂直步进器。为此,我们将使用<mat-vertical-stepper>指令作为包装器类。请看下面的代码;我们正在创建一个水平步进器:

<mat-card class="z-depth" >
<mat-card-title>Material Stepper</mat-card-title>
<mat-card-content>
<mat-horizontal-stepper [linear]="isLinear" #stepper>
<mat-step label="Personal Details">
Step #1
</mat-step>
<mat-step label="Professional Details">
Step #2
</mat-step>
<mat-step>
<ng-template matStepLabel>Done</ng-template>
You are now done.
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button (click)="stepper.reset()">Reset</button>
</div>
</mat-step>
</mat-horizontal-stepper>
</mat-card-content>
</mat-card>

在上面的代码中,我们创建了一个包含三个步骤的水平步进器。为了定义步进器,我们使用了<mat-horizontal-stepper>,用于定义实际步骤,我们使用了<mat-step>指令。上面代码的输出如下:

标签页

我们要学习的最后一个布局组件是标签页。Angular Material 提供了一个名为MatTabsModule的模块,该模块提供了<mat-tab-group><mat-tab>指令,以便我们可以轻松地在我们的应用程序中创建一个标签页组件。请看下面的示例代码:

<mat-card class="z-depth" >
 <mat-card-title>Material Tabs</mat-card-title>
 <mat-card-content>
 <mat-tab-group>
 <mat-tab label="Personal"> This is a Personal Tab </mat-tab>
 <mat-tab label="Professional"> This is a Professional tab </mat-tab>
 <mat-tab label="Contact"> This is Contacts Tab </mat-tab>
</mat-tab-group>
</mat-card-content>
</mat-card>

在上面的代码中,我们使用了<mat-tab-group>包装指令,在其中,我们使用了<mat-tab>指令来指定每个特定的标签页。每个标签页都有一个标签,将显示在标签页的顶部。在<mat-tab>内部,我们将显示每个标签页的内容。请看下面截图中上面代码的输出:

在下一节中,我们将学习关于 Angular Material 表单的知识。继续阅读。

表单控件

表单是任何交互式和动态应用程序的主要组成部分。Angular Material 原生支持表单和表单控件,可以轻松地集成到我们的应用程序中。在本节中,我们将学习如何使用 Angular Material 组合表单。

总的来说,表单在 UX/UI 方面已经有了很大的发展。Angular Material 支持涉及文本字段、文本区域、下拉选择选项、单选按钮和复选框等基本表单字段元素。Angular Material 还提供了高级表单元素,例如自动完成、日期选择器、滑动开关等。在我们进行实际示例的过程中,我们将学习如何将所有这些添加到我们的表单中。

Angular Material 提供了许多与表单和表单字段元素相关的模块,包括以下列出的模块:

  • MatFormFieldModule

  • MatInputField

  • MatRadioModule

  • MatChipModule

  • MatProgressBarModule

  • MatSelectModule

  • MatSlideModule

  • MatSlideToggleModule

  • MatListModule

  • MatDatePickerModule

  • MatAutocompleteModule

  • MatCheckboxModule

如前所述,我们可以单独导入这些,或者像在前一节中的MaterialModule文件中那样一次性导入所有模块。我们已经在AppModule中导入了我们的模块;我们可以开始将表单字段实现到我们的表单中。我们将把每个inputtextarea表单元素包装在一个<mat-form-field>包装指令中。为了实现输入文本框,我们将使用matInput属性,以及我们的HTML输入标签:

<mat-form-field>
<input matInput placeholder="Enter Email Address" value="">
</mat-form-field>

这非常简单明了,对吧?当然。现在,同样地,我们可以轻松地向我们的表单中添加一个textarea字段:

<mat-form-field class="example-full-width">
<textarea matInput placeholder="Enter your comments here"></textarea>
</mat-form-field>

好吧,添加InputTextarea表单元素并不是什么难事。接下来,我们将实现单选按钮和复选框字段元素:

 <mat-radio-group>
 <p>Select your Gender</p>
 <mat-radio-button>Male</mat-radio-button>
 <mat-radio-button>Female</mat-radio-button>
 </mat-radio-group>

为了在我们的表单中实现单选按钮,我们将使用<mat-radio-button>指令。在大多数情况下,我们还将使用多个单选按钮来提供不同的选项。这就是我们将使用<mat-radio-group>包装指令的地方。与单选按钮类似,Material 提供了一个指令,我们可以轻松地将复选框集成到我们的应用程序中。我们将使用<mat-checkbox>指令如下:

<mat-checkbox>
    Agree to Terms and Conditions
</mat-checkbox>

该指令由MatCheckboxModule模块提供,并提供了许多属性,我们可以用来扩展或处理数据。

为了在我们的表单中实现下拉选项,我们需要使用 HTML 的<select><option>标签。Material 库提供了我们可以轻松使用的指令,以扩展我们表单的功能:

<mat-form-field>
Select City
<mat-select matNativeControl required>
 <mat-option value="newyork">New York City</mat-option>
 <mat-option value="london">London</mat-option>
 <mat-option value="bangalore">Bangalore</mat-option>
 <mat-option value="dallas">Dallas</mat-option>
</mat-select>
</mat-form-field>

在前面的代码中,为了使用<select><option>标签,我们将使用<mat-select><mat-option>指令。我们在这里取得了很好的进展。让我们保持这种势头。我们要实现的下一个表单字段元素是滑块组件。

当用户想要指定起始值和结束值时,滑块可以非常有帮助。当用户可以开始浏览范围并且数据根据所选范围进行过滤时,它可以改善用户体验。要向我们的表单添加滑块,我们需要添加<mat-slider>指令:

<mat-form-field>
Select Range
<mat-slider></mat-slider>
</mat-form-field>

那很简单。MatSliderModule API 提供了许多选项,以便以许多有用的方式扩展和使用指令。我们可以指定最大和最小范围。我们可以设置间隔值,等等。谈到 UI 中的滑块功能,有一个组件可以使用,称为滑动切换。我们可以使用<mat-slide-toggle>指令来实现滑动切换:

 <mat-slide-toggle>Save Preferences</mat-slide-toggle>

我们使用了MatSlideToggleModule模块提供的<mat-slide-toggle>指令。该 API 提供了许多属性,例如dragChangetoggleChange、根据需要设置颜色或验证等。

现在我们已经在模板文件中放置了所有前面的表单字段元素,让我们运行应用程序以查看输出。使用ng serve命令运行应用程序,我们应该看到以下截图中显示的输出:

在下一节中,我们将学习由 Angular Material 提供的按钮和指示器组件。

按钮和指示器

这里有一个小小的趣闻——你见过没有任何按钮的网站或应用程序吗?如果有的话,请写信给我。

就我的经验而言,按钮是 Web 应用程序的一个组成部分。在本节中,我们将学习有关按钮、按钮组和指示器的所有内容。

Angular Material 提供了许多有用且易于附加到按钮标签的属性,然后,神奇发生了。开始使用 Angular Material 按钮的最简单方法是将mat-button属性添加到<button>标签中:

<div>
<button mat-button>Simple Button</button>
<button mat-button color="primary">Primary Button</button>
<button mat-button color="accent">Accent Button</button>
<button mat-button color="warn">Warn Button</button>
<button mat-button disabled>Disabled</button>
<a mat-button routerLink=".">Link</a>
</div>

在上述代码中,我们为添加到material-button.component.html模板文件中的所有按钮添加了mat-button属性。我们还使用了colordisabled等属性来自定义按钮的外观和行为。上述代码的输出如下:

上述截图中的按钮看起来更像链接而不是按钮,对吧?让我们自定义它们,使它们看起来更像按钮。我们可以通过添加mat-raised-button属性来轻松实现这一点。请注意,在上一个示例中,我们使用了mat-button属性,在这个示例中,我们添加了mat-raised-button。更新后的代码如下:

<div>
  <button mat-raised-button>Basic Button</button>
  <button mat-raised-button color="primary">Primary Button</button>
  <button mat-raised-button color="accent">Accent Button</button>
  <button mat-raised-button color="warn">Warn Button</button>
  <button mat-raised-button disabled>Disabled Button</button>
  <a mat-raised-button routerLink=".">Link</a>
</div>

上述代码的输出如下。请注意,现在添加了新属性后,按钮的外观和感觉有所不同:

这些是漂亮的按钮!使用预定义的属性可以让我们在整个应用程序中保持按钮的统一性。

接下来,我们将探索 Angular Material 提供的指示器。作为指示器组件的一部分,我们将学习徽章和进度条组件。

徽章是突出显示一些数据以及其他 UI 元素的一种方式。我们可能会遇到一些使用案例,希望在按钮上使用徽章。你可能已经在想,我们是否也可以为按钮添加一些 UX 来设计一些功能呢?是的,我们可以!

Angular Material 提供了一个名为MatBadgeModule的模块,其中包含了matBadgematBadgePositionmatBadgeColor属性的实现,可以轻松地用于设置按钮的徽章。看一下以下示例代码:

<button mat-raised-button color="primary"
 matBadge="10" matBadgePosition="before" matBadgeColor="accent">
 Left Badge
</button>

在上述代码中,我们添加了一个按钮元素,并指定了属性,如matBadgematBadgePositionmatBadgeColor。上述代码的输出如下:

这是一个带徽章的按钮。还有另一个名为 chips 的 UI 组件。我们也可以轻松使用这些来增强 UX。将 Material chips 想象成之前使用过的任何其他应用程序中的标签。Angular Material 提供了一个名为MatChipModule的模块,其中提供了<mat-chip-list><mat-chip>指令,我们可以轻松地集成到我们的应用程序中。看一下以下示例代码:

<mat-chip-list>
<mat-chip color="primary" selected>New York</mat-chip>
<mat-chip>London</mat-chip>
<mat-chip>Dallas</mat-chip>
<mat-chip>Accent fish</mat-chip>
</mat-chip-list>

在前面的代码中,我们使用了从MatChipModule中得到的指令,并将标签组合在一起。前面代码的输出如下:

很好。我们将学习实现的下一个指示器是非常重要的;进度条。我们需要向用户显示并告知正在后台执行的操作,或显示处理某些用户数据的进度。在这种情况下,我们需要清楚地使用进度条来显示这一点。

Angular Material 提供了名为MatProgressBarModuleMatProgressSpinnerModule的模块,使用这些模块,我们可以轻松地向我们的 Web 应用程序添加加载图标或旋转器。使用 API 属性和事件,我们还可以轻松地捕获和处理数据。看一下以下示例代码:

<mat-spinner></mat-spinner>

就这样?真的吗?我们在开玩笑吗?不,我们不是。只需使用这个模块,我们应该在我们的应用程序中看到旋转的轮子。看一下前面代码的输出:

在下一节中,我们将学习 Angular Material 提供的所有有关模态窗口和对话框窗口的信息。

弹出窗口和模态窗口

现代 Web 应用程序引入了许多创新的 UX 功能和功能。一个真正突出的功能必须是模态窗口。打开任何主要的 Web 应用程序;它都会有一些模态窗口的实现。Angular Material 库也为我们提供了一种轻松实现模态或对话框弹出窗口的方法。

Angular Material 有一个名为MatDialogModule的模块,它提供了我们可以在组件类中使用的各种类。与其他 UI 组件不同,没有指令可以直接在模板文件中使用;相反,我们需要以编程方式实现此功能。在我们开始创建对话框窗口实现之前,我们将需要一个组件来存储模态窗口内容。运行以下命令并生成一个组件。让我们称之为addDialog组件:

ng g c addDialog

当命令成功执行时,我们应该看到以下截图中显示的输出:

现在,打开新创建的add-dialog.component.html文件,并添加一些内容。即使现在只是Hello World也可以。

接下来,让我们开始修改我们的MaterialModalComponent类,并将以下代码添加到其中:

import { Component, OnInit, Inject} from '@angular/core';
import { VERSION, MatDialogRef, MatDialog} from '@angular/material';
import {AddDialogComponent} from '../add-dialog/add-dialog.component';

@Component({
 selector: 'app-material-modals',
 templateUrl: './material-modals.component.html',
 styleUrls: ['./material-modals.component.scss']
})
export class MaterialModalsComponent implements OnInit {

constructor(private dialog: MatDialog) { }

ngOnInit() { }

openDialog() {
 const dialogRef = this.dialog.open(AddDialogComponent);
 }
}

让我们分析前面的代码。我们将所有所需的模块导入到文件中。然后我们将VERSIONMatDialogRefMatDialog导入到我们的组件类中。我们还导入了AddNewComponent,我们希望在模态窗口中显示它。由于我们在类中导入了MatDialog,我们需要将其注入到我们的构造方法中,然后创建一个实例。然后我们将创建另一个名为openDialog的方法。在这个方法中,通过使用MatDialog实例,我们调用 open 方法并将AddNewComponent作为参数传递。我们已经实现了模态窗口的功能,但在实际调用openDialog方法之前,这不会起作用。

因此,让我们打开我们的material-modal.component.html模板文件,并在其中添加以下行:

<button mat-raised-button (click)="openDialog()">Pick one</button>

这里没有太多要描述的。我们只是添加了一个按钮,并附加了一个onclick事件,以便调用openDialog方法:简单而甜蜜。让我们使用ng serve命令运行应用程序,我们应该看到以下输出:

在我的AddDialogComponent中,我添加了一些文本和一个按钮。您也可以添加或设计自己的模板。API 提供了许多属性和事件,我们可以与对话框窗口关联起来。

在下一节中,我们将学习 Angular Material 提供的数据表功能。

数据表

表格是设计复杂的登录后屏幕功能的关键方面之一。我说在登录屏幕后面,因为这样,搜索引擎优化的争论就不会出现。传统表格的问题在于我们需要自己映射数据、行和列,并实现分页和响应性。多亏了 Angular Material,我们现在可以用一行命令就能生成所有这些。没错,你没看错——只用一个命令,当我们使用原理图时。运行以下命令,我们应该很快就能准备好我们的数据表:

ng generate @angular/material:table issueList

我们使用ng命令来指定我们要从 Angular Material 生成表格的原理图,并且应该在名为issueList的新组件中创建它。成功运行命令后,我们应该看到以下截图中显示的输出:

使用ng serve命令运行应用程序,并导航到表的路由。我们应该看到以下截图中显示的输出:

看!我们现在已经准备好使用我们的动态表格了。我们可以自定义数据源的值和需要显示和更新的列,只需使用我们component类中的配置。继续尝试一下吧。

总结

我们通过为 UI 组件的每个主要类别创建占位符组件来开始本章。这些组件分为各种类别布局、材料卡片、表单控件、导航、按钮和指示器、模态和弹出窗口以及表格。

我们首先创建了导航菜单组件。我们学习了如何使用原理图自动生成导航菜单组件。然后,我们还学习了如何为我们的应用程序实现自定义菜单。接下来,我们开始学习并实现由 Angular Material 提供的布局组件。在布局组件中,我们了解了 Material 卡片。我们学习了如何在 Material 卡片中包含各种内容。我们了解了 Material 支持的各种列表。我们了解了带有分隔线的列表和导航列表。我们还学习了如何实现手风琴和扩展面板,以更好地对数据进行分组和排列。我们还探索了如何使用步进器组件,在设计需要各种步骤的数据的 UX 时非常有用。同样,我们学习了如何使用选项卡来对事物进行分组。

接下来,我们探索了 Material 表单,并学习了如何实现表单字段元素,包括输入、文本区域、单选和复选按钮、滑块和滑动切换。我们还学习了 Material 提供的不同类型的按钮和指示器,包括徽章和标签。然后,我们了解并实现了由 Angular Material 提供的模态框和弹出窗口。

最后,我们了解了数据表,以及原理图如何帮助我们快速在应用程序中设置数据表。

如果我们想要涵盖 Angular Material 组件的每一个细节,就需要一本单独的书。我们试图为您概述不同的可用组件,以及在下一个项目中为什么您可能考虑使用 Material,并在合适的时候适合您/您的客户。这绝对值得一试!

第十章:处理表单

让我们从一个简单的猜谜游戏开始这一章。你能想到任何没有任何形式的网页应用程序,比如注册、登录、创建、联系我们、编辑表单等等;列表是无穷无尽的。(错误答案-甚至 Google 主页上也有一个搜索表单。)

从技术上讲,这是可能的。我 100%确定有一些网站根本不使用表单,但我同样确信它们将是静态的,不会与用户动态交互或互动,这就是本章的主要内容和重点:在我们的 Angular 应用程序中实现和使用表单。

好的,现在让我们来看看本章我们将涵盖的内容:

  • 引导表单简介

  • 引导表单类

  • 引导表单类-扩展

  • 角度形式

  • 模板驱动表单

  • 响应式表单

  • 表单验证

  • 提交和处理表单数据

引导表单

我们将学会使用强大的 Bootstrap 库,它为我们设计和开发应用程序中的表单提供了丰富的类和实用程序,使开发人员和设计人员的生活变得轻松!

什么是表单?

表单是一组输入字段的集合,通过键盘、鼠标或触摸输入,使我们能够从用户那里收集数据。

我们将学会将输入元素组合在一起,并构建一些示例表单,比如登录、注册,或者当用户忘记密码时。

在我们开始创建表单之前,这里有一个我们可以在应用程序中使用的可用 HTML 输入元素的快速列表:

  • 输入(包括文本、单选框、复选框或文件)

  • 文本区

  • 选择

  • 按钮

  • 形式

  • 字段集

如果你想快速复习 HTML 标签和元素,你可以访问W3schools.com

掌握了关于表单和可用的 HTML 元素的知识,现在是动手的时候了。

引导表单类

在本节中,我们将学习 Bootstrap 框架中可用的类,我们可以在构建表单时使用这些类。每个表单可以包含各种输入元素,如文本表单控件、文件输入控件、输入复选框和单选按钮。.form-group类是一种为我们的表单添加结构的简单方法。使用.form-group类,我们可以轻松地将输入元素、标签和帮助文本分组,以确保表单中元素的正确分组。在.form-group元素内,我们将添加输入元素,并为每个元素分配.form-control类。

使用.form-group类对元素进行分组的示例如下:

 <div class="form-group">
 <label for="userName">Enter username</label>
 <input type="text" class="form-control" id="userName" placeholder="Enter username">
 </div>

在上述代码中,我们创建了一个包含标签和文本输入元素的表单组。

在同样的线上,我们可以轻松地添加文本输入元素,比如emailpasswordtextarea。以下是添加类型为email的输入元素的代码:

<div class="form-group">
<label for="userEmailAddress">Enter email address</label>
<input type="email" class="form-control" id="emailAddress" placeholder="name@example.com">
</div>

同样,我们也可以轻松地添加类型为password的输入元素。再次注意,我们正在使用form-group作为包装,并将form-control添加到元素中:

<div class="form-group">
<label for="userPassword">Enter password</label>
<input type="password" class="form-control" id="userPassword">
</div>

不错。我们学会了在输入元素上使用form-groupform-control类。现在,让我们将相同的类添加到textarea元素上。以下是为textarea元素添加类的示例代码:

<div class="form-group">
<label for="userComments">Example comments</label>
<textarea class="form-control" id="userComments" rows="3"></textarea>
</div>

您会注意到所有上述元素都具有相同的结构和分组。对于selectmultiple select输入元素,也完全相同。

在以下示例代码中,我们创建了一个select下拉元素,并使用了form-control类:

<div class="form-group">
<label for="userRegion">Example select</label>
<select class="form-control" id="userRegion">
<option>USA</option>
<option>UK</option>
<option>APAC</option>
<option>Europe</option>
</select>
</div>

我们已经添加了一个select下拉元素,并且将允许用户从列表中选择一个选项。只需添加一个额外的属性multiple,我们就可以轻松地允许用户选择多个选项:


<div class="form-group">
<label for="userInterests">Example multiple select</label>
<select multiple class="form-control" id="userInterests">
<option>Biking</option>
<option>Skiing</option>
<option>Movies</option>
<option>Music</option>
<option>Sports</option>
</select>
</div>

这很简单明了。让我们继续前进。

现在,让我们继续其他重要的输入元素:复选框和单选按钮。但是,checkboxradio元素的类是不同的。

有三个新的类,我们将学习如何为checkboxradio元素实现:

  • 为了包装元素,我们将使用form-check

  • 对于输入类型为checkboxradio的元素,我们将使用form-check-input

  • 对于checkboxradio元素,我们需要显示标签,为此我们将使用form-check-label类:

<div class="form-check">
 <input class="form-check-input" type="checkbox" value="" id="Worldwide">
 <label class="form-check-label" for="Worldwide">
 Worldwide
 </label>
</div>

在上述代码中,我们使用.form-check类,.form-check-input.form-check-label来包装我们的divlabel元素。

同样,在类似的线上,我们将使用上述类来添加到输入radio元素中:


<div class="form-check">
 <input class="form-check-input" type="radio" name="gender" id="maleGender" 
    value="option1" checked>
 <label class="form-check-label" for="maleGender">
 Male
 </label>
</div>
<div class="form-check">
 <input class="form-check-input" type="radio" name="gender" id="femaleGender" 
    value="option2">
 <label class="form-check-label" for="femaleGender">
 Female
 </label>
</div>

在上述代码中,我们为用户创建了两个单选按钮,以选择他们的性别,并且用户只能在两个选项中选择一个。

在大多数现代 Web 应用程序中,我们需要用户能够上传文件或资源到我们的应用程序。Bootstrap 为我们提供了一个名为"form-control-file"的类,我们可以将其关联到文件上传元素。

我们将使用form-control-file类将其应用于我们的输入类型file元素。此示例代码如下:

<div class="form-group">
 <label for="userProfilePic">Upload Profile Pic</label>
 <input type="file" class="form-control-file" id="userProfilePic">
 </div>

很好。我们已经学会了如何组合所有元素,从而创建我们美丽而强大的表单。

Bootstrap 表单类 - 扩展

我们已经学会了创建带有输入元素的表单,并在 Bootstrap 中添加了一些可用的表单类来对元素进行分组,以及改善我们的应用程序。

在本节中,我们将查看 Bootstrap 框架提供的其他附加类和属性,这些类和属性可用于改善用户体验(UX),以及扩展元素的行为:

  • 大小

  • 只读

  • 内联表单

  • 使用 Bootstrap 网格类的表单

  • 禁用

  • 帮助文本

  • form-group内的纯文本

我们将逐个讨论上述选项,并学会实现它们并看到它们的效果。

大小

我们可以设置表单中输入元素的大小。我们可以使用各种类来控制元素的高度,适用于小、中和大分辨率。

我们已经在上一节中学会了使用.form-control类,默认情况下,使用.form-control-md类应用了中等大小的高度。还有其他类可用于设置高度为大或小。我们可以分别使用.form-control-lg.form-control-sm

以下是示例代码,我们将使用.form-control-lg类将电子邮件地址元素的高度设置为大,并使用.form-control-sm类将密码字段设置为小:

<form>
 <div class="form-group mb-2 mr-sm-2">
   <label for="userEmailAddress">Enter email address</label>
   <input type="email" class="form-control form-control-lg" 
     id="userEmailAddress">
 </div>

 <div class="form-group mb-2 mr-sm-2">
   <label for="userPassword">Enter password</label>
   <input type="password" class="form-control form-control-sm" 
     id="userPassword">
 </div>

<button type="submit" class="btn btn-primary">Submit</button>
</form>

我们已将form-control-lgform-control-sm类添加到表单控件的电子邮件地址和密码表单元素中,分别。

当我们运行应用程序时,上述代码的输出如下:

在上面的屏幕截图中,请注意输入元素高度的差异。电子邮件地址文本字段的高度增加了,密码字段很小。

只读

我们可能会遇到一个使用情况,需要禁用字段并使其只读。我们可以利用属性readonly。通过向任何表单控件元素添加布尔readonly属性,我们可以禁用该元素。

显示在用户名字段上使用readonly属性的示例代码如下:

<div class="form-group">
 <label for="userName">Enter username</label>
 <input type="text" class="form-control" id="userName" placeholder="E.g 
    packtpub" **readonly**>
 </div>

上述代码的输出如下所示。请注意,电子邮件地址字段已禁用,因此用户将无法添加/编辑该元素:

内联表单

设计也是我们如何显示表单的同样重要的方面。我们可能会遇到这样的用例,我们需要将我们的表单水平放置,而不是常规的垂直方式。

Bootstrap 有.form-inline类来支持内联或水平表单。当使用.form-inline类时,表单元素会自动水平浮动。

以下是一些示例代码,我们在其中使用电子邮件地址和密码创建登录表单。我们使用form-inline类使其成为内联表单:

<form class="form-inline">
 <div class="form-group">
 <label for="userEmailAddress">Enter email address</label>
 <input type="email" class="form-control" id="emailAddress" 
    placeholder="name@example.com">
 </div>

 <div class="form-group">
 <label for="userPassword">Enter password</label>
 <input type="password" class="form-control" id="userPassword">
 </div>
</form>

在上述代码中,需要注意的重要事项是使用.form-inline类。

上述代码的输出如下:

默认情况下,使用 Bootstrap 设计的所有表单都是垂直的。

使用 Bootstrap 网格类的表单

还记得我们在第三章中学到的 Bootstrap 网格类吗,Bootstrap-网格布局和组件?是的,行、列和设计屏幕布局。

在本节中,我们将学习在表单内部使用相同的行和列网格类,这是一个好消息,因为使用这些类,我们可以设计自定义布局并更新表单的外观。

此示例代码如下:

<form>
 <div class="row">
 <div class="col">
 <label for="userEmailAddress">Enter email address</label>
 <input type="email" class="form-control" id="emailAddress" readonly>
 </div>
 <div class="col">
 <label for="userPassword">Enter password</label>
 <input type="password" class="form-control" id="userPassword">
 </div>
 </div>
</form>

在上述代码中,我们不是使用.form-group类,而是使用rowcol类,这些类主要用于设计布局。

我们创建一个具有两列的单行,并在每列中添加输入元素。

上述代码的输出如下:

现在是你的作业。尝试使用表单和网格类进行这些有趣的用例:

  • 通过向同一行添加更多列 div 元素,可以在同一行中添加更多输入元素

  • 向表单添加多行

  • 为某些列(第 4 列或第 3 列)分配固定宽度

禁用

在开发具有关键和复杂合规要求的 Web 应用程序时,很常见的是我们将不得不根据用户选择禁用某些输入元素。

一个很好的用例是,某些字段不适用于用户选择的特定国家,因此我们需要禁用其他依赖字段。

使用disabled属性,该属性接受布尔值,我们可以禁用表单或特定元素。

让我们看看disabled属性的作用:

<form>
 <div class="row">
 <div class="col">
 <label for="userEmailAddress">Enter email address</label>
 <input type="email" class="form-control" id="emailAddress" disabled>
 </div>
 <div class="col">
 <label for="userPassword">Enter password</label>
 <input type="password" class="form-control" id="userPassword">
 </div>
 </div>
</form>

在上述代码中,我们使用了disabled属性。我们可以在以下截图中看到,电子邮件地址字段完全被禁用:

我们可以通过向元素添加disabled属性来使任何元素被禁用。这很好,但是如果我们想一次性禁用整个表单怎么办?我们也可以做到。

看一下以下代码:

<form>
 <fieldset disabled>
 <div class="row">
 <div class="col">
 <label for="userEmailAddress">Enter email address</label>
 <input type="email" class="form-control" id="emailAddress">
 </div>
 <div class="col">
 <label for="userPassword">Enter password</label>
 <input type="password" class="form-control" id="userPassword">
 </div>
 </div>
 </fieldset>
</form>

我们在表单内部添加fieldset标签,将表单的所有元素包装在一起,并将disabled属性应用于fieldset元素,这将一次性禁用整个表单。

上述代码的输出如下所示:

表单内的帮助文本

任何优秀的 Web 应用程序都将拥有美观而强大的表单,这些表单可以与用户交流,并创造良好的用户体验。

帮助文本是我们通知用户有关表单中任何错误、警告或必填字段的选项之一,以便用户可以采取必要的行动。

看一下以下代码:

<form>
 <div class="form-group">
 <label for="userEmailAddress">Enter email address</label>
 <input type="email" class="form-control" id="userEmailAddress">
 <small id="userEmailAddressHelp" class="form-text text-danger">
 Email address cannot be blank.
 Email address should be atleast 3 characters
 </small>
 </div>
 <div class="form-group">
 <label for="userPassword">Enter password</label>
 <input type="password" class="form-control" id="userPassword">
 </div>
</form>

在上述代码中,我们在<small>标签内添加文本,并分配.form-text类和.text-danger

上述代码的输出如下:

将输入元素显示为纯文本

我们可能会遇到这样的要求,我们需要将输入元素显示为纯文本,而不是输入元素。

我们可以通过自定义样式表来简单地实现这一点,或者只需在具有.form-group类的元素内使用.form-control-plaintext类。

看一下以下代码:

<form>
 <div class="form-group">
 <label for="userEmailAddress">Enter email address</label>
 <input type="email" class="form-control-plaintext" id="userEmailAddress" 
   placeholder="Enter email address">
 <small id="userEmailAddressHelp" class="form-text text-danger">
 Email address cannot be blank.
 Email address should be atleast 3 characters
 </small>
 </div>
 <div class="form-group">
 <label for="userPassword">Enter password</label>
 <input type="password" class="form-control" id="userPassword">
 </div>
</form>

在上述代码中,我们已经将.form-control-plaintext类添加到输入元素中。

上述代码的输出如下:

在本节中,我们已经了解了各种类和属性,我们可以使用它们来增强和使我们的表单更具交互性和强大性,最重要的是,为更好的用户设计和体验增添内容。

Angular 表单

在本节中,Angular 应用程序中的表单真正发挥作用。表单是任何应用程序的核心,也是收集、查看、捕获和处理用户提供的数据的主要构建块。在本节中,我们将继续使用 Bootstrap 库来增强我们表单的设计。

Angular 提供了两种不同的方法来构建应用程序内的表单。

Angular 提供的构建表单的两种方法如下:

  • 模板驱动表单:HTML 和数据绑定在模板文件中定义

  • 使用模型和验证在Component类文件中的响应式或模型驱动表单

尽管表单模型是模板驱动表单和响应式表单之间的共同点,但它们的创建方式不同。

当涉及到模板时,响应式表单和模板驱动表单的主要区别在于数据绑定。在模板驱动表单中,我们使用双向数据绑定将我们的数据模型直接绑定到表单元素。另一方面,使用响应式表单时,我们将我们的数据模型绑定到表单本身(而不是其各个表单元素)。

我们将详细探讨这些方法,了解这些方法的利弊,最后,我们将使用这两种方法构建一些表单。让我们开始吧。

模板驱动表单

模板驱动表单,顾名思义,涉及表单的所有繁重工作都在组件模板中进行。这种方法很好,建议在处理简单、直接的表单时使用,而不涉及太多复杂的验证或规则。

所有逻辑都在模板文件中,这基本上意味着我们将利用 HTML 元素和属性。在模板驱动的表单中,我们使用 HTML 来创建表单和输入元素,并将验证规则创建为 HTML 属性。双向数据绑定是关键部分,因此我们可以将表单元素与Component类中的属性绑定起来。

Angular 会自动生成表单模型,自动跟踪表单和输入元素的状态供我们使用。我们可以直接将表单作为对象并轻松处理数据。

在使用模板驱动方法时,我们首先导入FormsModule,这样我们就可以访问以下指令:

  • ngForm

  • ngModel

  • ngModelGroup

我们需要将FormsModule导入到我们的app.module.ts文件中。

让我们来看看在我们的应用程序中使用模板驱动表单方法的利弊。

模板驱动表单-优点

如果我们应用程序中的表单简单直接,没有太多元数据和验证,模板驱动表单可以非常有用和有帮助。在本节中,我们将强调在我们的应用程序中使用模板驱动表单的优点:

  • 模板驱动表单非常容易使用

  • 适用于简单和直接的用例

  • 易于使用的双向数据绑定,因此代码和复杂性很少

  • Angular 自动跟踪表单和输入元素的状态(如果表单状态不完整,则可以禁用提交按钮)

  • 如果表单具有复杂的表单验证或需要自定义表单验证,则不建议使用

基于模板的表单 - 缺点

在前一节中,我们已经了解了在应用程序中使用基于模板的表单的优势,并且我们已经就使用基于模板的表单方法的优点进行了充分论证。在本节中,我们将了解在我们的应用程序中使用基于模板的表单的一些缺点:

  • 不建议或适用于表单要求复杂且包括自定义表单验证的情况

  • 无法完全覆盖单元测试以测试所有用例

基于模板的表单 - 重要模块

掌握了使用基于模板的方法的优缺点的知识,我们将立即深入学习如何在我们的应用程序中实现基于模板的表单。我们将首先学习所需的模块,然后逐渐创建我们应用程序中的表单。如前所述,基于模板的表单大多在模板文件中定义。在我们开始创建基于模板的表单示例之前,我们应该了解与表单相关的一些最重要的概念,即ngFormngModel

  • ngForm:这是一个指令,用于在表单指令内部创建控件组

  • ngModel:当在ngForm内的元素上使用ngModel时,所有元素和数据都会在ngForm内注册。

如果 Angular 表单使用ngFormngModel,这意味着该表单是基于模板的。

构建我们的登录表单

到目前为止,我们对基于模板的表单有了一个很好的高层次理解。在本节中,我们将把我们的知识付诸实践,通过构建一个表单来实现。让我们使用我们在前一节中学到的类来组合一个表单。

我们将处理的用例是我们应用程序的用户登录表单。首先,我们需要生成我们的登录组件。运行以下ng命令以生成登录组件:

ng g c login

前面命令的输出如下所示:

我们需要在app-routing.module.ts文件中添加我们的路由路径,以便访问loginregister的路由。

我们正在使用模板驱动方法构建我们的表单,因此我们需要在我们的模板文件中做大部分工作。在开始修改我们的模板文件之前,我们需要将一个必需的模块导入到我们的app.module.ts文件中。

打开app.module.ts文件并添加以下代码行:

import {FormsModule} from '@angular/forms';

一旦我们将FormsModule导入到我们的app.module.ts文件中,不要忘记将其添加到ngModule内的导入列表中。

更新后的app.module.ts文件显示如下:

现在,让我们打开我们的登录组件模板文件,并在login.component.html文件中创建我们的登录表单。以下是我们将添加到模板文件中的代码:

<form #loginForm="ngForm" (ngSubmit)="login(loginForm.value)">
 <h3 class="text-center text-primary">Login</h3>
 <div class="form-group">
 <label for="username">Username:</label><br>
 <input type="text" [ngModel]="username" name="username" 
    class="form-control">
 </div>
 <div class="form-group">
 <label for="password">Password:</label><br>
 <input type="password" [ngModel]="password" name="password" 
   class="form-control">
 </div>

<button type="submit" class="btn btn-primary">Sign in</button>

 </form>

让我们深入分析上述代码。我们正在使用 HTML 输入元素创建一个表单,并向表单添加用户名、密码和提交按钮。需要注意的重要事项是,对于表单本身,我们告诉模板表单是ngFormngForm将把表单的所有输入元素组合到#loginForm模板变量中。对于输入元素,我们添加了ngModel属性,并为元素指定了name属性。

使用ngForm,我们现在可以轻松地检索表单内元素的值。由于我们已经定义了本地#loginForm模板变量,我们现在可以轻松地使用它的属性。loginForm具有以下属性:

  • loginForm.value:返回包含表单内所有输入元素值的对象

  • loginForm.valid:根据模板中应用的 HTML 属性验证器返回表单是否有效

  • loginForm.touched:根据用户是否触摸/编辑表单返回truefalse

在上述代码中,我们将loginForm.value传递给组件。我们可以将任何这些值传递给组件进行处理或验证。请注意,我们还调用了一个login方法,我们需要在我们的Component类文件中实现它。

现在,让我们在我们的Component类中创建一个方法来捕获来自我们的loginForm的数据。我们正在收集表单的值并在控制台中显示它:

import { Component, OnInit } from '@angular/core';
@Component({
 selector: 'app-login',
 templateUrl: './login.component.html',
 styleUrls: ['./login.component.scss']
})
export class LoginComponent {

constructor() { }

login(loginForm) {
 console.log(loginForm);
 console.log(loginForm.controls.username);
}
}

使用ng serve命令运行应用程序,我们应该看到以下截图中显示的输出:

记住,在典型的服务器端脚本中,我们过去常常为表单编写actionmethod属性。现在我们不需要再定义这些,因为它们在Component类中已经声明和使用了。

这是很好的东西和很好的进展。我们将继续使用前面的登录表单,并很快添加验证。让我们继续深入了解更多信息。

基于模型驱动的表单,或者叫做响应式表单

响应式表单也被称为基于模型驱动的表单。在基于模型驱动的表单中,模型是在Component类文件中创建的,并负责进行表单验证、处理数据等等。

Angular 在内部构建了 Angular 表单控件的树结构,这样更容易在数据模型和 UI 元素之间推送和管理数据。

我们需要在Component类中构建表单模型,通过创建构建块的实例(即FormControlFormGroup)来实现。此外,我们还在类中编写验证规则和验证错误消息。我们甚至在类中管理属性(即数据模型),而不是在 HTML 中使用数据绑定。

模板驱动的表单将表单的责任放在模板上,而响应式表单将验证的责任转移到Component类上。

在本章中,我们将同时使用这两个术语:基于模型驱动的表单和响应式表单,因为它们都指代同一件事情。

基于模型驱动的表单 - 优点

响应式表单在我们的应用程序中创建、验证和应用自定义表单验证非常有用。我们可以轻松地信任基于模型驱动的方法来完成通常与任何复杂表单相关的繁重工作。在本节中,我们将列出并了解在我们的应用程序中使用基于模型驱动的表单的优点:

  • 更灵活,适用于更复杂的验证场景和自定义复杂表单验证

  • 数据模型是不可变的

  • 由于数据模型是不可变的,所以不进行数据绑定

  • 使用表单数组动态添加输入元素更容易(例如,在任务表单上添加子任务)

  • 使用HostListenerHostBindings很容易将各种事件绑定到输入元素

  • 所有表单控件和验证的代码都在组件内部,这样模板会更简单、更易于维护

  • 更容易进行单元测试

基于模型驱动的表单 - 缺点

生活中所有美好的事物都有一些缺点。响应式表单也不例外。虽然使用响应式表单的优点和优势肯定可以超过缺点,但学习和理解在应用程序中使用响应式表单的缺点仍然很重要。在本节中,我们将列出在应用程序中使用模型驱动表单的缺点:

  • 初学者可能会觉得初始学习曲线太高

  • 开发人员应该了解与模型驱动表单一起使用所需的各种模块,比如ngvalidators等等

模型驱动表单 - 重要模块

我们使用 Angular 提供的两个强大类formGroupformControl来创建模型:

  • FormControl:跟踪单个表单输入元素的值和状态

  • FormGroup:跟踪一组表单控件的值和状态

  • FormBuilder:帮助我们使用它们的初始值和验证开发表单

就像我们在模板驱动表单中导入了FormsModule一样,我们需要在app.module.ts文件中导入ReactiveFormsModule

更新后的app.module.ts文件应该如下截图所示:

掌握了关于模型驱动表单方法的所有知识,现在是进行实际示例的时候了。

响应式表单 - 注册表单示例

在上一节中,我们在讲解模板驱动表单时,为我们的应用程序创建了登录表单。现在是使用响应式表单进行实际练习的时候了。使用不同方法实现登录和注册表单的基本想法是向您展示每种方法的实现差异。没有正确或错误的方法,决定是由应用程序中表单的复杂性和要求驱动的。

在本节中,我们将学习使用模型驱动方法实现我们的新用户注册表单。

首先,我们需要生成我们的register组件。运行以下ng命令来生成register组件:

ng g c register

上述命令的输出如下:

因为我们正在谈论模型驱动表单,所有的辛苦工作都必须在Component类中完成。我们仍然需要为我们的响应式表单准备一个模板,但我们不会在模板中添加任何验证或数据绑定。

我们希望我们的注册表单有四个表单元素,即全名、电子邮件地址、密码和条款与条件的字段。

让我们更新register.component.ts文件中的Component类,并创建一个formGroup实例:

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.scss']
})
export class RegisterComponent implements OnInit {

registerForm = new FormGroup({
  fullName: new FormControl(),
  emailAddress: new FormControl(''),
  password: new FormControl(''),
  termsConditions: new FormControl('')
 });
 constructor() { }

 ngOnInit() {
 }

 register()
 {
     console.log(this.registerForm.value);
 }

}

您会注意到在上面的代码中有很多新东西。让我们慢慢来,一步一步地。我们正在从angular/core中导入所需的模块FormGroupFormControl。在Component类内部,我们正在创建FormGroup类的一个实例registerForm。您会注意到我们现在正在创建多个FormControl实例,每个实例都是我们想要添加到我们的表单中的一个表单元素。

这就是我们需要做的全部吗?目前是的。请记住,如前所述,响应式表单也需要一个基本模板,但所有的逻辑和验证将在组件内部,而不是模板文件中。

现在,让我们更新我们的模板文件。在register.component.html文件中,添加以下代码:

<div>
   <form [formGroup]="registerForm" (ngSubmit)="register()">
 <h3 class="text-center text-primary">New User Registration</h3>
   <div class="form-group">
 <label for="fullName">Your Name</label><br>
   <input type="text" formControlName="fullName" class="form-control">
 </div>
 <div class="form-group">
 <label for="emailAddress">Enter Email Address:</label><br>
   <input type="text" formControlName="emailAddress" class="form-control">
 </div>
 <div class="form-group">
 <label for="password">Password:</label><br>
 <input type="password" formControlName="password" class="form-control">
 </div>
 <div class="form-group">
 <div class="form-check">
 <input class="form-check-input" type="checkbox" 
    formControlName="termsConditions" id="defaultCheck1">
 <label class="form-check-label" for="defaultCheck1">
 I agree to Terms and Conditions
 </label>
 </div>
 </div>
 <button type="submit" class="btn btn-primary">Sign in</button>

 </form>
</div>

在上面的代码中,我们正在创建一个动态的响应式表单。在上面的代码中,有许多重要的概念我们需要理解。我们在基于模型的表单中使用FormGroup属性。在基于模板的表单中,我们使用ngForm。请注意,对于每个表单元素,我们都提到了FormControlName属性,而此属性的值必须与在FormControl实例声明期间在Component类中提到的值完全相同。暂停一下,再读一遍最后几句话。

我们不再需要为元素提及ngModel,因为数据绑定已经紧密耦合在Component类本身内。我们还附加了一个ngSubmit事件,它将调用组件内实现的register方法,以在控制台上打印表单值。

太棒了。就是这样。现在使用ng serve命令启动您的应用程序,我们应该看到如下截图中显示的输出:

恭喜您使用 Angular 提供的方法成功启动并运行您的表单。我们已经学会了使用基于模板和基于模型的方法构建表单。在接下来的部分,我们将学习通过添加验证和自定义规则来扩展它们。

Angular 表单验证

到目前为止,我们已经了解到表单对于我们所有的应用程序是多么重要和关键。由于我们将处理来自用户的数据,确保我们接收到的数据是正确和有效的非常重要。

例如,当我们期望用户输入电子邮件地址时,我们不应该允许在电子邮件地址中输入空格或一些特殊字符。再举一个例子,如果我们要求用户输入电话号码,电话号码不应该超过 10 位数(当然不包括国家代码)。

我们可能希望在我们的表单中有许多这样的自定义有效检查点。

在本节中,我们将继续使用登录表单和注册表单,学习如何在模板驱动表单和模型驱动表单中添加验证。

模板驱动表单验证

打开我们使用模板驱动方法开发的登录表单。请记住,在模板驱动表单中,验证是在模板本身使用 HTML 属性进行的。

我们可以使用任何 HTML 属性,例如 required、maxlengthminlengthsizeemailnumberlength等,在表单中进行验证。我们还可以利用 HTML 模式属性在我们的表单元素中进行正则表达式检查。

我们可以利用各种类来实现表单验证:

  • ng-touched:输入控件已被访问

  • ng-untouched:输入控件尚未被访问

  • ng-dirty:输入控件数据已更改

  • ng-pristine:输入控件数据尚未更改/更新

  • ng-valid:输入控件数据是有效的,并使表单有效

  • ng-invalid:输入控件数据无效,因此表单无效

在模板驱动的表单中,Angular 会自动跟踪每个输入元素的状态以及表单的状态。因此,我们也可以在我们的 CSS/SCSS 中使用上述类来设计我们的错误通知,例如:

input.ng-invalid {
 border:2px solid red;
}

好了,现在我们已经了解了模板驱动表单中的验证,是时候更新我们的登录表单组件并使其更加时尚。我们将通过向表单元素添加验证来更新login.component.html文件。

<div>
 <form #loginForm="ngForm" (ngSubmit)="login(loginForm.value)">
 <h3 class="text-center text-primary">Login</h3>
  <div class="form-group">
 <label for="username">Username:</label><br>
  <input type="text" ngModel #username="ngModel" name="username" 
      placeholder="Enter username" required class="form-control">
  <span class="text-danger" *ngIf="username.touched && !username.valid"> 
     enter username </span>
 </div>
 <div class="form-group">
 <label for="password">Password:</label><br>
 <input type="password" [ngModel]="password" name="password" 
     required minlength="3" class="form-control">
 </div>
 <button type="submit" class="btn btn-primary" [disabled]="!loginForm.valid">
    Sign in</button>

 </form> 
</div>

让我们仔细看一下上面的代码。我们扩展了之前创建的登录表单。请注意,对于用户名表单控件,我们有 HTML 属性required,它将设置在表单控件上。如果用户没有为该字段输入任何值并且离开了该字段的焦点,使用ngIf条件,我们正在检查用户是否触摸了该字段,并且如果值无效,我们将显示错误消息。对于password字段,我们设置了其他 HTML 属性,如requiredminlength验证检查。如果表单控件数据无效,我们不应该启用表单,对吧?这就是我们通过向提交按钮添加disabled属性来做的。

现在让我们使用ng serve命令运行应用程序,我们应该看到输出,如下面的截图所示:

对于您的作业,请尝试在模板驱动表单中尝试这些用例:

  • 为用户名表单元素添加最小和最大长度

  • 添加一个新的表单元素,并添加验证,它应该是电子邮件格式

响应式表单或模型驱动表单验证

到目前为止,我们实现的所有验证都只是在模板文件中使用基本的 HTML 属性。在本节中,我们将学习如何在组件中使用模型驱动表单实现验证。

在之前的章节中,我们已经学会了在我们的Component类中使用formControlformGroup类创建表单。我们将继续使用相同的注册表单来扩展和实现验证。

我们通过在register.component.ts文件中添加验证来为我们的组件添加验证代码。看一下我们将在文件中添加的代码:

import { Component, OnInit } from '@angular/core';
import { FormGroup, Validators, FormControl } from '@angular/forms';

 @Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.scss']
})
export class RegisterComponent implements OnInit {
 registerForm = new FormGroup({ 
   fullName: new FormControl('',[Validators.required, 
   Validators.maxLength(15)]), emailAddress: 
   new FormControl('',[Validators.pattern('[a-zA-Z]*')]),
   password: new FormControl('',[Validators.required]),
   termsConditions: new FormControl('',[Validators.required])
 });

 constructor() { }

 ngOnInit() {
 }

 register()
 {
   console.log(this.registerForm.value);
 }
}

在上述代码中,您会注意到我们已经将所需的模块FormGroupFormControlValidators导入到我们的Component类中。我们已经导入并使用了FormGroupFormControlValidators模块是我们现在导入的唯一额外模块。我们将验证器作为选项传递给FormControl。对于fullname,我们将验证器添加为requiredmaxLength。请注意,我们可以为每个FormControl传递多个验证器。同样,对于电子邮件地址表单控件,我们正在传递一个验证器模式,其中包含正则表达式检查。我们已经在我们的组件中进行了所有必要的更改和验证。

现在是时候更新我们的模板register.component.html文件了:

<div>
   <form [formGroup]="registerForm" (ngSubmit)="register()">
<h3 class="text-center text-primary">New User Registration</h3>
   <div class="form-group">
<label for="fullName">Your Name</label><br>
<input type="text" formControlName="fullName" class="form-control">
</div>
<div class="form-group">
<label for="emailAddress">Enter Email Address:</label><br>
   <input type="text" formControlName="emailAddress" class="form-control">
</div>
<div class="form-group">
<label for="password">Password:</label><br>
<input type="password" formControlName="password" class="form-control">
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" formControlName="termsConditions" id="defaultCheck1">
<label class="form-check-label" for="defaultCheck1">
I agree to Terms and Conditions
</label>
</div>
</div>
<button type="submit" class="btn btn-primary" [disabled]="!registerForm.valid">Sign in</button>

</form>
</div>

HTML 模板与我们之前为我们的基于模型的表单创建的模板相同。我们为表单添加了一些功能。请注意,我们在提交按钮上添加了disabled属性,如果任何表单元素为空或无效,它将禁用表单。

看,我告诉过你,我们的模板文件只是一个占位符,几乎所有的操作都发生在我们的Component类中。

现在,让我们使用ng serve命令来启动应用程序,我们应该看到输出,就像下面的截图中显示的那样:

如果你看到了前面的截图,就跳到你的桌子上。因为我们现在已经学会并实现了使用模板驱动和基于模型的方法来创建表单。

如果你在整个章节中注意到了我们涵盖的示例,你也会注意到我们创建了处理表单数据的方法。

在下一节中,我们将专门关注这一点,并学习一些处理表单数据的最佳实践。

提交表单数据

到目前为止,我们已经学会了在我们的应用程序中设计和开发我们的表单。在本节中,我们将把事情带到下游系统,即捕获数据并处理数据。

Angular 在这两种方法中都生成了一个表单模型,无论是模板驱动表单还是响应式表单。表单模型保存了表单元素的数据和状态。

在之前的章节中,我们已经创建了一个方法来调用ngSubmit

对于我们的模板驱动登录表单,我们在login.component.ts文件中添加了以下代码:

login(loginForm)
{
  console.log(loginForm);
  console.log(loginForm.username);
}

我们将整个表单对象传递给登录方法。现在loginForm对象将包含表单控件的所有细节,以及状态。

在我们的注册表单中,我们使用了基于模型驱动的方法生成的实例formGroup,这个实例是在我们的Componentregister.component.ts文件中创建的。

以下是我们添加的用于捕获和处理数据的代码:

register()
 {
   console.log(this.registerForm.value);
 }

如果你注意到,对于响应式表单,我们不需要传递任何表单数据,因为我们已经创建了FormGroupregisterForm实例,所以它可以在我们的类中使用this运算符来访问。

一旦我们捕获了用户提供的数据,根据应用程序的要求,我们现在可以在组件内部实现我们的自定义逻辑。

一旦我们捕获数据,我们进行的一些常见活动如下:

  • 保护数据,以确保我们不允许垃圾数据进入我们的系统。

  • 处理/增强数据,例如将密码转换为加密值。

  • 检查是否有任何自动化机器人处理我们的应用程序。

  • 使用 Angular 服务向后端服务发出 HTTP 调用。我们有一个专门讨论这个特定主题的章节:第十二章,集成后端数据服务

这就结束了关于 Angular 表单的章节。我们涵盖了很多内容,我相信此时您一定会很兴奋地创建自己的表单,编写自定义验证并处理捕获的数据。

总结

表单是任何良好应用程序的核心和灵魂。我们首先学习了 Bootstrap 库提供的出色类和实用工具。我们详细探讨了form-groupform-control类。我们学习并实现了各种辅助和附加属性,以使我们的表单看起来和行为更好。

我们通过学习 Angular 提供的两种方法,即基于模板的表单和基于模型的表单,深入研究了 Angular 表单。

我们详细了解了每种方法的优缺点,并使用每种方法创建了我们的登录和注册表单。我们还探讨了我们在基于模板的表单和响应式表单中使用的各种类型的验证。

最后,但同样重要的是,我们学习了如何处理我们从表单接收到的表单数据。现在是时候展翅飞翔,创建您自己的精彩表单了。

在开发具有多个开发人员的复杂应用程序时,情况可能会失控。幸运的是,Angular 支持依赖注入和服务,这使我们能够创建可重用的服务并定义接口类。我们可以定义新的数据类型,并确保所有团队成员在不破坏彼此功能的情况下推送代码。我们将如何实现这一点?这将在下一章中介绍。继续阅读!

第十一章:依赖注入和服务

在本章中,我们将研究依赖注入DI)。虽然 DI 不是您必须直接在 Angular 中编程的东西(因为 Angular 会为我们处理所有 DI 管道),但了解它仍然非常有用。这是因为 Angular 在管理其服务时大量使用 DI,以及您在创建 Angular 应用程序时可能编写的任何自定义服务。

在下一章中,我们将研究 Angular 最重要的内置服务之一,即其 HTTP 服务,第十二章,集成后端数据服务。没有 HTTP 服务,我们的应用程序将非常无聊,因为它们将无法向外部来源(包括我们自己的后端 API)发送数据或接收数据。因此,本章将有助于我们更好地理解 Angular 如何将诸如其 HTTP 服务之类的服务注入到我们的应用程序中供我们使用。此外,这个主题是进入下一章的完美过渡。

以下是本章将涵盖的主题列表:

  • 什么是 DI?

  • 它解决了什么问题?

  • 使用 DI 的额外优势

  • 揭示 Angular 用于使一切正常运行的魔法

  • 我们如何防范代码最小化(以及为什么我们需要这样做)

在本章结束时,您将对这种经常被误解的软件设计模式有扎实的理解,更重要的是,它是如何工作的。我敢说,您甚至可能开始感觉比大多数同行更具技术先进性。许多开发人员有时甚至难以定义 DI——因为需要一些努力来理解它。

话不多说,让我们开始吧,通过注入更多软件设计知识来发现 DI 的全部内容。

什么是 DI?

不废话,DI 是控制反转IoC)设计模式的一个特定案例。

为了理解 DI 的高级定义,甚至是 IoC,我们首先需要快速定义设计模式。设计模式是软件设计中常见问题的可重用解决方案。有数十种软件设计模式,它们通常被分类为以下三个高级类别:

  • 创建模式

  • 结构模式

  • 行为模式

在我们的情况下,为了简洁起见,我们可以安全地忽略创建和结构类别的设计模式,因为 DI 是一种行为设计模式。在我们了解 IoC 设计模式之前,让我们先描述一下行为设计模式是什么。

简而言之,行为设计模式关注对象之间的通信方式。其中一种模式被称为观察者设计模式,它基本上规定了对象如何通知其依赖对象其状态何时发生变化。

另一个行为设计模式被称为发布-订阅设计模式,这是一种类似观察者模式但更加复杂的消息模式。另一个行为设计模式是模板方法。这种设计模式的目的是将算法的具体实现推迟到子类中。所有这些设计模式的总体思想都是它们之间的通信方式(即消息)。

拥有了模板方法的定义,我们离理解 DI 的本质更近了一步,但在我们开始之前,还有一个设计模式需要定义。你猜对了——IoC 设计模式。记住,DI 是 IoC 模式的一个特例,所以我们确实需要快速了解一下它是什么。

IoC 颠覆了典型的过程式或命令式代码流程。它不是由自定义对象的代码控制程序流程,而是将实例化的过程推迟到一个框架来完成。这一切马上就会变得清晰起来。有趣的是,有时候这被戏称为“不要打电话给我们,我们会打电话给你”。

我们很快将看一个例子,以便一切都说得通。然而,我需要定义一下我所说的框架实例化依赖对象的意思。你难道不喜欢我们需要了解的所有术语和概念吗?(笑)这个框架通常被称为 IoC 容器。这些容器足够智能,能够检查自定义代码,找出它依赖的其他对象,实例化这些对象,并将它们传递到自定义对象的构造函数中。这与传统方式相反,传统方式是在自定义对象本身内部实例化对象的依赖项。相反,IoC 容器为其执行这些职责。一会儿,我将把这与 Angular 联系起来,并给出 IoC 模式提供的一些非常重要的优势,但我们将从 DI 的角度来讨论——最后!

好的。让我们试着把这一切联系起来,并提供一个示例场景或用例。Angular 框架提供了 IoC 容器的功能——除了提供的所有其他功能之外。由于 Angular 是一个模块化框架,并且封装了大部分功能在分离的服务中,因此它的 IoC 功能也被封装在其中一个服务中——事实上,就是这种情况。

Angular 负责 DI 的服务是其注入器服务,恰如其名,因为它在实例化后将你的自定义类的依赖项注入到你的类构造函数中。不仅如此,它还为你调用自定义方法,回到我之前提到的,别打电话给我们,我们会打电话给你。我们所需要做的就是在自定义类的构造函数签名中列出依赖项的名称。

从现在开始,我不会再提 IoC,因为我们正在谈论 DI——再次强调,这在技术上不是 IoC,而是它的一个特例。我之所以提到这一点,是因为许多开发人员将 IoC 和 DI 视为同义词。

那么,让我们问几个问题:由于 DI 是一种设计模式,设计模式解决常见的软件设计问题,DI 解决了什么问题?DI 的优势是什么?这些都是很好的问题,我相信我可以在接下来的两段话中一举解答。

即使是面向对象的代码也存在一个很长时间的问题,那就是一个依赖其他类的类(这也是面向对象的重点——因为我们不希望一个类来完成所有的工作)在自身内部包含了实例化这些依赖关系的代码,并且结果是至少部分逻辑也与之交织在一起。这被称为紧密耦合的代码。紧密耦合的代码有两个问题:首先,实现逻辑通常封装在类内部——这是我们不想要的。我们不希望一个对象了解其他对象的内部工作。例如——如果我们想要更改依赖类中算法的实现,我们很可能也必须更改调用它的类中的代码。由此产生的另一个问题是,这种代码很难测试。我们的类耦合得越紧,对它们进行单元测试就越困难——这个问题已经存在了很长时间。

好的。那么 DI 是如何解决这些问题的呢?我们将会通过一个具体的用例来让我们更清楚地理解一切,但首先让我们描述一下 DI 给我们带来的一些优势。DI 原则的第一个优势是它强制我们编写解耦的代码。我们通过让我们依赖的类(用于其抽象实现)实现接口来实现这一点,我们这样做是因为我们调用的类只需要调用这些对象上的接口方法,而不关心底层类方法的实现细节。当我们以这种方式编写代码时,我们可以替换我们依赖的具有特定实现的类,用另一个具有另一种实现的类,而不需要更改我们的任何调用代码(因为我们的代码调用这些类实现的接口方法)。这有时也被称为按接口编码。还有一点有趣的是:这种技术也被用于一种称为面向方面编程(AOP)的编程风格中。

遵循 DI 设计原则所获得的一个非常有用的东西是,我们可以非常容易地测试我们的代码——与无法轻松测试我们的代码,或者根本无法测试我们的代码相比。我们如何做到这一点呢?通过编写存根和/或模拟类——这些类也实现了我们调用的这些相同的接口。

顺便说一句,存根和模拟之间有一个重要的区别。存根是愚蠢的类,通常只返回一个简单的值(通常是硬编码的)。另一方面,模拟对象通常具有完整的实现,以便测试边缘情况,以及进行数据库操作或进行 RESTful API 调用。模拟可以用来做任何你的测试需要的事情。所以,存根是愚蠢的,而模拟是聪明的。然而,它们的共同之处在于,它们通过具有相同的对象消息模式(也就是,它们的方法是通过接口调用的)来帮助我们对调用类的代码进行单元测试。

呼!我们完成了理论部分!你是不是已经睡着了,还是还在听我说话?啊,你醒着了——好的。现在所有的理论都已经讲完了,让我们来看一个使用 DI 的示例用例,以便我们可以将这些概念牢固地铭刻在我们的脑海中。

假设我们正在为一个在线商店构建一个电子商务应用程序,我们在这里出售我们自制的啤酒。我们的应用程序将需要一个购物车,我们还必须至少有一个商户账户(这是一个通道,被称为支付处理网关,这样我们就可以向我们的客户收取信用卡费用)。在这个假设的情景中,我们有两个商户账户——也许是因为我们想保留一个备用账户,以防主要的商户账户增加他们的折扣率(也就是费用),从而降低我们的利润——但重点是,我们有两个商户账户。

在实现购物车时,我们希望能够在不更改购物车类中的代码的情况下,将一个商家账户替换为另一个商家账户,如果需要的话。我们不希望更改任何代码的原因是,我们可能会在我们的应用程序(在线商店)中意外引入错误,这对顾客来说并不好看。你可能会说——嘿,我测试我的代码——所以错误都被找出来了——如果你这样说,那么你正好掉入了使用 DI 为我们的应用程序带来的下一个好处,那就是我们可以通过编写测试类轻松测试我们的应用程序——还记得我们的存根和模拟吗?是的——我们编写存根和模拟,这样我们就可以测试我们的代码。再次感谢 DI,我们不必更改我们的购物车类来实现这一点。我们的存根和模拟实现接口。我们会将银行的 API(即,由第三方编写的商家账户类)封装在一个实现我们接口的自定义类中,这样所有这些类(即我们的存根、模拟和封装的真实银行对象)都可以以完全相同的方式被调用。

很好。所以,作为一个额外的奖励,让我们快速看一下 Angular 如何知道我们的类需要什么,以及它如何为我们调用我们类的构造函数方法。嗯,这并不是魔术,但确实很巧妙。然而,Angular 确实需要我们的一点点前期帮助。当我们为我们的应用程序创建自定义类时,通常会将它们封装为 Angular 服务(我们将在下一章第十二章中看到服务,集成后端数据服务)。Angular 要求我们在其中注册这些服务,并且您将看到为什么我们需要在一会儿这样做。

Angular 的注入器服务扫描我们的代码,具体来说,扫描我们类的构造函数签名,并找出其参数。因为我们的参数是我们类需要的服务,它知道这些参数是服务。然后,它将服务名称的文本与自己的服务清单以及我们自己编写的任何自定义服务进行匹配,当找到匹配时,它实例化该服务对象。它之所以能够做到这一点,是因为它知道自己的服务,也知道我们编写的服务,因为我们必须在 Angular 中注册它们。

一旦 Angular 实例化了这些服务对象,下一步就是调用我们类的构造函数,并将对象作为参数传递进去。这就是 Angular 的注入器服务所做的注入过程。再说一遍:不要打电话给我们,我们会打电话给你。就像这样,Angular 背后的魔法已经被解释清楚了。不过,这仍然非常酷,我们应该向 Angular 开发团队致敬。

生成服务和接口

现在我们已经了解了 DI 和设计模式,在本节中,我们将学习如何创建我们的服务。Angular CLI 为我们提供了在项目内部生成服务的最快最简单的方法。我们将通过运行以下命令创建一个名为LearningDIServices的示例项目:

ng new LearningDIServices

我们使用ng命令创建一个新的 Angular 项目,并将项目命名为LearningDIServices。成功执行命令后,我们应该看到以下截图中显示的输出:

现在我们已经创建了项目目录,使用 Angular CLI,我们将生成一些服务和接口。我们将创建一个名为Photos的服务。运行以下命令,我们应该看到服务已添加到我们的项目目录中:

ng generate service photos

成功执行后,我们应该看到以下截图中显示的输出:

我们可以看到生成了两个新文件。一个是服务文件,另一个是用于编写服务测试的规范文件。让我们仔细看看包含自动生成代码的photo.service.ts文件:

import { Injectable } from  '@angular/core'; @Injectable({
 providedIn: 'root' })

export  class PhotosService { constructor() { } }

在前面的代码中,我们可以看到Injectable类需要从angular/core库中导入。Injectable类允许我们将服务注入到各种组件类中,以便我们可以重用方法。使用可注入的装饰器,我们明确指出服务需要在根中注入。最后,我们导出我们的PhotosService类,其中将包含我们将为我们的应用程序创建的构造方法和其他方法。

与 Angular 组件不同,无需更新app.module.ts文件以添加服务的条目。

在之前的章节中,我们学习了接口的概述。现在,让我们快速学习如何在我们的应用程序中使用接口。使用 Angular CLI,我们也可以快速创建接口:

ng generate interface photo

在上面的命令中,我们生成了一个名为photo的接口,一旦上面的命令成功执行,我们应该看到以下输出:

让我们仔细看看生成的接口文件。以下是默认生成的代码:

export  interface Photo { }

我们可以看到它是故意留空的。由于接口用于定义实体或模型类,应用程序中创建的每个接口都将是独特的,并且特定于每个应用程序。现在,如果我们想为我们的照片创建一个接口,我们将不得不定义如下:

export interface Photo {
 photoId: number;
 photoURL: string;
 photoOwner: string;
 isActive: boolean;
}

在上面的示例代码中,我们为照片创建了一个带有一些属性和它们的数据类型的接口。这将允许我们为照片创建严格类型的对象。

在本节中,我们学习了如何创建 Angular 服务和接口。即使一些概念不是很清楚,不要担心,我的朋友。我们有一个完整的章节专门向您展示如何在我们的应用程序中生成和实现服务。在下一章中,我们将学习如何实现和使用它们,并将它们集成到我们的组件中。

防止代码最小化

有一件我想很快覆盖的最后一件事,那就是代码缩小以及我们如何防范它。代码缩小是通过去除空格以及用非常短的符号替换变量名来压缩我们的代码的过程。这是在我们编译 Angular 应用程序时完成的,这样它就成为了一个更小的包,我们的用户必须下载(一旦我们部署了我们的应用程序)来检索我们的应用程序。但这对我们来说确实存在问题。它可能会通过更改参数名称来破坏我们的一天,然后 Angular 就无法再将名称与服务清单匹配。幸运的是,有一个简单的解决方案。如果我们在参数名称周围添加单引号,我们就可以保护我们的代码免受代码缩小的影响。怎么做呢?好吧,在服务名称周围加上引号会将它们转换为文字字符串,而缩小过程不会压缩或更改字符串——它们会保持原样。这是因为文字字符串在语法之外有意义,不是代码。缩小只是缩小代码(即变量和函数名称以及空格)。这就是你需要知道的关于保护你的代码免受代码缩小的影响的一切。

总结

现在你应该对 DI 是什么以及它解决了什么问题感到满意。你也应该能够列举一些优势,从而能够解释为什么 DI 是我们在设计应用程序时要遵循的一个好原则。你还应该能够轻松地解释 Angular 在使一切都能开箱即用方面表现出的看似神奇的技能。最后,你现在也应该知道如何保护你的 DI 代码免受代码缩小的影响。

掌握了这些 DI 知识,我们现在可以继续我们的旅程,探索 Angular 最有用的服务之一,即它的 HTTP 服务,在第十二章中,集成后端数据服务。一旦你完成了下一章,你就可以准备编写代码,将你的 Angular 应用程序与几乎任何符合 RESTful API 标准的应用程序和/或服务集成,只要你的应用程序被授权与之通信。这对你来说应该是令人兴奋的!如果是的话,翻页并继续你的 Angular 启蒙之旅。

第十二章:集成后端数据服务

欢迎来到第十二章!这绝对是我最喜欢的一章,因为我们将为我们的应用构建许多端到端的用例。

一个温和的警告——这一章内容密集——充满了大量的信息。你可能需要以较慢的速度阅读,并花更多时间在键盘上,比你在以前的章节中花的时间更多,但我必须说,这是非常值得的努力。

这是一个很好的方式来看待本书的整体进展:

  • 到目前为止,我们所看到的一切,包括最近的两章(第十章,使用表单,和第十一章,依赖注入和服务),都为这一章奠定了基础。有了这些知识,我们现在准备好把它们整合起来,以创建我们的应用。因此,从本质上讲,这一章也是为了回顾我们在以前章节中涵盖的许多主题。

  • 这一章对我们来说是一个关键的转折点,因为我们将把迄今为止学到的一切都用在这一章中构建我们应用的 95%。这是一个章节中的大量材料,但我们已经花了很多时间来讨论我们需要构建应用的所有 Angular 方面,所以我们将轻松地完成它。还有一些新的和略微离题的材料——学习如何构建后端 API——这比 Angular 材料更不重要。然而,我们需要有一个 API,所以我选择了一套简单的技术,可以快速上手。我们还要讨论这个问题,以帮助你了解我们将用来构建 API 的技术。

  • 在接下来的章节中,我们将为我们的应用添加一些东西(如路由守卫和自定义表单验证),并学习如何测试、调试、保护和部署我们的应用。

因此,从这个角度来看,我们已经准备好了。本章中的许多部分都是我认为重要学习的额外材料,因为我希望你不仅作为一个 Angular 开发者成功,而且作为一个网页开发者成功。这将帮助你提高你的技能,实际示例肯定会增加你作为网页开发者的技术知识。

我们将涵盖以下主题:

  • ListingApp - 概述

  • Angular 应用的基本概念

  • ListingApp - 技术要求

  • 为我们的应用构建 APIs

  • Google Firestore 数据库

  • Angular HttpClient

  • 集成后端服务

在这本书中,我们花了很多时间讨论了许多事情 - 主要是与 Angular 相关的(如组件、路由、flex-layout、NG Bootstrap、Angular Material 和处理表单),还有一些独立的事情(如线框、ES6、TypeScript 和 Bootstrap)。当然,拥有所有这些知识是很重要的,但我们还没有集成实时数据来使我们的 Angular 应用程序生动起来。然而,正如你从前面的项目列表中所看到的,这将发生改变。这就是 Angular 开发开始变得有趣的地方,也更加实用,因为一个不创建和使用数据的应用程序根本就不是一个应用程序。

好的。让我们立即开始学习构建任何应用程序基础的一些基本概念。然后,我们将看一下构建我们的 ListingApp 所涉及的步骤。

ListingApp - 概述

在本章中,我们将构建我们的ListingApp应用程序。在本节中,我们将介绍功能需求列表。我们的整体应用程序计划可以分为三个主要部分:

  • UI 层:UI 方面涉及设计或构建表单、显示数据、路由和验证。

  • 服务或中间件层:我们将学习如何编写共享服务,这些服务将负责与 API 和数据库进行后端集成。

  • 数据库或虚拟 API 设置:我们将学习如何使用 JSON Server 设置虚拟 API,并学习如何使用 Firestore 创建我们的 NoSQL 数据库。

这是我们将在本章学习过程中构建的功能用例的完整列表:

  • 显示所有列表

  • 按 ID 查看列表

  • 添加新的列表

  • 编辑列表

  • 删除列表

  • 添加评论

  • 更新评论

  • 删除评论

  • 编辑评论

列出的所有用例都需要我们实现 HTTP 调用。对于一些用例,我们需要进行 POST、GET 和 PUT HTTP 调用。

在我们进一步进行之前,现在是一个很好的时机,回顾我们在整本书中实施的所有学习和功能。我们需要回想一下我们如何设计和开发我们的表单,如何捕获表单数据,如何在组件模板中显示数据,如何使用参数实现路由,以及如何在组件内调用服务中实现的方法。

我们有很多工作要做,还有很多乐趣在等着我们,所以让我们开始吧!

Angular 应用程序的基本概念

在本章中,我们将学习和构建许多有趣的东西,但在开始之前,我们应该了解一些基本概念,包括强类型语言概念、Angular 模型、可观察对象、NoSQL 数据库和一般的 CRUD 操作。

强类型语言

强类型编程语言指的是每种数据类型都是预定义的,并且与变量紧密耦合。看看下面定义的变量:

int age = 10;

我们声明了一个变量,并明确指出变量的类型是整数,这使得很明显变量除了整数之外不能容纳任何其他数据类型。如果我们尝试提供任何不是整数的值,TypeScript 会抛出错误。TypeScript 也是一种强类型语言,因为我们在 TypeScript 中编写我们的 Angular 应用程序,我们可以得出结论,Angular 应用程序遵循强类型格式。

Typescript 接口

在本节中,我们将学习如何在 TypeScript 中创建我们自己的数据类型,这些类型可以在我们的 Angular 应用程序中使用。

Angular 模型是一种通过将多个数据类型组合成一个对象并定义一个新对象来创建复杂数据结构的方法,然后可以将其作为数据类型本身使用。这是 Angular 确保复杂数据对象遵守某些预定义数据规范的方式。

TypeScript 语言提供了接口,也具有相同的作用。我们还可以利用 ES6 类来定义我们的数据结构。我们可以扩展编程语法来创建我们自定义的数据类型。让我们通过创建一个示例模型来演示这一点。我们将创建一个名为Listing的模型,它将具有以下属性:

export class Listing {
 id: number;
 userId: number;
 title: string;
 status: string;
 price: number;
 active: boolean;
}

我们已经创建了一个 Angular 模型,这是一个具有属性的类,例如iduserIdtitlestatuspriceactive。现在我们可以在我们的应用程序中使用这个模型作为数据类型。我们可以将这个类导入到所有的组件和服务中,以确保我们的数据映射符合Listing数据规范。

在本章中,我们将在构建应用程序时使用先前定义的模型。

可观察对象

大多数传统应用程序都是基于请求和响应的架构运行的,这意味着我们的应用程序客户端会向服务器发出数据请求,而服务器会返回响应。在服务器返回响应的同时,我们的应用程序会进入等待模式,直到接收到所有响应,这显然会使应用程序变慢。

这种架构有多个缺点。首先,应用程序等待响应,这会导致应用程序延迟。其次,我们无法处理在一段时间内传入的多个数据。第三,由于我们的应用程序等待直到获得响应,这使得同步调用,我们无法执行异步编程。最后,事件处理对开发人员来说是一场噩梦。那么,我们如何解决上述问题?答案是使用可观察对象。

可观察对象是一种在一段时间内异步返回数据的数组类型。Angular 使用一个名为Reactive Extensions (RxJS)的第三方库,在框架内部实现了可观察对象,主要用于事件处理、树摇动等。我们还可以轻松导入、创建和订阅自定义可观察对象。

NoSQL 数据库概念

在本节中,我们将学习有关 NoSQL 数据库的知识。真的吗?NoSQL?我们不打算使用数据库来存储我们的关键数据吗?当然我们会使用数据库来存储我们的数据;但是,它不会是传统的关系型数据库,它具有严格的预定义模式和具有标准数据类型的列。使用 NoSQL 数据库,一切都是面向文档的,我们可以在一个地方存储数据,而不必担心数据类型。NoSQL 数据库保存文档集合。

我们仍然可以执行以下数据库活动:

  • 创建文档

  • 插入文档

  • 编辑现有文档

  • 删除文档

我们还可以执行许多高级功能,如索引和身份验证。有许多开源和商业解决方案提供 NoSQL 数据库。以下是一些 NoSQL 数据库提供商的快速列表:

  • MongoDB

  • Redis

  • RavenDB

  • Firestore

  • MemcacheDB

在本章开发我们的应用程序过程中,我们将实现 Firestore 作为我们的后端系统。在下一节中,我们将了解一些涉及这些数据库的重要任务。

CRUD 操作-概述

每当我们考虑将数据库作为应用程序的后端存储系统时,主要目标是能够添加、检索、搜索或修改数据,这更常被称为 CRUD 操作。

CRUD 代表计算机编程中的创建、读取、更新和删除,这些术语如下所述:

  • 创建:在数据库中创建或添加新数据。我们通常会在数据库中运行 INSERT 查询。这与 HTTP POST 方法相关联。

  • 读取:根据过滤器或搜索条件读取或检索数据。我们将在数据库中运行 SELECT 查询来执行此操作。这与 HTTP GET 方法相关联。

  • 更新:更新或编辑数据库中的现有记录。我们将在数据库中使用 UPDATE 查询。这与 HTTP PUT 方法相关联。

  • 删除:删除数据库中的现有记录。我们可以使用 DELETE 查询来删除记录,或者只是使用 UPDATE 查询设置一个指示记录已被删除的列。这与 DELETE 方法相关联。

在接下来的章节中,我们将使用这些概念来构建我们的ListingApp功能和我们应用程序的技术要求。

ListingApp - 技术要求

任何良好的动态应用程序都需要我们处理 API,并且我们需要将数据存储在数据库中。本节涵盖了构建任何动态应用程序所需的两个非常重要的技术方面 - JSON API 和动态数据库。我们将使用 JSON 服务器,而对于数据库,我们将使用 Google 的 Firestore 数据库。

为 ListingApp 构建 API

在任何项目的开发周期中,作为前端开发人员,我们将需要处理 API 并将其集成到我们的应用程序中。我们需要定义并就我们期望从 API 中得到的 JSON 合同达成一致。在本节中,我们将了解我们在后端开发人员仍在开发实际 API 时可以使用的各种生成 API 的选项。当我们有虚假 API 可用时,开发人员可以独立工作。

有各种各样的工具和库(可免费使用),我们可以用来处理虚假 API。我们将使用 JSON 服务器库来提供我们的 API。所以,让我们从以下步骤开始:

  1. 要安装json-server库,请在命令行界面中运行以下命令:
 npm i json-server --save

当命令成功运行时,您应该看到以下输出:

  1. 现在我们已经安装了json-server库,是时候创建我们的 API 和 JSON 结构了。在我们的项目目录中,我们将创建一个名为 APIs 的新文件夹,并创建一个名为data.json的新文件,其中将保存我们的 JSON 数据。创建文件夹和文件后,查看文件夹结构:

  1. 由于我们创建了两个 JSON 文件,现在是时候向文件添加一些列表和用户的 JSON 数据了。通过向listings.json文件添加以下数据来打开listings.json文件:
      {
        "listings": [
          { "id": 1, "title": "Sunset in New York", "price":"190", 
             "status": "Active" },
          { "id": 2, "title": "Dawn at Miami", "price":"150", 
              "status": "Active" },
          { "id": 3, "title": "Evening in California","price":"70", 
             "status": "Inactive" }
        ],
        "users": [
          { "id": 1, "username": "andrew", 
            "userEmail": "andrew@localhost.com" },
          { "id": 2, "username": "stacy", 
            "userEmail": "stacy@localhost.com" },
          { "id": 3, "username": "linda", 
            "userEmail": "linda@localhost.com" },
          { "id": 4, "username": "shane", 
            "userEmail": "shane@localhost.com" }
        ],
        "cities": [ 
            { "id":1, "name": "New York" },
            { "id":1, "name": "California" },
            { "id":1, "name": "Miami" }
        ]
       }

我们正在为列表、用户和城市创建 JSON 数组的虚拟数据。从技术上讲,在实际应用场景中,这些数据将在运行时从数据库中检索。

  1. 要开始提供带有数据的虚假 API,我们需要启动和初始化 JSON 文件。我们将转到我们创建了data.json文件的API文件夹,并运行以下命令:
 json-server --watch data.json
  1. 当我们成功运行命令时,应该看到以下输出:

请注意,在资源下,我们可以看到列出的虚假 API;即http://localhost:3000/listings

  1. 尝试在浏览器中打开 URL。您应该看到 JSON 数据显示为列表、用户和城市。输出显示在以下截图中:

太棒了!现在我们可以在我们的 HTTP 调用中使用这些 API。在我们直接学习 HTTP 功能之前,我们只需要再等待一个部分。对于那些全栈开发人员并且知道如何设置数据库的朋友来说,下一部分肯定是给你们的。我们将学习如何设置我们的 Firestore 数据库,用于存储我们的数据。稍后,我们将使用它来实现我们的应用程序。

Google Firestore 数据库

Google Firestore 数据库是 Google Cloud 平台的一部分。Google Cloud 的官方网站描述如下:

Cloud Firestore 是一个快速、完全托管的、无服务器的、云原生的 NoSQL 文档数据库,简化了在全球范围内为移动应用程序、Web 应用程序和物联网应用程序存储、同步和查询数据。参考:cloud.google.com/firestore/

Firestore 是由 Google 提供的作为服务的数据库,并提供易于使用的 NoSQL 文档数据库。由于 Firestore 也来自 Angular 的制造商,因此自然会有支持两者之间轻松集成的库。在本节中,我们将学习如何设置 Firestore 数据库。所以,让我们开始:

  1. 我们需要使用我们的凭据登录到我们的 Firebase 应用程序。成功登录后,我们应该看到欢迎屏幕,如下面的屏幕截图所示:

主页将列出我们在 Firebase 应用程序中创建的所有项目,您还会注意到一个大的“添加项目”链接。

  1. 现在,让我们通过单击“添加项目”链接为我们的应用程序创建一个新项目。我们将收到一个模态窗口的提示,需要为我们的项目输入一个“项目名称”,如下面的屏幕截图所示:

在这里,我们将输入“列表”作为我们的项目名称。一旦我们的项目被配置,我们将被带到新创建的项目页面。

  1. 现在,我们在侧边栏菜单中点击“数据库”。我们将被提示选择初始化数据库的模式。我们将为我们的测试选择测试模式,一旦我们执行了实现,我们将切换安全模式:

如前面的屏幕截图所示,我们正在使用测试模式中的数据库,这将使我们能够轻松地读取或写入文档。

如果您希望在生产环境中使用数据库,请不要忘记更改数据库的设置。

  1. 我们现在将继续创建我们的“评论”集合。我们将添加一个名为commentId的唯一标识符。此外,我们正在为将存储在集合中的文档添加三个字段作为模式,如下所示:

由于 Firestore 是一个 NoSQL 文档数据库,其模式不受任何数据类型的限制。我们现在可以执行 CRUD 操作,例如添加新文档,编辑,甚至删除 Firestore 数据库中的文档。

在过去的两个部分中,我们已经学习了如何使用 JSON Server 创建虚拟 API,并且还使用 Firestore 创建了一个 NoSQL 文档数据库。现在我们已经达到了一个阶段,我们已经学习了开始实现ListingApp端到端功能所需的所有基本概念,让我们进入 HTTP 世界吧!

Angular HttpClient

在本节中,我们将学习 Angular 最重要的方面——HttpClient。使用HttpClient接口,我们可以执行 HTTP 请求和响应调用。在上一章中,我们学习了依赖注入和服务;在本章中,我们将学习如何编写包含方法的服务,以便我们可以使用HttpClient进行 HTTP 调用和处理响应。

HttpClient是一个小巧、易于使用、功能强大的库,用于执行 HTTP 请求和响应调用。使用HttpClient,我们可以轻松地与后端服务进行通信,该模块支持大多数现代 Web 浏览器。HttpClient具有许多高级功能,如拦截器和进度事件。HttpClient支持各种 HTTP 方法,包括 GET、POST、PUT、PATCH、DELETE、JSONP 和 options。这些调用中的每一个都会返回一个 observable。我们必须订阅这些 observable 以处理响应。如果我们不订阅,将不会发生任何事情。

HttpClientModule位于@angular/common/http库中,需要被导入到app.module.ts文件中;否则,我们将遇到错误。

我们现在了解了HttpClient模块,但在我们开始在应用程序中实现该模块之前,了解一些被添加到HttpClient中的关键功能是很重要的:

  • HttpClient提供了强类型的响应体。

  • HttpClient中的请求/响应对象是不可变的。

  • JSON 格式的响应是默认的。我们不再需要将其映射为 JSON 对象。

  • HttpClient提供了拦截器,这在中间件中拦截HttpRequest以进行转换或处理响应非常有帮助。

  • HttpClient包括可测试性功能。我们可以轻松模拟请求并更有效地处理标头。

在接下来的部分中,我们将学习HttpClient模块,它需要被导入到组件或服务中,我们可以在那里进行 HTTP 调用。我们还将学习现代应用程序中可用的 HTTP 动词以及它们的目的。

HttpClient 和 HTTP 动词

如果前一部分是对HttpClientModuleHttpClient及其优势的介绍,那么在本节中,我们将深入了解并学习如何编写一些实现HttpClient的示例代码。

正如我们之前提到的,HttpClient支持 GET、POST、PUT、PATCH、DELETE、JSONP 和 options 方法,这些方法将返回可观察对象。HttpClient还提供了模块,可以使用HttpHeadersHttpParams轻松传递各种选项和数据。

为了使用HttpClient,我们需要将HttpClientModule导入到我们的应用程序模块(app.module.ts)文件中,还需要将HttpClient导入到我们的服务或组件中,并在构造函数中注入HttpClient,以便我们可以使用它进行 HTTP 调用。将以下代码添加到您的app.module.ts文件中,并不要忘记将其添加到导入模块的列表中:

// Import the module into the component or service
import { HttpClient } from '@angular/core/http';

// Inside the constructor method inject the HttpClient and create an instance
constructor(private http: HttpClient)

现在,让我们实现一些最常用的 HTTP 动词。

我们将分别为 JSON 服务器 API 和 Firestore 数据库实现 HTTP 方法。

HTTP GET

我们使用 HTTP GET 方法与后端服务通信,从特定 URL 资源中检索信息。获取所有列表的示例代码如下:

getAllListings():Observable<any>
{
   return this.http.get<Observable>('api/get-listing');
}

我们创建了一个名为getAllListings的方法,并明确指出该方法将返回任何数据类型的可观察值。我们需要将 URL 传递给 GET 方法。URL 是我们需要传递的必需值。我们还可以传递可选数据,如HeadersParamsreportProgressresponseType。GET 方法将返回 RxJS 可观察对象的实例,我们可以订阅以监听响应。

在类似的条件下,我们可以轻松地使用 POST、PUT 和 DELETE 方法创建 HTTP 调用。

HTTP POST

每当我们需要安全地向服务器发送任何数据或信息,例如用户名、密码和电子邮件时,我们总是使用 POST 方法。HTTP POST 动词总是与创建或添加新数据相关联。它是安全的,不像 GET 方法那样在 URL 中显示数据。在 POST 方法中,我们需要将数据传递给 URL,以及 URL 作为字符串。我们还可以向 POST 方法传递选项,例如 Headers 和 Params。以下是编写示例 HTTP POST 调用的示例代码:

addNewListing(listing) {
     let httpHeaders  = new HttpHeaders();
     httpHeaders.set('Content-Type', 'application/json');
     let options =  { headers: httpHeaders};

    return this.http.post('api/add-listing', listing, options);

}

在前面的代码中,我们正在创建一个名为addNewListing的新方法,它接受一个名为 listing 的参数,我们将使用它作为我们的数据。我们正在创建一个HttpHeaders的实例,所以我们创建了一个类的对象,并且我们正在将Content-Type对象的值设置为application/json。然后,我们正在创建变量选项并对其进行格式化以发送标头。最后,我们正在使用http.post方法进行 POST 请求。

HTTP PUT

在这一部分,我们将学习如何进行 HTTP PUT 调用。PUT 方法用于更新或编辑服务器中的现有数据集。HTTP PUT 方法涉及一个两步过程。首先,我们需要检索需要更新的数据,然后使用 POST 方法将更新后的信息传递回服务器。以下是创建 PUT 方法的示例代码:

this.http.put(url, options);

我们需要将 URL 作为 PUT 方法的必需参数传递。幸运的是,有各种可用的选项。例如,我们可以在选项中传递标头、参数等。

HTTP DELETE

DELETE 是 CRUD 功能的重要操作。我们可以使用 HTTP DELETE 方法轻松执行删除操作。delete操作可以根据用例和应用程序的合规性来实现。我们可以进行两种类型的删除操作——软删除和硬删除:

  • 软删除:在使用软删除时,我们不会从数据库系统中删除或擦除记录;相反,我们会更新记录并设置一个列或字段,并将其标记为已删除,以便用户不会看到这些记录。

  • 硬删除:请求的数据从数据库系统中永久删除。一旦数据被删除,就无法恢复或恢复。

让我给你举一个很好的例子。如果你试图删除你的谷歌账户,它会通知你,在x天内你可以回来恢复你的账户,之后数据将会从他们的服务器上完全删除。

回到我们的实现。我们可以使用http.delete()方法来实现应用程序中的 DELETE 功能。示例代码如下:

this.http.delete(url, options);

我们需要将 URL 值作为 PUT 方法的必需参数传递,而选项则是可选的。

通过承诺进行 HTTP。

Promises 只是对现实世界承诺的技术实现!假设你答应了老板你会完成分配给你的任务。如果你做到了,那意味着承诺已经实现,如果你没有,那意味着它被拒绝了。同样,HTTP 实现中的 Promise 意味着我们将等待未来的数据,无论是 resolved 还是 rejected,然后我们将根据收到的输出进行一些逻辑处理。

HTTP promises 是一种基于成功或失败状态的未来数据的占位符。这听起来是否类似于常规的 HTTP 调用?是的,它们是,但有一个重大的区别——promises 是异步的。当我们在 Angular 中进行 HTTP 调用时,它会等待直到请求完成并收到响应;JavaScript 将继续执行,如果遇到同步赋值/操作,它将立即执行并在它们依赖于先前状态或数据时失败。

一个 promise 接受一个回调方法,该方法将带有两个参数——resolverejectresolve意味着该方法将返回一个带有给定消息的 promise 对象,而reject意味着 promise 对象被拒绝了。然后,你可以期待.then.catch被调用,如果一切顺利或不顺利的话。以下是编写 promise 的示例代码,展示了对resolvereject的处理响应:

//check if the listing status is active
ListingDetails(listing){
let promise = new Promise(function(resolve, reject) {
if(listing.status == 'active') { 
  resolved("listing is active");
}
else {
  reject("listing is not active");
}

promise.then((s => { 
//next steps after the promise has returned resolved
}).catch((err => {
// what to do when it's error or rejected
})

}

让我们详细分析前面的代码。我们已经实现了一个 promise,并且按照规定,callback方法将会带有两个参数,resolvereject。我们检查列表的状态是否为活动状态;如果是,我们就会 resolve 这个 promise;否则,我们会 reject 这个 promise。默认情况下,resolved 方法返回的数据将会传递给.then方法,而任何失败或异常将会传递给.catch方法。

由于 promises 是异步的,这意味着我们可以链接事件或方法,继续添加一个将在.then方法内调用的方法。

太棒了!我们现在掌握了关于 Angular 提供的用于 HTTP 功能的类和模块的所有理论知识。我们了解了HttpClientModuleHttpClient,最重要的是,我们了解了我们可以在应用程序中使用的各种 HTTP 动词。我们还了解了 HTTP observables 和 promises。

现在,是时候动手写代码了。我们将学习如何创建我们需要使用 HTTP 调用集成的多个数据源。第一个将使用虚假的 JSON 服务器 API,而第二个将使用 Firestore 数据库。在下一节中,我们将学习并创建我们在开始端对端集成功能之前需要的服务。

集成后端服务

我们在这里取得了非常好的进展,所以让我们继续前进。软件开发中的最佳实践之一是创建可重用、通用和可维护的代码。在大多数动态应用程序中,我们需要进行大量的 HTTP 调用来根据应用程序的功能需求创建、保存、检索、编辑或删除数据。如果我们没有共享的 HTTP 调用,可能会导致有很多具有 HTTP 实现的方法,并且在长期内很难维护它们。我们如何解决这种情况?你已经知道答案了,我的朋友。没错——通过使用服务。在第十一章中,依赖注入和服务,我们学习了关于 Angular 服务和依赖注入的最佳实践。

Angular 指南明确规定所有 HTTP 调用和功能应该放在服务中,这样可以轻松地重用现有代码。Angular 服务是共享函数,允许我们访问其中定义的属性和方法。我们还将创建自定义服务,在其中实现我们的 HTTP 调用,并可以在各种组件中轻松重用。让我们创建两个服务——一个用于使用 JSON 服务器 API,另一个用于 Firestore 数据库操作。对于使用 JSON 服务器 API,我们将调用我们的DbOperationsService服务,对于使用 Firestore 数据库,我们将调用我们的CRUDService服务。这些服务中的每一个都将具有用于创建、读取、更新和删除数据的方法。现在,让我们运行以下ng命令,它将生成我们的服务:

ng generate service db-operations

在成功执行上述命令后,我们将执行以下命令来生成另一个服务。让我们称之为crud。我们将使用以下ng命令来生成该服务。

ng generate service crud

成功运行后,我们应该看到服务文件和它们各自的规范文件被生成。到目前为止,一切顺利。当我们开始端到端集成工作时,我们将需要这些服务。这可能看起来很复杂,但相信我,接下来的章节中所有这些都会有很多意义。

将 Angular HTTP 与后端 API 集成

这一部分非常重要,因为这是我们在整本书中学到的大部分主题的熔炉。我们将进行完整的端到端集成,从 UI 到服务,再到数据源。

我们需要生成我们将在应用程序中使用的组件。让我们运行以下ng命令来生成四个组件:

ng g component createListing
ng g component viewListing
ng g component deleteListing
ng g component updateListing

当这些命令成功运行时,我们应该看到以下截图中显示的输出:

现在我们已经生成了我们的组件,我们将利用在上一节中生成的DbOperationsService服务。我们还将使用我们使用 JSON 服务器创建的虚拟 API。我们将实现获取所有列表、查看特定列表、编辑现有列表以及最后删除列表的方法。为了实现这一点,我们需要将HttpClientModule导入到我们的app.module.ts文件中。我们还需要将HttpClient导入到我们的db-operations.service.ts服务文件中。我们还将导入HttpHeaders模块。这不是强制性的,但是出于良好的实践,我们将在进行 HTTP 调用时导入并使用它。我们将向db-operations.service.ts文件添加以下代码:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';

@Injectable({
    providedIn: 'root'
})
export class DbOperationsService {

constructor(private http: HttpClient) { }

getListings(){
    return this.http.get('http://localhost:3000/listings');
}
viewListing(id){
    return this.http.get('http://localhost:3000/listings/'+id);
}
addListing(newList){
    let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
    return this.http.post('http://localhost:3000/listings', newList);
}
editListing(id, newList){
    let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
    return this.http.put('http://localhost:3000/listings/'+id, newList);
}
    deleteListing(id){
    return this.http.delete('http://localhost:3000/listings/'+id);
}

}

让我们详细分析前面的代码。首先,我们正在导入所需的模块:InjectableHttpClientHttpHeadersHttpParams。然后我们将HttpClient注入到我们的构造函数中,并创建一个名为http的实例。然后,我们创建了四种方法,分别是getListingsviewListingeditListingdeleteListing。在getListings方法中,我们使用 HTTP GET 方法调用 API URL。这将从我们之前创建的data.json文件中返回所有列表。在viewListing中,我们传递 Listing 的 ID 以使用 HTTP GET 方法检索列表的数据。在addListing方法中,我们调用 API 并使用 HTTP POST 方法传递数据对象。这将在我们的 JSON 文件中创建一行新数据。接下来是editListing方法,它接受两个参数——列表的 ID 和我们需要保存的更新后的数据对象。最后一个方法是deleteListing,我们将传递要删除的列表的 ID。

在更实际的世界中,我们需要传递身份验证令牌、额外的安全性、清理数据等等。

我们现在已经制作了我们的自定义服务,其中包括将进行 HTTP 调用的方法。在我们开始处理组件之前,我们将创建一些路由,我们将在其中映射我们生成的组件。打开app-routing.module.ts文件,并在其中导入我们所有的组件。然后,我们需要将路由添加到其中,如下面的代码块所示:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {UpdateListingComponent} from './update-listing/update-listing.component';
import {CreateListingComponent} from './create-listing/create-listing.component';
import {ViewListingComponent} from './view-listing/view-listing.component';
import {DeleteListingComponent} from './delete-listing/delete-listing.component';

const routes: Routes = [
  {path:'create-listing', component:CreateListingComponent   },
  { path:'view-listing', component:ViewListingComponent },
  { path:'delete-listing/:id', component:DeleteListingComponent},
  {path:'update-listing/:id', component:UpdateListingComponent}
];

@NgModule({
 imports: [RouterModule.forRoot(routes)],
 exports: [RouterModule]
})
export class AppRoutingModule { }

在前面的代码中,我们正在更新我们的AppRoutingModule并添加五个路由。我们创建了create-listingview-listing路由,并将它们分别映射到CreateListingComponentViewListingComponent。这非常直接了当。对于delete-listingupdate-listing路由,注意我们传递了一个名为 ID 的参数。我们将使用这些参数传递列表 ID 以便删除或更新列表的数据。

现在我们已经创建了我们的服务和路由,它们已经准备好在我们的组件中实现。让我们开始处理我们的组件。首先,我们将从ViewListingComponent开始。打开view-listing.component.ts文件,并添加检索所有列表的功能,如下面的代码块所示:

import { Component, OnInit } from '@angular/core';
import {DbOperationsService} from '../db-operations.service';
import { Listing} from '../models/listing';
import {Observable} from 'rxjs';

@Component({
 selector: 'app-view-listing',
 templateUrl: './view-listing.component.html',
 styleUrls: ['./view-listing.component.scss']
})

export class ViewListingComponent implements OnInit {

 listArr: Observable<any[]>;
 viewList:Observable<Listing>;
 isViewPage: boolean = false;

 constructor(private dbOps: DbOperationsService ) { }

 ngOnInit() {
 this.dbOps.getListings().subscribe((data) =>  {this.listArr = data});
 }

 showListing(listing){
 this.isViewPage = true;
 this.dbOps.viewListing(listing.id).subscribe((data) => {this.viewList = data});
 }
}

让我们详细分析上述代码。首先,我们需要导入所有必需的模块和类。我们导入了我们创建的DbOperationsService。我们还导入了之前创建的 listing 接口类。由于我们将使用Listing接口类,我们需要从rxjs中导入Observable。接下来,我们将声明我们的选择器为app-view-listing;我们将在模板view-listing.component.html文件中调用这个指令。我们现在将创建三个变量,名为listArrviewListisViewPage。请注意,listArrviewList被声明为ObservablelistArrviewList变量之间的区别在于,listArr是 Listing 类型的 observable 并且是一个数组,而viewList是 Listing 类型的Observable并且将保存单个列表值。由于我们导入了一个服务,我们需要在构造方法中创建一个名为dbOps的实例。我们将在这里实现ngOnInIt方法;我们正在使用dbOps服务的实例调用getListings方法。我们正在订阅该方法,这意味着我们将把数据映射到listArr变量上。然后我们将使用listArr变量在模板文件中显示它。最后,我们正在创建一个showListing方法,我们正在将列表对象传递给它。使用服务的实例,我们正在调用viewListing方法并传递列表 ID。我们正在订阅数据并将其映射到viewList变量上。

现在,我们需要更新view-listing.component.html文件中的模板,并使用listArrviewList变量在页面中显示数据,如下面的代码块所示:

<h4>Show All Listings</h4>

<table class="table table-bordered"> 
 <tbody>
 <tr>
 <th>Title</th>
 <th>Description</th>
 <th>Price</th>
 <th>Status</th>
 <th>Actions</th>
 </tr>
 <tr *ngFor="let listing of listArr;let i = index">
 <td>{{listing.title}}</td>
 <td>{{listing.description}}</td>
 <td>{{listing.price}}</td>
 <td>{{listing.status}}</td>
 <td><a [routerLink]="'/update-listing/'+listing.id">Edit</a> | 
    <a [routerLink]="'/delete-listing/'+listing.id">Delete</a></td>
 </tr>
 </tbody>
</table>

在上面的代码中,我们创建了一个表格。使用ngFor,我们正在循环从 API 获取的数据,并使用插值在表格行中显示数据。请注意,对于锚标签,我们使用routerLink指令动态创建链接,并传递编辑和删除链接的 ID。

我相信你对最终结果感到兴奋。让我们运行ng serve命令。您应该看到以下输出:

太棒了!现在事情真的开始变得有意思了!看到代码在运行中没有比这更好的鼓励了。我们已经添加了“添加新列表”菜单链接,现在是时候在我们的createListing组件中实现该功能了。

打开createListingComponent,并通过向其中添加以下代码来修改create-listing.component.ts文件:

import { Component, OnInit } from '@angular/core';
import {DbOperationsService} from '../db-operations.service';

@Component({
 selector: 'app-create-listing',
 templateUrl: './create-listing.component.html',
 styleUrls: ['./create-listing.component.scss']
})

export class CreateListingComponent implements OnInit { 
 userId = 1;
 newListing;
 successMsg;

 constructor(private dbOps: DbOperationsService) { }

 ngOnInit() {
 }
 addNewList(listForm)
 {
  this.newListing = {
 "userId":this.userId,
 "id": 152,
 "title":listForm.title,
 "price":listForm.price,
 "status":listForm.status,
 };

 this.dbOps.addListing(this.newListing).subscribe((data) => {
 this.successMsg = data;
 });
}
}

让我们详细分析上述代码。我们正在文件中导入所需的模块。我们还导入了我们之前创建的DbOperationsService。我们创建了一些变量,即userIdnewListingsuccessMsg,并分配了一些初始值。我们创建了一个addNewList方法,并传递了listForm数据。我们还创建了一个类似于我们创建的列表模型的数据结构。接下来,使用服务的实例,我们调用addListing方法并传递我们需要保存的数据对象。这将在我们的data.json文件中创建一个新记录。最后,我们将结果映射到successMsg变量。我们将使用这个变量向用户显示成功消息。

由于我们使用的是虚拟 API,我们已经存根化了 ID 的值。在更实时的情况下,这个 ID 将在数据库端自动递增,并且始终是一个唯一的值。

现在,是时候更新我们的模板文件,以便我们可以使用表单从用户那里获取数据。打开create-listing.component.html文件,并将以下代码添加到其中:

<h4>Add New Listing</h4>
<p>
<div class="container">

<div *ngIf="successMsg">List Added Successful</div>

<form #listingForm="ngForm" (ngSubmit)="addNewList(listingForm)">
 <div class="form-group">
 <label for="title">Enter Listing Title</label>
 <input type="text" [ngModel]="title" name="title" class="form-control" 
    placeholder="Enter title">
 </div>
 <div class="form-group">
 <label for="price">Enter Description</label>
 <input type="text" [ngModel]="description" name="description" 
   class="form-control" placeholder="Enter Description">
 </div>
 <div class="form-group">
 <label for="price">Enter Price</label>
 <input type="number" [ngModel]="price" name="price" class="form-control" 
    placeholder="Enter price here">
 </div>
 <div class="form-group form-check">
 <input type="checkbox" [ngModel]="status" name="status" 
    class="form-check-input">
 <label class="form-check-label" for="status">Active?</label>
 </div>
 <button type="submit" class="btn btn-primary">Add New Listing</button>
</form>
</div>

在上述代码中,我们正在使用基于模板的表单创建表单。我们创建了一些表单字段来捕获数据,例如标题、描述、价格和活动状态。我们正在使用模板变量来引用表单和字段。我们还在ngSubmit事件上调用addNewList方法并提交整个表单。通过运行ng serve命令,我们应该看到以下输出:

现在,继续向表单字段添加一些数据,然后单击“提交”按钮。如果记录已成功创建,您应该会看到成功消息:

现在,点击菜单中的“获取所有列表”链接。您应该在表中看到新创建的记录显示在列表中。您还记得我们为列表添加了编辑和删除链接吗?现在是时候实现它们了。我们将首先实现编辑功能,然后再实现删除功能。

打开我们的更新列表组件,编辑update-listing.component.ts文件,然后将以下代码添加到其中:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from "@angular/router";
import {DbOperationsService} from '../db-operations.service';
import { Listing} from '../models/listing';
import {Observable} from 'rxjs';

@Component({
    selector: 'app-update-listing',
    templateUrl: './update-listing.component.html',
    styleUrls: ['./update-listing.component.scss']
})
export class UpdateListingComponent implements OnInit {

 listId;
 successMsg = false;
 viewList: Observable<Listing>;

 constructor(private route:ActivatedRoute, private 
   dbOps:DbOperationsService) { }

ngOnInit() {
    this.listId = this.route.snapshot.paramMap.get("id");
    this.dbOps.viewListing(this.listId).subscribe((data) 
     => {this.viewList = data});
 }
editListing(updatedList){
    this.dbOps.editListing(updatedList.id, updatedList).subscribe((data) => {
        this.successMsg = data;
    });
  }
}

让我们详细分析前面的代码。我们正在将所需的模块导入到我们的组件文件中。我们正在导入ActivatedRoute,我们的服务,列表接口类和可观察对象到组件文件中。为了实现更新功能,我们需要做两件事。首先,我们需要检索传递了 ID 的列表的数据。一旦用户更新了数据并单击“提交”按钮,我们将持久化该列表的数据。我们还需要将路由器和服务注入到我们的构造函数中。在ngOnInit方法中,使用路由器快照,我们正在从 URL 中捕获列表的 ID。然后,使用服务的实例,我们正在调用viewListing方法来获取基于传递的 ID 的列表的详细信息。最后,我们创建了一个editListing方法。使用服务的实例,我们正在调用editListing方法,因此我们需要传递两个参数,一个用于传递列表的 ID,另一个用于传递列表的更新数据。

现在,让我们更新我们的模板文件。打开update-listing.component.html文件并添加以下代码:

<div class="container">
<div *ngIf="successMsg">List Updated Successful</div>
<form #editlistingForm="ngForm" (ngSubmit)="editListing(editlistingForm)">
 <div class="form-group">
 <input type="hidden" class="form-control" name="id" 
    [(ngModel)]="viewList.id" ngModel #id>
 </div>
 <div class="form-group">
 <input type="hidden" class="form-control" name="userId" 
    [(ngModel)]="viewList.userId" ngModel #userId>
 </div>
 <div class="form-group">
 <label for="title">Enter Listing Title</label>
 <input type="text" class="form-control" name="title" 
    [(ngModel)]="viewList.title" ngModel #title required>
 </div>
 <div class="form-group">
 <label for="price">Enter Description</label>
 <input type="text" name="description" [(ngModel)]="viewList.description" 
    ngModel #description class="form-control" required>
 </div>
 <div class="form-group">
 <label for="price">Enter Price</label>
 <input type="number" [(ngModel)]="viewList.price" name="price" 
    class="form-control" ngModel #price required>
 </div>
 <div class="form-group form-check">
 <input type="checkbox" [(ngModel)]="viewList.status" 
   checked="{{viewList.status}}" name="status" ngModel 
   #status class="form-check-input" required>
 <label class="form-check-label" for="status">Active?</label>
 </div>
 <button type="submit" [disabled]="!editListingForm.valid" 
   class="btn btn-primary">Update Listing</button>
</form>
</div>

在上述代码中,我们再次基于模板驱动的表单方法创建了一个表单。您会注意到编辑表单与创建列表表单非常相似。你几乎是正确的,但有一些重要的区别。请注意,我们现在正在使用ngModel进行双向数据绑定,并将值绑定到表单字段。有了这个,当我们获取初始数据时,它会显示在表单字段中。现在,用户可以编辑数据,当单击“更新列表”按钮时,数据将被发送到addListing方法并持久化在后端 API 中。现在,让我们看看它的运行情况。通过运行ng serve命令,我们应该看到以下输出:

请注意,URL 中包含作为参数传递的列表的 ID。数据已被检索并显示在页面加载上。现在,当用户更新表单中的详细信息并单击“提交”按钮时,这将更新列表的数据。这是你的作业。

好了,我们已经实现了创建、编辑和查看功能。接下来,我们将实现列表的删除功能。请记住,对于删除和编辑功能,用户将始终通过单击锚标签导航到页面。打开DeleteListingComponent并更新delete-listing.component.ts文件,如下面的代码块所示:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from "@angular/router";
import {DbOperationsService} from '../db-operations.service';
import { Listing} from '../models/listing';
import {Observable} from 'rxjs';

@Component({
 selector: 'app-delete-listing',
 templateUrl: './delete-listing.component.html',
 styleUrls: ['./delete-listing.component.scss']
})
export class DeleteListingComponent implements OnInit {
viewList:Observable<Listing>;
listId;
successMsg:Observable<Listing>;

constructor(private route:ActivatedRoute, private dbOps:DbOperationsService) { }

ngOnInit() {
 this.listId = this.route.snapshot.paramMap.get("id");
 this.dbOps.deleteListing(this.listId).subscribe((data) => {
 this.successMsg = data;
 });
 }

}

让我们详细分析上述代码。我们在组件文件中导入所需的模块;即ActivatedRouteDbOperationsServiceListingObservable。我们还创建了一些变量——viewListListIdsuccessMsg。然后,我们将路由和服务注入到构造方法中。最后,使用ngOnInIt方法,我们传递需要删除的列表的 ID。我们订阅数据并将其映射到successMsg

在本节中,我们学习了如何为我们的ListingApp实现基本的 CRUD 操作。然后,我们学习了如何对 GET、POST、PUT 和 DELETE 方法进行 HTTP 调用。最后,我们学习了如何使用 JSON Server 创建虚拟 API。在下一节中,我们将学习如何使用云 NoSQL Firestore 数据库实现 CRUD 操作。

将 Angular HTTP 与 Google Firebase 集成

在本节中,我们将学习如何为 NoSQL Firestore 数据库实现 HTTP 功能。我们在之前的部分中创建了我们的 Firestore 数据库。现在是集成 Angular HTTP 调用的合适时机,它将调用并与 Firestore 数据库一起工作。

我们将实现哪些用例?对于我们的ListingApp,我们将需要一个评论系统。作为用户,我们应该能够添加、编辑、删除和查看评论。所有这些用例都将需要我们调用 API 来保存、检索和删除评论。

Angular Fire 是 Firebase 的官方库。该库提供了许多内置模块,支持诸如身份验证、与 Firestore 数据库的交互、基于 observable 的推送通知等活动。

我们需要在@angular/fire下安装此模块。在命令行界面中运行以下命令以安装库:

npm i @angular/fire 

当我们成功运行上述命令时,我们应该看到以下输出:

安装完库后,我们将继续创建一个新的自定义服务,用于与 Firestore 数据库集成。

运行以下命令生成一个新的服务:

ng generate service crudService

当我们成功运行上述命令时,我们应该看到以下输出:

您会注意到生成了两个文件。我们将在服务内实现所有我们的 HTTP 调用。正如我们之前提到的,我们需要创建一些组件,这些组件将映射到每个功能,并在内部调用具有 HTTP 实现的服务。

运行以下ng generate命令为评论功能生成组件:

ng generate component addComments

ng generate component viewComments ng generate component editComments ng generate component deleteComments

当我们成功运行上述命令时,我们应该看到以下输出:

您会注意到组件已经生成并添加到我们的项目目录中。您还会注意到app.module.ts文件已经更新,其中包含了组件的条目。

我们已经生成了我们的组件和所需的服务,还安装了 Angular Fire 库。为了在我们的应用程序中使用 Angular Fire 库,我们需要将该库导入到我们的app.module.ts文件中。将所需的模块导入到应用程序模块文件中,并在应用程序的导入列表中列出这些模块,如下所示:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule} from '@angular/common/http';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CreateListingComponent } from './create-listing/create-listing.component';
import { ViewListingComponent } from './view-listing/view-listing.component';
import { DeleteListingComponent } from './delete-listing/delete-listing.component';
import { UpdateListingComponent } from './update-listing/update-listing.component';

import {FormsModule} from '@angular/forms';

import { AngularFireModule} from 'angularfire2';
import {AngularFireDatabaseModule} from 'angularfire2/database';
import { AngularFireAuth } from '@angular/fire/auth';
import { environment } from './firebase-config';
import { AngularFirestore } from '@angular/fire/firestore';
import { AddCommentsComponent } from './add-comments/add-comments.component';
import { EditCommentsComponent } from './edit-comments/edit-comments.component';
import { ViewCommentsComponent } from './view-comments/view-comments.component';
import { DeleteCommentsComponent } from './delete-comments/delete-comments.component';

@NgModule({
  declarations: [
    AppComponent,
    CreateListingComponent,
    ViewListingComponent,
    DeleteListingComponent,
    UpdateListingComponent,
    AddCommentsComponent,
    EditCommentsComponent,
    ViewCommentsComponent,
    DeleteCommentsComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule,
    AngularFireModule.initializeApp(environment.firebaseConfig),
    AngularFireDatabaseModule,
    FormsModule
  ],
  providers: [AngularFirestore],
  bootstrap: [AppComponent]
})
export class AppModule { }

在上述代码中需要注意的一点是,我们正在从 Angular Fire 导入所需的模块,并在导入模块列表下列出它们。请注意,我们导入了一个名为firebase-config的文件。这些是环境变量,将保存用于与 Firebase 进行身份验证的 API 密钥。我们可以在 Firebase 帐户下找到列出的 API 密钥,如下面的屏幕截图所示:

我们需要将详细信息复制到firebase-config.ts文件中。以下屏幕截图显示了我们的ListingApp中指定的设置:

到目前为止,一切顺利。现在我们已经安装了所需的库,导入了模块,并完成了配置设置,现在是时候开始处理我们的应用程序组件了。我们在这里取得了很大的进展。让我们保持这种势头。

现在我们已经创建了我们的组件,我们将快速修改我们的app-routing.module.ts文件,并为每个组件创建一个新的路由。

我们已经掌握了 Angular 路由,在第四章 路由。如果需要快速复习,请重新阅读该章节。

在以下代码中,我们已经将所有所需的组件类导入到app-routing.module.ts文件中,并在路由文件中添加了相应的路由:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {UpdateListingComponent} from './update-listing/update-listing.component';
import {CreateListingComponent} from './create-listing/create-listing.component';
import {ViewListingComponent} from './view-listing/view-listing.component';
import {DeleteListingComponent} from './delete-listing/delete-listing.component';

import { AddCommentsComponent } from './add-comments/add-comments.component';
import { EditCommentsComponent } from './edit-comments/edit-comments.component';
import { ViewCommentsComponent } from './view-comments/view-comments.component';
import { DeleteCommentsComponent } from './delete-comments/delete-comments.component';

const routes: Routes = [
  { path:'create-listing', component:CreateListingComponent },
  { path:'view-listing', component:ViewListingComponent },
  { path:'delete-listing/:id', component:DeleteListingComponent},
  { path:'update-listing/:id', component:UpdateListingComponent},
  { path:'add-comment', component:AddCommentsComponent },
  { path:'view-comment', component:ViewCommentsComponent },
  { path:'delete-comment/:id', component:DeleteCommentsComponent},
  { path:'update-comment/:id', component:EditCommentsComponent}
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

我们将使用四个新创建的路由来在ListingApp中实现评论功能。我们将使用 Firestore 数据库添加 CRUD 操作。我们需要将AngularFirestore模块导入到我们的服务中,如下所示:

import { AngularFirestore } from '@angular/fire/firestore';

在我们将模块导入到我们的文件后,我们需要在constructor方法中注入它,如下所示:

constructor(private afStore : AngularFirestore, private route: Router ) { }

现在我们可以利用AngularFirestore模块并使用 Firestore 实现 CRUD 操作。查看crud-service.service.ts文件中的完整更新代码。

import { Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { environment } from './firebase-config';
import { AngularFirestore } from '@angular/fire/firestore';

@Injectable({
  providedIn: 'root'
})
export class CrudServiceService {

  constructor(private afStore : AngularFirestore) { }

  getComments() {
    return this.afStore.collection('comments');
  }

  deleteComment(id) {
    this.afStore.collection('comments').doc(id).delete();
  }

  addComment(newComment) {
    this.afStore.collection('comments').add(newComment);
  }

  updateComment(id, editedComment) {    
    this.afStore.collection('comments').doc(id).set(editedComment);
  }
}

让我们详细分析前面的代码。我们已经导入了所有必需的模块,包括我们的 Angular Fire 模块和我们的firebase-config文件。由于我们已经导入了AngularFireStore模块,我们需要将其注入到我们的constructor方法中并创建一个实例。我们为评论功能的每个操作创建了方法。在getComments方法中,我们正在从comments集合中检索所有数据。在deleteComment方法中,我们正在传递需要删除的评论的 ID。在addComment方法中,我们正在传递我们想要存储在我们的集合中的数据。在updateComment方法中,我们传递了两个参数;第一个是我们想要更新的评论的 ID,第二个是我们需要在数据库中持久保存的更新数据。

你可能会想为什么我们在这些方法中没有进行任何 HTTP 调用?AngularFireStore模块在内部对服务进行 HTTP 调用,并将从 firebase 配置文件中进行身份验证并获取特定于帐户的信息。

在早期的章节中,我们学习了如何从组件发送数据到服务,对吧?沿着同样的思路,继续尝试评论功能。这是你的家庭作业。

总结

你感觉如何?你应该感到很棒,应该为自己感到骄傲!这一章节是很多工作,但我们做完了会变得更好。它汇集了我们迄今为止学到的所有方面,如表单、组件、路由、服务等。

对于前端开发人员来说,在本地开发环境中设置一个虚拟 API 总是有助于我们独立工作,而不依赖后端开发人员或 API。我们学习了如何使用 JSON 服务器构建虚拟 API。我们学习了 NoSQL 文档数据库,特别是由谷歌云提供的 Firestore 数据库。我们深入研究了 Angular HTTP 的概念和功能。我们学会了如何进行 HTTP POST、GET、PUT 和 DELETE 调用。我们还使用 JSON 服务器和 Firestore 数据库实现了整个应用程序的功能用例。

到目前为止,我们取得了巨大的进步。我们现在能够端到端地开发 Angular 应用程序,利用 Angular 提供的所有超能力,包括表单、组件、服务、路由等等。在本章结束时,我相信我们能够将 Angular 框架的所有部分整合到一个正常运行的应用程序中。

拥有一个正常运行的应用程序是进步的一个好迹象。但在评估应用程序时的重要因素是查看质量检查或单元测试。

在下一章中,我们将学习如何编写单元测试,以确保在产品开发生命周期的早期发现任何缺陷。编写测试脚本可以确保质量,并且是处理应用程序的所有用例的一个很好的标志,包括应用程序的正常和负面路径。

第十三章:单元测试

您可能已经为传统的服务器端代码编写了单元测试,比如 Java、Python 或 C#。当然,在客户端,单元测试同样重要,在本章中,您将了解 Angular 测试,包括 Jasmine 和 Karma 框架,这两个优秀的工具用于对客户端代码进行单元测试。

我们将一起探讨如何对 Angular 应用的各个部分进行单元测试,例如组件、路由和依赖注入(DI)。

本章将涵盖以下主题:

  • Jasmine 和 Karma 简介

  • 测试指令

  • 测试组件

  • 测试路由

  • 测试依赖注入

  • 测试 HTTP

测试框架简介

在本节中,我们将学习两个重要的测试框架,即 Jasmine 和 Karma。

测试和开发本身一样重要。这是一个备受争议的话题,一些专家认为测试驱动开发(TDD)非常重要,这意味着在编写开发代码之前编写测试脚本非常重要。

Angular 框架的美妙之处在于它原生支持测试框架,并提供了许多测试工具,使开发人员的工作变得轻松愉快。我们一点也不抱怨。

Angular 为我们提供了一个核心测试模块,其中有很多我们可以利用的优秀类,并且原生支持两个重要的测试框架,即 Jasmine 和 Karma:

  • 我们使用 Jasmine 框架编写我们的测试脚本。

  • 我们使用 Karma 框架来执行测试脚本。

关于 Jasmine 框架

Jasmine 是一个领先的开源测试框架,用于编写和测试现代 Web 框架的自动化测试脚本。

当然,对于 Angular 来说,Jasmine 已经成为事实上的首选框架。以下摘自官方网站:

"Jasmine 是一个用于测试 JavaScript 代码的行为驱动开发框架。它不依赖于任何其他 JavaScript 框架。它不需要 DOM。它有一个清晰明了的语法,让您可以轻松编写测试。"

编写 Jasmine 测试脚本的理念是基于行为和功能驱动的。测试脚本有两个重要的元素——describe和规范(it):

  • describe函数用于将相关的规范分组在一起。

  • 规范是通过调用it函数来定义的。

以下是一个用 Jasmine 编写的示例测试脚本:

describe("Test suite", function() {
  it("contains spec with an expectation", function() {
    expect(true).toBe(true);
  });
});

在编写测试规范的过程中,我们必须使用大量的条件检查来匹配数据、元素、结果、断言条件等等。Jasmine 框架提供了许多匹配器,我们可以在编写测试规范时方便地使用。在前面的示例代码中,toBe 就是一个匹配器的例子。

以下是 Jasmine 中最常用的匹配器列表:

  • 等于

  • 为真

  • 为假

  • 大于或等于

  • 小于或等于

  • 已调用

  • 具有类

  • 匹配

我们将在接下来的几节中学习如何使用这些匹配器。好的,我们已经编写了我们的测试规范,那么现在怎么办?我们如何运行它们?谁会为我们运行它们?答案可以在下一节找到。

关于 Karma 框架

Karma 是一个测试运行器框架,用于在服务器上执行测试脚本并生成报告。

以下内容来自官方网站:

“Karma 本质上是一个工具,它生成一个 Web 服务器,针对每个连接的浏览器执行源代码与测试代码。针对每个浏览器的每个测试的结果都会被检查,并通过命令行显示给开发人员,以便他们可以看到哪些浏览器和测试通过或失败。”

Karma 框架被添加到我们的依赖列表中,因为它包含在 Angular CLI 安装中。在我们继续编写和执行测试脚本之前,验证我们是否已在package.json文件中正确安装了 Jasmine 和 Karma 是一个良好的实践。我们还可以验证正在使用的库的版本号。

我敢打赌你已经猜到这也是指定要使用的 Jasmine 和 Karma 的特定版本的地方。

在下面的截图中,我们可以验证我们已将 Jasmine 和 Karma 添加到package.json文件中的devDependencies列表中:

太好了。现在,是时候深入了解 Angular 测试概念并编写一些测试脚本了。

Angular 测试自动化

我相信你会同意测试自动化是产品开发中最重要的方面之一。在前面的部分中,我们探讨了 Jasmine 和 Karma 框架。在接下来的部分中,我们将通过一些实际示例来学习如何自动化各种 Angular 框架构建模块。我们将学习如何测试 Angular 组件、指令、路由等等。让我们开始吧。

测试 Angular 组件

在使用 Angular CLI 的过程中,我们已经生成了多个组件和服务。暂停一下,查看文件和文件夹结构。您会注意到,对于每个组件和服务,都生成了一个.spec.ts文件。

恍然大悟!Angular CLI 一直在为相应的组件和服务生成所需的外壳测试脚本。让我们在这里进行一个快速的实践练习。让我们生成一个名为auto-list的组件:

ng g component auto-list

Angular CLI 会自动生成所需的文件,并在所需的文件(AppModuleAngular.json等)中进行条目。

以下截图描述了 CLI 生成的测试规格:

仔细看一下生成的文件。您会看到为组件生成了以下文件:

  • auto-list.component.html

  • auto-list.component.spec.ts

  • auto-list.component.ts

  • auto-list.component.scss

我们对 Angular CLI 生成的 spec 文件感兴趣。spec 文件是为相应组件生成的测试脚本。spec 文件将导入基本所需的模块,以及Component类。spec 文件还将包含一些基本的测试规格,可以用作起点,或者作为我们的动力。

让我们更仔细地看一下在 spec 文件中生成的代码:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AutoListComponent } from './auto-list.component';

在上面的代码中,您会注意到所需的模块是从 Angular 测试核心导入的。这当然不是我们将使用的模块的最终列表,而只是基本的起始模块。您还会注意到新创建的组件AutoListComponent也被导入到我们的 spec 文件中,这意味着我们可以在 spec 文件中创建我们类的一个实例,并开始模拟测试目的的对象。很酷,对吧?继续看代码行,我们可以看到以下内容:

describe('AutoListComponent', () => {
    let component: AutoListComponent;
    let fixture: ComponentFixture<AutoListComponent>;
beforeEach(async(() => {
    TestBed.configureTestingModule({
    declarations: [ AutoListComponent]
 })
 .compileComponents();
 }));

beforeEach(() => {
    fixture = TestBed.createComponent(AutoListComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
});

在上面的代码中,您会注意到一些关键点。有一个describe语句,用于将相关的测试规格分组在一起。我们将在describe函数内创建测试规格。在 spec 文件中定义了两个beforeEach方法。

第一个beforeEach方法是一个异步 promise,它将设置我们的TestBed,这意味着在继续之前必须解决其中声明的所有内容;否则,我们的测试将无法工作。第二个beforeEach方法将为测试创建一个AutoList组件的实例。您会注意到调用fixture.detectChanges(),这会强制 Angular 的变更检测运行并影响测试中的元素。

现在,是时候了解实际的测试规范了,这是在规范文件中生成的:

it('should create', () => {
 expect(component).toBeTruthy();
 });

正如我们之前提到的,Jasmine 测试规范是写在it语句内的,这种情况下,只是一个简单的断言,用于检查组件是否存在并且为真,使用toBeTruthy匹配器。

这就是我们的规范文件。乐趣在于看到它的工作。让我们运行 Angular 为我们生成的默认测试。要运行 Angular 应用程序中编写的测试,我们在命令行界面上使用ng test命令:

ng test

如果你看到一个新窗口被打开,不要惊慌。您会注意到 Karma 运行器打开了一个新的浏览器窗口来执行测试,并生成了测试执行报告。以下截图显示了为我们的组件生成的测试规范的报告:

测试通过了。现在,让我们稍微修改一下脚本。我们将在组件中创建一个名为title的变量并赋值。在我们的测试规范中,我们将验证该值是否匹配。这是一个直接的用例,相信我,这也是您在应用程序中实现的最常见的用例。让我们打开app.component.spec.ts文件并在测试脚本中进行更改:

it(`should have as title 'testing-app'`, () => {
 const fixture = TestBed.createComponent(AppComponent);
 const app = fixture.debugElement.componentInstance;
 expect(app.title).toEqual('AutoStop');
});

在上面的代码中,我们正在编写一个测试规范,并使用TestBed创建了AppComponent的 fixture 元素。使用 fixture 元素的debugElement接口,我们获取了componentInstance属性。接下来,我们编写了一个expect语句来断言title变量的值是否等于AutoStop。很整洁。让我们尝试再写一个测试规范。我们要解决的用例是:我们有一个H1元素,并且我们想要断言它,如果H1标签内的值等于Welcome to Autostop。以下是相关的示例代码:

it('should render title in a h1 tag', () => {
 const fixture = TestBed.createComponent(AppComponent);
 fixture.detectChanges();
 const compiled = fixture.debugElement.nativeElement;
 expect(compiled.querySelector('h1').textContent).toContain('Welcome to 
  AutoStop');
});

在上述代码中,我们断言h1元素的textContent是否包含文本Welcome to AutoStop。请注意,在以前的测试规范中,我们使用了componentInstance接口,在这个测试规范中,我们使用了nativeElement属性。再次使用ng test命令运行测试。以下屏幕截图显示了生成的测试报告:

到目前为止,我们已经概述了 Jasmine 和 Karma 框架,还学习了如何运行我们的测试脚本。我们还了解了 Angular 为我们生成的默认 spec 文件,并学习了如何修改测试规范。

在接下来的章节中,我们将学习如何编写测试规范和脚本,以测试 Angular 内置指令、服务、路由等等。

测试指令

Angular 提供了许多内置的强大指令,如ngForngIf等,可以用于扩展原生 HTML 元素的行为和功能。我们在第七章中学习了关于 Angular 模板和指令的知识,快速回顾从未有过害处。Angular 为我们提供了两种类型的指令,我们可以用来开发和扩展元素的行为:

  • 内置指令

  • 自定义指令

本节的重点是学习如何编写用于内置 Angular 指令(如ngIfngForngSwitchngModel)的测试脚本。在开始编写测试用例之前,我们需要做一些准备工作,以更新我们的组件,以便我们可以开始编写测试用例。我们将编写一些变量,用于保存各种类型的数据。我们将使用ngFor在模板中显示数据,并使用ngIf编写一些条件检查。

如果您想快速复习 Angular 模板和指令,请参阅第七章 Templates, Directives, and Pipes

我们将继续使用在上一节中创建的相同组件AutoListComponent。让我们开始吧。我们的起点将是AutoListComponent类,所以让我们修改auto-list.component.ts文件:

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

@Component({
 selector: 'app-auto-list',
 templateUrl: './auto-list.component.html',
 styleUrls: ['./auto-list.component.scss']
})
export class AutoListComponent implements OnInit {

cars = [
 { 'id': '1', 'name': 'BMW' },
 { 'id': '2', 'name': 'Force Motors' },
 { 'id': '3', 'name': 'Audi' }
 ];

 tab = "1";

 constructor() { }

 ngOnInit() {
 }

 findAuto() {
     console.log("Method findAuto has been called");
  }

}

在上面的代码中,我们添加了一个名为cars的 JSON 对象类型的变量,并为其分配了数据。我们将通过在模板中显示数据来使用这些数据。我们还声明了一个名为tab的变量,并分配了一个值1。我们将在模板中使用tab变量进行条件检查。最后,我们添加了一个名为findAuto的方法,并在控制台中显示输出。

我们已经修改了我们的组件类。我们还需要更新我们的模板文件,以便在组件内部处理数据。以下是我们将在模板文件auto-list.component.html中添加的示例代码:

<h4 class="c2">ngFor directive</h4>
<ul class="cars-list">
 <li *ngFor="let car of cars">
 <a [routerLink]="[car.id]">{{ car.name }}</a>
 </li>
</ul>

<h4 class="c1">ngIf directive</h4>
<div *ngIf="cars.length" id="carLength">
 <p>You have {{cars.length}} vehicles</p>
</div>

<h4 class="c3">ngSwitch directive</h4>
<div [ngSwitch]="tab" class="data-tab">
 <p>This is ngSwitch example</p>
 <div *ngSwitchCase="1">ngSwitch Case 1</div>
 <div *ngSwitchCase="2">ngSwitch Case 2</div>
</div>
<hr>

<button (click)="findAuto()" id="btn">Click to findAutoDealers</button>

在上面的代码中,我们正在对模板文件进行更改。首先,我们使用ngFor指令循环行并显示汽车。接下来,我们添加了一个ngIf条件来检查汽车的长度是否大于 0,然后我们将显示carLength元素的计数。我们已经添加了一个ngSwitch指令来检查tab变量的值是否设置,并根据选项卡的值来相应地显示相应的选项卡。在我们的情况下,由于选项卡分配的值为1,我们将显示第一个选项卡。最后,我们添加了一个按钮,并将findAuto方法与单击事件相关联。

很好。我们的组件和模板已经准备好了,现在是时候编写一些良好的测试脚本来测试前面的逻辑,特别是 Angular 内置指令。我们将测试的一些用例包括测试 UI 中显示的汽车数量,测试哪个选项卡是活动的,验证元素内的内容等等。以下是一些用例,并且我们将学习如何为这些用例编写测试脚本:

用例#1:我们有一列汽车,我们想要验证总数为3

// ngFor test case to test the count is 4
 it('Should have 3 Brands coming from ngFor directive', async(() => {
 const fixture = TestBed.createComponent(AutoListComponent);
 fixture.detectChanges();
 const el = fixture.debugElement.queryAll(By.css('.cars-list > li'));
 expect(el.length).toBe(3);
 }));

在上面的代码中,我们正在创建AutoListComponent组件的 fixture。我们已经学会了如何使用debugElement来定位元素,并且在这个测试规范中,我们使用queryAll方法来获取具有className .cars-list > li的元素列表。最后,我们编写了一个expect语句来断言总数是否等于3

使用ng test命令运行测试。我们应该看到以下输出:

用例#2:我们要验证 HTML 元素内的文本是否包含vehicles键盘:

// ngIf test script
 it('Test ngIf directive in component', async(() => {
 const fixture = TestBed.createComponent(AutoListComponent);
 fixture.detectChanges();
 const compiled = fixture.debugElement.nativeElement;
 const el = compiled.querySelector('#carLength');
 fixture.detectChanges();
 const content = el.textContent;
 expect(content).toContain('vehicles', 'vehicles');
 }));

在上述代码中有一些重要的事情需要注意。我们继续使用组件AutoListComponent的相同装置元素。这一次,我们使用debugElement接口,使用querySelector方法来查找具有标识符carLength的元素。最后,我们编写一个expect语句来断言文本内容是否包含vehicles关键字。

让我们再次使用ng test命令运行测试。我们应该看到以下输出:

用例#3:我们想使用ngSwitch来验证是否选择了tab1,如果是,则显示相应的 div:

// ngSwitch test script
 it('Test ngSwitch directive in component', async(() => {
 const fixture = TestBed.createComponent(AutoListComponent);
 fixture.detectChanges();
 const compiled = fixture.debugElement.nativeElement;
 const el = compiled.querySelector('.data-tab > div');
 const content = el.textContent;
 expect(content).toContain('ngSwitch Case 1');
 }));

在上述代码中,我们继续使用AutoListComponent组件的 fixture 元素。使用debugElementquerySelector方法,我们正在使用className '.data-tab > div'来定位元素。我们断言ngSwitch条件是否为true,并显示相应的div。由于我们在组件中将选项卡的值设置为1,因此选项卡 1 显示在屏幕上,并且测试规范通过:

用例#4:测试AutoListComponent中定义的方法,并断言该方法是否已被调用:

// Test button is clicked
 it('should test the custom directive', async(() => {
 const fixture = TestBed.createComponent(AutoListComponent);
 component = fixture.componentInstance;
 fixture.detectChanges();
 spyOn(component, 'findAuto');
 component.findAuto();
 expect(component.findAuto).toHaveBeenCalled();

}));

在上述代码中,我们正在创建AutoListComponent组件的 fixture。我们使用spyOn方法来监听组件实例。我们正在调用findAuto()方法。最后,我们编写一个expect语句来断言findAuto方法是否已被调用,使用toHaveBeenCalled

使用ng test命令运行测试。我们应该看到以下输出:

在本节中,我们学习了如何编写单元测试脚本来测试 Angular 内置指令,例如ngForngIfngSwitch,最后,断言方法是否被点击和调用。

在下一节中,我们将学习有关测试 Angular 路由的知识。

测试 Angular 路由

很可能,您的应用程序中会有多个链接,以导航菜单或深链接的形式存在。这些链接在 Angular 中被视为路由,并且通常在您的app-routing.module.ts文件中定义。

我们在第四章中学习并掌握了如何使用 Angular 路由。在本节中,我们将学习如何编写用于测试 Angular 路由和测试应用程序中的链接和导航的测试脚本。

我们的应用程序需要一个漂亮的menu组件。使用ng generate component menu命令,我们将生成menu组件。现在,让我们转到menu.component.html并创建一个名为navbar的菜单,其中包含两个链接:

<nav class="navbar navbar-expand-lg navbar-light bg-light">
 <a class="navbar-brand" href="#">AutoStop </a>
 <button class="navbar-toggler" type="button" data-toggle="collapse" 
    data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" 
    aria-expanded="false" aria-label="Toggle navigation">
 <span class="navbar-toggler-icon"></span>
 </button>

<div class="collapse navbar-collapse" id="navbarSupportedContent">
 <ul class="navbar-nav mr-auto">
 <li class="nav-item active">
 <a class="nav-link" routerLink="/list-cars">Cars <span class="sr-only">
   (current)</span></a>
 </li>
 <li class="nav-item">
 <a class="nav-link" routerLink="/list-trucks">Trucks</a>
 </li>
 </ul>
 </div>
</nav>

前面的代码并不花哨,至少目前还不是。这是使用 Bootstrap 生成navbar组件的标准代码。仔细看,你会发现我们在菜单栏中定义了两个链接,list-carslist-trucks,它们的类是nav-link

现在我们可以围绕菜单功能编写一些测试规范,以测试navbar组件,其中将涵盖导航、链接计数等。

用例#1:我们需要测试navbar菜单是否恰好有两个链接。

以下是检查是否有确切两个链接的代码:

// Check the app has 2 links
 it('should check routerlink', () => {
 const fixture = TestBed.createComponent(MenuComponent);
 fixture.detectChanges();
 const compiled = fixture.debugElement.nativeElement;

let linkDes = fixture.debugElement.queryAll(By.css('.nav-link'));
 expect(linkDes.length).toBe(2);

});

在前面的代码中,我们正在为我们的MenuComponent组件创建一个固定装置。由于我们分配了nav-link类,因此很容易定位组件中对应的链接。使用debugElementqueryAll方法,我们正在查找所有类名为nav-link的链接。最后,我们正在编写一个expect语句来断言返回的链接数组的长度是否等于2

使用ng test命令运行测试。我们应该会看到以下输出:

这是测试我们菜单功能的一个良好开端。现在我们知道我们的菜单中有两个链接,我们想要测试的下一个用例是第一个链接是否为list-cars

以下是测试链接数组中第一个链接是否为list-cars的代码:

// Check the app has first link as "List Cars"
 it('should check that the first link is list-cars ', () => {
 const fixture = TestBed.createComponent(MenuComponent);
 fixture.detectChanges();
 const compiled = fixture.debugElement.nativeElement;

 let linkDes = fixture.debugElement.queryAll(By.css('.nav-link'));

 expect(linkDes[0].properties.href).toBe('/list-cars', '1st link should  
    go to Dashboard');
 });

在前面的代码中,我们正在为我们的MenuComponent组件创建一个固定装置。使用debugElementqueryAll方法,我们正在查找所有类名为nav-link的链接。我们将获得所有具有类名nav-link的链接。菜单中可能有多个链接,但我们感兴趣的是通过index [0]读取第一个元素的href属性,并断言该值是否匹配/list-cars

再次运行ng test命令。我们应该会看到我们的测试报告已更新,如下图所示:

好的,公平的。我们得到了一个线索,即list-cars菜单链接是菜单列表中的第一个。如果我们不知道我们正在搜索的链接的索引或位置会怎么样?让我们也解决这个用例。

看一下以下代码片段:

// Check the app if "List Cars" link exist
 it('should have a link to /list-cars', () => {
 const fixture = TestBed.createComponent(AppComponent);
 fixture.detectChanges();
 const compiled = fixture.debugElement.nativeElement;
 let linkDes = fixture.debugElement.queryAll(By.css('.nav-link'));
 const index = linkDes.findIndex(de => {
 return de.properties['href'] === '/list-cars';
 });
 expect(index).toBeGreaterThan(-1);
 });

需要注意的一些事情是,我们正在查找路由路径/list-cars的索引,并且我们还在使用分配的类nav-link,并使用queryAll方法获取所有匹配元素的数组。使用findIndex方法,我们正在循环数组元素以找到匹配href/list-cars的索引。

再次使用ng test命令运行测试,更新后的测试报告应如下所示:

在本节中,我们学习了各种方法来定位路由链接。同样的原则也适用于查找深链接或子链接。

这就是你的作业。

测试依赖注入

在之前的章节中,我们学习了如何编写测试脚本来测试 Angular 组件和路由。在本节中,我们将学习如何测试依赖注入以及如何测试 Angular 应用程序中的服务。我们还将学习如何将服务注入到 Angular 组件中,并编写测试脚本来测试它们。

什么是依赖注入?

依赖注入DI)在 Angular 框架中是一个重要的设计模式,它允许在运行时将服务、接口和对象注入到类中,从而实现灵活性。

DI 模式有助于编写高效、灵活、可维护的可测试和易于扩展的代码。

如果你需要快速回顾,请转到第十一章,依赖注入和服务,其中深入介绍和解释了 DI 机制。

测试 Angular 服务

在本节中,我们将学习如何通过服务和接口测试 Angular 依赖注入。为了测试一个 Angular 服务,我们首先需要在我们的应用程序中创建一个服务!

在 Angular CLI 中使用ng generate命令,我们将在项目文件夹中生成服务:

ng generate service services/dealers

成功执行后,我们应该看到以下文件已被创建:

  • services/dealers.service.spec.ts

  • services/dealers.service.ts

现在我们已经生成了我们的经销商服务和相应的测试规范文件,我们将在服务中添加一些方法和变量,以便在我们的测试规范中使用它们。导航到我们的服务类并更新dealers.service.ts文件。更新后的代码应如下所示:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class DealersService {
  dealers: any;

  constructor(private http : HttpClient) { }

  getDealers(){
    this.dealers = [
      { id: 1, name: 'North Auto'},
      { id: 2, name: 'South Auto'},
      { id: 3, name: 'East Auto'},
      { id: 4, name: 'West Auto'},
    ];

    return this.dealers;
  }

}

在上述代码中,我们进行了简单的更改,以便我们可以围绕经销商服务编写一些测试规范。我们定义了一个any类型的变量。我们正在定义一个getDealers方法,它将返回一个带有idname键对的 JSON 响应。好了,现在让我们想出一些用例来编写我们的测试脚本,比如获取经销商的数量,查找匹配的经销商等。

使用案例#1:当调用getDealers方法时,它应返回经销商列表,计数应等于4

以下是此测试规范:

it('Test Dependency Injection to get 4 dealers', () => {
const service: DealersService = TestBed.get(DealersService);
let dealers = service.getDealers();
expect(dealers.length).toBe(4);
});

使用案例#2:我们想要检查第一个经销商的名称是否为North Auto

以下是此测试规范:

it('Test if the first Dealer is North Auto', () => {
const service: DealersService = TestBed.get(DealersService);
let dealers = service.getDealers();
expect(dealers[0].name).toBe('North Auto');
});

太棒了!到目前为止,一切顺利。因此,我们已经学会了如何为我们新创建的经销商服务编写测试规范。这只是依赖注入的一部分。作为依赖注入的一部分,我们可能需要在运行时将其他所需的类注入到服务中。

让我们快速创建一个名为Dealers的类,并在其中定义两个变量,即usernamename。现在,让我们将此文件保存为dealers.ts

export class Dealers {

 constructor(
  public username: string = '',
  public name: string = ''
 ) {};

}

我们现在将在我们的经销商服务中包含新创建的类,并创建一个方法来初始化该类并创建一个对象来返回一些数据:

getDealerObject()
 {
 this.dealerObj= new Dealers('World','Auto');
 return this.dealerObj;
 }

这将引出我们下一个要测试的用例。

使用案例#3:测试通过已注入到服务中的类进行依赖注入。

看一下以下代码:

 it('Test if the dealer returned from object is World Auto', () => {
 const service: DealersService = TestBed.get(DealersService);
 let dealerObj = service.getDealerObject();
 expect(dealerObj.name).toBe('Auto');
 });

在上述代码中,我们创建了我们服务的一个实例并调用了getDealerObject()方法。我们断言返回的值是否与响应的name属性匹配Auto

我们正在调用服务中定义的方法,该方法在内部依赖于Dealers类。

使用案例#4:如果我们只想测试Dealers类的属性怎么办?

我们也可以测试。以下是此示例代码:


it('should return the correct properties', () => {
var dealer = new Dealers();
dealer.username = 'NorthWest';
dealer.name = 'Auto';

expect(dealer.username).toBe('NorthWest');
expect(dealer.name).toBe('Auto');

});

现在,让我们运行ng test命令。我们应该看到以下输出:

在同一行上,您可以编写测试脚本来测试您的服务、依赖类或接口类。

用例#5:在组件内测试 Angular 服务。

我们将继续测试 Angular 依赖注入。这一次,我们将把我们的服务导入到组件中,并验证它是否按预期工作。

为了实现这个用例,我们需要对AutoListComponent进行更改。

看一下我们将在auto-list.component.ts文件中进行的更改:

import { DealersService } from '../services/dealers.service';
constructor(private _dealersService : DealersService) { }
findAuto() {
 this.dealers = this._dealersService.getDealers();
 return this.dealers;
 }

在上面的代码中,我们将服务商服务导入到组件中。我们在构造方法中创建了服务的实例。我们添加了一个findAuto方法,它使用class _dealersService服务的实例调用getDealers方法。为了在我们的组件中测试服务,让我们通过添加以下代码修改auto-list.component.spec.ts文件:

import { DealersService } from '../services/dealers.service';
beforeEach(() => {
 fixture = TestBed.createComponent(AutoListComponent);
 component = fixture.componentInstance;
 fixture.detectChanges();
 service = TestBed.get(DealersService);
 });

在上面的代码中,我们已经将我们的服务商导入到AutoListComponent的测试规范文件中。我们在beforeEach方法中使用TestBed创建了服务的实例。现在我们可以开始编写我们的测试规范,以测试服务。在auto-list.component.spec.ts中添加以下代码:

it('should click a button and call method findAuto', async(() => {
    const fixture = TestBed.createComponent(AutoListComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    spyOn(component, 'findAuto');
    let dealers = component.findAuto();
    expect(dealers.length).toEqual(4);

  }));

在上面的代码中,使用组件的实例,我们调用findAuto方法,它将从服务返回数据。它期望计数等于4

使用ng test命令运行测试。我们应该看到以下输出:

在本节中,我们学习了各种测试 Angular 依赖注入的技术,包括服务、依赖类和在 Angular 组件内测试服务。

测试 HTTP

在第十二章中,集成后端数据服务,我们学习了如何集成后端服务,还学习了HTTPModuleHTTPClient。我们还学习了如何向服务器发出 HTTP 请求并处理响应。

在本节中,我们将学习如何编写测试脚本来测试 HTTP 请求和响应。我们将继续使用本章中创建的同一个项目——AutoStop 项目。在我们进一步进行之前,有必要准备好 REST API 端点,以便我们可以在我们的应用程序中使用它们。

我们将学习如何使用公共 API https://jsonplaceholder.typicode.com/,这在互联网上是免费的。我们还将创建一个本地服务器,从本地静态 JSON 文件返回模拟的 JSON 响应。

我们必须将HttpClientModuleHttpClientTestingModule导入到我们的app.module.ts文件中。

在我们继续编写用于测试 Angular HTTP 的测试脚本之前,我们需要更新我们在本章中一直使用的经销商服务。我们将实现一些方法,这些方法将进行 HTTP 调用 - POST/GET 以处理数据到 REST API 端点。

我们正在按照以下方式处理dealers.service.ts文件:

import { HttpClient } from '@angular/common/http';
import { HttpHeaders, HttpParams, HttpErrorResponse } from '@angular/common/http';
readonly REST_ENDPOINT = 'https://jsonplaceholder.typicode.com/users';
readonly DEALER_REST_ENDPOINT = 'https://jsonplaceholder.typicode.com/users/1';
private _carurl = 'http://localhost:3000/cars';

在上述代码中,我们正在导入所需的 HTTP 模块;即HttpClientHttpHeadersHttpParamsHttpErrorResponse,并定义了两个具有用户 API URL 和特定用户的 REST 端点。

我们也可以启动本地服务器。您可以使用 JSON 服务器创建本地 API。您可以在github.com/typicode/json-server了解更多信息。

是时候添加一些方法了,通过这些方法我们将对 REST 端点进行 HTTP 调用:

getAllDealers()
{
this.allDealers = this.http.get(this.REST_ENDPOINT,
{
headers: new HttpHeaders().set('Accept', 'aplication/json')
});
return this.allDealers;
}

getDealerById(){
let params = new HttpParams().set('id', '1');
this.dealerDetails = this.http.get(this.REST_ENDPOINT, {params});
return this.dealerDetails;
}

在上述代码中,我们正在创建两个方法,它们进行 HTTP GET 请求。第一个方法getAllDealers进行调用,并期望获得用户的 JSON 响应。第二个方法getDealerById将传递id1,并期望获得单个用户数据的响应。在getDealerById方法中,我们使用HttpParams来设置要发送到端点的参数。我们还将修改我们的autoListComponent组件,向我们的Component类中添加一些方法。

我们将向我们的auto-list.component.ts文件添加以下代码:

findAuto() {
 this.dealers = this._dealersService.getDealers();
 return this.dealers;
 }

listAllDealers(){
 this.allDealers = this._dealersService.getAllDealers();
 }

listDealerById(){
 this.showDealerInfo = true;
 this.dealerDetail = this._dealersService.getDealerById();
 return this.dealerDetail;
 }

getCarList() {
 this.carList = this.http.get<Cars[]>(this._carurl);
 }

在上述代码中,我们正在添加一些方法,即findAutolistDealerByIdgetCarList,它们进行了 HTTP 调用并调用了经销商服务中的方法。

好了,现在我们已经设置好了进行 HTTP 调用的组件和服务,我们可以开始编写我们的 HTTP 测试了。

用例#1:我们要测试是否对特定 URL 进行了GET调用。

我们将向auto-list.component.spec.ts文件添加以下代码:

// Test HTTP Request From Component
 it('Test HTTP Request Method', async(() => {
 const fixture = TestBed.createComponent(AutoListComponent);

 component = fixture.componentInstance; 
 httpMock = TestBed.get(HttpTestingController);

 let carList = component.getCarList();

 fixture.detectChanges();
 const req = httpMock.expectOne('http://localhost:3000/cars');

 expect(req.request.method).toBe('GET');
 req.flush({});

 }));

在上述代码中,我们正在创建AutoListComponent的实例,使用它来调用getCarList方法。在getCarList方法中,我们正在调用http://localhost:3000/cars的 URL 来检索数据。我们创建了一个名为httpMockHttpTestingController类的实例。使用httpMock实例,我们断言至少应该对该 URL 进行一次调用。

用例#2:我们希望期望结果返回的数据多于1

it('Test HTTP Request GET Method With subscribe', async(() => {
const fixture = TestBed.createComponent(AutoListComponent);
component = fixture.componentInstance;
component.listDealerById().subscribe(result => 
expect(result.length).toBeGreaterThan(0));

}));

在上述代码中,我们使用AutoListComponent的实例调用listDealerById方法。使用subscribe,我们正在映射结果并验证结果数据长度是否大于0

用例#3:我们想要验证从 HTTP 调用返回的数据是否匹配数据。以下是此用例场景的示例代码。

it('Test if the first Dealer is North Auto', () => {
const service: DealersService = TestBed.get(DealersService);
let dealers = service.getDealers();
expect(dealers[0].name).toBe('North Auto');
});

在上述代码中,我们使用DealersService实例调用getDealers方法。我们断言第一个索引属性名称的数据应为North Auto

使用ng test命令运行测试。我们应该看到以下输出,如下面的截图所示:

如果您看到了上述输出,那太棒了。

在本节中,我们学习了如何测试进行 HTTP 请求调用的组件、服务和方法。

摘要

测试是应用程序生命周期中的重要方面,编写测试脚本对于应用程序开发成功至关重要。我们首先概述了 Angular 支持的框架,即 Jasmine 和 Karma。我们学习了如何使用ng test命令运行测试。然后,我们学习了如何使用 Angular 自动生成的 spec 文件来为所有组件和服务编写测试脚本。

我们学习了如何编写测试脚本来测试 Angular 组件、内置指令、服务和路由。我们为内置指令编写了测试脚本,例如ngForngIfngSwitchngModel。我们还涵盖了用于测试 Angular 路由的用例。然后,我们创建了一个menu组件,并编写了测试脚本来测试menu组件的各种用例。

我们还探讨了测试依赖注入和服务。我们学习了各种用例,并为 Angular 服务和 HTTP 调用编写了测试脚本。

在下一章中,我们将探讨高级的 Angular 主题,如自定义指令和自定义表单验证。

继续阅读!

第十四章:高级 Angular 主题

在之前的章节中,我们学习了如何使用指令和表单验证器。在本章中,我们将通过自定义指令和自定义验证器来扩展我们的知识。我们还将学习如何使用 Angular 构建单页应用(SPA)。

此外,我们将探讨如何将身份验证集成到我们的 Angular 应用程序中,使用两个流行的身份验证提供者:Google Firebase 身份验证和 Auth0。

本章将涵盖以下主题:

  • 自定义指令

  • 自定义表单验证器

  • 构建 SPA

  • 用户身份验证

  • 使用 Firebase 身份验证进行身份验证

  • 使用 Auth0 进行身份验证

  • 客户端的连接

自定义指令

在本节中,我们将学习如何创建自定义指令。

首先,让我们了解什么是 Angular 指令。

Angular 指令是扩展 HTML 功能和元素行为的一种方式。

在之前的章节中,我们学习了并实现了许多内置指令,比如*ngIf*ngFor*ngSwitchngModel

在本节中,我们将学习如何创建我们自己的自定义指令来扩展 HTML 元素的功能。

用例:我们想为表单元素和onfocus创建一个自定义指令。背景颜色应设置为浅蓝色,边框为深蓝色,onblur事件应以红色突出显示。所以,让我们开始:

  1. 让我们使用ng命令生成指令:
 ng g directive onFocusBlur

运行上述命令后,屏幕上会显示以下内容:

请注意,指令文件已经生成,并且我们的app.module.ts文件也已更新,这意味着该指令可以在整个应用程序中使用,在任何组件中使用。

  1. 在指令文件on-focus-blur.directive.ts中,添加以下代码行:
      import { Directive } from '@angular/core';
      import { HostListener, HostBinding } from '@angular/core';

      @Directive({
      selector: '[appOnFocusBlur]'
      })
      export class OnFocusBlurDirective {

      constructor() { }

      @HostBinding("style.background-color") backgroundColor;

      @HostListener('focus') onFocus() {
        this.backgroundColor = '#19ffe4';
      }

      @HostListener('blur') onBlur() {
        this.backgroundColor = '#ff1934';
      }

      }

在上面的代码中,应注意以下重要事项:

  • 我们正在导入所需的模块,即DirectiveHostListenerHostBinding

  • 使用@directive装饰器,我们通过选择器定义指令的名称。

  • @HostBinding用于在元素上设置属性。

  • @HostListener用于监听宿主元素上的事件。

  • 在上面的示例中,我们绑定了样式背景颜色属性。我们可以在宿主元素上绑定任何样式、类或事件属性。

  • 使用@HostListener,我们监听事件,并使用onFocus改变背景颜色。通过使用onBlur,我们重置颜色。

现在,我们可以在应用程序的任何地方使用这个装饰器。

  1. 我们将在app.component.html文件中的表单控件输入元素中使用它:
      <input type="text" appOnFocusBlur class="nav-search" >
  1. 使用ng serve命令运行应用程序,并单击Input button。我们应该看到以下截图中显示的输出和行为:

很好。现在我们知道如何编写自定义指令,我们将继续尝试创建我们自己的自定义指令。

在下一节中,我们将学习如何编写自定义表单验证。

自定义表单验证

在之前的章节中,我们学习了表单和实现表单验证。我们使用了内置的表单验证或 HTML5 属性验证。但是,在更复杂的场景中,我们将需要实现自定义表单验证。这些验证因应用程序而异。在本节中,我们将学习自定义表单验证。简而言之,Angular 通过Validators模块为我们提供了各种选项,通过它们我们可以在 Angular 表单中实现表单验证。

以下代码示例中展示了使用验证器:

loginForm = new FormGroup({
 firstName: new FormControl('',[Validators.required, 
 Validators.maxLength(15)]),
 lastName: new FormControl('',[Validators.required]),
 });

在上述代码中,我们使用Validators模块应用了requiredmaxLength等验证。

现在,让我们学习如何创建我们自己的自定义表单验证。首先,我们将生成一个组件,在其中我们将实现一个表单和一些元素,以便我们可以应用我们新创建的指令:

ng g c customFormValidation

成功运行上述命令后,我们应该看到以下输出:

现在我们已经生成了我们的组件,让我们生成一个指令,在其中我们将实现自定义表单验证。

我们将实现一个自定义指令来检查 ISBN 字段。

什么是 ISBN? ISBN 是每本出版书籍的唯一标识符。

以下是 ISBN 号码所需的条件:

  • ISBN 号码应该正好是 16 个字符

  • 只允许使用整数作为 ISBN。

现在,使用ng命令,我们将生成我们的指令:

ng g directive validISBN

成功执行上述命令后,我们应该看到以下截图中显示的输出

valid-isbn.directive.ts文件中,添加以下代码行:

import { Directive } from  '@angular/core'; import { NG_VALIDATORS, ValidationErrors, Validator, FormControl } from  '@angular/forms'; 
@Directive({
    selector: '[validISBN]',
    providers: [
         { provide: NG_VALIDATORS, 
            useExisting: ValidISBNDirective, multi: true }
    ]
})  
export  class ValidISBNDirective implements Validator { static validateISBN(control: FormControl): ValidationErrors | null {       
 if (control.value.length <  13) {
 return { isbn: 'ISBN number must be 13 digit long' };        }
 if (!control.value.startsWith('Packt')) {
 return { isbn: 'Value should start with Packt' };        }
 return  null;
    }

    validate(c: FormControl): ValidationErrors | null {        return ValidISBNDirective.validateISBN(c);    }
}

让我们详细分析上面的代码片段。首先,使用ng CLI 命令,我们生成了一个名为validISBN的指令。Angular CLI 将自动生成所需的文件,并预填充基本语法。我们正在导入所需的模块,即NG_VALIDATORSValidationErrorsValidatorFormControl。我们正在将所需的模块作为我们的提供者的一部分注入。接下来,我们实现了一个名为validateISBN的方法,它接受FormControl类型的参数。我们将我们的表单控件字段传递给这个方法,它将验证表单控件的值是否与方法中实现的条件匹配。最后,我们在validate方法中调用validateISBN方法。

现在,我们可以在任意数量的地方使用这个自定义表单验证,也就是说,无论我们需要验证或验证 ISBN 号码的地方。让我们使用ng serve命令运行应用程序。我们应该看到以下输出:

到目前为止,在本章中,我们已经在一些情况下应用了一些开箱即用的想法,并学习了如何构建我们自定义的指令和自定义表单验证。我们还学会了如何轻松地将它们集成到现有或任何新的应用程序中。所有这些也可以成为单页应用的一部分。等等。什么?单页应用?那是什么?在下一节中,我们将学习关于单页应用的一切,并构建我们自己的单页应用。

构建单页应用

在本节中,我们将学习构建单页应用。

什么是单页应用?

单页应用是一种与用户交互的 Web 应用程序或网站,它通过动态重写当前页面与用户交互,而不是从服务器加载全新的页面。

把它想象成一个只有一个 HTML 文件的应用程序,页面的内容根据用户的请求动态加载。我们只创建在运行时动态渲染在浏览器中的模板。

让我给你一个很好的例子。

在第十五章中,部署 Angular 应用程序,使用ng build命令,我们生成了 Angular 应用程序的编译代码。

查看由 Angular 生成的编译源代码:

在上面的截图中,你将只看到一个名为index的 HTML 文件。

继续打开文件 - 您会发现它是空白的。这是因为 Angular 应用程序是单页面应用程序,这意味着内容和数据将根据用户操作动态生成。

可以说所有的 Angular 应用程序都是单页面应用程序。

以下是构建单页面应用程序的一些优势:

  • 页面是动态呈现的,因此我们的应用程序源代码是安全的。

  • 由于编译后的源代码在用户的浏览器中呈现,页面加载速度比传统的请求和响应模型快得多。

  • 由于页面加载速度更快,这导致了更好的用户体验。

  • 使用Router组件,我们只加载特定功能所需的组件和模块,而不是一次性加载所有模块和组件。

在本书的整个过程中,我们创建了许多 Angular 应用程序,每个应用程序都是单页面应用程序。

用户认证

在本节中,我们将学习如何在我们的 Angular 应用程序中实现用户认证。

在广义上,用户认证包括安全地将用户登录到我们的应用程序中,用户应该能够在安全页面上查看、编辑和创建数据,最后从应用程序中注销!

在现实世界的应用程序中,需要进行大量的额外检查和安全实施,以清理用户输入,并检查他们是否是有效用户,或验证会话超时的身份验证令牌,以及其他数据检查,以确保不良元素不会进入应用程序。

以下是一些重要的用户认证模块:

  • 注册新用户

  • 现有用户的登录

  • 密码重置

  • 已登录用户的会话管理

  • 一次性密码或双重认证

  • 注销已登录的用户

在接下来的章节中,我们将学习如何使用 Firebase 和 Auth0 框架实现上述功能。

使用 Firebase 进行用户认证

在本节中,我们将学习如何使用 Firebase 实现用户认证。

什么是 Firebase?

Firebase 是由 Google 提供的托管服务。Firebase 为我们提供了诸如分析、数据库、消息传递和崩溃报告等功能,使我们能够快速移动并专注于我们的用户。您可以在firebase.com了解更多有关该服务的信息。现在,让我们立即开始在我们的 Angular 应用程序中实现 Firebase。

第一步是创建一个谷歌账户来使用 Firebase 服务。您可以使用您的谷歌账户登录 Firebase。一旦您成功创建了 Firebase 账户,您应该会看到以下输出:

要创建一个新项目,请点击“添加项目”链接。

您将看到以下对话框窗口,提示您输入项目名称;在我们的情况下,我们正在将我们的项目命名为 AutoStop:

请注意,谷歌将为您的项目分配一个唯一的项目 ID。

现在,点击左侧菜单上的认证链接,设置用户认证功能,我们可以在我们的 Angular 应用程序中嵌入和设置:

我们可以在这里做很多其他很酷的事情,但现在我们将专注于认证模块。

现在,点击登录方法选项卡,设置如何允许用户登录到我们的 Angular 应用程序的选项:

在上述截图中,您将注意到以下重要事项:

  • 谷歌 Firebase 提供了各种选项,我们可以启用这些选项,通过这些选项,我们希望我们应用程序的用户登录。

  • 我们需要单独启用每个提供者选项。

  • 我们已在我们的应用程序中启用了电子邮件/密码和谷歌选项。

  • 为了启用 Facebook、Twitter 和其他应用程序,我们需要输入各自服务提供的开发者 API 密钥。

现在,在页面上向下滚动一点,您将看到一个名为授权域的设置选项。

我们将看到 Firebase 应用程序上设置了两个默认值,即 localhost 和一个唯一的子域,在下面的截图中显示:

我们已经做出了必要的更改。现在,我们需要设置 Google Firebase 的应用设置。现在是在我们的 Angular 应用程序中实现用户认证的时候了。

先决条件:我们期望用户已经有一个正在运行的 Angular 应用程序。

打开 Angular CLI 命令提示符;我们需要安装一些模块。我们需要先安装 Angular Fire2 和 Firebase:

请注意,Angular Fire2 现在是 Angular Fire。

我们需要运行以下命令在我们的应用程序中安装 Angular Fire:

npm install angularfire2 

在成功执行上述命令后,我们应该看到以下截图中显示的输出:

一切就绪。现在,我们需要创建一个处理身份验证功能的服务。

ng g service appAuth

使用ng命令,我们正在生成一个名为appAuth的新服务:

现在,是时候修改appAuth.service.ts文件并添加以下代码了:

import { Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { auth } from 'firebase/app';
import { Router } from '@angular/router';

@Injectable({
providedIn: 'root'
})
export class AppAuthService {

    private authUser:any;
    private authState:any;
    private loggedInUser = false;
    private userToken ='';

constructor(public afAuth: AngularFireAuth, private router :Router) { }

login() {
this.afAuth.auth.signInWithPopup(new auth.GoogleAuthProvider());

this.loggedInUser = true;

this.afAuth.currentUser.getIdToken(true).then(token => this.userToken = token);

this.afAuth.authState.subscribe((auth) => {
this.authState = auth;
});

this.router.navigate(['/profile']);
}

isLoggedInUser(){
if(this.userToken != '')
return true;
else 
return false;
}

logout() {
this.afAuth.auth.signOut();
this.loggedInUser = false;
this.userToken = '';
}

}

在上述代码中,我们正在对app-auth.service.ts文件进行更改。应注意以下重要点:

  • 我们正在将所需的类,即AngularFireAuthAuthRouter,导入到服务中。

  • 使用@Injectable,我们指定该服务在 Angular 树结构中的根级别注入。

  • 我们正在定义一些私有变量,我们将在整个应用程序中使用。

  • 在构造函数方法中,我们正在注入AngularFireAuthRouter类。

  • 我们正在定义三种方法:LoginLogoutisLoggedInUser

  • login方法中,我们正在使用this.afAuth实例,调用signInWithPopup方法,并传递auth.GoogleAuthProvider参数,该参数来自我们在本地安装的 Firebase 应用程序:

this.afAuth.auth.signInWithPopup(new auth.GoogleAuthProvider());
  • 当调用此方法时,将打开一个新窗口,在其中我们可以看到谷歌登录选项,使用它我们可以登录到应用程序。

  • 我们正在将this.loggedInUser变量设置为true

  • 我们将已登录用户的令牌设置为this.userToken变量。

  • 我们还订阅以获取authState响应。

  • 最后,使用路由器实例和使用navigate方法,我们将用户重定向到个人资料页面。

  • isLoggedInUser方法中,我们正在验证userToken是否已设置。如果用户已正确登录,userToken将被设置;否则,该方法将返回false

  • logout方法中,再次使用afauth的实例,我们正在调用signout方法,这将注销用户。

  • 最后,我们将userToken设置为empty

太棒了。我们已经在app-auth.service.ts文件中完成了所有繁重的工作。现在,是时候在我们的组件中调用这些方法了:loginprofilelog out

login.component.html文件中,我们将添加以下登录表单:

<div *ngIf="!_appAuthService.loggedInUser">
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">

<label>
First Name:
<input type="text" formControlName="firstName">
</label>

<label>
Last Name:
<input type="text" formControlName="lastName">
</label>

<button>Login</button>

</form>
</div>

在上述代码中,我们只是使用FormGroupFormControllers添加了一个 Angular 响应式登录表单。

登录表单的输出显示在以下截图中:

profile.component.ts文件中,我们只是调用了login方法:

onSubmit(){
 this._appAuthService.login();
 console.warn(this.loginForm.value);
 }

现在,在profile.component.ts文件中,我们添加了一个检查,以查看用户是否已登录:

<div *ngIf="_appAuthService.isLoggedInUser">
<p>
profile works!
</p>

User Token is {{_appAuthService.userToken}}
</div>

当用户导航到个人资料页面时,如果他们已登录,他们将看到详细信息;否则,用户将被重定向到登录页面。

现在,进入最后一部分;我们将在我们的app.component.html文件中有一个注销链接:

<nav>
 <a routerLink='/login' *ngIf="!_appAuthService.isLoggedInUser()">Login</a>
 <a routerLink='/register'>Register</a>
 <a routerLink='/logout' *ngIf="_appAuthService.isLoggedInUser()">Logout</a>
</nav>

我们正在添加带有*ngIf条件的链接,以在用户已登录或未登录时显示相应的链接:

 ngOnInit() {
 this._appAuthService.logout();
 this.router.navigate(['/login']);
 }

当用户点击注销链接时,我们调用appAuthService的注销方法,并在成功注销后将用户重定向回登录页面。

现在,让我们使用ng serve命令来运行应用程序。我们应该看到以下输出:

使用 Auth0 进行用户身份验证

在本节中,我们将学习如何使用 Auth0 实现用户身份验证。在我们继续在我们的 Angular 应用程序中实现 Auth0 之前,我们需要实现一些先决条件。让我们开始吧:

  1. 首先,我们需要在 Auth0.com 上创建一个帐户。成功登录到帐户后,我们应该看到以下仪表板屏幕:

我们将不得不注册我们的应用程序,以便我们可以创建所需的设置来在我们的应用程序中实现Auth0

  1. 点击左侧菜单上的“应用程序”链接:

  1. 现在,点击“创建应用”按钮创建一个应用:

  1. 我们需要输入应用程序的名称并选择我们正在构建的应用程序类型。在我们的情况下,这是一个单页 Web 应用程序,所以请继续选择该选项并点击“创建”按钮。

  2. 我们需要做的下一件事是更新应用程序的重要设置。因此,点击应用程序名称并导航到“设置”选项卡:

以下是一些需要牢记的重要事项:

  • 我们需要更新允许的回调 URL、允许的 Web 起源和允许的起源(CORS)。

  • 如果我们更新了允许的 Web 起源和允许的起源的详细信息,我们将收到跨源请求(CORS)错误。

我们已经在 Auth0 中调整了所需的设置,所以现在可以在我们的应用程序中实现 Auth0 了。

为了在我们的应用程序中实现 Auth0,我们需要安装一些模块,即auth0-jsauth0-lockangular2-jwt

在上述截图中,使用npm install命令,我们安装了所需的Auth0模块。现在,是时候为我们的应用程序生成服务和组件了。

首先,我们需要生成我们的服务;让我们称之为authService。我们需要运行以下命令来生成我们的服务:

ng g service services/auth

在成功执行上述命令后,我们应该看到以下输出:

我们可以验证并确认我们的服务已经生成,以及规范文件(用于编写我们的测试规范的文件)。现在我们已经创建了我们的服务,是时候生成组件了。我们将使用ng CLI 运行以下命令以生成所需的组件:

ng g c login
ng g c profile

在成功执行上述命令后,我们应该看到以下输出:

在上述截图中,我们可以验证并确认我们的所需组件,即loginprofile,已成功生成。现在,我们可以继续实现我们组件的功能了。

为了使我们的应用程序更美观,让我们也安装bootstrap CSS 框架:

npm i bootstrap 

我们还需要安装jquery模块:

npm i jquery 

在成功执行上述命令后,我们应该看到以下输出:

太酷了。现在,是时候在Nav组件中添加一些链接了:

<nav class="navbar navbar-expand-lg navbar-light bg-light">
 <a class="navbar-brand" href="#">Auth0</a>
 <button class="navbar-toggler" type="button" 
    data-toggle="collapse" data-target="#navbarSupportedContent" 
    aria-controls="navbarSupportedContent" aria-expanded="false" 
    aria-label="Toggle navigation">
 <span class="navbar-toggler-icon"></span>
 </button>

<div class="collapse navbar-collapse" id="navbarSupportedContent">
 <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#">Home 
         <span class="sr-only">(current)</span></a>
      </li>
      <li class="nav-item">
        <a class="nav-link" *ngIf="!authService.isLoggedIn();" 
           (click)="authService.login()">Login</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" *ngIf="authService.isLoggedIn();" >Profile</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" *ngIf="!authService.isLoggedIn();"
           href="#">Register</a>
      </li>
       <li class="nav-item">
        <a class="nav-link" *ngIf="authService.isLoggedIn()" 
           (click)="authService.logout()">Logout</a>
      </li>
    </ul>
 </div>
</nav>

在上述代码中,应该注意以下重要点:

  • 我们正在使用 Bootstrap 的nav组件。

  • 我们正在添加一些链接并附加点击事件,例如根据用户状态登录和注销。如果用户已登录,我们将显示注销链接,否则我们将显示注册链接。

  • 我们将在我们的 nav.component.ts 文件中实现这些方法。

  • 我们正在使用*ngIf来检查用户是否已登录,并相应地切换登录和注销链接。

上述代码的输出如下截图所示:

现在我们需要在我们生成的auth服务上工作。在services/auth.service.ts文件中,我们需要首先导入所需的模块,然后添加我们的方法loginlogout

import { tokenNotExpired } from 'angular-jwt';
import { Auth0Lock} from 'auth0-lock';

一旦我们导入了Auth0LockTokenNotExpired类,我们将创建实例以便我们可以使用它们。

看一下基本的Auth0Lock对象实例创建代码:

var lock = new Auth0Lock( 'YOUR_CLIENT_ID', 'YOUR_AUTH0_DOMAIN' );

为了创建一个Lock类的新对象,我们需要将客户端 ID 和域名传递给实例。

让我们在我们的auth.service.ts文件中实现这个:

public _idToken: string;
private _accessToken: string;
private _expiresAt: number;

 lock = new Auth0Lock('XvVLuuMQr3kKAR3ECAmBZOiPPyVYehvU','srinix.auth0.com',{
 allowedConnections: ["Username-Password-Authentication","google-oauth2"],
 rememberLastLogin: false,
 socialButtonStyle: "big",
 languageDictionary: {"title":"Auth0"},
 language: "en",
 responseType: 'token id_token',
 theme: {}
 });

在上述代码中,应该注意以下重要点:

  • 我们创建了三个变量,分别是_idToken_accessToken_expiresAt

  • 我们正在创建一个Auth0Lock的实例,并且需要向对象传递参数。

  • Auth0Lock对象将需要传递两个必需的参数。第一个参数是ClientId,第二个是域名。

  • 第三个参数包括allowedConnections、主题等选项,因为它说它们是可选的。

  • 客户端 ID 和域名可以从 Auth0 应用程序设置中获取,如下面的截图所示:

我们现在可以监听附加到lock对象的事件:

constructor(private router: Router) {

this.lock.on('authenticated', (authResult: any) => {
localStorage.setItem("userToken", authResult.accessToken);
this.router.navigate(['/profile']); 
});

this.lock.on('authorization_error', error => {
console.log('something went wrong', error);
});

}

在上述代码中,我们正在执行以下步骤:

  1. constructor方法中,我们正在监听authenticatedauthorization_error状态的on事件。

  2. 当我们从lock实例获得认证消息时,我们正在存储一个名为userTokenlocalStorage项目,并将accessToken设置为其值。

  3. 我们还在监听错误消息并将消息记录在控制台中。

现在,是时候实现loginlogout方法了:

login() {
 this.lock.show(function(err, profile, token){
 console.log(err);
 console.log(profile);
 console.log(token);
 });
 }

login方法中,我们正在调用lock对象的show方法。这将带您进入 Auth0 的对话框,其中有登录、注册或忘记密码的选项。如果您选择了任何社交选项,登录对话框将包含社交选项。

对于logout方法,我们只需清除用户登录时设置的userToken,并将用户重定向回主页登录页面。

logout(){
localStorage.setItem('userToken','');
this.router.navigate(['/']);
}

清除userToken后,应用程序将知道用户未登录。

我们已经实现了loginlogout方法,但我们还需要一个方法来检查用户是否已登录:

 isLoggedIn() {
 var token = localStorage.getItem('userToken');
 if(token != '')
 {
 return true;
 }
 else {
 return false;
 }
 }

isLoggedIn方法中,我们正在检查本地存储中userToken变量的值是否设置。如果设置了值,这意味着用户已登录;否则,用户未登录。

只需将服务导入到我们的app.component.ts文件中,并将其注入到构造函数中:

import { Component } from '@angular/core';
import { AuthService } from './services/auth.service';

@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.scss']
})
export class AppComponent {
 title = 'Auth0 Tutorial';
 userToken:string;

 constructor(private authService: AuthService) {}
}

就是这样。是不是很简单?

我们应该看到以下输出:

如果我们点击登录链接,我们应该看到 Auth0 对话框弹出窗口:

现在,继续点击“注册”选项卡创建一个账户,一旦成功注册,您应该看到该用户也已添加到 Auth0 仪表板中:

成功登录后,我们应该只能看到注销链接,如下面的屏幕截图所示:

当我们点击注销链接时,用户应该被带回默认的登陆页面,并应该看到登录和注册选项。还要注意 URL 中提供的参数,如access_token expires_in等。

太棒了!我们刚刚在我们的应用程序中使用 Auth0 实现了整个用户身份验证。

总结

在本章中,我们学习了一些高级的 Angular 主题,从创建自定义指令到扩展原生 HTML 元素的行为。我们还创建了自定义表单验证,这在开发具有许多验证和合规性要求的复杂应用程序时非常有用。我们深入研究了 Angular 单页应用程序,并了解了它们的工作和行为。我们通过原生代码在我们的 Angular 应用程序中实现了用户身份验证。

我们还学习了如何使用现有框架构建和实现安全的用户身份验证管理系统,即 Firebase 和 Auth0。然后,我们学习了如何实现登录、注册和注销功能,以确保我们可以保护应用程序的数据和功能。现在我们已经掌握了前面的概念,可以实现一个完整的、有线的端到端 Angular 应用程序了。

现在我们已经学会了如何开发我们的 Angular 应用程序,唯一隔我们的应用程序和真实用户之间的就是部署我们的应用程序。这是我们下一章的重点。在本书的下一章和最后一章中,我们将学习如何部署我们的 Angular 应用程序。

第十五章:部署 Angular 应用程序

一旦您完成了构建应用程序,它就必须部署到测试环境,供测试团队在将应用程序部署到生产环境供用户使用之前进行测试。虽然您可以在几乎任何地方托管您的应用程序,但有三种主要方式可以打包和部署您的 Angular 应用程序。我们将在本章中探讨这些方法:

  • 部署 Angular 应用程序

  • 部署复合 Angular 应用程序

  • 部署到 GitHub 页面

部署 Angular 应用程序

部署我们的应用程序和构建应用程序本身一样重要。毕竟,我们的用户需要访问它;否则,构建它就没有意义,对吧?

在我们详细学习和探索如何部署应用程序之前,有一个运行的服务器是前提条件。服务器可以托管在任何操作系统上,无论是 Windows 还是 Linux,并且可以在任何应用程序服务器上运行,比如 Apache Tomcat 或 IIS。或者,我们可以选择任何可靠的云提供商,比如 AWS、Azure 或 Bluehost,它们提供托管能力。

技术栈可以因项目而异;一些客户更喜欢基于 Java 的微服务,一些可能更喜欢.NET,其他人可能更喜欢 Ruby on Rails。我们需要将我们的 Angular 应用程序与后端 API 集成。客户端代码大部分将是 Angular,这基本上意味着 Angular 应用程序可以部署和运行在任何带有任何后端 API 服务的服务器上。

在本章中,我们将使用 XAMPP 服务器。XAMPP 是 Apache、MySQL 的免费分发版,可以轻松快速地设置我们的本地服务器。您可以在www.apachefriends.org/download.html下载它。

Angular 应用程序的编译选项

我相信您现在已经意识到,我们为 Angular 编写的所有代码都是 TypeScript,并且我们需要使用ng命令进行编译和生成可部署文件:ng build。这个命令将生成相应的等效 JavaScript 代码,可以直接复制到我们要部署的环境中。

部署 Angular 应用程序非常简单和容易。在实时场景中,构建和部署命令已经集成到构建管道中。一个常见的做法是在一个存储库中运行一个单一的 Angular 项目。然而,我们也可以在一个存储库中运行多个项目。

在本节中,我们将首先了解我们可以考虑用于部署 Angular 应用程序的各种编译选项。在接下来的章节中,我们将学习如何部署独立应用程序,以及如何部署复合 Angular 应用程序。在学习如何部署我们的应用程序之前,了解构建应用程序源代码时会发生什么是很重要的。

Angular 有两种编译选项,根据我们使用的命令和元标志来应用:

  • 即时编译

  • 提前编译

什么是即时编译?

Angular 的即时JIT)编译是指在运行时在浏览器中编译代码。这是每当我们运行ng build命令时的默认行为:

ng build

这种机制会增加请求和引导时间。更改会在我们的浏览器中反映出来,这在开发应用程序时非常好。这个选项允许开发人员在开发过程中快速测试更改。

什么是提前编译?

Angular 的提前AOT)编译意味着将源 TypeScript 代码、组件、Angular HTML、库和模块编译成本机 JavaScript,以便它可以在任何浏览器上平稳运行。换句话说,Angular 会在代码被浏览器下载之前进行转换。

让我们来看看 AOT 的一些好处:

  • 更好的安全性

  • 更快的渲染

  • 更小的框架和应用程序大小

  • 提前发现错误

提前编译或只是 AOT 编译在运行ng build --prod元标志时会默认应用:

ng build --prod

现在我们已经了解了 Angular 提供的不同类型的编译,现在终于是时候实际部署 Angular 应用程序了。在下一节中,我们将学习如何部署 Angular 应用程序。

部署独立的 Angular 应用程序

掌握了部署和编译策略的知识,现在是时候部署我们的 Angular 应用程序了。当我们运行ng buildng build --prod命令时,会生成本机 JavaScript 文件,我们可以部署到我们的服务器上。如果我们要部署单个项目应用程序,这是很好的。

在本节中,我们将学习如何部署更复杂的用例,比如当我们的 Angular 应用程序中有多个项目时。

为了使我们的读者能够轻松跟随这些步骤,我们将保持我们的应用程序简单。但是,您可以通过部署到目前为止开发的 Angular 项目来练习部署命令。让我们开始创建一个新的 Angular 应用程序:

  1. 要安装 Angular CLI,让我们快速使用以下命令:
 npm i -g @angular/cli

上述运行命令的输出如下所示。我们刚刚安装了 Angular CLI,我们将使用它来生成我们的应用程序:

  1. 既然我们已经成功安装了 Angular CLI,现在是时候创建一个名为 prod-ready 的 Angular 应用程序了:
 ng new prod-ready

使用上述命令,我们已经生成了一个新项目。以下截图显示了生成的输出:

太棒了!我们有了新生成的应用程序。

  1. 现在,让我们转到 prod-ready 应用程序文件夹,如下所示:
 cd prod-ready
  1. 全部完成。我们现在不打算更改或添加任何新组件。现在,我希望您了解部署应用程序的最简单方法。现在,使用 ng serve 命令启动应用程序:
 ng serve

上述命令将启动应用程序,我们应该看到以下截图中显示的输出:

  1. 启动浏览器,然后输入 http://localhost:4200。默认的原始应用程序应该显示如下:

太棒了。到目前为止一切顺利。我们在本地环境中让我们的应用程序正常工作,现在是时候将其部署到我们的应用程序中了!

为了让您对整个部署过程感到舒适,我们将部署原始应用程序,而不进行任何更改。

  1. 要部署,请运行以下 ng 命令:
 ng build --prod

一旦命令成功运行,您应该看到以下文件夹和文件已被创建。让我们看一下一些重要的注意事项:

  • 您应该注意到一个名为 dist/<defaultProject> 的新文件夹。

  • 您还应该注意到 dist 文件夹中创建的以下文件:

  • 运行时

  • 主要

  • 填充

  • 样式

上述 build 命令的输出如下。输出将位于 dist 文件夹中,与应用程序名称相同:

  1. 我们不一定要使用默认的文件夹名称;也就是说,我们可以将输出路径和文件夹名称作为参数提供,Angular 将在该文件夹中生成代码。很容易定制我们希望生成文件的输出目录:
 ng build --prod --output-path dist/compiled

运行上述命令,我们应该看到我们的自定义文件夹和文件在我们的文件夹中生成。在上述命令中,我们指定了我们希望我们的文件生成在名为compiled的文件夹中,并提供了路径。以下是命令成功运行后的屏幕截图:

这就是我们需要做的来生成和部署我们的 Angular 应用程序。只需将所有文件复制到服务器的根目录,就完成了。

在下一节中,我们将学习如何部署一个更复杂的 Angular 应用程序架构,然后我们将以多种方式部署复合应用程序。

部署复合 Angular 应用程序

在上一节中,我们学习了如何部署一个独立的 Angular 应用程序,这是相当简单的。然而,我们可能会遇到需要构建和部署多个应用程序并运行在单个存储库中的情况。这是可能的吗?当然可以。在本节中,我们将创建一个具有多个项目的 Angular 存储库,并学习如何部署一个复合应用程序。

创建和部署多个 Angular 应用程序

在更现实的实际应用程序中,我们将需要运行多个 Angular 应用程序,这些应用程序将由多个项目、库、模块和微服务组成,如下图所示:

在上图中,一些重要的事项如下所述:

  • 有多个 Angular 项目和应用程序。

  • 库 #1库 #2 可以通过导入库在多个项目中重复使用。

  • 在开发阶段,我们将创建多个模块,这些模块也可以在多个项目中重复使用。

因此,让我们立即开始创建多个项目、库和模块。最后,我们将以不同的方式打包应用程序。所以,让我们开始让我们的 Angular 应用程序运行起来:

  1. 首先要做的是,我们需要生成一个应用程序,我们将使用 Angular CLI 来生成应用程序。我们首先需要使用以下命令安装 Angular CLI:
 npm install @angular/cli

在成功执行上述命令后,我们应该看到以下输出:

  1. 现在我们已经安装了 Angular CLI,让我们使用以下命令创建应用程序。我们将其称为shopping-cart。现在,运行以下ng命令生成新项目:
 ng new shopping-cart

使用上述命令,我们生成了一个名为shopping-cart的新应用程序。上述命令的输出如下:

  1. 我们现在创建了一个名为shopping cart的新应用程序。让我们修改app.component.html并添加两个名为list-jacketslist-vendorsrouterLink超链接:
      <div style="text-align:center">
       <h1>
       Welcome to {{ title }}!
       </h1>
      </div>
      <ul>
       <li>
       <h2><a routerLink="/list-jackets" class="nav-link">List 
         Jackets</a></h2>
       </li>
       <li>
       <h2><a routerLink="/list-vendors" class="nav-link">List 
         Vendors</a></h2>
       </li>
      </ul><router-outlet></router-outlet>

在上述代码中,我们在app.component.html文件中创建了两个链接。结果显示如下:

到目前为止,一切都很好。基本上,我们已经有了一个正在运行的 Angular 应用程序。现在,我们将学习如何在同一个存储库中运行和部署多个 Angular 项目。为了做到这一点,我们将按照以下步骤进行:

  1. 让我们使用以下命令在同一个存储库中创建一个新应用程序。我们正在生成一个名为jackets的新应用程序:
 ng g application jackets

我们使用ng命令创建一个名为jackets的新应用程序。我们应该看到以下输出:

  1. 哇哦!使用 Angular CLI 的 schematics,很容易在同一个应用程序中创建多个项目。看一下自动生成的文件以及 Angular CLI 为我们更新的一些文件:

如果您仔细观察,您会注意到以下是我们应用程序结构和文件发生的一些重要变化:

  • 一个名为Projects的新文件夹被自动生成,并且在angular.json文件中生成了相应的条目。

  • Projects文件夹中,我们将看到具有相同默认 vanilla 应用程序文件的新Jackets项目已生成。

  1. 现在,为了验证是否已添加新的Jackets项目,请查看Angular.json文件:

您会注意到在Angular.json文件中,我们有针对 shopping-cart、shopping-cart-e2e、jackets 和 jackets-e2e 的项目特定条目。很棒。从技术上讲,我们现在在同一个存储库中运行两个应用程序。

  1. 现在是时候通过添加一些组件、库和模块来扩展我们的应用程序了。首先,我们需要在我们的jackets项目中创建一个组件。运行以下ng命令来生成组件:
 ng g c jacket-list --skip-import

运行上述命令,我们应该看到生成的组件和相应文件。我们应该看到以下输出:

  1. 现在我们已经在Jackets项目中创建了一个新的组件,是时候将其添加到app-routing.module.ts中,以便在Jackets项目中可以使用。

在以下代码片段中,我们在app-routing.module.ts文件中导入了新创建的组件:

      import { NgModule } from '@angular/core';
      import { Routes, RouterModule } from '@angular/router';
      import { AppComponent } from './app.component';
 import { JacketListComponent } from 
      '../../projects/jackets/src/app/jacket-list/jacket-list.component';
      import { VendorsComponent } from 
      '../../projects/vendors/src/lib/vendors.component';
  1. 导入组件后,是时候为我们的组件创建一个路由了:
      const routes: Routes = [
       {
       path:'home',
       component:AppComponent
       },
       {
       path:'list-jackets',
       component:JacketListComponent
       },
       {
       path:'list-vendors',
       component:VendorsComponent
       }
      ];

在上述代码片段中,我们创建了list-jacketslist-vendors路由,它们分别映射到相应的JacketListComponentVendorsComponent组件。在上述代码片段中有两个重要的事项需要注意:

  • 我们正在运行多个 Angular 项目。

  • 我们正在在各个项目中相互链接组件。

  1. 我们已经将路由链接添加到app.component.html。现在,让我们通过运行ng serve命令启动我们的应用程序:
 ng serve 
  1. 在浏览器中输入http://localhost:4200,我们应该看到以下输出显示:

所以,现在我们有两个运行的应用程序,并且我们有跨不同项目共享的组件。

很好。现在,为什么我们不添加一些可以在多个项目之间共享的库呢?让我们开始吧:

  1. 我们将创建一个名为vendors的新的 Angular 库。我们将使用ng命令并将库命名为vendors。让我们运行以下命令来生成库:
 ng g library vendors --prefix=my

成功运行上述命令后,我们应该看到以下输出:

  1. 一旦库被生成,Angular CLI 将创建以下文件夹和文件:

  1. 以下是一些重要的事项,一旦命令成功运行:
  • Projects下创建一个新的Vendors库项目。

  • Angular 还将在Angular.json文件中进行必要的更改和条目。

  • 请注意,projecTypelibrary类型。

以下截图显示了新创建的库项目的显示数据:

  1. 现在,打开vendors文件夹,在src/lib下编辑vendors.component.ts文件并添加一些花哨的文本:
      import { Component, OnInit } from '@angular/core';

      @Component({
       selector: 'my-vendors',
       template: `
       <p>
       vendors works!
       </p>
       `,
       styles: []
      })
      export class VendorsComponent implements OnInit {

      constructor() { }

      ngOnInit() {
       }

      }
  1. 记住,我们之前为vendor组件创建了路由链接,所以我们应该在应用程序中看到反映出的更改:

现在我们已经构建了一个具有多个项目、库和路由系统以共享不同组件的 Angular 应用程序,是时候部署应用程序了。

部署很简单,就像我们为独立应用程序所做的一样:

ng build --prod --base-href "http://localhost/deploy-angular-app/"

运行命令后,将发生一些重要的事情:

  • 为了生成最终部署文件,我们正在运行ng build命令。

  • 我们正在使用--prod元标志,编译时将应用 AOT 编译。

  • 最重要的是,我们需要传递--base-href元标志,它将指向服务器的根文件夹/路径。

没有适当的--base-href值,Angular 应用程序将无法正常工作,并会给您链接生成的文件的错误。

从前面的部分,我们已经知道运行build命令后,Angular 将生成编译后的文件夹和文件,如下截图所示:

从上面的截图中需要注意的一些重要点:

  • 该命令将生成编译文件的输出,其中包含多个项目、库和组件。

  • 仔细考虑我们设置的--base-href值。我们在本地运行 XAMPP,因此路径指向localhost

现在,让我们将所有代码从dist文件夹复制并粘贴到我们的XAMPP文件夹中。

使用本地服务器启动 Angular 应用程序,您应该看到以下显示的输出:

这真的很酷!即便如此,我们还可以大大改进。在更现实的设置中,任何大型的 Angular 实现都将拥有特性团队,由一个团队开发的库或模块应该很容易地与其他团队共享作为一个模块。这就是可重用模块的编写方式。我们将学习如何将 Angular 模块分发为npm模块。

将 Angular 项目打包为 npm 包

现在,让我们学习如何将我们的 Angular 项目导出为npm模块。我们将继续使用在上一个示例中创建的vendors库:

  1. 请注意,我们希望部署整个应用程序,而是只想部署vendors库。我们将使用相同的ng build命令来构建vendorsAngular 项目:
 ng build vendors
  1. 一旦命令成功执行,我们将看到 Angular 将在dist文件夹下为我们的vendors项目生成编译文件,如下所示:

  1. 转到dist/vendors文件夹并运行以下命令:
 npm pack

我们正在使用npm pack命令从当前文件夹生成一个包,其中包含来自vendors项目的文件。我们应该看到以下输出:

  1. 成功执行后,我们将在文件夹中看到创建的vendors-0.01.tgz文件。现在我们可以将此文件作为npm包进行分发,可以在任何项目中重复使用:

  1. 现在让我们进行测试,通过将新生成的npm模块安装到我们的应用程序中来进行测试。要安装该包,请运行npm install命令,指向vendors-0.0.1.tgz
 npm install dist\vendors\vendors-0.0.1.tgz
  1. 完成后,我们应该看到以下输出,通知我们已添加了该包:

  1. 我们还可以验证包是否成功添加到package.json文件中。我们应该看到package.json中显示如下条目:

太棒了!在本节中,我们学习了如何将 Angular 应用程序部署为独立应用程序,也学习了如何将其部署为复合应用程序。

我们还学习了如何创建一个 Angular 项目的包,可以在多个 Angular 项目中进行分发和使用。

将 Angular 应用程序部署到 GitHub Pages

在之前的部分中,我们学习了如何部署我们的独立应用程序,以及通过导出应用程序的编译源文件将复合应用程序部署到任何服务器。

在本节中,我们将学习如何将我们的 Angular 应用程序部署到 GitHub Pages。

在整本书中,我们创建了许多 Angular 项目,现在是时候免费托管它们了!

在 GitHub Pages 中创建和部署应用程序

GitHub Pages 是托管在 GitHub 上的项目的网站。我们说了免费吗?当然,GitHub Pages 是免费的!只需编辑、推送,就可以在您的免费网站上实时查看更改。

让我们逐步看看如何在 GitHub Pages 上创建和托管我们的应用程序:

  1. 让我们通过使用npm install命令来安装 Angular CLI:
 npm install @angular/cli
  1. 命令完成后,是时候创建一个新的 Angular 项目了。让我们称之为deploying-angular
 ng new deploying-angular

成功执行命令后,我们应该看到以下截图:

  1. 现在是时候初始化一个 Git 仓库了。我们可以通过执行以下命令来做到这一点:
 git init
  1. 成功执行后,您将看到仓库已初始化,或者在以下情况下,如果仓库已存在,则将重新初始化如下:

  1. 随意对app.component.html或任何要修改的文件进行任何更改。然后,一旦准备部署,通过执行commit Git 命令来首先提交代码/更改。我们还可以传递-m元标志并向提交添加消息:
 git commit -m "deploying angular"
  1. 接下来,我们需要将origin设置为仓库。以下命令将远程origin设置为仓库:
 git remote add origin      
      https://<token>@github.com/<username>/<repo-name>

好的。一切准备就绪。

  1. 现在,超级能力来了。要直接将您的 Angular 应用程序部署到 GitHub,我们需要安装一个名为angular-cli-ghpages的软件包。这是一个官方分发,可直接将 Angular 应用程序部署到 GitHub Pages:
 npm install -g angular-cli-ghpages

这是我们在运行上述代码后将得到的输出:

现在我们已经安装了angular-cli-ghpages,是时候构建我们的应用程序并获取编译后的源文件了。

  1. 让我们使用--prod元标志运行ng build命令,并设置--base-href
 ng build --prod --base-href  
      "https://<username>.github.io/deploying-angular"

--base-href标志指向 GitHub 上的源仓库。您需要在 GitHub 上注册并获取授权令牌,以便托管您的应用程序。

  1. 这是base href URL,作者的 GitHub 主页,以及相应的deploying-angular仓库:
 ng build --prod --base-href  
      "https://srinixrao.github.io/deploying-angular"
  1. 构建 Angular 应用程序后,我们将看到编译后的源代码生成在dist/<defaultProject> -defaultProject下。编译后的源代码通常是我们指定的应用程序名称作为文件夹名称:

  1. 现在我们已经生成了编译文件,是时候将应用程序部署到 GitHub Pages 了。我们通过运行npx ngh --no-silent命令来实现这一点:
 npx ngh --no-silent --dir=dist/deploying-angular
  1. 请记住,可选地,我们需要提到我们想要部署的相应dist文件夹:

  1. 成功执行命令后,我们安装的用于将 Angular 应用程序部署到 GitHub Pages 的包将运行所需的作业,例如清理、获取原始代码、检出代码,最后将最新代码推送到存储库,并准备在 GitHub Pages 上托管:

  1. 一旦命令执行完毕,请转到您的 GitHub 帐户,并在存储库下点击“设置”。您将看到网站发布到以下网址:

  1. 点击存储库下显示的链接,我们应该看到我们的应用程序正在运行!

恭喜!我们刚刚将我们的第一个 Angular 应用程序发布到了 GitHub Pages:

在前面的一系列步骤中,我们学会了如何将我们的 Angular 应用程序部署到 GitHub Pages。在更现实的情况下,我们还需要将 API 或后端服务部署到我们的服务器上。我们可以通过将 API 部署到 Firebase 或自托管服务器来实现。

现在,继续为到目前为止创建的所有项目和应用程序重复相同的步骤。

总结

部署应用程序非常重要:我们的所有辛勤工作将在网站上线后展现出来。

部署 Angular 应用程序非常简单,只需生成所需的编译源代码,而且在最新版本的 Angular 中,AOT 编译默认为使用--prod meta标志生成的任何构建。我们了解了 AOT 的重要性以及对整体应用程序性能和安全性的关键性。我们学会了部署独立的 Angular 应用程序,以及由多个项目、库和组件组成的复合 Angular 应用程序。

最后,我们学会了如何使用官方的angular-cli-ghpages包将我们的 Angular 应用程序部署到 GitHub Pages。

这就是我们在本书的最后一章的结论。在学习过程中,我们从理解 TypeScript 语言的基础知识到学习如何通过实现 Angular 框架的组件、路由系统、指令、管道、表单、后端服务等来构建我们的 Angular 应用程序,我们走了很长一段路。

我们还学习了如何在我们的 Angular 应用中实现各种 CSS 框架,比如 Bootstrap、Angular Material 和 Flex 布局。此外,我们还学会了如何设计和使我们应用的用户界面更具吸引力和互动性。

我们探讨了使用 Jasmine 和 Karma 框架进行单元测试,这确保我们的应用经过了充分测试,并且实现非常稳固。

作为学习 Angular 高级主题的一部分,我们还实现了使用 Auth0 和 Firebase 的用户认证机制。最后,我们讨论了 Angular 应用的部署。

这是使用 Angular 框架开发应用的所有方面的 360 度概述。我们希望你现在有能力使用 Angular 框架构建世界一流的产品。

祝你一切顺利,并期待很快听到你的成功故事。

祝你好运!继续向前,不断进步。

posted @ 2024-05-18 12:03  绝不原创的飞龙  阅读(24)  评论(0编辑  收藏  举报