Angular9 高级教程(一)
一、做好准备
Angular 利用了服务器端开发的一些最佳方面,并使用它们来增强浏览器中的 HTML,从而为构建更简单、更容易的富应用奠定了基础。Angular 应用是围绕一个清晰的设计模式构建的,该模式强调创建
-
可扩展的:一旦你理解了基础知识,就很容易弄清楚一个复杂的 Angular 应用是如何工作的——这意味着你可以轻松地增强应用,为你的用户创造新的有用的功能。
-
可维护:Angular app 易于调试和修复,意味着长期维护简化。
-
可测试性:Angular 对单元测试和端到端测试有很好的支持,这意味着你可以在你的用户之前找到并修复缺陷。
-
标准化 : Angular 构建在 web 浏览器的固有功能上,不会妨碍您,允许您创建符合标准的 web 应用,这些应用利用最新的 HTML 和功能,以及流行的工具和框架。
Angular 是一个开源的 JavaScript 库,由 Google 赞助和维护。它已经被用在一些最大最复杂的网络应用中。在这本书里,我向你展示了在你自己的项目中获得 Angular 的好处所需要知道的一切。
你需要知道什么?
在阅读本书之前,您应该熟悉 web 开发的基础知识,了解 HTML 和 CSS 的工作原理,并具备 JavaScript 的工作知识。如果你对这些细节有些模糊,我在第五章第五章、第六章和第七章中提供了我在本书中使用的 HTML、CSS 和 JavaScript 的刷新工具。不过,你不会找到关于 HTML 元素和 CSS 属性的全面参考,因为一本关于 Angular 的书中没有足够的篇幅来涵盖所有的 HTML。
这本书的结构是什么?
这本书分为三部分,每一部分都涵盖了一系列相关的主题。
第一部分:Angular 入门
本书的第一部分提供了您需要为本书的其余部分做准备的信息。它包括本章和关键技术的入门/刷新者,包括 HTML、CSS 和 TypeScript,后者是 Angular 开发中使用的 JavaScript 的超集。我还将向您展示如何构建您的第一个 Angular 应用,并带您完成构建一个更真实的应用(名为 SportsStore)的过程。
第二部分:详细的 Angular
本书的第二部分将带您浏览 Angular 提供的用于创建应用的构建模块,依次浏览每一个模块。Angular 包含了许多内置功能,我将对这些功能进行深入描述,并提供了无尽的定制选项,我将对所有这些进行演示。
第三部分:高级 Angular 特征
本书的第三部分解释了如何使用高级特性来创建更复杂和可伸缩的应用。我演示了如何在 Angular 应用中发出异步 HTTP 请求,如何使用 URL 路由在应用中导航,以及如何在应用的状态改变时动画显示 HTML 元素。
这本书没有涵盖什么?
这本书是给有经验的 web 开发者的,他们是 Angular 的新手。它没有解释 web 应用或编程的基础,尽管有关于 HTML、CSS 和 JavaScript 的入门章节。我没有详细描述服务器端开发——如果您想创建支持 Angular 应用所需的后端服务,请参阅我的其他书籍。
而且,尽管我喜欢深入书中的细节,但并不是每一个 Angular 都在主流开发中有用,我必须将我的书保持在可打印的大小。当我决定省略一个特性时,是因为我认为它不重要,或者是因为使用我所涉及的技术可以达到相同的结果。
Angular 开发需要什么软件?
你需要一个代码编辑器和第二章中描述的工具。Angular 开发所需的一切都是免费的,可以在 Windows、macOS 和 Linux 上使用。
我如何设置开发环境?
第二章通过创建一个简单的应用来介绍 Angular,作为这个过程的一部分,我将告诉你如何创建一个使用 Angular 的开发环境。
如果我对这些例子有疑问怎么办?
首先要做的是回到这一章的开头,重新开始。大多数问题都是由于遗漏了一个步骤或者没有完全按照清单来做造成的。请密切注意代码清单中的重点,它突出了需要进行的更改。
接下来,查看勘误表/更正表,它包含在本书的 GitHub 资源库中。尽管我和我的编辑尽了最大的努力,技术书籍还是很复杂,错误是不可避免的。请查看勘误表,了解已知错误列表以及解决这些错误的说明。
如果您仍然有问题,那么从本书的 GitHub 资源库 https://github.com/Apress/pro-angular-9 下载您正在阅读的章节的项目,并将其与您的项目进行比较。我通过阅读每一章来创建 GitHub 存储库的代码,所以您的项目中应该有相同内容的相同文件。
如果你仍然不能让例子工作,那么你可以联系我在adam@adam-freeman.com寻求帮助。请在邮件中明确你在看哪本书,哪个章节/例子导致了这个问题。请记住,我会收到很多电子邮件,我可能不会立即回复。
如果我发现书中有错误怎么办?
您可以在adam@adam-freeman.com通过电子邮件向我报告错误,尽管我要求您首先检查这本书的勘误表/更正列表,您可以在本书的 GitHub 资源库 https://github.com/Apress/pro-angular-9 中找到,以防它已经被报告。
我在 GitHub 存储库的勘误表/更正文件中添加了可能给读者造成困惑的错误,尤其是示例代码的问题,并对第一个报告该错误的读者表示感谢。我保留了一个不太严重的问题的列表,这通常意味着例子周围的文本中的错误,当我编写新版本时,我会使用它们。
有很多例子吗?
有个载荷的例子。学习 Angular 的最好方法是通过例子,我已经尽可能多地将它们打包到本书中。为了使本书中的例子数量最大化,我采用了一个简单的约定来避免一遍又一遍地列出文件的内容。我第一次在一个章节中使用一个文件时,我会列出完整的内容,就像我在清单 1-1 中所做的一样。我在列表的标题中包含了文件的名称,以及您应该创建它的文件夹。当我修改代码时,我用粗体显示修改后的语句。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { AppComponent } from "./app.component";
import { StoreModule } from "./store/store.module";
@NgModule({
imports: [BrowserModule, StoreModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
Listing 1-1.A Complete Example Document
该列表摘自第七章。不要担心它做什么;请注意,这是一个完整的列表,显示了文件的全部内容。
当我对同一个文件进行一系列修改时,或者当我对一个大文件进行小的修改时,我只向您展示发生变化的元素,以创建一个部分清单。您可以发现部分清单,因为它以省略号(...)开始和结束,如清单 1-2 所示。
...
<table class="table table-sm table-bordered table-striped">
<tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
<tr *ngFor="let item of getProducts(); let i = index" pa-attr>
<td>{{i + 1}}</td>
<td>{{item.name}}</td>
<td pa-attr pa-attr-class="bg-warning">{{item.category}}</td>
<td pa-attr pa-attr-class="bg-info">{{item.price}}</td>
</tr>
</table>
...
Listing 1-2.A Partial Listing
列表 1-2 来自后面的章节。您可以看到只显示了body元素及其内容,并且我突出显示了一些语句。这就是我如何将您的注意力吸引到清单中已经更改的部分,或者强调示例中显示我所描述的特性或技术的部分。在某些情况下,我需要对同一个文件的不同部分进行修改,在这种情况下,为了简洁起见,我省略了一些元素或语句,如清单 1-3 所示。
import { ApplicationRef, Component } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
import { ProductFormGroup } from "./form.model";
@Component({
selector: "app",
templateUrl: "app/template.html"
})
export class ProductComponent {
model: Model = new Model();
form: ProductFormGroup = new ProductFormGroup();
// ...other members omitted for brevity...
showTable: boolean = true;
}
Listing 1-3.Omitting Statements for Brevity
这种约定让我可以提供更多的例子,但是这确实意味着很难找到一种特定的技术。为此,我在第二部分和第三部分中描述 Angular 特性的章节以一个汇总表开始,该表描述了本章中包含的技术以及演示如何使用它们的清单。
从哪里可以获得示例代码?
你可以从 https://github.com/Apress/pro-angular-9 下载本书所有章节的范例项目。
我如何联系作者?
你可以在adam@adam-freeman.com给我发邮件。自从我第一次在我的书中发表电子邮件地址已经有几年了。我不能完全肯定这是一个好主意,但我很高兴我这样做了。我收到了来自世界各地的电子邮件,来自各行各业工作或学习的读者,无论如何,在大多数情况下,这些电子邮件都是积极的,礼貌的,并且很高兴收到。
我试着及时回复,但我会收到很多邮件,有时会积压,尤其是当我埋头写完一本书的时候。我总是试图帮助那些被书中的一个例子困住的读者,尽管我要求你在联系我之前按照本章前面描述的步骤去做。
虽然我欢迎读者的电子邮件,但有一些常见问题的答案永远是“不”。我担心我不会为你的新公司编写代码,不会帮助你完成大学作业,不会参与你的开发团队的设计争议,也不会教你如何编程。
如果我真的喜欢这本书呢?
请发电子邮件到adam@adam-freeman.com告诉我。收到一个快乐的读者的来信总是一件令人高兴的事,我很感激花时间发送这些邮件。写这些书可能很难,而这些邮件为坚持一项有时感觉不可能的活动提供了必要的动力。
如果这本书让我生气了,我想投诉怎么办?
你仍然可以在adam@adam-freeman.com给我发邮件,我仍然会尽力帮助你。请记住,只有当你解释了问题是什么,以及你希望我做些什么时,我才能提供帮助。你应该明白,有时唯一的结果是接受我不是你的作者,只有当你归还这本书并选择另一本时,我们才会结束。我会仔细考虑让你心烦意乱的事情,但是经过 25 年的写书生涯,我逐渐明白,并不是每个人都喜欢读我喜欢写的书。
摘要
在这一章中,我概述了这本书的内容和结构。学习 Angular 开发的最好方法是通过例子,所以在下一章,我将直接向您展示如何设置您的开发环境,并使用它来创建您的第一个 Angular 应用。
二、你的第一个 Angular 应用
开始使用 Angular 的最佳方式是深入研究并创建一个 web 应用。在本章中,我将向您展示如何设置您的开发环境,并带您完成创建一个基本应用的过程,从功能的静态模型开始,应用 Angular 特性来创建一个动态的 web 应用,尽管是一个简单的应用。在第七章 7 章–10 章中,我将向你展示如何创建一个更加复杂和真实的 Angular 应用,但是现在,一个简单的例子将足以演示 Angular 应用的主要组件,并为本书这一部分的其他章节做好准备。
如果你没有遵循本章的所有内容,也不要担心。Angular 有一个陡峭的学习曲线,所以这一章的目的只是介绍 Angular 开发的基本流程,并给你一些东西是如何组合在一起的感觉。现在这一切都没有意义,但是当你读完这本书的时候,你会明白我在这一章中采取的每一步,以及更多。
准备开发环境
角发育需要一些准备。在接下来的部分中,我将解释如何设置和准备创建您的第一个项目。流行的开发工具中广泛支持 Angular,您可以挑选自己喜欢的。
安装 Node.js
许多用于 Angular 开发的工具都依赖于 Node . js——也称为 Node——它创建于 2009 年,是用 JavaScript 编写的服务器端应用的一个简单高效的运行时。Node.js 基于 Chrome 浏览器中使用的 JavaScript 引擎,并提供了一个在浏览器环境之外执行 JavaScript 代码的 API。
Node.js 作为应用服务器已经取得了成功,但对于本书来说,它很有趣,因为它为新一代跨平台开发和构建工具提供了基础。Node.js 团队的一些聪明的设计决策和 Chrome JavaScript 运行时提供的跨平台支持创造了一个机会,被热情的工具作者抓住了。简而言之,Node.js 已经成为 web 应用开发的基础。
重要的是,您下载的 Node.js 版本与我在本书中使用的版本相同。尽管 Node.js 相对稳定,但仍不时会有突破性的 API 变化,这可能会使我在本章中包含的示例无法工作。
我使用的版本是 12.15.0,这是我撰写本文时的当前长期支持版本。在您阅读本文时,可能会有更高的版本,但是对于本书中的示例,您应该坚持使用 12.15.0 版本。在 https://nodejs.org/dist/v12.15.0 可以获得完整的 12.15.0 版本,包括 Windows 和 macOS 的安装程序以及其他平台的二进制包。运行安装程序,确保选中“npm 包管理器”选项和两个添加到路径选项,如图 2-1 所示。
图 2-1。
配置节点安装
安装完成后,运行清单 2-1 中所示的命令。
node -v
Listing 2-1.Running Node.js
如果安装正常进行,您将会看到下面显示的版本号:
v12.15.0
Node.js 安装程序包括节点包管理器(NPM),用于管理项目中的包。运行清单 2-2 中所示的命令,确保 NPM 正在工作。
npm -v
Listing 2-2.Running NPM
如果一切正常,您将看到以下版本号:
6.13.4
安装 angular-cli 软件包
angular-cli包已经成为开发期间创建和管理 Angular 项目的标准方式。在这本书的最初版本中,我演示了如何从零开始建立一个 Angular 项目,这是一个冗长且容易出错的过程,通过angular-cli得到了简化。要安装angular-cli,打开一个新的命令提示符并运行清单 2-3 中所示的命令。
npm install --global @angular/cli@9.0.1
Listing 2-3.Installing the angular-cli Package
注意在global参数前有两个连字符。如果你使用的是 Linux 或者 macOS,你可能需要使用sudo,如清单 2-4 所示。
sudo npm install --global @angular/cli@9.0.1
Listing 2-4.Using sudo to Install the angular-cli Package
安装编辑器
Angular 开发可以用任何一个程序员的编辑器来完成,从中有数不尽的选择。一些编辑器增强了对 Angular 的支持,包括突出显示关键术语和良好的工具集成。
选择编辑器时,最重要的考虑因素之一是过滤项目内容的能力,以便您可以专注于文件的子集。在一个 Angular 项目中可能会有很多文件,并且许多文件都有相似的名称,因此能够找到并编辑正确的文件是非常重要的。编辑器以不同的方式实现了这一点,要么显示打开进行编辑的文件列表,要么提供排除具有特定扩展名的文件的能力。
本书中的例子不依赖于任何特定的编辑器,我使用的所有工具都是从命令行运行的。如果你还没有一个 web 应用开发的首选编辑器,那么我推荐使用 Visual Studio Code,它是微软免费提供的,对 Angular 开发有极好的支持。可以从 https://code.visualstudio.com 下载 Visual Studio Code。
安装浏览器
最后要选择的是在开发过程中用来检查工作的浏览器。所有的当代浏览器都有很好的开发者支持,并且与 Angular 配合得很好。我在这本书里一直使用谷歌浏览器,这也是我推荐你使用的浏览器。
创建和准备项目
一旦你有了 Node.js、angular-cli包、编辑器和浏览器,你就有足够的基础来开始开发过程。
创建项目
要创建项目,选择一个方便的位置,并使用命令提示符运行清单 2-5 中所示的命令,这将创建一个名为todo的新 Angular 项目。
Note
如果您在 Windows 上使用 PowerShell,那么在运行清单 2-5 中的命令之前,您可能需要使用Set-ExecutionPolicy RemoteSigned命令来启用脚本执行。
ng new todo --routing false --style css --skip-git --skip-tests
Listing 2-5.Creating the Angular Project
ng命令由angular-cli包提供,ng new建立一个新项目。自变量配置项目,选择适用于第一个项目的选项(配置选项在第十一章中描述)。
安装过程会创建一个名为todo的文件夹,其中包含开始 Angular 开发所需的所有配置文件、一些开始开发的占位符文件以及开发、运行和部署 Angular 应用所需的 NPM 包。(项目创建可能需要一段时间,因为有大量的包要下载。)
Tip
你可以从 https://github.com/Apress/pro-angular-9 下载本章以及本书其他章节的示例项目。如果在运行示例时遇到问题,请参见第一章获取帮助。
创建项目后,使用您喜欢的代码编辑器打开它进行编辑。todo文件夹包含许多用于 Angular 开发的工具的配置文件(在第十一章中描述),而src/app文件夹包含应用的代码和内容,也是大多数开发工作完成的文件夹。图 2-2 显示了 Visual Studio Code 中出现的项目文件夹的初始内容,并突出显示了src/app文件夹。您可能会看到与其他编辑器略有不同的视图,其中一些编辑器隐藏了开发过程中不经常直接使用的文件和文件夹,例如node_modules文件夹,它包含了 Angular 开发工具所依赖的包。
图 2-2。
项目文件夹的内容
启动开发工具
一切就绪,现在是测试 Angular 开发工具的时候了。使用命令提示符运行todo文件夹中清单 2-6 所示的命令。
ng serve
Listing 2-6.Starting the Angular Development Tools
该命令启动 Angular 开发工具,这些工具通过初始构建过程为开发会话准备应用。此过程需要一段时间,并将生成类似于以下内容的输出:
Compiling @angular/core : es2015 as esm2015
Compiling @angular/common : es2015 as esm2015
Compiling @angular/platform-browser : es2015 as esm2015
Compiling @angular/platform-browser-dynamic : es2015 as esm2015
chunk {main} main.js, main.js.map (main) 57.8 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 140 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.15 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 9.74 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 2.67 MB [initial] [rendered]
Date: 2020-02-09T11:23:46.619Z - Hash: 3f280025364478cce5b4 - Time: 12859ms
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
: Compiled successfully.
Date: 2020-02-09T11:23:47.497Z - Hash: 3f280025364478cce5b4
5 unchanged chunks
Time: 465ms
: Compiled successfully.
如果您看到略有不同的输出,请不要担心,只要准备工作完成后,您会看到“编译成功”的消息。
项目中的开发工具包括一个 HTTP 服务器。一旦构建过程完成,打开一个新的浏览器窗口并使用它来请求http://localhost:4200,您将看到如图 2-3 所示的内容,它显示了创建项目时添加到项目中的占位符内容。
图 2-3。
占位符内容
向项目中添加 Angular 特征
现在开发工具正在运行,我将创建一个简单的 Angular 应用来管理待办事项列表。用户将能够看到待办事项列表,检查已完成的项目,并创建新项目。为了使应用简单,我假设只有一个用户,并且我不必担心保存应用中数据的状态,这意味着如果关闭或重新加载浏览器窗口,对待办事项列表的更改将会丢失。(后面的例子,包括在第 7–10 章中开发的 SportsStore 应用,演示了持久数据存储。)
创建数据模型
大多数应用的起点是数据模型,它描述了应用运行的领域。数据模型可能很大很复杂,但是对于我的待办应用,我只需要描述两件事:一个待办事项和这些事项的列表。
Angular 应用是用 TypeScript 编写的,TypeScript 是 JavaScript 的超集。我在第六章中介绍了 TypeScript,但是它的主要优点是支持静态数据类型,这使得 JavaScript 开发对于 C#和 Java 开发人员来说更加熟悉。(JavaScript 有一个基于原型的类型系统,许多开发人员对此感到困惑。)命令包括将 TypeScript 代码编译成可由浏览器执行的纯 JavaScript 所需的包。
为了启动应用的数据模型,我在todo/src/app文件夹中添加了一个名为todoItem.ts的文件,其内容如清单 2-7 所示。(TypeScript 文件的扩展名为.ts。)
export class TodoItem {
constructor(taskVal: string, completeVal: boolean = false) {
this.task = taskVal;
this.complete = completeVal;
}
task: string;
complete: boolean;
}
Listing 2-7.The Contents of the todoItem.ts File in the src/app Folder
清单 2-7 中使用的语言特性是标准 JavaScript 特性和 TypeScript 提供的额外特性的混合。当代码被编译时,TypeScript 特性被移除,结果是可以被浏览器执行的 JavaScript 代码。
例如,export、class和constructor关键字就是标准的 JavaScript。并不是所有的浏览器都支持这些特性,这些特性是 JavaScript 规范中相对较新的内容,正如我在第十一章中解释的那样,Angular 应用的构建过程可以将这种类型的特性翻译成老浏览器可以理解的代码。
export关键字与 JavaScript 模块相关。当使用模块时,每个 TypeScript 或 JavaScript 文件都被认为是一个自包含的功能单元,而export关键字用于标识您想要在应用的其他地方使用的数据或类型。JavaScript 模块用于管理项目中文件之间的依赖关系,避免手动管理 HTML 文件中一组复杂的script元素。有关模块如何工作的详细信息,请参见第十一章。class关键字声明一个类,constructor关键字表示一个类构造器。与 C#等其他语言不同,JavaScript 不使用类名来表示构造函数。
Tip
如果您不熟悉 JavaScript 规范的最新版本中添加的特性,也不用担心。第五章和第六章提供了使用使 Angular 更容易使用的特性编写 JavaScript 的入门知识,第六章也描述了一些有用的特定于 TypeScript 的特性。
清单 2-7 中的其他特性由 TypeScript 提供。当您第一次开始使用 TypeScript 时,最不和谐的特性之一是它的简洁构造函数特性,尽管您很快就会依赖它。清单 2-7 中定义的TodoItem类定义了一个接收两个参数的构造函数,名为task和complete。这些参数的值被分配给同名的public属性。如果没有为complete参数提供值,那么将使用默认值false。
简洁的构造函数避免了可能需要的样板代码块,这些代码块容易出现打字错误或者只是忘记给属性分配参数。如果没有简洁的构造函数,我将不得不像这样编写TodoItem类:
...
class TodoItem {
constructor(taskVal: string, completeVal: boolean = false) {
this.task = taskVal;
this.complete = completeVal;
}
task: string;
complete: boolean;
}
...
事实上,我本可以不用简洁的构造函数来编写TodoItem类。TypeScript 试图在不碍事的情况下提供帮助,您可以忽略或禁用它的所有功能,更多地依赖 JavaScript 的标准功能。正如我在后面的章节中解释的那样,Angular 开发依赖于一些特性,但是您可以逐渐接受 TypeScript 特性,或者,如果您愿意,只选择您喜欢的特性。
标题 TypeScript 特性是静态类型。清单 2-7 中的每个构造函数参数都被标注了一个类型,如下所示:
...
constructor(taskVal: string, completeVal: boolean = false) {
...
如果在调用构造函数时使用了不兼容的类型,TypeScript 编译器将报告错误。如果您从 C#或 Java 开始进行 Angular 开发,这似乎是显而易见的,但这不是 JavaScript 通常的工作方式。
创建待办事项列表类
为了创建一个表示待办事项列表的类,我在src/app文件夹中添加了一个名为todoList.ts的文件,并添加了清单 2-8 中所示的代码。
import { TodoItem } from "./todoItem";
export class TodoList {
constructor(public user: string, private todoItems: TodoItem[] = []) {
// no statements required
}
get items(): readonly TodoItem[] {
return this.todoItems;
}
addItem(task: string) {
this.todoItems.push(new TodoItem(task));
}
}
Listing 2-8.The Contents of the todoList.ts File in the src/app Folder
import关键字声明了对TodoItem类的依赖,并指定了定义它的代码文件。TodoList类定义了一个接收初始待办事项集合的构造函数。我不想无限制地访问TodoItem对象的数组,所以我定义了一个名为items的属性,它返回一个只读数组,这是使用readonly关键字完成的。对于任何试图修改数组内容的语句,TypeScript 编译器都会生成错误,如果您使用的编辑器具有良好的 TypeScript 支持,如 Visual Studio Code,则编辑器的自动完成功能不会提供会触发编译器错误的方法和属性。
向用户显示数据
我需要一种方法向用户显示模型中的数据值。在 Angular 中,这是使用一个模板来完成的,这个模板是 HTML 的一个片段,包含 Angular 评估的表达式,并将结果插入到发送给浏览器的内容中。该项目的angular-cli设置在src/app文件夹中创建了一个名为app.component.html的模板文件。我编辑了这个文件,删除了占位符内容,并添加了清单 2-9 中所示的内容。
<h3>
{{ username }}'s To Do List
<h6>{{ itemCount }} Items</h6>
</h3>
Listing 2-9.Replacing the Contents of the app.component.html File in the src/app Folder
我很快会在这个文件中添加更多的元素,但是两个 HTML 元素就足够了。在一个模板中包含一个数据值是通过使用双括号来完成的— {{和}}——Angular 会计算您放在双括号之间的任何内容,以获得要显示的值。
{{和}}字符是数据绑定的一个例子,这意味着它们创建了模板和数据值之间的关系。数据绑定是一个重要的 Angular 特性,当我在示例应用中添加特性时,您将在本章中看到更多的数据绑定(我将在本书的第二部分详细描述它们)。在这种情况下,数据绑定告诉 Angular 获取username和itemCount属性的值,并将它们插入到h3和div元素的内容中。
一旦保存了文件,Angular 开发工具就会尝试构建项目。编译器将生成以下错误:
ERROR in src/app/app.component.html:1:8 - error TS2339: Property 'username' does not exist on type 'AppComponent'.
1 {{ username }}'s To Do List
~~~~~~~~~
src/app/app.component.ts:5:16
5 templateUrl: './app.component.html',
~~~~~~~~~~~~~~~~~~~~~~
Error occurs in the template of component AppComponent.
src/app/app.component.html:2:9 - error TS2339: Property 'itemCount' does not exist on type 'AppComponent'.
2 <h6>{{ itemCount }} Incomplete Items</h6>
~~~~~~~~~~
src/app/app.component.ts:5:16
5 templateUrl: './app.component.html',
~~~~~~~~~~~~~~~~~~~~~~
Error occurs in the template of component AppComponent.
出现这些错误是因为我在数据绑定中使用的属性不存在,所以 Angular 无法获得我告诉它在模板中使用的值。我将在下一节中解决这个问题。
更新组件
Angular 组件负责管理模板,并为其提供所需的数据和逻辑。如果这似乎是一个宽泛的说法,那是因为组件是 Angular 应用的一部分,它承担了大部分繁重的工作。因此,它们可以用于各种任务。
在这种情况下,我需要一个组件作为数据模型类和模板之间的桥梁,这样我就可以创建一个TodoList类的实例,用一些示例TodoItem对象填充它,并且在这样做的时候,为模板提供它需要的username和itemCount属性。angular-cli设置在todo/src/app文件夹中创建了一个名为app.component.ts的占位符组件文件,我对其进行了编辑,以做出清单 2-10 中突出显示的更改。
import { Component } from '@angular/core';
import { TodoList } from "./todoList";
import { TodoItem } from "./todoItem";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
private list = new TodoList("Bob", [
new TodoItem("Go for run", true),
new TodoItem("Get flowers"),
new TodoItem("Collect tickets"),
]);
get username(): string {
return this.list.user;
}
get itemCount(): number {
return this.list.items
.filter(item => !item.complete).length;
}
}
Listing 2-10.Editing the Contents of the app.component.ts File in the src/app Folder
清单中的代码可以分为三个主要部分,如以下部分所述。
了解进口
import关键字与export关键字相对应,用于声明对 JavaScript 模块内容的依赖。在清单 2-10 中,import关键字被使用了三次。
...
import { Component } from '@angular/core';
import { TodoList } from "./todoList";
import { TodoItem } from "./todoItem";
...
清单中的第一个import语句用于加载@angular/core模块,该模块包含关键的 Angular 功能,包括对组件的支持。当处理模块时,import语句指定在花括号中导入的类型。在这种情况下,import语句用于从模块加载Component类型。@angular/core模块包含许多打包在一起的类,这样浏览器就可以将它们全部加载到一个 JavaScript 文件中。
其他的import语句用于声明对前面定义的数据模型类的依赖。这种导入的目标以./开始,这表明该模块是相对于当前文件定义的。
注意,import语句都不包含文件扩展名。这是因为一个import语句的目标和浏览器加载的文件之间的关系是由 Angular build 工具处理的,它将应用打包并发送给浏览器,我将在第十一章中对此进行更详细的解释。
了解装修工
清单中最奇怪的代码部分是这样的:
...
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
...
这是一个装饰器的例子,它提供了一个类的元数据。这是@Component装饰器,顾名思义,它告诉 Angular 这是一个组件。装饰器通过其属性提供配置信息。这个@Component装饰器指定了三个属性:selector、templateUrl和styleUrls。
属性指定了一个 CSS 选择器,它匹配组件将要应用到的 HTML 元素。这个装饰器指定的app-root元素是由angular-cli包默认设置的。它对应于一个添加到index.html文件中的 HTML 元素,您可以在src文件夹中找到该文件,它是用以下内容创建的:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Todo</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>
我突出显示了 HTML 元素。属性告诉 Angular 由组件生成的内容应该插入到元素中。
templateUrl属性用于指定组件的模板,即该组件的app.component.html文件。styleUrls属性指定了一个或多个 CSS 样式表,用于样式化组件及其模板产生的元素。稍后我将使用这个特性来改进示例应用的外观。
理解课程
清单的最后一部分定义了一个类,Angular 可以实例化这个类来创建组件。
...
export class AppComponent {
private list = new TodoList("Bob", [
new TodoItem("Go for run", true),
new TodoItem("Get flowers"),
new TodoItem("Collect tickets"),
]);
get username(): string {
return this.list.user;
}
get itemCount(): number {
return this.list.items.filter(item => !item.complete).length;
}
}
...
这些语句定义了一个名为AppComponent的类,该类有一个私有的list属性,该属性被赋予一个TodoList对象,并由一组TodoItem对象填充。AppComponent类定义了名为username和itemCount的只读属性,这些属性依赖于TodoList对象来产生它们的值。username属性返回TodoList.user属性的值,itemCount属性使用标准的 JavaScript 数组特性来过滤由TodoList管理的Todoitem对象,以选择那些不完整的对象,并返回它找到的匹配对象的数量。
使用λ函数生成itemCount属性的值,也称为粗箭头函数,这是表达标准 JavaScript 函数的一种更简洁的方式。lambda 表达式中的箭头读作“goes to”,例如“item goes to not item.complete”Lambda 表达式是 JavaScript 语言规范中的一个新成员,它提供了一种使用函数作为参数的传统方法的替代方法,如下所示:
...
return this.model.items.filter(function (item) { return !item.complete });
...
当您保存对 TypeScript 文件的更改时,Angular 开发工具将构建项目。这一次应该没有错误,因为组件已经定义了模板所需的属性。浏览器窗口将自动重新加载,显示图 2-4 中的输出。
图 2-4。
在示例应用中生成内容
HTML 元素的样式
我已经到了 Angular 生成内容的地步,但是结果只是纯文本。我将把引导 CSS 框架添加到应用中,并用它来设计内容的样式。有许多好的 CSS 框架可用,但 Bootstrap 是最受欢迎的一个。我在第四章提供了使用 Bootstrap 的简单介绍,所以如果你以前没有用过也不用担心。停止 Angular 开发工具,使用命令提示符运行清单 2-11 中所示的命令,将引导包添加到项目中。
npm install bootstrap@4.4.1
Listing 2-11.Adding a Package to the Example Project
这个命令安装 4.4.1 版的引导包,这是我在本书中使用的版本。要在发送到浏览器的 HTML 内容中包含引导 CSS 样式,请将清单 2-12 中所示的条目添加到angular.json文件的styles部分,该文件是在创建项目时通过ng new命令添加到todo文件夹中的。
Caution
在angular.json文件中有两个styles部分。将清单 2-12 中所示的设置添加到最靠近文件顶部的位置。
...
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/todo",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": []
},
...
Listing 2-12.Configuring CSS in the angular.json File in the todo Folder
正如我在第十一章中解释的那样,angular.json文件用于配置项目工具,清单中显示的语句将引导 CSS 文件合并到项目中,这样它将包含在发送到浏览器的内容中。
Bootstrap 通过向类中添加元素来工作。在清单 2-13 中,我已经将模板中的元素添加到类中,这些元素将改变它们的外观。
<h3 class="bg-primary text-center text-white p-2">
{{ username }}'s To Do List
<h6 class="mt-1">{{ itemCount }} Incomplete Items</h6>
</h3>
Listing 2-13.Styling Content in the app.component.html File in the src/app Folder
使用命令提示符运行清单 2-14 中所示的命令,这将再次启动 Angular 开发工具。
ng serve
Listing 2-14.Starting the Angular Development Tools
浏览器可能会自动更新,但如果没有,则手动重新加载以查看样式化的内容,如图 2-5 所示。
图 2-5。
应用生成的 HTML 样式
显示待办事项列表
下一步是显示待办事项。清单 2-15 向组件添加一个属性,提供对列表中项目的访问。
import { Component } from '@angular/core';
import { TodoList } from "./todoList";
import { TodoItem } from "./todoItem";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
private list = new TodoList("Bob", [
new TodoItem("Go for run"),
new TodoItem("Get flowers"),
new TodoItem("Collect tickets"),
]);
get username(): string {
return this.list.user;
}
get itemCount(): number {
return this.list.items.filter(item => !item.complete).length;
}
get items(): readonly TodoItem[] {
return this.list.items;
}
}
Listing 2-15.Adding a Property in the app.component.ts File in the src/app Folder
为了向用户显示每个项目的细节,我将清单 2-16 中所示的元素添加到模板中。
<h3 class="bg-primary text-center text-white p-2">
{{ username }}'s To Do List
<h6 class="mt-1">{{ itemCount }} Incomplete Items</h6>
</h3>
<table class="table table-striped table-bordered table-sm">
<thead>
<tr><th>#</th><th>Description</th><th>Done</th></tr>
</thead>
<tbody>
<tr *ngFor="let item of items; let i = index">
<td>{{ i + 1 }}</td>
<td>{{ item.task }}</td>
<td [ngSwitch]="item.complete">
<span *ngSwitchCase="true">Yes</span>
<span *ngSwitchDefault>No</span>
</td>
</tr>
</tbody>
</table>
Listing 2-16.Adding Elements in the app.component.html File in the src/app Folder
对模板的添加依赖于几个不同的 Angular 特征。第一个是*ngFor表达式,用于为数组中的每一项重复一个内容区域。这是一个指令的例子,我在第 13–16 章中描述了它(指令是 Angular 发展的一个很大的部分,这就是为什么它们在几章中被描述)。*ngFor表达式应用于元素的属性,如下所示:
...
<tr *ngFor="let item of items; let i = index">
...
这个表达式告诉 Angular 将它所应用到的tr元素作为一个模板,应该为组件的items属性返回的每个对象重复这个模板。表达式的let item部分指定每个对象应该被分配给一个名为item的变量,这样它就可以在模板中被引用。
ngFor表达式还跟踪正在处理的数组中当前对象的索引,并将其赋给第二个变量i。
...
<tr *ngFor="let item of items; let i = index">
...
结果是,tr元素及其内容将被复制并插入到由items属性返回的每个对象的 HTML 文档中;对于每次迭代,可以通过名为item的变量访问当前的待办对象,通过名为i的变量访问对象在数组中的位置。
Tip
使用*ngFor时记住*字符很重要。我会在第十三章中解释它的含义。
在tr模板中,有两个数据绑定,可以通过{{和}}字符识别,如下所示:
...
<td>{{ i + 1 }}</td>
<td>{{ item.task }}</td>
...
这些绑定引用由*ngFor表达式创建的变量。绑定不仅仅用于引用属性和方法名;它们也可以用来执行简单的 JavaScript 操作。您可以在第一个绑定中看到这样的例子,我将变量i和 1 相加。
Tip
对于简单的转换,你可以像这样直接在绑定中嵌入你的 JavaScript 表达式,但是对于更复杂的操作,Angular 有一个叫做 pipes 的特性,我在第十八章中描述过。
tr模板中剩余的模板表达式演示了如何有选择地生成内容。
...
<td [ngSwitch]="item.complete">
<span *ngSwitchCase="true">Yes</span>
<span *ngSwitchDefault>No</span>
</td>
...
[ngSwitch]表达式是一个条件语句,用于根据指定的值将不同的元素集插入到文档中,该值在本例中是item.complete属性。嵌套在td元素中的是两个用*ngSwitchCase和*ngSwitchDefault标注的span元素,它们相当于普通 JavaScript switch块的case和default关键字。我在第十三章中详细描述了ngSwitch(以及第十四章中方括号的含义),但结果是当item.complete属性值为true时第一个span元素被添加到文档中,当item.complete为false时第二个span元素被添加到文档中。结果是item.complete属性的true / false值被转换成包含Yes或No的span元素。当您保存对模板的更改时,浏览器将重新加载,并显示待办事项表,如图 2-6 所示。
图 2-6。
显示待办事项表
如果您使用浏览器的 F12 开发工具,您将能够看到模板生成的 HTML 内容。(查看页面源代码是做不到这一点的,它只显示了服务器发送的 HTML,而没有显示 Angular 使用 DOM API 所做的更改。)
您可以看到模型中的每个待办事项如何在表格中生成一行,该行填充有local项和i变量,以及开关表达式如何显示 Yes 或 No 来指示任务是否已完成。
...
<tr>
<td>2</td>
<td>Get flowers</td>
<td><span>No</span></td>
</tr>
...
创建双向数据绑定
目前,模板只包含单向数据绑定,这意味着它们用于显示数据值,但不能改变它。Angular 还支持双向数据绑定,可以用来显示数据值和修改它。HTML 表单元素使用双向绑定,清单 2-17 向模板添加了一个 checkbox input元素,允许用户将待办事项标记为完成。
<h3 class="bg-primary text-center text-white p-2">
{{ username }}'s To Do List
<h6 class="mt-1">{{ itemCount }} Incomplete Items</h6>
</h3>
<table class="table table-striped table-bordered table-sm">
<thead>
<tr><th>#</th><th>Description</th><th>Done</th></tr>
</thead>
<tbody>
<tr *ngFor="let item of items; let i = index">
<td>{{ i + 1 }}</td>
<td>{{ item.task }}</td>
<td><input type="checkbox" [(ngModel)]="item.complete" /></td>
<td [ngSwitch]="item.complete">
<span *ngSwitchCase="true">Yes</span>
<span *ngSwitchDefault>No</span>
</td>
</tr>
</tbody>
</table>
Listing 2-17.Adding a Two-Way Binding in the app.component.html File in the src/app Folder
ngModel模板表达式在数据值(本例中为item.complete属性)和表单元素(本例中为input元素)之间创建了一个双向绑定。当您保存对模板的更改时,您将看到一个包含复选框的新列出现在表格中。复选框的初始值是使用item.complete属性设置的,就像常规的单向绑定一样,但是当用户切换复选框时,Angular 通过更新指定的模型属性来响应。
当保存对模板的更改时,Angular 开发工具将报告一个错误,因为ngModel功能尚未启用。Angular 应用有一个根模块,用于配置应用。示例应用的根模块在app.module.ts文件中定义,清单 2-18 中显示的更改启用了双向绑定特性。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from "@angular/forms";
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule, FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Listing 2-18.Enabling a Feature in the app.module.ts File in the src/app Folder
Angular 提供的功能是在单独的 JavaScript 模块中提供的,这些模块必须用一个import语句添加到应用中,并使用由NgModule装饰器定义的imports属性进行注册。当 Angular 开发工具构建应用时,它们将由imports属性指定的特性合并到发送到浏览器的文件中。对根模块的更改不会自动处理,所以停止 Angular 开发工具,运行todo文件夹中清单 2-19 所示的命令,再次启动它们。(开发工具将不再报告错误,但是在您重新启动工具之前,复选框将不起作用。)
ng serve
Listing 2-19.Starting the Angular Development Tools
浏览器将在包含复选框的表格中显示一个附加列。每个复选框的状态基于一个TodoItem对象的complete属性的值。为了演示复选框是用双向绑定设置的,我留下了包含 Yes/No 值的列。当您切换复选框时,相应的是/否值也会改变,如图 2-7 所示。
图 2-7。
使用双向数据绑定更改模型值
请注意,未完成项目的数量也会更新。这揭示了一个重要的 Angular 特征:数据模型是活动的。这意味着当数据模型发生变化时,数据绑定(甚至是单向数据绑定)也会更新。这简化了 web 应用的开发,因为这意味着您不必担心在应用状态改变时显示更新。
过滤待办事项
复选框允许更新数据模型,下一步是删除标记为完成的待办事项。清单 2-20 改变了组件的items属性,这样它就可以过滤掉所有已经完成的项目。
import { Component } from '@angular/core';
import { TodoList } from "./todoList";
import { TodoItem } from "./todoItem";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
private list = new TodoList("Bob", [
new TodoItem("Go for run", true),
new TodoItem("Get flowers"),
new TodoItem("Collect tickets"),
]);
get username(): string {
return this.list.user;
}
get itemCount(): number {
return this.items.length;
}
get items(): readonly TodoItem[] {
return this.list.items.filter(item => !item.complete);
}
}
Listing 2-20.Filtering To-Do Items in the app.component.ts File in the src/app Folder
filter 方法是一个标准的 JavaScript 数组函数,因为这是我之前在itemCount属性中使用的同一表达式,所以我更新了该属性以避免代码重复。由于数据模型是动态的,变化会立即反映在数据绑定中,所以选中某个项目的复选框会将其从视图中移除,如图 2-8 所示。
图 2-8。
过滤待办事项
添加待办事项
如果没有向列表中添加新项目的能力,待办事项应用就没有多大用处。清单 2-21 向模板中添加元素,允许用户输入任务的细节。
<h3 class="bg-primary text-center text-white p-2">
{{ username }}'s To Do List
<h6 class="mt-1">{{ itemCount }} Incomplete Items</h6>
</h3>
<div class="container-fluid">
<div class="row">
<div class="col">
<input class="form-control" placeholder="Enter task here" #todoText />
</div>
<div class="col-auto">
<button class="btn btn-primary" (click)="addItem(todoText.value)">
Add
</button>
</div>
</div>
</div>
<div class="m-2">
<table class="table table-striped table-bordered table-sm">
<thead>
<tr><th>#</th><th>Description</th><th>Done</th></tr>
</thead>
<tbody>
<tr *ngFor="let item of items; let i = index">
<td>{{ i + 1 }}</td>
<td>{{ item.task }}</td>
<td><input type="checkbox" [(ngModel)]="item.complete" /></td>
<td [ngSwitch]="item.complete">
<span *ngSwitchCase="true">Yes</span>
<span *ngSwitchDefault>No</span>
</td>
</tr>
</tbody>
</table>
</div>
Listing 2-21.Adding Elements in the app.component.html File in the src/app Folder
大多数新元素创建一个网格布局来显示一个input元素和一个button元素。input 元素有一个属性,其名称以#字符开头,用于定义一个变量来引用模板数据绑定中的元素。
...
<input class="form-control" placeholder="Enter task here" #todoText />
...
变量的名称是todoText,它被应用于button元素的绑定所使用。
...
<button class="btn btn-primary mt-1" (click)="addItem(todoText.value)">
...
这是一个事件绑定的例子,它告诉 Angular 调用一个名为addItem的组件方法,使用input元素的value属性作为方法参数。清单 2-22 向组件添加了addItem方法。
Tip
现在不要担心区分绑定。我在第二部分中解释了 Angular 支持的不同类型的绑定,以及每种绑定需要的不同类型的括号或圆括号的含义。它们并不像第一次出现时那么复杂,尤其是当你看到它们是如何融入其他 Angular 框架的时候。
import { Component } from '@angular/core';
import { TodoList } from "./todoList";
import { TodoItem } from "./todoItem";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
private list = new TodoList("Bob", [
new TodoItem("Go for run", true),
new TodoItem("Get flowers"),
new TodoItem("Collect tickets"),
]);
get username(): string {
return this.list.user;
}
get itemCount(): number {
return this.items.length;
}
get items(): readonly TodoItem[] {
return this.list.items.filter(item => !item.complete);
}
addItem(newItem) {
if (newItem != "") {
this.list.addItem(newItem);
}
}
}
Listing 2-22.Adding a Method in the app.component.ts File in the src/app Folder
addItem方法接收模板中事件绑定发送的文本,并使用它向待办事项列表添加一个新项目。这些改变的结果是你可以通过在input元素中输入文本并点击添加按钮来创建新的待办事项,如图 2-9 所示。
图 2-9。
创建待办事项
显示已完成的待办事项
基本特性已经就绪,现在是时候结束这个项目了。我首先从模板中删除了表中的 Yes/No 列,并添加了显示已完成任务的选项,如清单 2-23 所示。
<h3 class="bg-primary text-center text-white p-2">
{{ username }}'s To Do List
<h6 class="mt-1">{{ itemCount }} {{ showComplete ? "" : "Incomplete" }} Items</h6>
</h3>
<div class="container-fluid">
<div class="row">
<div class="col">
<input class="form-control" placeholder="Enter task here" #todoText />
</div>
<div class="col-auto">
<button class="btn btn-primary" (click)="addItem(todoText.value)">
Add
</button>
</div>
</div>
</div>
<div class="m-2">
<table class="table table-striped table-bordered table-sm">
<thead>
<tr><th>#</th><th>Description</th><th>Done</th></tr>
</thead>
<tbody>
<tr *ngFor="let item of items; let i = index">
<td>{{ i + 1 }}</td>
<td>{{ item.task }}</td>
<td><input type="checkbox" [(ngModel)]="item.complete" /></td>
<!-- <td [ngSwitch]="item.complete">
<span *ngSwitchCase="true">Yes</span>
<span *ngSwitchDefault>No</span>
</td> -->
</tr>
</tbody>
</table>
</div>
<div class="bg-secondary text-white text-center p-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="showComplete" />
<label class="form-check-label" for="defaultCheck1">
Show Completed Tasks
</label>
</div>
</div>
Listing 2-23.Adding and Removing Elements in the app.component.html File in the src/app Folder
新元素提供了一个复选框,它有一个名为showComplete的属性的双向数据绑定。在新的表达式中使用相同的属性来改变告诉用户显示多少项的文本。正如我前面提到的,数据绑定可以包含 JavaScript 表达式,在这种情况下,我使用showComplete属性的值来控制单词Incomplete是否包含在输出中。
在清单 2-24 中,我添加了showComplete属性的定义,并使用它的值来决定是否向用户显示已完成的任务。
import { Component } from '@angular/core';
import { TodoList } from "./todoList";
import { TodoItem } from "./todoItem";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
private list = new TodoList("Bob", [
new TodoItem("Go for run", true),
new TodoItem("Get flowers"),
new TodoItem("Collect tickets"),
]);
get username(): string {
return this.list.user;
}
get itemCount(): number {
return this.items.length;
}
get items(): readonly TodoItem[] {
return this.list.items.filter(item => this.showComplete || !item.complete);
}
addItem(newItem) {
if (newItem != "") {
this.list.addItem(newItem);
}
}
showComplete: boolean = false;
}
Listing 2-24.Showing Completed Tasks in the app.component.ts File in the src/app Folder
结果是用户可以决定是否查看已完成的任务,如图 2-10 所示。
图 2-10。
显示已完成的任务
摘要
在这一章中,我向你展示了如何创建你的第一个简单的 Angular 应用,它允许用户创建新的待办事项并将现有的事项标记为完成。
如果这一章中的所有内容都有意义,请不要担心。在这个阶段,重要的是理解 Angular 应用的一般形状,它是围绕数据模型、组件和模板构建的。如果你把这三个关键的组成部分记在心里,那么你就会对接下来的事情有一个背景。在下一章,我将 Angular 放在上下文中。
三、将 Angular 放在上下文中
在这一章中,我将 Angular 放在 web 应用开发的环境中,为后面的章节打下基础。Angular 的目标是将只适用于服务器端开发的工具和功能引入到 web 客户端,这样做可以使开发、测试和维护丰富复杂的 web 应用变得更加容易。
Angular 的工作原理是允许你扩展 HTML,这看起来是一个奇怪的想法,直到你习惯了它。Angular 应用通过自定义元素表达功能,复杂的应用可以生成一个包含标准和自定义标记的 HTML 文档。
Angular 支持的开发风格是通过使用模型-视图-控制器 (MVC)模式派生出来的,尽管这有时被称为模型-视图- 什么的,因为在使用 Angular 时可以遵循这种模式的无数变体。在本书中,我将把重点放在标准 MVC 模式上,因为它是最成熟和最广泛使用的模式。在接下来的章节中,我解释了 Angular 可以带来显著好处的项目的特征(以及那些存在更好的替代方案的项目),描述了 MVC 模式,并描述了一些常见的陷阱。
This Book and the Angular Release Schedule
谷歌对 Angular 采取了积极的发布时间表。这意味着每六个月就会有一个小版本和一个大版本。次要版本不应该破坏任何现有功能,并且应该主要包含错误修复。主要版本可能包含重大更改,并且可能不提供向后兼容性。
要求读者每六个月购买这本书的新版本似乎不公平也不合理,尤其是因为即使在主要版本中,大多数 Angular 特征也不太可能改变。取而代之的是,我将在本书 https://github.com/Apress/pro-angular-9 的 GitHub 资源库中发布主要版本的更新。
对我来说(对出版社来说)这是一个正在进行的实验,但目标是通过补充书中包含的例子来延长这本书的寿命。
我不承诺更新会是什么样的,它们会采取什么形式,或者在我把它们折叠成这本书的新版本之前,我会花多长时间来制作它们。请保持开放的心态,并在新版本发布时查看这本书的资源库。如果您对如何改进更新有任何想法,请发电子邮件至adam@adam-freeman.com告诉我。
了解 Angular 的优势
Angular 不是所有问题的解决方案,知道什么时候应该使用 Angular,什么时候应该寻求替代方案是很重要的。Angular 提供了以前只有服务器端开发人员才能使用的功能,但完全是在浏览器中。这意味着 Angular 在每次加载应用了 Angular 的 HTML 文档时都有大量的工作要做——必须编译 HTML 元素,必须评估数据绑定,必须执行组件和其他构建块,等等。所有这些工作都是构建我在第二章中演示的特性以及我在本书后面描述的特性所必需的。
这种工作需要时间,时间的长短取决于 HTML 文档的复杂程度、相关的 JavaScript 代码,以及关键的浏览器质量和设备的处理能力。在功能强大的台式机上使用最新的浏览器时,你不会注意到任何延迟,但功能不足的智能手机上的旧浏览器确实会减慢 Angular 应用的初始设置。
目标是尽可能少地执行这种设置,并在执行时向用户交付尽可能多的应用。这意味着仔细考虑您构建的 web 应用的类型。从广义上讲,web 应用有两种:往返和单页。
了解往返和单页应用
很长一段时间以来,web 应用的开发都遵循一个往返模型。浏览器向服务器请求一个初始的 HTML 文档。用户交互——比如单击一个链接或提交一个表单——使浏览器请求并接收一个全新的 HTML 文档。在这种应用中,浏览器本质上是 HTML 内容的呈现引擎,所有的应用逻辑和数据都驻留在服务器上。浏览器发出一系列无状态的 HTTP 请求,服务器通过动态生成 HTML 文档来处理这些请求。
许多当前的 web 开发仍然是针对往返应用的,尤其是因为它们对浏览器的要求很少,这确保了尽可能广泛的客户端支持。但是往返应用也有一些缺点:它们让用户在请求和加载下一个 HTML 文档时等待,它们需要大型的服务器端基础设施来处理所有请求和管理所有应用状态,并且它们需要更多的带宽,因为每个 HTML 文档都必须是自包含的(导致服务器的每个响应中都包含大量相同的内容)。
单页应用采取了不同的方法。一个初始的 HTML 文档被发送到浏览器,但是用户交互会导致 Ajax 请求将小的 HTML 片段或数据插入到向用户显示的现有元素集中。初始的 HTML 文档永远不会被重新加载或替换,当 Ajax 请求被异步执行时,用户可以继续与现有的 HTML 交互,即使这只是意味着看到一个“数据加载”消息。
Angular 擅长单页应用,尤其是复杂的往返应用。对于更简单的项目,直接使用 DOM API 或者通过更简单的库(比如 jQuery)使用通常是更好的选择,尽管没有什么可以阻止您在所有项目中使用 Angular。
单页应用模型对于 Angular 来说是完美的,不仅仅是因为初始化过程,还因为使用 MVC 模式(我将在本章后面描述)的好处真正开始在更大更复杂的项目中体现出来,这些项目正在向单页模型推进。
Tip
你可能遇到的另一个短语是渐进式网络应用 (PWAs)。渐进式应用即使在与网络断开连接时也能继续工作,并且可以访问推送通知等功能。PWA 不是 Angular 特有的,但是我在第十章演示了如何使用简单的 PWA 特性。
比较 Angular to React 和 Vue.js
Angular 有两个主要的竞争对手:React 和 Vue.js。它们之间有一些低级的差异,但是,在大多数情况下,所有这些框架都很优秀,它们都以相似的方式工作,并且它们都可以用来创建丰富而流畅的客户端应用。
这些框架之间的主要区别在于开发人员的体验。例如,Angular 要求您使用 TypeScript 才能有效。如果您习惯于使用 C#或 Java 之类的语言,那么 TypeScript 将会很熟悉,并且可以避免处理 JavaScript 语言的一些奇怪之处。Vue.js 和 React 不需要 TypeScript(尽管这两个框架都支持),但倾向于将 HTML、JavaScript 和 CSS 内容混合在一个文件中,这不是每个人都喜欢的。
我的建议很简单:选择你最喜欢的框架,如果你不喜欢,就换一个。这可能看起来是一种不科学的方法,但是这并不是一个坏的选择,并且您会发现许多核心概念会在框架之间延续,即使您进行了转换。
理解 MVC 模式
术语模型-视图-控制器从 20 世纪 70 年代末就开始使用,起源于施乐 PARC 公司的 Smalltalk 项目,当时它被认为是一种组织早期 GUI 应用的方法。最初 MVC 模式的一些细节依赖于 Smalltalk 特有的概念,例如屏幕和工具,但是更广泛的思想仍然适用于应用,并且它们特别适合于 web 应用。
MVC 模式最初通过 Ruby on Rails 和 ASP.NET MVC 框架等工具包在 web 开发的服务器端站稳了脚跟。近年来,MVC 模式也被视为管理客户端 web 开发日益丰富和复杂的一种方式,Angular 就是在这种环境下出现的。
应用 MVC 模式的关键是实现关注点分离的关键前提,其中应用中的数据模型与业务和表示逻辑相分离。在客户端 web 开发中,这意味着分离数据、对数据进行操作的逻辑以及用于显示数据的 HTML 元素。结果是客户端应用更容易开发、维护和测试。
三个主要的构建模块是模型、控制器和视图。在图 3-1 中,你可以看到 MVC 模式应用于服务器端开发的传统阐述。
图 3-1。
MVC 模式的服务器端实现
我从我的一本Pro ASP.NET 核心书中获得了这个数字,该书描述了微软的 MVC 模式的服务器端实现。您可以看到期望是从数据库中获得模型,并且应用的目标是服务来自浏览器的 HTTP 请求。这是我前面描述的往返 web 应用的基础。
当然,浏览器中存在 Angular,导致了 MVC 主题的扭曲,如图 3-2 所示。
图 3-2。
MVC 模式的客户端实现
MVC 模式的客户端实现从服务器端组件获取数据,通常是通过 RESTful web 服务,我在第二十四章对此进行了描述。控制器和视图的目标是操作模型中的数据以执行 DOM 操作,从而创建和管理用户可以与之交互的 HTML 元素。这些交互被反馈给控制器,形成一个交互应用的闭环。
Angular 对其构建块使用了稍微不同的术语,这意味着使用 Angular 实现的 MVC 模型看起来更像图 3-3 。
图 3-3。
MVC 模式的 Angular 实现
该图显示了 Angular 构建块到 MVC 模式的基本映射。为了支持 MVC 模式,Angular 提供了一系列额外的特性,我在整本书中都有描述。
Tip
使用像 Angular 这样的客户端框架并不排除使用服务器端 MVC 框架,但是您会发现 Angular 客户端承担了一些原本存在于服务器端的复杂性。这通常是一件好事,因为它将工作从服务器转移到了客户端,这样就可以用更少的服务器容量支持更多的客户端。
Patterns and Pattern Zealots
一个好的模式描述了一种解决问题的方法,这种方法对其他项目中的其他人有效。模式是食谱,而不是规则,您需要调整任何模式以适应您的特定项目,就像厨师调整食谱以适应不同的烤箱和配料一样。
你偏离一个模式的程度应该由需求和经验来决定。你在类似的项目中应用一个模式所花费的时间将会告诉你什么对你有用,什么对你没用。如果您是一个模式的新手,或者您正在着手一个新的项目,那么您应该尽可能地坚持这个模式,直到您真正理解等待您的好处和陷阱。但是,注意不要围绕一个模式来改革你的整个开发工作,因为大范围的中断通常会导致生产力的损失,破坏你希望该模式给出的任何结果。
模式是灵活的工具,而不是固定的规则,但并不是所有的开发人员都理解这种差异,有些人成为了模式狂热者。这些人花更多的时间谈论模式,而不是将其应用到项目中,并认为任何偏离他们对模式的解释都是严重的犯罪。我的建议是简单地忽略这种人,因为任何一种接触都会让你筋疲力尽,而且你永远无法改变他们的想法。相反,只要继续做一些工作,并通过实际的应用和交付演示一个模式的灵活应用如何产生好的结果。
记住这一点,你会看到我在本书的例子中遵循了 MVC 模式的广泛概念,但是我调整了模式来演示不同的特性和技术。这就是我在我自己的项目中的工作方式——拥抱模式中提供价值的部分,将那些不提供价值的部分放在一边。
理解模型
模型 MVC 中的M——包含用户使用的数据。有两种广泛的模型类型:视图模型,它只表示从组件传递到模板的数据,以及域模型,它包含业务域中的数据,以及创建、存储和操作这些数据的操作、转换和规则,统称为模型逻辑。
Tip
许多不熟悉 MVC 模式的开发人员对在数据模型中包含逻辑的想法感到困惑,认为 MVC 模式的目标是将数据与逻辑分离。这是一个误解:MVC 框架的目标是将应用分成三个功能区域,每个区域可能包含逻辑和数据。目标不是从模型中消除逻辑。相反,它是为了确保模型只包含用于创建和管理模型数据的逻辑。
你不可能在没有被单词 business 绊倒的情况下阅读 MVC 模式的定义,这是不幸的,因为许多 web 开发远远超出了导致这种术语的业务线应用。然而,业务应用仍然是开发世界的一大块,如果你正在编写,比如说,一个销售会计系统,那么你的业务领域将包含与销售会计相关的过程,你的领域模型将包含帐户数据和创建、存储和管理帐户的逻辑。如果你是在创建猫视频网站,那么你还是有业务领域的;只是它可能不适合公司的结构。您的领域模型将包含 cat 视频以及创建、存储和操作这些视频的逻辑。
许多 Angular 模型会有效地将逻辑推到服务器端,并通过 RESTful web 服务调用它,因为浏览器中很少支持数据持久性,而且通过 Ajax 更容易获得所需的数据。我会在第二十四章中解释 Angular 如何用于 RESTful web 服务。对于 MVC 模式中的每一个元素,我将描述哪些应该包含,哪些不应该包含。使用 MVC 模式构建的应用中的模型应该
-
包含域数据
-
包含创建、管理和修改域数据的逻辑(即使这意味着通过 web 服务执行远程逻辑)
-
提供一个清晰的 API,公开模型数据和操作
型号不应
-
公开如何获得或管理模型数据的细节(换句话说,数据存储机制或远程 web 服务的细节不应该向控制器和视图公开)
-
包含基于用户交互转换模型的逻辑(因为这是组件的工作)
-
包含向用户显示数据的逻辑(这是模板的工作)
确保模型与控制器和视图隔离的好处是你可以更容易地测试你的逻辑(我在第二十九章描述了 Angular 单元测试),并且增强/维护整个应用更加简单和容易。
最好的域模型包含持久获取和存储数据的逻辑,包含创建、读取、更新和删除操作的逻辑(统称为 CRUD)或查询和修改数据的独立模型,称为命令和查询责任分离(CQRS)模式。
这可能意味着模型直接包含逻辑,但更常见的是,模型将包含调用 RESTful web 服务的逻辑,以调用服务器端数据库操作(当我构建一个真实的 Angular 应用时,我将在第八章中演示,我将在第二十四章中详细描述)。
了解控制器/组件
控制器在 Angular 中被称为组件,是 Angular web app 中的结缔组织;它们充当数据模型和视图之间的管道。组件添加了表示模型的各个方面并对其执行操作所需的业务领域逻辑。遵循 MVC 模式的组件应该
-
包含设置模板初始状态所需的逻辑
-
包含模板所需的逻辑/行为,以呈现模型中的数据
-
包含基于用户交互更新模型所需的逻辑/行为
组件不应
-
包含操作 DOM 的逻辑(这是模板的工作)
-
包含管理数据持久性的逻辑(这是模型的工作)
了解视图数据
领域模型并不是 Angular 应用中的唯一数据。组件可以创建视图数据(也称为视图模型数据或视图模型)来简化模板及其与组件的交互。
了解视图/模板
视图,在 Angular 中被称为模板,是使用通过数据绑定增强的 HTML 元素定义的。正是数据绑定使得 Angular 如此灵活,它们将 HTML 元素转化为动态 web 应用的基础。我将在第二部分详细解释 Angular 提供的不同类型的数据绑定。模板应该
- 包含向用户显示数据所需的逻辑和标记
模板不应
-
包含复杂的逻辑(最好放在一个组件或其他有 Angular 的构建块中,如指令、服务或管道)
-
包含创建、存储或操作领域模型的逻辑
模板可以包含逻辑,但是应该简单,少用。除了最简单的方法调用或表达式之外,在模板中放置任何东西都会使整个应用更难测试和维护。
理解 RESTful 服务
Angular 应用中的领域模型逻辑通常在客户端和服务器端分开。服务器包含持久性存储,通常是一个数据库,并包含管理它的逻辑。例如,在 SQL 数据库的情况下,所需的逻辑将包括打开到数据库服务器的连接,执行 SQL 查询,并处理结果以便将它们发送到客户机。
您不希望客户端代码直接访问数据存储—这样做会在客户端和数据存储之间产生紧密耦合,这会使单元测试变得复杂,并且在不更改客户端代码的情况下很难更改数据存储。
通过使用服务器来协调对数据存储的访问,可以防止紧耦合。客户端上的逻辑负责从服务器获取数据,并不知道在后台如何存储或访问数据的细节。
在客户机和服务器之间传递数据有很多种方式。最常见的一种是使用异步 JavaScript 和 XML (Ajax)请求来调用服务器端代码,让服务器发送 JSON 并使用 HTML 表单对数据进行更改。
这种方法可以很好地工作,并且是 RESTful web 服务的基础,RESTful web 服务使用 HTTP 请求的性质对数据执行 CRUD 操作。
Note
REST 是 API 的一种风格,而不是一个定义良好的规范,对于什么样的 web 服务才是 RESTful 的还存在争议。争论的一点是纯粹主义者不认为返回 JSON 的 web 服务是 RESTful 的。就像任何关于架构模式的分歧一样,分歧的原因是任意的和乏味的,根本不值得担心。就我而言,JSON 服务是 RESTful 的,我在本书中也是这样看待它们的。
在 RESTful web 服务中,被请求的操作通过 HTTP 方法和 URL 的组合来表达。例如,想象这样一个 URL:
http://myserver.mydomain.com/people/bob
RESTful web 服务没有标准的 URL 规范,但其思想是使 URL 不言自明,这样 URL 所指的内容就很明显了。在这种情况下,很明显有一个名为people的数据对象集合,并且 URL 指向该集合中标识为bob的特定对象。
Tip
在实际项目中并不总是能够创建这种不言自明的 URL,但是您应该努力保持简单,不要通过 URL 暴露数据存储的内部结构(因为这只是组件之间的另一种耦合)。尽可能保持你的 URL 简单,并保持 URL 格式和服务器中数据结构之间的映射。
URL 标识我想要操作的数据对象,HTTP 方法指定我想要执行什么操作,如表 3-1 所述。
表 3-1。
响应 HTTP 方法时通常执行的操作
|方法
|
描述
|
| --- | --- |
| GET | 检索由 URL 指定的数据对象 |
| PUT | 更新由 URL 指定的数据对象 |
| POST | 创建新的数据对象,通常使用表单数据值作为数据字段 |
| DELETE | 删除由 URL 指定的数据对象 |
您不必使用 HTTP 方法来执行我在表中描述的操作。一个常见的变化是 POST 方法通常用于双重用途,如果存在对象,它将更新对象,如果不存在,它将创建一个对象,这意味着不使用 PUT 方法。在第二十四章中,我描述了 Angular 为 Ajax 和 RESTful 服务提供的支持。
Idempotent HTTP Methods
您可以在 HTTP 方法和数据存储上的操作之间实现任何映射,尽管我建议您尽可能遵循我在表中描述的约定。
如果您偏离了正常的方法,请确保遵循 HTTP 规范中定义的 HTTP 方法的本质。GET 方法是nullipent,这意味着响应这个方法而执行的操作应该只检索数据,而不修改数据。浏览器(或任何中间设备,如代理)希望能够重复发出 GET 请求,而不改变服务器的状态(尽管这并不意味着服务器的状态不会因为来自其他客户机的请求而在相同的 GET 请求之间改变)。
PUT 和 DELETE 方法是等幂,这意味着多个相同的请求应该具有与单个请求相同的效果。因此,例如,使用带有/people/bob URL 的 DELETE 方法应该为第一个请求从people集合中删除bob对象,然后对后续请求不做任何事情。(同样,当然,如果另一个客户机重新创建了bob对象,这就不成立。)
POST 方法既不是无效的也不是等幂的,这就是为什么一个常见的 RESTful 优化是处理对象创建和更新。如果没有bob对象,使用 POST 方法将创建一个对象,对同一 URL 的后续 POST 请求将更新已创建的对象。
只有当您实现自己的 RESTful web 服务时,所有这些才是重要的。如果您正在编写一个使用 RESTful 服务的客户端,那么您只需要知道每个 HTTP 方法对应的数据操作。我将在第八章中演示如何使用这样的服务,并在第二十四章中更详细地描述 HTTP 请求的 Angular 特性。
常见的设计陷阱
在这一节中,我描述了我在 Angular 项目中遇到的三个最常见的设计陷阱。这些不是编码错误,而是 web 应用整体形状的问题,这些问题阻碍了项目团队获得 Angular 和 MVC 模式所能提供的好处。
把逻辑放在错误的地方
最常见的问题是将逻辑放入了错误的组件中,从而破坏了 MVC 关注点分离。以下是这一问题的三种最常见形式:
-
将业务逻辑放在模板中,而不是组件中
-
将领域逻辑放在组件中,而不是模型中
-
当使用 RESTful 服务时,将数据存储逻辑放在客户端模型中
这些都是棘手的问题,因为它们需要一段时间才能显现为问题。应用仍然可以运行,但是随着时间的推移,它将变得更加难以增强和维护。在第三种情况下,只有当数据存储发生变化时,问题才会变得明显(这种情况很少发生,直到项目成熟,并且已经超出了最初的用户预测)。
Tip
对逻辑应该去哪里有一个感觉需要一些经验,但是如果你使用单元测试,你会更早发现问题,因为你必须写的覆盖逻辑的测试不能很好地适应 MVC 模式。我在第二十九章中描述了单元测试的 Angular 支持。
当你在 Angular 发展中获得更多经验时,知道在哪里放置逻辑成为第二天性,但是这里有三个规则:
-
模板逻辑应该只准备用于显示的数据,而不修改模型。
-
组件逻辑不应该直接从模型中创建、更新或删除数据。
-
模板和组件不应该直接访问数据存储。
如果您在开发时记住这些,您将避免最常见的问题。
采用数据存储数据格式
当开发团队构建依赖于服务器端数据存储的应用时,下一个问题就出现了。在一个从 RESTful 服务获取数据的设计良好的 Angular 应用中,服务器的工作是隐藏数据存储实现细节,并以有利于简化客户端的适当数据格式向客户端呈现数据。例如,决定客户端需要如何表示日期,然后确保您在数据存储中使用该格式——如果数据存储本身不支持该格式,则由服务器执行转换。
只是足够制造麻烦的知识
Angular 是一个复杂的框架,在你习惯它之前,你会感到困惑。有许多不同的构建模块可用,它们可以以不同的方式组合来实现类似的结果。这使得 Angular 开发变得灵活,并且意味着您将通过创建适合您的项目和工作风格的特性组合来开发您自己的问题解决风格。
精通 Angular 需要时间。在了解 Angular 的不同部分是如何组合在一起的之前,你很容易就开始创建自己的项目。你可能在没有真正理解它为什么会起作用的情况下生产出一些有用的东西,当你需要做出改变的时候,这是一个灾难的处方。我的建议是慢慢来,花时间了解 Angular 提供的所有功能。无论如何,尽早开始创建项目,但是要确保你真的了解它们是如何工作的,并且当你找到更好的方法来达到你想要的结果时,准备好做出改变。
摘要
在这一章中,我为 Angular 提供了一些上下文。我解释了 Angular 如何支持 MVC 模式进行应用开发,并且简要概述了 REST 以及如何使用它来表达 HTTP 请求上的数据操作。我通过描述 Angular 项目中三个最常见的设计问题结束了这一章。在下一章中,我将提供一个快速入门的 HTML 和引导 CSS 框架,我将在本书的例子中使用。
四、HTML 和 CSS 入门
开发人员通过许多途径进入 web 应用开发的世界,并不总是基于 web 应用所依赖的基本技术。在这一章中,我提供了一个 HTML 的简单入门,并介绍了引导 CSS 库,我用它来设计本书中的例子。在第 5 和 6 章中,我介绍了 JavaScript 和 TypeScript 的基础知识,并给出了理解本书其余部分中的例子所需的信息。如果你是一个有经验的开发者,你可以跳过这些初级章节,直接跳到第七章中,在那里我使用 Angular 创建了一个更复杂和真实的应用。
准备示例项目
对于这一章,我只需要一个简单的示例项目。我首先创建了一个名为HtmlCssPrimer的文件夹,在其中创建了一个名为package.json的文件,并添加了清单 4-1 中所示的内容。
{
"dependencies": {
"bootstrap": "4.4.1"
}
}
Listing 4-1.The Contents of the package.json File in the HtmlCssPrimer Folder
在HtmlCssPrimer文件夹中运行以下命令,下载并安装package.json文件中指定的 NPM 软件包:
npm install
接下来,我在HtmlCssPrimer文件夹中创建了一个名为index.html的文件,并添加了清单 4-2 中所示的内容。
<!DOCTYPE html>
<html>
<head>
<title>ToDo</title>
<meta charset="utf-8" />
<link href="node_modules/bootstrap/dist/css/bootstrap.min.css"
rel="stylesheet" />
</head>
<body class="m-1">
<h3 class="bg-primary text-white p-3">Adam's To Do List</h3>
<div class="my-1">
<input class="form-control" />
<button class="btn btn-primary mt-1">Add</button>
</div>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
<tr><td>Buy Flowers</td><td>No</td></tr>
<tr><td>Get Shoes</td><td>No</td></tr>
<tr><td>Collect Tickets</td><td>Yes</td></tr>
<tr><td>Call Joe</td><td>No</td></tr>
</tbody>
</table>
</body>
</html>
Listing 4-2.The Contents of the index.html File in the HtmlCssPrimer Folder
这是一个简单的 HTML 文档,包含一个基本的待办事项列表,类似于我在第二章 ?? 中用 Angular 创建的那个。在HtmlCssPrimer文件夹中运行以下命令,下载并运行 HTTP 服务器:
npx lite-server@2.5.4
lite-server包包含一个开发 HTTP 服务器,当它检测到文件改变时,会自动触发浏览器刷新。一旦包被下载,服务器将启动,一个浏览器窗口将打开,显示如图 4-1 所示的内容。(如果您的系统配置为阻止浏览器自动打开,您可以请求http://localhost:3000。)
图 4-1。
运行示例应用
理解 HTML
HTML 的核心是元素,它告诉浏览器 HTML 文档的每个部分代表什么样的内容。以下是示例 HTML 文档中的一个元素:
...
<td>Buy Flowers</td>
...
如图 4-2 所示,这个元素有三个部分:开始标签、结束标签和内容。
图 4-2。
简单 HTML 元素的剖析
这个元素的名称(也称为标签名称或者仅仅是标签)是td,它告诉浏览器标签之间的内容应该被当作一个表格单元格。您可以通过将标签名称放在尖括号中(<和>字符)来开始一个元素,并以类似的方式使用标签来结束一个元素,除了您还可以在左尖括号(<)后添加一个/字符。出现在标签之间的是元素的内容,可以是文本(比如本例中的Buy Flowers)或其他 HTML 元素。
了解空元素
HTML 规范包括不允许包含内容的元素。这些被称为 void 或自闭元素,它们没有单独的结束标记,就像这样:
...
<input />
...
在单个标记中定义了一个 void 元素,并在最后一个尖括号(>字符)前添加了一个/字符。input元素是最常用的 void 元素,其目的是允许用户通过文本字段、单选按钮或复选框提供输入。在后面的章节中,你会看到很多使用这个元素的例子。
了解属性
您可以通过向元素添加属性来为浏览器提供附加信息。以下是示例文档中带有属性的元素:
...
<link href="node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
...
这是一个link元素,它将内容导入文档。有两个属性,我已经强调过了,所以它们更容易看到。属性总是被定义为开始标签的一部分,这些属性有一个名为的和一个值为的。
本例中两个属性的名称是href和rel。对于link元素,href属性指定要导入的内容,rel属性告诉浏览器这是哪种内容。这个link元素的属性告诉浏览器导入bootstrap.min.css文件,并把它当作一个样式表,它是一个包含 CSS 样式的文件。
应用不带值的属性
并非所有属性都应用了值;只需将它们添加到元素中,就可以告诉浏览器您想要某种特定的行为。下面是一个具有这种属性的元素的示例(不是来自示例文档;我只是虚构了这个示例元素):
...
<input class="form-control" required />
...
这个元素有两个属性。第一个是class,它被赋值,就像前面的例子一样。另一个属性就是required这个词。这是一个不需要值的属性的例子。
在属性中引用文字值
Angular 依赖 HTML 元素属性来应用它的许多功能。大多数时候,属性的值是作为 JavaScript 表达式来计算的,比如这个元素,摘自第二章:
...
<td [ngSwitch]="item.complete">
...
应用于td元素的属性告诉 Angular 读取一个对象上名为complete的属性的值,该对象已被分配给一个名为item的变量。有时,您需要提供一个特定的值,而不是让 Angular 从数据模型中读取一个值,这需要额外的引用来告诉 Angular 它正在处理一个文字值,如下所示:
...
<td [ngSwitch]="'Apples'">
...
属性值包含字符串Apples,用单引号和双引号引起来。当 Angular 计算属性值时,它会看到单引号并将该值作为文字字符串处理。
了解元素内容
元素可以包含文本,但也可以包含其他元素,如下所示:
...
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
...
HTML 文档中的元素形成一个层次结构。html元素包含body元素,后者包含内容元素,每个内容元素可以包含其他元素,依此类推。在清单中,thead元素包含tr元素,而tr元素又包含th元素。排列元素是 HTML 中的一个关键概念,因为它将外部元素的重要性传递给内部元素。
了解文档结构
有一些关键元素定义了 HTML 文档的基本结构:DOCTYPE、html、head和body元素。以下是这些元素之间的关系,其余内容已删除:
<!DOCTYPE html>
<html>
<head>
...head content...
</head>
<body>
...body content...
</body>
</html>
这些元素中的每一个在 HTML 文档中都扮演着特定的角色。元素告诉浏览器这是一个 HTML 文档,更确切地说,这是一个 HTML5 文档。早期版本的 HTML 需要额外的信息。例如,下面是 HTML4 文档的DOCTYPE元素:
...
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
...
html元素表示包含 HTML 内容的文档区域。这个元素总是包含另外两个关键的结构元素:head和body。正如我在本章开始时解释的那样,我不打算讨论单个的 HTML 元素。它们太多了,描述 HTML5 完全花了我 HTML 书 1000 多页。也就是说,表 4-1 提供了我在清单 4-2 中的index.html文件中使用的元素的简要描述,以帮助您理解元素如何告诉浏览器它们代表哪种内容。
表 4-1。
示例文档中使用的 HTML 元素
|元素
|
描述
|
| --- | --- |
| DOCTYPE | 指示文档中内容的类型 |
| body | 降级包含内容元素的文档区域 |
| button | 表示一个按钮;通常用于向服务器提交表单 |
| div | 通用元素;通常用于为文档添加结构,以用于演示目的 |
| h3 | 降级标题 |
| head | 降级包含元数据的文档区域 |
| html | 表示文档中包含 HTML 的区域(通常是整个文档) |
| input | 降级用于从用户处收集单个数据项的字段 |
| link | 将内容导入 HTML 文档 |
| meta | 提供关于文档的描述性数据,如字符编码 |
| table | 表示表格,用于将内容组织成行和列 |
| tbody | 表示表格的正文(与页眉或页脚相对) |
| td | 将表格行中的内容单元格降级 |
| th | 降级表格行中的标题单元格 |
| thead | 降级表格的标题 |
| title | 表示文档的标题;由浏览器用来设置窗口或选项卡的标题 |
| tr | 降级表格中的行 |
Understanding the Document Object Model
当浏览器加载并处理一个 HTML 文档时,它会创建文档对象模型 (DOM)。DOM 是一种模型,其中 JavaScript 对象用于表示文档中的每个元素,DOM 是一种机制,通过它您可以以编程方式处理 HTML 文档的内容。
在 Angular 中很少直接使用 DOM,但是理解浏览器维护由 JavaScript 对象表示的 HTML 文档的动态模型是很重要的。当 Angular 修改这些对象时,浏览器会更新其显示的内容以反映修改。这是 web 应用的关键基础之一。如果我们不能修改 DOM,我们就不能创建客户端 web 应用。
了解引导程序
HTML 元素告诉浏览器它们代表什么样的内容,但是它们不提供任何关于内容应该如何显示的信息。关于如何显示元素的信息是使用级联样式表 (CSS)提供的。CSS 由可用于配置元素外观各个方面的属性和允许应用这些属性的选择器组成。
CSS 是灵活和强大的,但它需要时间和对细节的密切关注来获得良好、一致的结果,特别是当一些传统浏览器实现的功能不一致时。CSS 框架提供了一组样式,可以很容易地应用这些样式来在整个项目中产生一致的效果。
使用最广泛的框架是 Bootstrap,它由 CSS 类和 JavaScript 代码组成,CSS 类可以应用于元素以保持一致的样式,JavaScript 代码执行额外的增强。我在本书中使用了引导 CSS 样式,因为它们让我不必在每一章中定义自定义样式就可以对我的例子进行样式化。我在本书中根本没有使用引导 JavaScript 特性,因为示例的交互部分是使用 Angular 提供的。
关于 Bootstrap,我不想讲太多细节,因为这不是本书的主题,但是我想给你足够的信息,这样你就可以知道例子的哪些部分是 Angular 特征,哪些部分是 Bootstrap 样式。参见 http://getbootstrap.com 了解 Bootstrap 提供的特性的全部细节。
应用基本引导类
引导样式是通过class属性应用的,该属性用于对相关元素进行分组。class属性不仅用于应用 CSS 样式,而且是最常见的用法,它支持 Bootstrap 和类似框架的操作方式。下面是一个带有class属性的 HTML 元素,取自index.html文件:
...
<button class="btn btn-primary mt-1">Add</button>
...
class属性将button元素分配给三个类,它们的名称由空格分隔:btn、btn-primary和mt-1。这些类对应于 Bootstrap 定义的样式,如表 4-2 所述。
表 4-2。
三个按钮元素类
|名字
|
描述
|
| --- | --- |
| btn | 这个类应用按钮的基本样式。它可以应用于button或a元素,以提供一致的外观。 |
| btn-primary | 该类应用样式上下文来提供关于按钮用途的视觉提示。请参见“使用上下文类”一节。 |
| mt-1 | 这个类在元素的顶部和它周围的内容之间添加一个间隙。请参见“使用边距和填充”一节。 |
使用上下文类
使用像 Bootstrap 这样的 CSS 框架的主要优点之一是简化了在整个应用中创建一致主题的过程。Bootstrap 定义了一组样式上下文,用于一致地设计相关元素的样式。这些上下文在表 4-3 中描述,用于将引导样式应用于元素的类的名称中。
表 4-3。
自举风格的上下文
|名字
|
描述
|
| --- | --- |
| primary | 该上下文用于指示主要动作或内容区域。 |
| secondary | 该上下文用于指示内容的支持区域。 |
| success | 此上下文用于指示成功的结果。 |
| info | 该上下文用于呈现附加信息。 |
| warning | 该上下文用于显示警告。 |
| danger | 此上下文用于表示严重警告。 |
| muted | 这种语境是用来淡化内容的。 |
| dark | 该上下文通过使用深色来增加对比度。 |
| white | 该上下文用于通过使用白色来增加对比度。 |
Bootstrap 提供了允许样式上下文应用于不同类型元素的类。下面是应用于h3元素的primary上下文,取自本章开始时创建的index.html文件:
...
<h3 class="bg-primary text-white p-3">Adam's To Do List</h3>
...
元素被分配到的类之一是bg-primary,它使用样式上下文的颜色来设置元素的背景颜色。下面是应用于button元素的相同样式上下文:
...
<button class="btn btn-primary mt-1">Add</button>
...
btn-primary类使用样式上下文的颜色来设计按钮或锚元素的样式。使用相同的上下文来设计不同元素的样式将确保它们的外观是一致和互补的,如图 4-3 所示,该图突出显示了应用了样式上下文的元素。
图 4-3。
使用样式上下文保持一致性
使用边距和填充
Bootstrap 包括一些实用程序类,用于添加填充(元素内边缘与其内容之间的空间)和边距(元素边缘与其周围元素之间的空间)。使用这些类的好处是它们在整个应用中应用一致的间距。
这些类的名称遵循一种定义良好的模式。下面是在本章开始时创建的index.html文件中的body元素,已经对其应用了边距:
...
<body class="m-1">
...
对元素应用边距和填充的类遵循一个定义良好的命名模式:首先是字母m(用于边距)或p(用于填充),然后是一个连字符,然后是一个数字,指示应该应用多少空间(0表示没有间距,或者1、2或3表示增加的数量)。您还可以添加一个字母,仅将间距应用于特定的边,因此t用于顶部、b用于底部、l用于左侧、r用于右侧、x用于左侧和右侧、y用于顶部和底部。
为了帮助把这个方案放在上下文中,表 4-4 列出了在index.html文件中使用的组合。
表 4-4。
示例引导边距和填充类
|名字
|
描述
|
| --- | --- |
| p-1 | 这个类将填充应用到元素的所有边缘。 |
| m-1 | 这个类将边距应用于元素的所有边缘。 |
| mt-1 | 这个类将边距应用于元素的上边缘。 |
| mb-1 | 这个类将边距应用于元素的下边缘。 |
更改元素大小
您可以使用大小修改类来更改某些元素的样式。这些是通过组合基本类名、连字符和lg或sm来指定的。在清单 4-3 中,我使用 Bootstrap 为按钮提供的大小修改类,将button元素添加到了index.html文件中。
<!DOCTYPE html>
<html>
<head>
<title>ToDo</title>
<meta charset="utf-8" />
<link href="node_modules/bootstrap/dist/css/bootstrap.min.css"
rel="stylesheet" />
</head>
<body class="m-1">
<h3 class="bg-primary text-white p-3">Adam's To Do List</h3>
<div class="my-1">
<input class="form-control" />
<button class="btn btn-lg btn-primary mt-1">Add</button>
<button class="btn btn-primary mt-1">Add</button>
<button class="btn btn-sm btn-primary mt-1">Add</button>
</div>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
<tr><td>Buy Flowers</td><td>No</td></tr>
<tr><td>Get Shoes</td><td>No</td></tr>
<tr><td>Collect Tickets</td><td>Yes</td></tr>
<tr><td>Call Joe</td><td>No</td></tr>
</tbody>
</table>
</body>
</html>
Listing 4-3.Using Button Size Modification Classes in the index.html File in the HtmlCssPrimer Folder
btn-lg类创建一个大按钮,btn-sm类创建一个小按钮。省略 size 类将使用元素的默认大小。请注意,我能够将一个上下文类和一个大小类结合起来。引导类修改一起工作,给你完全的控制元素的样式,创造出如图 4-4 所示的效果。
图 4-4。
更改元素大小
使用引导程序设计表格
Bootstrap 包括对样式化table元素及其内容的支持,这是我在本书中使用的一个特性。表 4-5 列出了使用表的关键引导类。
表 4-5。
表格的引导 CSS 类
|名字
|
描述
|
| --- | --- |
| table | 对一个table元素及其行应用常规样式 |
| table-striped | 对table正文中的行应用隔行条带化 |
| table-bordered | 将边框应用于所有行和列 |
| table-hover | 当鼠标悬停在表格中的某一行上时,显示不同的样式 |
| table-sm | 减少表格中的间距以创建更紧凑的布局 |
所有这些类都直接应用于table元素,如清单 4-4 所示,其中突出显示了应用于index.html文件中的表的引导类。
...
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
<tr><td>Buy Flowers</td><td>No</td></tr>
<tr><td>Get Shoes</td><td>No</td></tr>
<tr><td>Collect Tickets</td><td>Yes</td></tr>
<tr><td>Call Joe</td><td>No</td></tr>
</tbody>
</table>
...
Listing 4-4.Using Bootstrap to Style Tables
Tip
注意,在定义清单 4-4 中的表格时,我使用了thead元素。如果一个tbody元素没有被使用,浏览器会自动添加任何tr元素,这些元素是table元素的直接后代。如果您在使用 Bootstrap 时依赖于这种行为,您会得到奇怪的结果,因为应用于table元素的大多数 CSS 类会导致样式被添加到tbody元素的后代中。
使用 Bootstrap 创建表单
Bootstrap 包括表单元素的样式,允许它们与应用中的其他元素保持一致。在清单 4-5 中,我扩展了index.html文件中的表单元素,并临时删除了表格。
<!DOCTYPE html>
<html>
<head>
<title>ToDo</title>
<meta charset="utf-8" />
<link href="node_modules/bootstrap/dist/css/bootstrap.min.css"
rel="stylesheet" />
</head>
<body class="m-2">
<h3 class="bg-primary text-white p-3">Adam's To Do List</h3>
<form>
<div class="form-group">
<label>Task</label>
<input class="form-control" />
</div>
<div class="form-group">
<label>Location</label>
<input class="form-control" />
</div>
<div class="form-group">
<input type="checkbox" />
<label>Done</label>
</div>
<button class="btn btn-primary">Add</button>
</form>
</body>
</html>
Listing 4-5.Defining Additional Form Elements in the index.html File in the HtmlCssPrimer Folder
表单的基本样式是通过将form-group类应用到包含label和input元素的div元素来实现的,其中输入元素被分配给form-control类。Bootstrap 对元素进行样式化,使label显示在input元素上方,而input元素占据 100%的可用水平空间,如图 4-5 所示。
图 4-5。
样式表单元素
使用引导程序创建网格
Bootstrap 提供了样式类,可用于创建不同种类的网格布局,从 1 列到 12 列不等,并支持响应式布局(网格布局根据屏幕宽度而变化)。清单 4-6 替换了示例 HTML 文件的内容来演示网格特性。
<!DOCTYPE html>
<html>
<head>
<title>ToDo</title>
<meta charset="utf-8" />
<link href="node_modules/bootstrap/dist/css/bootstrap.min.css"
rel="stylesheet" />
<style>
.row > div {
border: 1px solid lightgrey; padding: 10px;
background-color: aliceblue; margin: 5px 0;
}
</style>
</head>
<body class="m-2">
<h3>Grid Layout</h3>
<div class="container">
<div class="row">
<div class="col-1">1</div>
<div class="col-1">1</div>
<div class="col-2">2</div>
<div class="col-2">2</div>
<div class="col-6">6</div>
</div>
<div class="row">
<div class="col-3">3</div>
<div class="col-4">4</div>
<div class="col-5">5</div>
</div>
<div class="row">
<div class="col-6">6</div>
<div class="col-6">6</div>
</div>
<div class="row">
<div class="col-11">11</div>
<div class="col-1">1</div>
</div>
<div class="row">
<div class="col-12">12</div>
</div>
</div>
</body>
</html>
Listing 4-6.Using a Bootstrap Grid in the index.html File in the HtmlCssPrimer Folder
自举网格布局系统易于使用。一个顶级的div元素被分配给container类(或者是container-fluid类,如果你想让它跨越可用空间的话)。通过将row类应用到div元素来指定列,这具有为div元素包含的内容设置网格布局的效果。
每行定义 12 列,您可以通过指定一个名为col-后跟列数的类来指定每个子元素将占用多少列。例如,类col-1指定一个元素占据一列,col-2指定两列,依此类推,直到col-12,它指定一个元素填充整个行。在清单中,我用row类创建了一系列的div元素,每个元素都包含我应用了col-*类的进一步的div元素。在图 4-6 中可以看到浏览器中的效果。
Tip
Bootstrap 不会对一行中的元素应用任何样式,这就是为什么我使用了一个style元素来创建一个自定义的 CSS 样式,该样式设置背景颜色、设置行间距并添加边框。
图 4-6。
创建引导网格布局
创建响应式网格
响应式网格根据浏览器窗口的大小调整布局。响应式网格的主要用途是允许移动设备和桌面显示相同的内容,充分利用任何可用的屏幕空间。为了创建一个响应网格,用表 4-6 中显示的类之一替换单个单元格上的col-*类。
表 4-6。
响应网格的引导 CSS 类
|引导类
|
描述
|
| --- | --- |
| col-sm-* | 当屏幕宽度大于 576 像素时,网格单元水平显示。 |
| col-md-* | 当屏幕宽度大于 768 像素时,网格单元水平显示。 |
| col-lg-* | 当屏幕宽度大于 992 像素时,网格单元格水平显示。 |
| col-xl-* | 当屏幕宽度大于 1200 像素时,网格单元水平显示。 |
当屏幕的宽度小于类支持的宽度时,网格行中的单元格垂直堆叠,而不是水平堆叠。清单 4-7 展示了index.html文件中的响应网格。
<!DOCTYPE html>
<html>
<head>
<title>ToDo</title>
<meta charset="utf-8" />
<link href="node_modules/bootstrap/dist/css/bootstrap.min.css"
rel="stylesheet" />
<style>
#gridContainer {padding: 20px;}
.row > div {
border: 1px solid lightgrey; padding: 10px;
background-color: aliceblue; margin: 5px 0;
}
</style>
</head>
<body class="m-1">
<h3>Grid Layout</h3>
<div class="container">
<div class="row">
<div class="col-sm-3">3</div>
<div class="col-sm-4">4</div>
<div class="col-sm-5">5</div>
</div>
<div class="row">
<div class="col-sm-6">6</div>
<div class="col-sm-6">6</div>
</div>
<div class="row">
<div class="col-sm-11">11</div>
<div class="col-sm-1">1</div>
</div>
</div>
</body>
</html>
Listing 4-7.Creating a Responsive Grid in the index.html File in the HtmlCssPrimer Folder
我从前面的例子中删除了一些网格行,并用col-sm-*替换了col-*类。其效果是当浏览器窗口宽度大于 576 像素时,该行中的单元格将水平堆叠,当浏览器窗口宽度小于 576 像素时,该行中的单元格将水平堆叠,如图 4-7 所示。
图 4-7。
创建响应式网格布局
创建简化的网格布局
对于本书中依赖于 Bootstrap 网格的大多数示例,我使用一种简化的方法,在单行中显示内容,并且只需要指定列数,如清单 4-8 所示。
<!DOCTYPE html>
<html>
<head>
<title>ToDo</title>
<meta charset="utf-8" />
<link href="node_modules/bootstrap/dist/css/bootstrap.min.css"
rel="stylesheet" />
</head>
<body class="m-1">
<h3 class="bg-primary text-white p-3">Adam's To Do List</h3>
<div class="container-fluid">
<div class="row">
<div class="col-4">
<form>
<div class="form-group">
<label>Task</label>
<input class="form-control" />
</div>
<div class="form-group">
<label>Location</label>
<input class="form-control" />
</div>
<div class="form-group">
<input type="checkbox" />
<label>Done</label>
</div>
<button class="btn btn-primary">Add</button>
</form>
</div>
<div class="col-8">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
<tr><td>Buy Flowers</td><td>No</td></tr>
<tr><td>Get Shoes</td><td>No</td></tr>
<tr><td>Collect Tickets</td><td>Yes</td></tr>
<tr><td>Call Joe</td><td>No</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
Listing 4-8.Using a Simplified Grid Layout in the index.html File in the HtmlCssPrimer Folder
这个清单使用col-4和col-8类并排显示两个div元素,允许显示待办事项的表单和表格水平显示,如图 4-8 所示。
图 4-8。
使用简化的网格布局
摘要
在这一章中,我提供了 HTML 和引导 CSS 框架的简要概述。您需要很好地掌握 HTML 和 CSS,以便在 web 应用开发中真正有效,但最好的学习方法是通过第一手经验,本章中的描述和示例将足以让您入门,并为前面的示例提供足够的背景信息。在下一章,我将继续初级主题,介绍我在本书中使用的 JavaScript 的基本特性。