Angular9 高级教程(九)
二十一、使用和创建模块
在这一章中,我将描述最后一个有 Angular 的构件:模块。在本章的第一部分,我描述了根模块,每个 Angular 应用都用它来描述 Angular 应用的配置。在这一章的第二部分,我描述了特性模块,它们被用来给应用增加结构,这样相关的特性就可以被组合成一个单元。表 21-1 将模块放在上下文中。
表 21-1。
将模块放在上下文中
|问题
|
回答
|
| --- | --- |
| 它们是什么? | 模块向 Angular 提供配置信息。 |
| 它们为什么有用? | 根模块描述了 Angular 的应用,设置了组件和服务等基本功能。特性模块对于向复杂项目添加结构很有用,这使得它们更容易管理和维护。 |
| 它们是如何使用的? | 模块是已经应用了@NgModule装饰器的类。装饰器使用的属性对于根模块和特性模块有不同的含义。 |
| 有什么陷阱或限制吗? | 提供者没有模块范围,这意味着由一个特性模块定义的提供者将是可用的,就像它们是由根模块定义的一样。 |
| 有其他选择吗? | 每个应用都必须有一个根模块,但是功能模块的使用完全是可选的。但是,如果不使用功能模块,应用中的文件可能会变得难以管理。 |
表 21-2 总结了本章内容。
表 21-2。
章节总结
|问题
|
解决办法
|
列表
| | --- | --- | --- | | 描述应用及其包含的构造块 | 使用根模块 | 1–7 | | 将相关功能分组在一起 | 创建特征模块 | 8–28 |
准备示例项目
与本书这一部分的其他章节一样,我将使用在第十一章中创建的示例项目,并且此后在每一章中都进行了扩展和扩展。
Tip
你可以从 https://github.com/Apress/pro-angular-9 下载本章以及本书其他章节的示例项目。如果在运行示例时遇到问题,请参见第一章获取帮助。
为了准备本章,我已经从组件模板中移除了一些功能。清单 21-1 显示了产品表的模板,其中我注释掉了折扣编辑器和显示组件的元素。
<table class="table table-sm table-bordered table-striped">
<thead>
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
</thead>
<tbody>
<tr *paFor="let item of getProducts(); let i = index">
<td>{{i + 1}}</td>
<td>{{item.name}}</td>
<td>{{item.category}}</td>
<td [pa-price]="item.price" #discount="discount">
{{ discount.discountAmount | currency:"USD":"symbol"}}
</td>
<td class="text-center">
<button class="btn btn-danger btn-sm"
(click)="deleteProduct(item.id)">
Delete
</button>
</td>
</tr>
</tbody>
</table>
<!-- <paDiscountEditor></paDiscountEditor> -->
<!-- <paDiscountDisplay></paDiscountDisplay> -->
Listing 21-1.The Contents of the productTable.component.html File in the src/app Folder
清单 21-2 显示了来自产品表单组件的模板,其中我已经注释掉了我在第二十章中用来演示视图子代和内容子代的提供者之间的区别的元素。
<form novalidate #form="ngForm" (ngSubmit)="submitForm(form)">
<div class="form-group">
<label>Name</label>
<input class="form-control"
name="name" [(ngModel)]="newProduct.name" />
</div>
<div class="form-group">
<label>Category</label>
<input class="form-control"
name="category" [(ngModel)]="newProduct.category" />
</div>
<div class="form-group">
<label>Price</label>
<input class="form-control"
name="name" [(ngModel)]="newProduct.price" />
</div>
<button class="btn btn-primary" type="submit">
Create
</button>
</form>
<!-- <div class="bg-info text-white m-2 p-2">
View Child Value: <span paDisplayValue></span>
</div>
<div class="bg-info text-white m-2 p-2">
Content Child Value: <ng-content></ng-content>
</div> -->
Listing 21-2.The Contents of the productForm.component.html File in the src/app Folder
在example文件夹中运行以下命令,启动 Angular 开发工具:
ng serve
打开一个新的浏览器窗口并导航到http://localhost:4200以查看如图 21-1 所示的内容。
图 21-1。
运行示例应用
了解根模块
每个角都至少有一个模块,称为根模块。根模块通常定义在src/app文件夹中名为app.module.ts的文件中,它包含一个应用了@NgModule装饰器的类。清单 21-3 显示了示例应用的根模块。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
import { LOCALE_ID } from "@angular/core";
import localeFr from '@angular/common/locales/fr';
import { registerLocaleData } from '@angular/common';
import { PaDiscountDisplayComponent } from "./discountDisplay.component";
import { PaDiscountEditorComponent } from "./discountEditor.component";
import { DiscountService } from "./discount.service";
import { PaDiscountPipe } from "./discount.pipe";
import { PaDiscountAmountDirective } from "./discountAmount.directive";
import { SimpleDataSource } from "./datasource.model";
import { Model } from "./repository.model";
import { LogService, LOG_SERVICE, SpecialLogService,
LogLevel, LOG_LEVEL} from "./log.service";
import { VALUE_SERVICE, PaDisplayValueDirective} from "./valueDisplay.directive";
let logger = new LogService();
logger.minimumLevel = LogLevel.DEBUG;
registerLocaleData(localeFr);
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
declarations: [ProductComponent, PaAttrDirective, PaModel,
PaStructureDirective, PaIteratorDirective,
PaCellColor, PaCellColorSwitcher, ProductTableComponent,
ProductFormComponent, PaToggleView, PaAddTaxPipe,
PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
PaDiscountPipe, PaDiscountAmountDirective, PaDisplayValueDirective],
providers: [DiscountService, SimpleDataSource, Model, LogService,
{ provide: VALUE_SERVICE, useValue: "Apples" }],
bootstrap: [ProductComponent]
})
export class AppModule { }
Listing 21-3.The Root Module in the app.module.ts File in the src/app Folder
一个项目中可以有多个模块,但是根模块是在引导文件中使用的模块,引导文件习惯上称为main.ts,在src文件夹中定义。清单 21-4 显示了示例项目的main.ts文件。
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
Listing 21-4.The Angular Bootstrap in the main.ts File in the src Folder
Angular 应用可以在不同的环境中运行,比如 web 浏览器和本地应用容器。引导文件的工作是选择平台并识别根模块。platformBrowserDynamic方法创建浏览器运行时,bootstrapModule方法用于指定模块,该模块是清单 21-3 中的AppModule类。
定义根模块时,使用表 21-3 中描述的@NgModule装饰器属性。(还有一些附加的装饰器属性,将在本章后面介绍。)
表 21-3。
@NgModule 装饰器根模块属性
|名字
|
描述
|
| --- | --- |
| imports | 此属性指定支持应用中的指令、组件和管道所需的 Angular 模块。 |
| declarations | 此属性用于指定应用中使用的指令、组件和管道。 |
| providers | 此属性定义模块的注入器将使用的服务提供者。正如第二十章中所描述的,这些提供者将在整个应用中可用,并且在没有服务的本地提供者可用时使用。 |
| bootstrap | 此属性指定应用的根组件。 |
了解导入属性
属性用来列出应用需要的其他模块。在示例应用中,这些都是 Angular 框架提供的模块。
...
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
...
BrowserModule提供了在 web 浏览器中运行 Angular 应用所需的功能。其他两个模块为处理 HTML 表单和基于模型的表单提供支持,如第十四章所述。还有其他 Angular 模块,将在后面的章节中介绍。
属性还用于声明对定制模块的依赖,定制模块用于管理复杂的 Angular 应用和创建可重用功能的单元。我在“创建特性模块”一节中解释了如何定义定制模块。
了解声明属性
declarations属性用于向 Angular 提供应用所需的指令、组件和管道的列表,统称为可声明类。示例项目根模块中的declarations属性包含一个很长的类列表,每个类都可以在应用的其他地方使用,只是因为它在这里列出了。
...
declarations: [ProductComponent, PaAttrDirective, PaModel,
PaStructureDirective, PaIteratorDirective,
PaCellColor, PaCellColorSwitcher, ProductTableComponent,
ProductFormComponent, PaToggleView, PaAddTaxPipe,
PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
PaDiscountPipe, PaDiscountAmountDirective, PaDisplayValueDirective],
...
请注意,内置的可声明类,如第十三章中描述的指令和第十八章中描述的管道,不包含在根模块的declarations属性中。这是因为它们是BrowserModule模块的一部分,当您将一个模块添加到imports属性时,它的可声明类在应用中自动可用。
了解提供者属性
providers属性用于定义服务提供者,当没有合适的本地提供者可用时,该服务提供者将用于解析依赖性。第十九章和第二十章详细描述了服务供应器的使用。
了解 bootstrap 属性
bootstrap属性指定应用的根组件。当 Angular 处理主 HTML 文档(通常称为index.html)时,它检查根组件,并使用@Component装饰器中的selector属性的值来应用它们。
Tip
在bootstrap属性中列出的组件也必须包含在declarations列表中。
下面是示例项目根模块中的bootstrap属性:
...
bootstrap: [ProductComponent]
...
ProductComponent类提供了根组件,其selector属性指定了app元素,如清单 21-5 所示。
import { Component } from "@angular/core";
@Component({
selector: "app",
templateUrl: "template.html"
})
export class ProductComponent {
}
Listing 21-5.The Root Component in the component.ts File in the src/app Folder
当我开始第十一章中的示例项目时,根组件有很多功能。但是自从引入了额外的组件后,这个组件的作用已经减少了,它现在本质上是一个占位符,告诉 Angular 将app/template.html文件的内容投影到 HTML 文档中的app元素,这允许加载在应用中执行实际工作的组件。
这种方法没有错,但是它确实意味着应用中的根组件没有太多事情要做。如果这种冗余感觉不整洁,那么你可以在根模块中指定多个根组件,它们都将用于 HTML 文档中的目标元素。为了演示,我已经从根模块的bootstrap属性中移除了现有的根组件,并用负责产品表单和产品表的组件类来替换它,如清单 21-6 所示。
...
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
declarations: [ProductComponent, PaAttrDirective, PaModel,
PaStructureDirective, PaIteratorDirective,
PaCellColor, PaCellColorSwitcher, ProductTableComponent,
ProductFormComponent, PaToggleView, PaAddTaxPipe,
PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
PaDiscountPipe, PaDiscountAmountDirective, PaDisplayValueDirective],
providers: [DiscountService, SimpleDataSource, Model, LogService,
{ provide: VALUE_SERVICE, useValue: "Apples" }],
bootstrap: [ProductFormComponent, ProductTableComponent]
})
...
Listing 21-6.Specifying Multiple Root Components in the app.module.ts File in the src/app Folder
清单 21-7 反映了主 HTML 文档中根组件的变化。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example</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 class="m-2 row">
<div class="col-8 p-2">
<paProductTable></paProductTable>
</div>
<div class="col-4 p-2">
<paProductForm></paProductForm>
</div>
</body>
</html>
Listing 21-7.Changing the Root Component Elements in the index.html File in the src Folder
与前面的例子相比,我颠倒了这些组件出现的顺序,只是为了在应用的布局中创建一个可察觉的变化。当所有的更改都被保存并且浏览器重新加载页面后,你会看到新的根组件被显示出来,如图 21-2 所示。
图 21-2。
使用多个根组件
模块的服务提供者用于解析所有根组件的依赖关系。在示例应用的情况下,这意味着整个应用共享一个单一的Model服务对象,它允许用 HTML 表单创建的产品自动显示在表格中,即使这些组件已经被提升为根组件。
创建功能模块
根模块已经变得越来越复杂,因为我在前面的章节中添加了一些特性,加载 JavaScript 模块的一长串import语句和跨越几行的@NgModule装饰器的declarations属性中的一组类,如清单 21-8 所示。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
import { LOCALE_ID } from "@angular/core";
import localeFr from '@angular/common/locales/fr';
import { registerLocaleData } from '@angular/common';
import { PaDiscountDisplayComponent } from "./discountDisplay.component";
import { PaDiscountEditorComponent } from "./discountEditor.component";
import { DiscountService } from "./discount.service";
import { PaDiscountPipe } from "./discount.pipe";
import { PaDiscountAmountDirective } from "./discountAmount.directive";
import { SimpleDataSource } from "./datasource.model";
import { Model } from "./repository.model";
import { LogService, LOG_SERVICE, SpecialLogService,
LogLevel, LOG_LEVEL} from "./log.service";
import { VALUE_SERVICE, PaDisplayValueDirective} from "./valueDisplay.directive";
let logger = new LogService();
logger.minimumLevel = LogLevel.DEBUG;
registerLocaleData(localeFr);
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
declarations: [ProductComponent, PaAttrDirective, PaModel,
PaStructureDirective, PaIteratorDirective,
PaCellColor, PaCellColorSwitcher, ProductTableComponent,
ProductFormComponent, PaToggleView, PaAddTaxPipe,
PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
PaDiscountPipe, PaDiscountAmountDirective, PaDisplayValueDirective],
providers: [DiscountService, SimpleDataSource, Model, LogService,
{ provide: VALUE_SERVICE, useValue: "Apples" }],
bootstrap: [ProductFormComponent, ProductTableComponent]
})
export class AppModule { }
Listing 21-8.The Contents of the app.module.ts File in the src/app Folder
特征模块用于对相关功能进行分组,以便将其作为单个实体使用,就像 Angular 模块BrowserModule一样。例如,当我需要使用这些特性来处理表单时,我不必为每个单独的指令、组件或管道添加import语句和declarations条目。相反,我只是将BrowserModule添加到装饰者的imports属性中,它包含的所有功能在整个应用中都是可用的。
当您创建一个功能模块时,您可以选择专注于一个应用功能,或者选择将一组提供应用基础结构的相关构建块进行分组。我将在接下来的小节中介绍这两种方法,因为它们的工作方式略有不同,并且有不同的考虑因素。特征模块使用相同的@NgModule装饰器,但是有一组重叠的配置属性,其中一些是新的,一些与根模块共同使用,但是有不同的效果。我将在下面的章节中解释如何使用这些属性,但是表 21-4 提供了一个快速参考的摘要。
表 21-4。
功能模块的@NgModule 装饰器属性
|名字
|
描述
|
| --- | --- |
| imports | 此属性用于导入模块中的类所需的模块。 |
| providers | 此属性用于定义模块的提供者。当加载功能模块时,提供者集合与根模块中的提供者组合在一起,这意味着功能模块的服务在整个应用中都是可用的(而不仅仅是在模块内)。 |
| declarations | 此属性用于指定模块中的指令、组件和管道。此属性必须包含在模块内使用的类以及由模块向应用的其余部分公开的类。 |
| exports | 此属性用于定义模块的公共导出。它包含来自declarations属性的一些或全部指令、组件和管道,以及来自imports属性的一些或全部模块。 |
创建模型模块
术语模型模块可能是一个绕口令,但是当使用特性模块重构应用时,它通常是一个很好的起点,因为应用中的几乎所有其他构建块都依赖于模型。
第一步是创建包含该模块的文件夹。模块文件夹在src/app文件夹中定义,并被赋予一个有意义的名称。对于这个模块,我创建了一个src/app/model文件夹。
用于 Angular 文件的命名约定使得移动和删除多个文件变得容易。在example文件夹中运行以下命令来移动文件(它们将在 Windows PowerShell、Linux 和 macOS 中工作):
mv src/app/*.model.ts src/app/model/
mv src/app/limit.formvalidator.ts src/app/model/
结果是表 21-5 中列出的文件被移动到model文件夹中。
表 21-5。
模块所需的文件移动
|文件
|
新位置
|
| --- | --- |
| src/app/datasource.model.ts | src/app/model/datasource.model.ts |
| src/app/form.model.ts | src/app/model/form.model.ts |
| src/app/limit.formvalidator.ts | src/app/model/limit.formvalidator.ts |
| src/app/product.model.ts | src/app/model/product.model.ts |
| src/app/repository.model.ts | src/app/model/repository.model.ts |
如果在移动文件后尝试构建项目,TypeScript 编译器将列出一系列编译器错误,因为一些关键的可声明类不可用。我将很快处理这些问题。
创建模块定义
下一步是定义一个模块,将已经移动到新文件夹的文件中的功能集合在一起。我在src/app/model文件夹中添加了一个名为model.module.ts的文件,并定义了清单 21-9 中所示的模块。
import { NgModule } from "@angular/core";
import { SimpleDataSource } from "./datasource.model";
import { Model } from "./repository.model";
@NgModule({
providers: [Model, SimpleDataSource]
})
export class ModelModule { }
Listing 21-9.The Contents of the model.module.ts File in the src/app/model Folder
功能模块的目的是有选择地向应用的其余部分公开文件夹的内容。这个模块的@NgModule装饰器只使用providers属性来定义Model和SimpleDataSource服务的类提供者。当您在特性模块中使用提供者时,它们被注册到根模块的注入器中,这意味着它们在整个应用中都是可用的,这正是示例应用中的数据模型所需要的。
Tip
一个常见的错误是认为模块中定义的服务只能被该模块中的类访问。Angular 中没有模块范围。由功能模块定义的提供者就像由根模块定义的一样被使用。由特性模块中的指令和组件定义的本地提供者可用于它们的视图和内容子级,即使它们是在其他模块中定义的。
更新应用中的其他类
将类移动到model文件夹会破坏应用其他部分的import语句。下一步是更新那些import语句以指向新模块。受影响的文件有四个:attr.directive.ts、categoryFilter.pipe.ts、productForm.component.ts和productTable.component.ts。清单 21-10 显示了attr.directive.ts文件所需的更改。
import { Directive, ElementRef, Attribute, Input,
SimpleChange, Output, EventEmitter, HostListener, HostBinding }
from "@angular/core";
import { Product } from "./model/product.model";
@Directive({
selector: "[pa-attr]"
})
export class PaAttrDirective {
// ...statements omitted for brevity...
}
Listing 21-10.Updating the Import Reference in the attr.directive.ts File in the src/app Folder
唯一需要的改变是更新在import语句中使用的路径,以反映代码文件的新位置。清单 21-11 显示了应用于categoryFilter.pipe.ts文件的相同变化。
import { Pipe } from "@angular/core";
import { Product } from "./model/product.model";
@Pipe({
name: "filter",
pure: false
})
export class PaCategoryFilterPipe {
transform(products: Product[], category: string): Product[] {
return category == undefined ?
products : products.filter(p => p.category == category);
}
}
Listing 21-11.Updating the Import Reference in the categoryFilter.pipe.ts File in the src/app Folder
清单 21-12 更新了productForm.component.ts文件中的import语句。
import { Component, Output, EventEmitter, ViewEncapsulation,
Inject, SkipSelf } from "@angular/core";
import { Product } from "./model/product.model";
import { Model } from "./model/repository.model";
import { VALUE_SERVICE } from "./valueDisplay.directive";
@Component({
selector: "paProductForm",
templateUrl: "productForm.component.html",
viewProviders: [{ provide: VALUE_SERVICE, useValue: "Oranges" }]
})
export class ProductFormComponent {
// ...statements omitted for brevity...
}
Listing 21-12.Updating Import Paths in the productForm.component.ts File in the src/app Folder
清单 21-13 更新最终文件productTable.component.ts中的路径。
import { Component, Input, ViewChildren, QueryList } from "@angular/core";
import { Model } from "./model/repository.model";
import { Product } from "./model/product.model";
import { DiscountService } from "./discount.service";
import { LogService } from "./log.service";
@Component({
selector: "paProductTable",
templateUrl: "productTable.component.html",
providers:[LogService]
})
export class ProductTableComponent {
// ...statements omitted for brevity...
}
Listing 21-13.Updating Import Paths in the productTable.component.ts File in the src/app Folder
Using a Javascript Module with an Angular Module
创建一个 Angular 模块允许将相关的应用特性组合在一起,但是当应用的其他地方需要时,仍然需要从自己的文件中导入每一个特性,正如您在本节的清单中看到的那样。
您还可以定义一个 JavaScript 模块来导出 Angular 模块的面向公众的特性,这样就可以使用与用于@angular/core模块相同的import语句来访问它们。要使用 JavaScript 模块,在定义 Angular 模块的 TypeScript 文件旁边的module文件夹中添加一个名为index.ts的文件,对于本节中的示例来说,这个文件是src/app/model文件夹。对于您想在应用之外使用的每个应用特性,添加一个export...from这样陈述:
...
export { ModelModule } from "./model.module";
export { Product } from "./product.model";
export { ProductFormGroup } from "./form.model";
export { SimpleDataSource } from "./datasource.model";
export { LimitValidator } from "./limit.formvalidator";
export { Model } from "./repository.model";
...
这些语句导出各个 TypeScript 文件的内容。然后,您可以导入所需的功能,而不必指定单独的文件,如下所示:
...
import { Component, Output, EventEmitter, ViewEncapsulation,
Inject, SkipSelf } from "@angular/core";
import { Product, Model } from "./model";
import { VALUE_SERVICE } from "./valueDisplay.directive";
...
使用文件名index.ts意味着您只需在import语句中指定文件夹的名称,从而产生一个更整洁且与 Angular 核心包更一致的结果。
也就是说,我没有在自己的项目中使用这种技术。使用一个index.ts文件意味着你必须记住将每个特性添加到 Angular 和 JavaScript 模块中,这是一个额外的步骤,我经常忘记做。相反,我使用本章介绍的方法,直接从包含应用特性的文件中导入。
更新根模块
最后一步是更新根模块,以便功能模块中定义的服务在整个应用中可用。清单 21-14 显示了所需的更改。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
import { LOCALE_ID } from "@angular/core";
import localeFr from '@angular/common/locales/fr';
import { registerLocaleData } from '@angular/common';
import { PaDiscountDisplayComponent } from "./discountDisplay.component";
import { PaDiscountEditorComponent } from "./discountEditor.component";
import { DiscountService } from "./discount.service";
import { PaDiscountPipe } from "./discount.pipe";
import { PaDiscountAmountDirective } from "./discountAmount.directive";
//import { SimpleDataSource } from "./datasource.model";
//import { Model } from "./repository.model";
import { LogService, LOG_SERVICE, SpecialLogService,
LogLevel, LOG_LEVEL} from "./log.service";
import { VALUE_SERVICE, PaDisplayValueDirective} from "./valueDisplay.directive";
import { ModelModule } from "./model/model.module";
let logger = new LogService();
logger.minimumLevel = LogLevel.DEBUG;
registerLocaleData(localeFr);
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule, ModelModule],
declarations: [ProductComponent, PaAttrDirective, PaModel,
PaStructureDirective, PaIteratorDirective,
PaCellColor, PaCellColorSwitcher, ProductTableComponent,
ProductFormComponent, PaToggleView, PaAddTaxPipe,
PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
PaDiscountPipe, PaDiscountAmountDirective, PaDisplayValueDirective],
providers: [DiscountService, LogService,
{ provide: VALUE_SERVICE, useValue: "Apples" }],
bootstrap: [ProductFormComponent, ProductTableComponent]
})
export class AppModule { }
Listing 21-14.Updating the Root Module in the app.module.ts File in the src/app Folder
我导入了特性模块,并将其添加到根模块的导入列表中。因为特性模块为Model和SimpleDataSource定义了提供者,所以我从根模块的提供者列表中删除了条目,并删除了相关的导入语句。
一旦保存了更改,就可以运行ng serve来启动 Angular 开发工具。应用将被编译,并且修改后的根模块将提供对模型服务的访问。浏览器中显示的内容没有可见的更改,并且更改仅限于项目的结构。(您可能需要重新启动 Angular 开发工具,并重新加载浏览器以查看更改。)
创建实用功能模块
模型模块是一个很好的起点,因为它展示了特性模块的基本结构以及它与根模块的关系。然而,对应用的影响是轻微的,并且没有实现大量的简化。
复杂性的下一步是实用功能模块,它将应用中所有常见的功能组合在一起,比如管道和指令。在一个实际的项目中,您可能会更有选择性地将这些类型的构建块组合在一起,以便有几个模块,每个模块包含相似的功能。对于示例应用,我将把所有管道、指令和服务移到一个模块中。
创建模块文件夹并移动文件
与前面的模块一样,第一步是创建文件夹。对于这个模块,我创建了一个名为src/app/common的文件夹。在example文件夹中运行以下命令,移动管道和指令的 TypeScript 文件:
mv src/app/*.pipe.ts src/app/common/
mv src/app/*.directive.ts src/app/common/
这些命令应该可以在 Windows PowerShell、Linux 和 macOS 中运行。应用中的一些指令和管道依赖于通过依赖注入提供给它们的DiscountService和LogServices类。在example文件夹中运行以下命令,将服务的 TypeScript 文件移动到module文件夹中:
mv src/app/*.service.ts src/app/common/
结果是表 21-6 中列出的文件被移动到common模块文件夹中。
表 21-6。
模块所需的文件移动
|文件
|
新位置
|
| --- | --- |
| app/addTax.pipe.ts | app/common/addTax.pipe.ts |
| app/attr.directive.ts | app/common/attr.directive.ts |
| app/categoryFilter.pipe.ts | app/common/categoryFilter.pipe.ts |
| app/cellColor.directive.ts | app/common/cellColor.directive.ts |
| app/cellColorSwitcher.directive.ts | app/common/cellColorSwitcher.directive.ts |
| app/discount.pipe.ts | app/common/discount.pipe.ts |
| app/discountAmount.directive.ts | app/common/discountAmount.directive.ts |
| app/iterator.directive.ts | app/common/iterator.directive.ts |
| app/structure.directive.ts | app/common/structure.directive.ts |
| app/twoway.directive.ts | app/common/twoway.directive.ts |
| app/valueDisplay.directive.ts | app/common/valueDisplay.directive.ts |
| app/discount.service.ts | app/common/discount.service.ts |
| app/log.service.ts | app/common/log.service.ts |
更新新模块中的类
一些被移动到新文件夹中的类有import语句,这些语句必须被更新以反映模型模块的新路径。清单 21-15 显示了对attr.directive.ts文件所需的更改。
import { Directive, ElementRef, Attribute, Input,
SimpleChange, Output, EventEmitter, HostListener, HostBinding }
from "@angular/core";
import { Product } from "../model/product.model";
@Directive({
selector: "[pa-attr]"
})
export class PaAttrDirective {
// ...statements omitted for brevity...
}
Listing 21-15.Updating the Imports in the attr.directive.ts File in the src/app/common Folder
清单 21-16 显示了对categoryFilter.pipe.ts文件的相应更改。
import { Pipe } from "@angular/core";
import { Product } from "../model/product.model";
@Pipe({
name: "filter",
pure: false
})
export class PaCategoryFilterPipe {
transform(products: Product[], category: string): Product[] {
return category == undefined ?
products : products.filter(p => p.category == category);
}
}
Listing 21-16.Updating the Imports in the categoryFilter.pipe.ts File in the src/app/common Folder
创建模块定义
下一步是定义一个模块,将已经移动到新文件夹的文件中的功能集合在一起。我在src/app/common文件夹中添加了一个名为common.module.ts的文件,并定义了清单 21-17 中所示的模块。
import { NgModule } from "@angular/core";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaAttrDirective } from "./attr.directive";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { PaDiscountPipe } from "./discount.pipe";
import { PaDiscountAmountDirective } from "./discountAmount.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaModel } from "./twoway.directive";
import { VALUE_SERVICE, PaDisplayValueDirective} from "./valueDisplay.directive";
import { DiscountService } from "./discount.service";
import { LogService } from "./log.service";
import { ModelModule } from "../model/model.module";
@NgModule({
imports: [ModelModule],
providers: [LogService, DiscountService,
{ provide: VALUE_SERVICE, useValue: "Apples" }],
declarations: [PaAddTaxPipe, PaAttrDirective, PaCategoryFilterPipe,
PaCellColor, PaCellColorSwitcher, PaDiscountPipe,
PaDiscountAmountDirective, PaIteratorDirective, PaStructureDirective,
PaModel, PaDisplayValueDirective],
exports: [PaAddTaxPipe, PaAttrDirective, PaCategoryFilterPipe,
PaCellColor, PaCellColorSwitcher, PaDiscountPipe,
PaDiscountAmountDirective, PaIteratorDirective, PaStructureDirective,
PaModel, PaDisplayValueDirective]
})
export class CommonModule { }
Listing 21-17.The Contents of the common.module.ts File in the src/app/common Folder
这是一个比数据模型所需模块更复杂的模块。在接下来的小节中,我将描述装饰器的每个属性所使用的值。
了解进口
模块中的一些directives和pipes依赖于本章前面创建的model模块中定义的服务。为了确保该模块中的特性可用,我添加了公共模块的imports属性。
了解提供者
providers属性确保特性模块中的指令和管道所服务的服务能够访问它们所需要的服务。这意味着添加类提供者来创建LogService和DiscountService服务,这些服务将在模块加载时被添加到根模块的提供者中。这些服务不仅可用于common模块中的指令和管道;它们也将在整个应用中可用。
理解声明
属性用于向 Angular 提供模块中的指令和管道(以及组件,如果有的话)的列表。在功能模块中,该属性有两个用途:它使可声明类能够在模块中包含的任何模板中使用,并且它允许模块使那些可声明类在模块外部可用。我在本章的后面创建了一个包含模板内容的模块,但是对于这个模块来说,declarations属性的值是为了准备exports属性而必须使用的,这将在下一节描述。
了解出口
对于包含用于应用其他地方的指令和管道的模块,exports属性在@NgModule装饰器中是最重要的,因为它定义了当模块被导入到应用的其他地方时,它所提供的一组指令、组件和管道。exports属性可以包含单独的类和模块类型,尽管两者都必须已经在declarations或imports属性中列出。当模块被导入时,列出的类型表现得好像它们已经被添加到导入模块的declarations属性中。
更新应用中的其他类
既然已经定义了模块,我可以更新应用中的其他文件,这些文件包含现在属于common模块的类型的import语句。清单 21-18 显示了对discountDisplay.component.ts文件所需的更改。
import { Component, Input } from "@angular/core";
import { DiscountService } from "./common/discount.service";
@Component({
selector: "paDiscountDisplay",
template: `<div class="bg-info text-white p-2">
The discount is {{discounter.discount}}
</div>`
})
export class PaDiscountDisplayComponent {
constructor(public discounter: DiscountService) { }
}
Listing 21-18.Updating the Import in the discountDisplay.component.ts File in the src/app Folder
清单 21-19 显示了对discountEditor.component.ts文件的修改。
import { Component, Input } from "@angular/core";
import { DiscountService } from "./common/discount.service";
@Component({
selector: "paDiscountEditor",
template: `<div class="form-group">
<label>Discount</label>
<input [(ngModel)]="discounter.discount"
class="form-control" type="number" />
</div>`
})
export class PaDiscountEditorComponent {
constructor(public discounter: DiscountService) { }
}
Listing 21-19.Updating the Import Reference in the discountEditor.component.ts File in the src/app Folder
清单 21-20 显示了对productForm.component.ts文件的修改。
import { Component, Output, EventEmitter, ViewEncapsulation,
Inject, SkipSelf } from "@angular/core";
import { Product } from "./model/product.model";
import { Model } from "./model/repository.model";
import { VALUE_SERVICE } from "./common/valueDisplay.directive";
@Component({
selector: "paProductForm",
templateUrl: "productForm.component.html",
viewProviders: [{ provide: VALUE_SERVICE, useValue: "Oranges" }]
})
export class ProductFormComponent {
// ...statements omitted for brevity...
}
Listing 21-20.Updating the Import Reference in the productForm.component.ts File in the src/app Folder
最后的更改是对productTable.component.ts文件的,如清单 21-21 所示。
import { Component, Input, ViewChildren, QueryList } from "@angular/core";
import { Model } from "./model/repository.model";
import { Product } from "./model/product.model";
import { DiscountService } from "./common/discount.service";
import { LogService } from "./common/log.service";
@Component({
selector: "paProductTable",
templateUrl: "productTable.component.html",
providers:[LogService]
})
export class ProductTableComponent {
// ...statements omitted for brevity...
}
Listing 21-21.Updating the Import Reference in the productTable.component.ts File in the src/app Folder
更新根模块
最后一步是更新根模块,以便它加载common模块来提供对它包含的指令和管道的访问,如清单 21-22 所示。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaDiscountDisplayComponent } from "./discountDisplay.component";
import { PaDiscountEditorComponent } from "./discountEditor.component";
import { ModelModule } from "./model/model.module";
import { CommonModule } from "./common/common.module";
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule,
ModelModule, CommonModule],
declarations: [ProductComponent, ProductTableComponent,
ProductFormComponent, PaDiscountDisplayComponent, PaDiscountEditorComponent],
bootstrap: [ProductFormComponent, ProductTableComponent]
})
export class AppModule { }
Listing 21-22.Importing a Feature Module in the app.module.ts File in the src/app Folder
随着common模块的创建,根模块得到了极大的简化,并被添加到了imports列表中。指令和管道的所有单独的类都已经从declarations列表中删除,并且它们相关的import语句也已经从文件中删除。当common模块被导入时,在其exports属性中列出的所有类型都将被添加到根模块的declarations属性中。
一旦保存了本节中的更改,就可以运行ng serve命令来启动 Angular 开发工具。同样,呈现给用户的内容没有明显的变化,不同之处都在应用的结构上。
使用组件创建特征模块
我要创建的最后一个模块将包含应用的组件。创建模块的过程与前面示例中的过程相同,这将在后面的部分中描述。
创建模块文件夹并移动文件
该模块将被称为components,我创建了文件夹src/app/components来包含这些文件。在example文件夹中运行以下命令,将指令 TypeScript、HTML 和 CSS 文件移动到新文件夹中,并删除相应的 JavaScript 文件:
mv src/app/*.component.ts src/app/components/
mv src/app/*.component.html src/app/components/
mv src/app/*.component.css src/app/components/
这些命令的结果是组件代码文件、模板和样式表被移动到新文件夹中,如表 21-7 所列。
表 21-7。
组件模块所需的文件移动
|文件
|
新位置
|
| --- | --- |
| src/app/app.component.ts | src/app/components/app.component.ts |
| src/app/app.component.html | src/app/components/app.component.html |
| src/app/app.component.css | src/app/components/app.component.css |
| src/app/discountDisplay.component.ts | src/app/components/discountDisplay.component.ts |
| src/app/discountEditor.component.ts | src/app/components/discountEditor.component.ts |
| src/app/productForm.component.ts | src/app/components/productForm.component.ts |
| src/app/productForm.component.html | src/app/components/productForm.component.html |
| src/app/productForm.component.css | src/app/components/productForm.component.css |
| src/app/productTable.component.ts | src/app/components/productTable.component.ts |
| src/app/productTable.component.html | src/app/components/productTable.component.html |
| src/app/productTable.component.css | src/app/components/productTable.component.css |
| src/app/toggleView.component.ts | src/app/components/toggleView.component.ts |
| src/app/toggleView.component.html | src/app/components/toggleView.component.ts |
创建模块定义
为了创建这个模块,我在src/app/components文件夹中添加了一个名为components.module.ts的文件,并添加了清单 21-23 中所示的语句。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { CommonModule } from "../common/common.module";
import { FormsModule, ReactiveFormsModule } from "@angular/forms"
import { PaDiscountDisplayComponent } from "./discountDisplay.component";
import { PaDiscountEditorComponent } from "./discountEditor.component";
import { ProductFormComponent } from "./productForm.component";
import { ProductTableComponent } from "./productTable.component";
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule, CommonModule],
declarations: [PaDiscountDisplayComponent, PaDiscountEditorComponent,
ProductFormComponent, ProductTableComponent],
exports: [ProductFormComponent, ProductTableComponent]
})
export class ComponentsModule { }
Listing 21-23.The Contents of the components.module.ts File in the src/app/components Folder
这个模块导入了BrowserModule和CommonModule来确保指令可以访问它们需要的服务和可声明的类。它导出了ProductFormComponent和ProductTableComponent组件,这是根组件的bootstrap属性中使用的两个组件。其他组件是模块私有的。
更新其他类
将 TypeScript 文件移动到components文件夹需要对import语句中的路径进行一些更改。清单 21-24 显示了discountDisplay.component.ts文件所需的变更。
import { Component, Input } from "@angular/core";
import { DiscountService } from "../common/discount.service";
@Component({
selector: "paDiscountDisplay",
template: `<div class="bg-info text-white p-2">
The discount is {{discounter.discount}}
</div>`
})
export class PaDiscountDisplayComponent {
constructor(public discounter: DiscountService) { }
}
Listing 21-24.Updating a Path in the discountDisplay.component.ts File in the src/app/component Folder
清单 21-25 显示了对discountEditor.component.ts文件所需的更改。
import { Component, Input } from "@angular/core";
import { DiscountService } from "../common/discount.service";
@Component({
selector: "paDiscountEditor",
template: `<div class="form-group">
<label>Discount</label>
<input [(ngModel)]="discounter.discount"
class="form-control" type="number" />
</div>`
})
export class PaDiscountEditorComponent {
constructor(public discounter: DiscountService) { }
}
Listing 21-25.Updating a Path in the discountEditor.component.ts File in the src/app/component Folder
清单 21-26 显示了productForm.component.ts文件所需的更改。
import { Component, Output, EventEmitter, ViewEncapsulation,
Inject, SkipSelf } from "@angular/core";
import { Product } from "../model/product.model";
import { Model } from "../model/repository.model";
import { VALUE_SERVICE } from "../common/valueDisplay.directive";
@Component({
selector: "paProductForm",
templateUrl: "productForm.component.html",
viewProviders: [{ provide: VALUE_SERVICE, useValue: "Oranges" }]
})
export class ProductFormComponent {
newProduct: Product = new Product();
constructor(private model: Model,
@Inject(VALUE_SERVICE) @SkipSelf() private serviceValue: string) {
console.log("Service Value: " + serviceValue);
}
submitForm(form: any) {
this.model.saveProduct(this.newProduct);
this.newProduct = new Product();
form.reset();
}
}
Listing 21-26.Updating a Path in the productForm.component.ts File in the src/app/component Folder
清单 21-27 显示了对productTable.component.ts文件所需的更改。
import { Component, Input, ViewChildren, QueryList } from "@angular/core";
import { Model } from "../model/repository.model";
import { Product } from "../model/product.model";
import { DiscountService } from "../common/discount.service";
import { LogService } from "../common/log.service";
@Component({
selector: "paProductTable",
templateUrl: "productTable.component.html",
providers:[LogService]
})
export class ProductTableComponent {
constructor(private dataModel: Model) { }
getProduct(key: number): Product {
return this.dataModel.getProduct(key);
}
getProducts(): Product[] {
return this.dataModel.getProducts();
}
deleteProduct(key: number) {
this.dataModel.deleteProduct(key);
}
taxRate: number = 0;
dateObject: Date = new Date(2020, 1, 20);
dateString: string = "2020-02-20T00:00:00.000Z";
dateNumber: number = 1582156800000;
selectMap = {
"Watersports": "stay dry",
"Soccer": "score goals",
"other": "have fun"
}
numberMap = {
"=1": "one product",
"=2": "two products",
"other": "# products"
}
}
Listing 21-27.Updating a Path in the productTable.component.ts File in the src/app/component Folder
更新根模块
最后一步是更新根模块,删除对单个文件的过时引用,并导入新模块,如清单 21-28 所示。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { ProductTableComponent } from "./components/productTable.component";
import { ProductFormComponent } from "./components/productForm.component";
// import { PaDiscountDisplayComponent } from "./discountDisplay.component";
// import { PaDiscountEditorComponent } from "./discountEditor.component";
import { ModelModule } from "./model/model.module";
import { CommonModule } from "./common/common.module";
import { ComponentsModule } from "./components/components.module";
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule,
ModelModule, CommonModule, ComponentsModule],
declarations: [ProductComponent],
bootstrap: [ProductFormComponent, ProductTableComponent]
})
export class AppModule { }
Listing 21-28.Importing a Feature Module in the app.module.ts File in the src/app Folder
重新启动 Angular 开发工具来构建和显示应用。向应用添加模块从根本上简化了根模块,并允许在自包含的块中定义相关功能,这些块可以在与应用的其余部分相对隔离的情况下进行扩展或修改。
摘要
在这一章中,我描述了最后一个有 Angular 的构件:模块。我解释了根模块的作用,并演示了如何创建特性模块来为应用添加结构。在本书的下一部分,我将描述 Angular 提供的特性,这些特性将构建模块塑造成复杂的、响应性强的应用。
二十二、创建示例项目
在本书前一部分的所有章节中,我向示例项目添加了类和内容来演示不同的 Angular 特性,然后,在第二十一章中,我引入了特性模块来给项目添加一些结构。结果是一个项目有很多冗余和未使用的功能,对于这本书的这一部分,我将开始一个新的项目,它采用了前几章的一些核心功能,并为后面的章节提供了一个清晰的基础。
Tip
你可以从 https://github.com/Apress/pro-angular-9 下载本章以及本书其他章节的示例项目。如果在运行示例时遇到问题,请参见第一章获取帮助。
开始示例项目
要创建项目并用工具和占位符内容填充它,打开一个新的命令提示符,导航到一个方便的位置,并运行清单 22-1 中所示的命令。
ng new exampleApp --routing false --style css --skip-git --skip-tests
Listing 22-1.Creating the Example Project
为了将本书这一部分中使用的项目与早期的示例区分开来,我创建了一个名为 exampleApp 的项目。项目初始化过程需要一段时间才能完成,因为所有必需的包都已下载。
添加和配置引导 CSS 包
在本章和本书的其余部分,我继续使用 Bootstrap CSS 框架来设计 HTML 元素的样式。运行清单 22-2 中的命令,导航到exampleApp文件夹,并将引导包添加到项目中。
cd exampleApp
npm install bootstrap@4.4.1
Listing 22-2.Installing the Bootstrap Package
将清单 22-3 中所示的行添加到angular.json文件中,以将引导 CSS 样式包含在 Angular 开发工具准备的包中。
...
"styles": [
"styles.css",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
],
...
Listing 22-3.Configuring a CSS File in the angular.json File in the exampleApp Folder
创建项目结构
为了准备示例应用的内容,我添加了一系列子文件夹,用于包含应用代码和一些功能模块,如表 22-1 中所列。
表 22-1。
为示例应用创建的文件夹
|名字
|
描述
|
| --- | --- |
| src/app/model | 该文件夹将包含一个包含数据模型的功能模块。 |
| src/app/core | 该文件夹将包含一个功能模块,该模块包含提供应用核心功能的组件。 |
| src/app/messages | 该文件夹将包含用于向用户显示消息和错误的功能模块。 |
创建模型模块
第一个特性模块将包含项目的数据模型,它类似于第二部分中使用的数据模型,尽管它不包含表单验证逻辑,表单验证逻辑将在其他地方处理。
创建产品数据类型
为了定义应用所基于的基本数据类型,我在src/app/model文件夹中添加了一个名为product.model.ts的文件,并定义了清单 22-4 中所示的类。
export class Product {
constructor(public id?: number,
public name?: string,
public category?: string,
public price?: number) {}
}
Listing 22-4.The Contents of the product.model.ts File in the src/app/model Folder
创建数据源和存储库
为了给应用提供一些初始数据,我在src/app/model文件夹中创建了一个名为static.datasource.ts的文件,并定义了清单 22-5 中所示的服务。在第二十四章之前,这个类将被用作数据源,在那里我将解释如何使用异步 HTTP 请求从 web 服务请求数据。
Tip
当在一个特征模块中创建文件时,我更倾向于遵循 Angular 文件的命名规则,特别是当模块的用途从它的名字就可以看出来的时候。
import { Injectable } from "@angular/core";
import { Product } from "./product.model";
@Injectable()
export class StaticDataSource {
private data: Product[];
constructor() {
this.data = new Array<Product>(
new Product(1, "Kayak", "Watersports", 275),
new Product(2, "Lifejacket", "Watersports", 48.95),
new Product(3, "Soccer Ball", "Soccer", 19.50),
new Product(4, "Corner Flags", "Soccer", 34.95),
new Product(5, "Thinking Cap", "Chess", 16));
}
getData(): Product[] {
return this.data;
}
}
Listing 22-5.The Contents of the static.datasource.ts File in the src/app/model Folder
下一步是定义存储库,应用的其余部分将通过它来访问模型数据。我在src/app/model文件夹中创建了一个名为repository.model.ts的文件,并用它来定义清单 22-6 中所示的类。
import { Injectable } from "@angular/core";
import { Product } from "./product.model";
import { StaticDataSource } from "./static.datasource";
@Injectable()
export class Model {
private products: Product[];
private locator = (p: Product, id: number) => p.id == id;
constructor(private dataSource: StaticDataSource) {
this.products = new Array<Product>();
this.dataSource.getData().forEach(p => this.products.push(p));
}
getProducts(): Product[] {
return this.products;
}
getProduct(id: number): Product {
return this.products.find(p => this.locator(p, id));
}
saveProduct(product: Product) {
if (product.id == 0 || product.id == null) {
product.id = this.generateID();
this.products.push(product);
} else {
let index = this.products
.findIndex(p => this.locator(p, product.id));
this.products.splice(index, 1, product);
}
}
deleteProduct(id: number) {
let index = this.products.findIndex(p => this.locator(p, id));
if (index > -1) {
this.products.splice(index, 1);
}
}
private generateID(): number {
let candidate = 100;
while (this.getProduct(candidate) != null) {
candidate++;
}
return candidate;
}
}
Listing 22-6.The Contents of the repository.model.ts File in the src/app/model Folder
完成模型模块
为了完成数据模型,我需要定义模块。我在src/app/model文件夹中创建了一个名为model.module.ts的文件,并用它来定义清单 22-7 中所示的 Angular 模块。
import { NgModule } from "@angular/core";
import { StaticDataSource } from "./static.datasource";
import { Model } from "./repository.model";
@NgModule({
providers: [Model, StaticDataSource]
})
export class ModelModule { }
Listing 22-7.The Contents of the model.module.ts File in the src/app/model Folder
创建核心模块
核心模块将包含应用的核心功能,构建在第二部分描述的特性之上,为用户提供模型中的产品列表以及创建和编辑它们的能力。
创建共享状态服务
为了帮助该模块中的组件进行协作,我将添加一个记录当前模式的服务,记录用户是在编辑还是在创建产品。我在src/app/core文件夹中添加了一个名为sharedState.model.ts的文件,并定义了清单 22-8 中所示的枚举和类。
Tip
我使用了model.ts文件名,而不是service.ts,因为这个类的角色将在后面的章节中改变。请暂时容忍我,尽管我打破了命名惯例。
export enum MODES {
CREATE, EDIT
}
export class SharedState {
mode: MODES = MODES.EDIT;
id: number;
}
Listing 22-8.The Contents of the sharedState.model.ts File in the src/app/core Folder
SharedState类包含两个属性,反映当前模式和正在操作的数据模型对象的 ID。
创建表格组件
该组件将为用户提供一个表,该表列出了应用中的所有产品,并且将成为应用中的主要焦点,通过允许创建、编辑或删除对象的按钮提供对其他功能区域的访问。清单 22-9 显示了我在src/app/core文件夹中创建的table.component.ts文件的内容。
import { Component } from "@angular/core";
import { Product } from "../model/product.model";
import { Model } from "../model/repository.model";
import { MODES, SharedState } from "./sharedState.model";
@Component({
selector: "paTable",
templateUrl: "table.component.html"
})
export class TableComponent {
constructor(private model: Model, private state: SharedState) { }
getProduct(key: number): Product {
return this.model.getProduct(key);
}
getProducts(): Product[] {
return this.model.getProducts();
}
deleteProduct(key: number) {
this.model.deleteProduct(key);
}
editProduct(key: number) {
this.state.id = key;
this.state.mode = MODES.EDIT;
}
createProduct() {
this.state.id = undefined;
this.state.mode = MODES.CREATE;
}
}
Listing 22-9.The Contents of the table.component.ts File in the src/app/core Folder
该组件提供了与第二部分相同的基本功能,并增加了editProduct和createProduct方法。当用户想要编辑或创建产品时,这些方法会更新共享状态服务。
创建表格组件模板
为了给表格组件提供一个模板,我在src/app/core文件夹中添加了一个名为table.component.html的 HTML 文件,并添加了清单 22-10 中所示的标记。
<table class="table table-sm table-bordered table-striped">
<tr>
<th>ID</th><th>Name</th><th>Category</th><th>Price</th><th></th>
</tr>
<tr *ngFor="let item of getProducts()">
<td>{{item.id}}</td>
<td>{{item.name}}</td>
<td>{{item.category}}</td>
<td>{{item.price | currency:"USD" }}</td>
<td class="text-center">
<button class="btn btn-danger btn-sm mr-1"
(click)="deleteProduct(item.id)">
Delete
</button>
<button class="btn btn-warning btn-sm" (click)="editProduct(item.id)">
Edit
</button>
</td>
</tr>
</table>
<button class="btn btn-primary m-1" (click)="createProduct()">
Create New Product
</button>
Listing 22-10.The Contents of the table.component.html File in the src/app/core Folder
该模板使用ngFor指令为数据模型中的每个产品在表中创建行,包括调用deleteProduct和editProduct方法的按钮。表外还有一个button元素,当它被单击时调用组件的createProduct方法。
创建表单组件
对于这个项目,我将创建一个表单组件,它将管理一个 HTML 表单,该表单允许创建新产品,并允许修改现有产品。为了定义组件,我在src/app/core文件夹中添加了一个名为form.component.ts的文件,并添加了清单 22-11 中所示的代码。
import { Component } from "@angular/core";
import { NgForm } from "@angular/forms";
import { Product } from "../model/product.model";
import { Model } from "../model/repository.model"
import { MODES, SharedState } from "./sharedState.model";
@Component({
selector: "paForm",
templateUrl: "form.component.html",
styleUrls: ["form.component.css"]
})
export class FormComponent {
product: Product = new Product();
constructor(private model: Model,
private state: SharedState) { }
get editing(): boolean {
return this.state.mode == MODES.EDIT;
}
submitForm(form: NgForm) {
if (form.valid) {
this.model.saveProduct(this.product);
this.product = new Product();
form.reset();
}
}
resetForm() {
this.product = new Product();
}
}
Listing 22-11.The Contents of the form.component.ts File in the src/app/core Folder
相同的组件和表单将用于创建新产品和编辑现有产品,因此与第二部分中的等效组件相比,有一些额外的功能。视图中将使用editing属性来表示共享状态服务的当前设置。resetForm方法是另一个新增加的方法,它重置了用于向表单提供数据值的对象。submitForm方法没有改变,它依赖于数据模型来确定传递给saveProduct方法的对象是模型的新增对象还是现有对象的替换对象。
创建表单组件模板
为了给组件提供一个模板,我在src/app/core文件夹中添加了一个名为form.component.html的 HTML 文件,并添加了清单 22-12 中所示的标记。
<div class="bg-primary text-white p-2" [class.bg-warning]="editing">
<h5>{{editing ? "Edit" : "Create"}} Product</h5>
</div>
<form novalidate #form="ngForm" (ngSubmit)="submitForm(form)" (reset)="resetForm()" >
<div class="form-group">
<label>Name</label>
<input class="form-control" name="name"
[(ngModel)]="product.name" required />
</div>
<div class="form-group">
<label>Category</label>
<input class="form-control" name="category"
[(ngModel)]="product.category" required />
</div>
<div class="form-group">
<label>Price</label>
<input class="form-control" name="price"
[(ngModel)]="product.price"
required pattern="^[0-9\.]+$" />
</div>
<button type="submit" class="btn btn-primary m-1"
[class.btn-warning]="editing" [disabled]="form.invalid">
{{editing ? "Save" : "Create"}}
</button>
<button type="reset" class="btn btn-secondary m-1">Cancel</button>
</form>
Listing 22-12.The Contents of the form.component.html File in the src/app/core Folder
该模板最重要的部分是form元素,它包含创建或编辑产品所需的name、category和price属性的input元素。模板顶部的标题和表单的提交按钮根据编辑模式改变它们的内容和外观,以区分不同的操作。
创建表单组件样式
为了保持示例简单,我使用了基本的表单验证,没有任何错误消息。相反,我依赖于使用 Angular 验证类应用的 CSS 样式。我在src/app/core文件夹中添加了一个名为form.component.css的文件,并定义了清单 22-13 中所示的样式。
input.ng-dirty.ng-invalid { border: 2px solid #ff0000 }
input.ng-dirty.ng-valid { border: 2px solid #6bc502 }
Listing 22-13.The Contents of the form.component.css File in the src/app/core Folder
完成核心模块
为了定义包含组件的模块,我在src/app/core文件夹中添加了一个名为core.module.ts的文件,并创建了清单 22-14 中所示的 Angular 模块。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { ModelModule } from "../model/model.module";
import { TableComponent } from "./table.component";
import { FormComponent } from "./form.component";
import { SharedState } from "./sharedState.model";
@NgModule({
imports: [BrowserModule, FormsModule, ModelModule],
declarations: [TableComponent, FormComponent],
exports: [ModelModule, TableComponent, FormComponent],
providers: [SharedState]
})
export class CoreModule { }
Listing 22-14.The Contents of the core.module.ts File in the src/app/core Folder
此模块导入本章前面创建的核心 Angular 功能、Angular 形状特征和应用的数据模型。它还为SharedState服务设置了一个提供者。
创建消息模块
messages 模块将包含一个用于报告应该向用户显示的消息或错误的服务,以及一个呈现它们的组件。这是整个应用都需要的功能,实际上不属于其他两个模块。
创建消息模型和服务
为了表示应该向用户显示的消息,我在src/app/messages文件夹中添加了一个名为message.model.ts的文件,并添加了清单 22-15 中所示的代码。
export class Message {
constructor(public text: string,
public error: boolean = false) { }
}
Listing 22-15.The Contents of the message.model.ts File in the src/app/messages Folder
Message类定义了表示将向用户显示的文本以及消息是否表示错误的属性。接下来,我在src/app/messages文件夹中创建了一个名为message.service.ts的文件,并使用它来定义清单 22-16 中所示的服务,该服务将用于注册应该显示给用户的消息。
import { Injectable } from "@angular/core";
import { Message } from "./message.model";
@Injectable()
export class MessageService {
private handler: (m: Message) => void;
reportMessage(msg: Message) {
if (this.handler != null) {
this.handler(msg);
}
}
registerMessageHandler(handler: (m: Message) => void) {
this.handler = handler;
}
}
Listing 22-16.The Contents of the message.service.ts File in the src/app/messages Folder
该服务充当生成错误消息的应用部分和需要接收错误消息的应用部分之间的代理。我将在第二十三章中介绍反应式扩展包的特性时改进这个服务的工作方式。
创建组件和模板
现在我有了消息源,我可以创建一个组件,将它们显示给用户。我在src/app/messages文件夹中添加了一个名为message.component.ts的文件,并定义了清单 22-17 中所示的组件。
import { Component } from "@angular/core";
import { MessageService } from "./message.service";
import { Message } from "./message.model";
@Component({
selector: "paMessages",
templateUrl: "message.component.html",
})
export class MessageComponent {
lastMessage: Message;
constructor(messageService: MessageService) {
messageService.registerMessageHandler(m => this.lastMessage = m);
}
}
Listing 22-17.The Contents of the message.component.ts File in the src/app/messages Folder
该组件接收一个MessageService对象作为它的构造函数参数,并使用它注册一个处理函数,当服务接收到一条消息时将调用该函数,将最近的消息分配给一个名为lastMessage的属性。为了给组件提供模板,我在src/app/messages文件夹中创建了一个名为message.component.html的文件,并添加了清单 22-18 中所示的标记,向用户显示消息。
<div *ngIf="lastMessage"
class="bg-info text-white p-2 text-center"
[class.bg-danger]="lastMessage.error">
<h4>{{lastMessage.text}}</h4>
</div>
Listing 22-18.The Contents of the message.component.html File in the src/app/messages Folder
完成消息模块
我在src/app/messages文件夹中添加了一个名为message.module.ts的文件,并定义了清单 22-19 中所示的模块。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { MessageComponent } from "./message.component";
import { MessageService } from "./message.service";
@NgModule({
imports: [BrowserModule],
declarations: [MessageComponent],
exports: [MessageComponent],
providers: [MessageService]
})
export class MessageModule { }
Listing 22-19.The Contents of the message.module.ts File in the src/app/messages Folder
完成项目
为了将所有不同的模块放在一起,我对根模块做了清单 22-20 中所示的更改。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
//import { AppComponent } from './app.component';
import { ModelModule } from "./model/model.module";
import { CoreModule } from "./core/core.module";
import { TableComponent } from "./core/table.component";
import { FormComponent } from "./core/form.component";
import { MessageModule } from "./messages/message.module";
import { MessageComponent } from "./messages/message.component";
@NgModule({
imports: [BrowserModule, ModelModule, CoreModule, MessageModule],
bootstrap: [TableComponent, FormComponent, MessageComponent]
})
export class AppModule { }
Listing 22-20.Configuring the Application in the app.module.ts File in the src/app Folder
该模块导入本章创建的功能模块,并指定三个引导组件,其中两个在CoreModule中定义,一个来自MessageModule。这将显示产品表和表单以及任何消息或错误。
最后一步是更新 HTML 文件,使其包含与引导组件的selector属性相匹配的元素,如清单 22-21 所示。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ExampleApp</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 class="m-2">
<paMessages></paMessages>
<div class="row m-2">
<div class="col-8 p-2">
<paTable></paTable>
</div>
<div class="col-4 p-2">
<paForm></paForm>
</div>
</div>
</body>
</html>
Listing 22-21.Adding Custom Elements in the index.html File in the src Folder
在exampleApp文件夹中运行以下命令,启动 Angular 开发工具并构建项目:
ng serve
一旦初始构建过程完成,打开一个新的浏览器窗口并导航到http://localhost:4200以查看如图 22-1 所示的内容。
图 22-1。
运行示例应用
并不是示例应用中的所有东西都可以工作。您可以通过单击“创建新产品”和“编辑”按钮在两种操作模式之间切换,但是编辑功能不起作用。在接下来的章节中,我完成了核心功能并添加了新特性。
摘要
在这一章中,我创建了将在本书的这一部分使用的示例项目。基本结构与前几章中使用的示例相同,但是没有我用来演示早期特性的多余代码和标记。在下一章中,我将介绍 Reactive Extensions 包,它用于处理 Angular 应用中的更新。
二十三、使用反应式扩展
Angular 有很多特性,但最引人注目的是更改在应用中传播的方式,这样填写表单字段或单击按钮就会立即更新应用状态。但是 Angular 能够检测到的变化是有限的,有些特性需要直接使用 Angular 用来在整个应用中分发更新的库。这个库被称为反应式扩展,也称为 RxJS。
在这一章中,我将解释为什么高级项目需要使用 Reactive Extensions,介绍 Reactive Extensions 的核心特性(称为Observer和Observable),并展示如何使用它们来增强应用,以便用户可以编辑模型中的现有对象,以及创建新的对象。表 23-1 将反应式扩展放入上下文中。
表 23-1。
将反应式扩展库放在上下文中
|问题
|
回答
|
| --- | --- |
| 这是什么? | Reactive Extensions 库提供了一种异步事件分发机制,在 Angular 内部广泛用于变化检测和事件传播。 |
| 为什么有用? | RxJS 允许标准 Angular 变化检测过程没有处理的应用部分接收重要事件的通知并做出适当的响应。因为 RxJS 是使用 Angular 所必需的,所以它的功能很容易使用。 |
| 如何使用? | 一个Observer被创建,它收集事件并通过一个Observable将它们分发给订阅者。实现这一点的最简单的方法是创建一个Subject,它同时提供Observer和Observable功能。可以使用一组操作符来管理到订阅者的事件流。 |
| 有什么陷阱或限制吗? | 一旦您掌握了基础知识,RxJS 包就很容易使用,尽管有太多的功能需要进行一些实验才能找到有效实现特定结果的组合。 |
| 还有其他选择吗? | RxJS 需要访问一些 Angular 特性,比如更新子查询和查看子查询,以及发出异步 HTTP 请求。 |
Note
本章的重点是在 Angular 项目中最有用的 RxJS 特性。RxJS 包有很多特性,如果你想了解更多信息,你可以在 https://github.com/reactivex/rxjs 查阅项目主页。
表 23-2 总结了本章内容。
表 23-2。
章节总结
|问题
|
解决办法
|
列表
|
| --- | --- | --- |
| 在应用中分发事件 | 使用反应式扩展 | 1–5 |
| 等待模板中的异步结果 | 使用async管道 | 6–9 |
| 使用事件实现组件间的协作 | 使用一个Observable | 10–12 |
| 管理事件流 | 使用运算符,如filter或map | 13–18 |
准备示例项目
本章使用在第二十二章中创建的 exampleApp 项目。本章不需要修改。在exampleApp文件夹中运行以下命令,启动 Angular 开发工具:
ng serve
打开一个新的浏览器选项卡并导航至http://localhost:4200以查看图 23-1 中所示的内容。
图 23-1。
运行示例应用
Tip
你可以从 https://github.com/Apress/pro-angular-9 下载本章以及本书其他章节的示例项目。如果在运行示例时遇到问题,请参见第一章获取帮助。
理解问题
Angular 擅长检测用于数据绑定的表达式的变化。它无缝而高效地做到了这一点,其结果是一个框架,使创建动态应用变得容易。通过单击 Create New Product 按钮,您可以在示例应用中看到工作中的变更检测。提供共享状态信息的服务由表组件更新,然后反映在控制表单组件管理的元素外观的数据绑定中,如图 23-2 所示。当您单击“创建新产品”按钮时,表单中的标题和按钮的颜色会立即改变。
图 23-2。
更新数据绑定表达式
随着应用中对象数量的增加,变化检测可能会失控,并对应用的性能造成巨大的消耗,尤其是在功能较弱的设备上,如手机和平板电脑。Angular 没有跟踪应用中的所有对象,而是专注于数据绑定,特别是当属性值改变时。
这就产生了一个问题,因为 Angular 自动管理 HTML 元素的绑定,但是它不支持对组件内部的服务变化做出响应。
通过单击表格中的一个编辑按钮,您可以看到组件中缺少更改的直接后果。尽管数据绑定会立即更新,但是当单击按钮时,组件不会收到通知,也不知道它需要更新填充表单元素以进行编辑的属性。
缺少更新意味着表单组件需要依靠最初在第十五章中描述的ngDoCheck方法来确定重要的变化何时发生,如清单 23-1 所示。
import { Component } from "@angular/core";
import { NgForm } from "@angular/forms";
import { Product } from "../model/product.model";
import { Model } from "../model/repository.model"
import { MODES, SharedState } from "./sharedState.model";
@Component({
selector: "paForm",
templateUrl: "form.component.html",
styleUrls: ["form.component.css"]
})
export class FormComponent {
product: Product = new Product();
lastId: number;
constructor(private model: Model,
private state: SharedState) { }
get editing(): boolean {
return this.state.mode == MODES.EDIT;
}
submitForm(form: NgForm) {
if (form.valid) {
this.model.saveProduct(this.product);
this.product = new Product();
form.reset();
}
}
resetForm() {
this.product = new Product();
}
ngDoCheck() {
if (this.lastId != this.state.id) {
this.product = new Product();
if (this.state.mode == MODES.EDIT) {
Object.assign(this.product, this.model.getProduct(this.state.id));
}
this.lastId = this.state.id;
}
}
}
Listing 23-1.Monitoring Service Changes in the form.component.ts File in the src/app/core Folder
要查看这一更改的效果,请单击表中的一个编辑按钮,表单中将填充要编辑的详细信息。当您编辑完表单中的值后,单击保存按钮,数据模型将被更新,反映您在表中的更改,如图 23-3 所示。
图 23-3。
更新产品
这段代码的问题是,每当 Angular 检测到应用中的任何变化,就会调用ngDoCheck方法。无论发生什么或在哪里发生,Angular 都必须调用ngDoCheck方法来给组件一个自我更新的机会。您可以最小化在ngDoCheck方法中完成的工作量,但是随着应用中指令和组件数量的增加,变更事件的数量和对ngDoCheck方法的调用数量也会增加,这会降低应用的性能。
正确处理变更检测也比您想象的要困难。例如,尝试使用示例应用编辑一个产品,单击 Save 按钮来存储模型中的更改,然后再次单击 Edit 按钮来编辑同一个产品:什么都不会发生。这是实现ngDoCheck方法时的一个常见错误,即使组件本身触发了一个变化,也会调用这个方法,混淆了ngDoCheck方法中试图避免做不必要工作的检查。总的来说,这种方法不可靠,成本高,而且扩展性不好。
用反应式扩展解决问题
反应式扩展库在 angle 应用中非常有用,因为它为发送和接收通知提供了一个简单明确的系统。这听起来不像是一个巨大的成就,但它支撑了大多数内置的 Angular 功能,并且它可以被应用直接使用,以避免使用ngDoCheck实现变化检测所带来的问题。为了准备直接使用反应式扩展,清单 23-2 定义了一个不透明的令牌,该令牌将用于提供一个使用反应式扩展分发更新的服务,并更改SharedState类,以便它定义一个构造函数。这些变化会暂时中断应用,因为 Angular 在试图实例化一个实例以用作服务时,将无法为SharedState构造函数提供值。一旦反应式扩展所需的更改完成,应用将再次开始工作。
import { InjectionToken } from "@angular/core";
export enum MODES {
CREATE, EDIT
}
export const SHARED_STATE = new InjectionToken("shared_state");
export class SharedState {
constructor(public mode: MODES, public id?: number) { }
}
Listing 23-2.Defining a Provider Token in the sharedState.model.ts File in the src/app/core Folder
理解可观测量
关键的反应式扩展构件是一个Observable,它代表了一个可观察到的事件序列。一个对象,比如一个组件,可以订阅一个Observable并在每次事件发生时接收一个通知,允许它只在事件被观察到时才做出响应,而不是每次应用中的任何地方发生变化时都做出响应。
an Observable提供的基本方法是subscribe,它接受三个函数作为参数,如表 23-3 所示。
表 23-3。
可观察订户论点
|名字
|
描述
|
| --- | --- |
| onNext | 当一个新事件发生时,这个函数被调用。 |
| onError | 当错误发生时,这个函数被调用。 |
| onCompleted | 当事件序列结束时,调用该函数。 |
订阅一个Observable只需要onNext函数,尽管实现其他函数来提供错误处理并在您期望事件序列结束时做出响应是一个很好的实践。对于这个例子来说,事件不会结束,但是对于Observable的其他用途,比如处理 HTTP 响应,知道事件序列何时结束会更有用。清单 23-3 修改了表单组件,使其声明了对Observable服务的依赖。
import { Component, Inject } from "@angular/core";
import { NgForm } from "@angular/forms";
import { Product } from "../model/product.model";
import { Model } from "../model/repository.model"
import { MODES, SharedState, SHARED_STATE } from "./sharedState.model";
import { Observable } from "rxjs";
@Component({
selector: "paForm",
templateUrl: "form.component.html",
styleUrls: ["form.component.css"]
})
export class FormComponent {
product: Product = new Product();
// lastId: number;
constructor(private model: Model,
@Inject(SHARED_STATE) public stateEvents: Observable<SharedState>) {
stateEvents.subscribe((update) => {
this.product = new Product();
if (update.id != undefined) {
Object.assign(this.product, this.model.getProduct(update.id));
}
this.editing = update.mode == MODES.EDIT;
});
}
editing: boolean = false;
submitForm(form: NgForm) {
if (form.valid) {
this.model.saveProduct(this.product);
this.product = new Product();
form.reset();
}
}
resetForm() {
this.product = new Product();
}
//ngDoCheck() {
// if (this.lastId != this.state.id) {
// this.product = new Product();
// if (this.state.mode == MODES.EDIT) {
// Object.assign(this.product, this.model.getProduct(this.state.id));
// }
// this.lastId = this.state.id;
// }
//}
}
Listing 23-3.Using an Observable in the form.component.ts File in the src/app/core Folder
Reactive Extensions NPM 包为它提供的每种类型都包含了单独的 JavaScript 模块,因此您可以从rxjs模块导入Observable类型。
为了接收通知,组件声明了对SHARED_STATE服务的依赖,该服务作为Observable<SharedState>对象被接收。这个对象是一个Observerable,它的通知将是SharedState对象,这将代表用户启动的编辑或创建操作。该组件调用Observable.subscribe方法,该方法提供一个接收每个SharedState对象并使用它来更新其状态的函数。
What About Promises?
您可能习惯于使用Promise对象来表示异步活动。Observable s 执行相同的基本任务,但更灵活,功能更多。Angular 确实提供了对使用Promise对象的支持,这在您转换到 Angular 和使用依赖于Promise对象的库时会很有用。
反应式扩展提供了一个Observable.fromPromise方法,该方法将使用一个Promise作为事件源来创建一个Observable。如果你有一个Observable并且因为某种原因需要一个Promise,还有一个Observable.toPromise方法。
此外,还有一些 Angular 功能可以让您选择使用,例如第二十七章中描述的防护功能,这两种功能都支持。
但是 Reactive Extensions 库是使用 Angular 的一个重要部分,你会在本书这一部分的章节中经常遇到它。我建议您在遇到Observable时使用它,并尽量减少与Promise对象的相互转换。
理解观察者
反应式扩展Observer提供了创建更新的机制,使用表 23-4 中描述的方法。
表 23-4。
观察者方法
|名字
|
描述
|
| --- | --- |
| next(value) | 此方法使用指定的值创建一个新事件。 |
| error(errorObject) | 此方法报告一个错误,用参数描述,该参数可以是任何对象。 |
| complete() | 此方法结束序列,指示不再发送事件。 |
清单 23-4 更新了表格组件,这样当用户点击 Create New Product 按钮或其中一个编辑按钮时,它会使用一个Observer来发送事件。
import { Component, Inject } from "@angular/core";
import { Product } from "../model/product.model";
import { Model } from "../model/repository.model";
import { MODES, SharedState, SHARED_STATE } from "./sharedState.model";
import { Observer } from "rxjs";
@Component({
selector: "paTable",
templateUrl: "table.component.html"
})
export class TableComponent {
constructor(private model: Model,
@Inject(SHARED_STATE) public observer: Observer<SharedState>) { }
getProduct(key: number): Product {
return this.model.getProduct(key);
}
getProducts(): Product[] {
return this.model.getProducts();
}
deleteProduct(key: number) {
this.model.deleteProduct(key);
}
editProduct(key: number) {
this.observer.next(new SharedState(MODES.EDIT, key));
}
createProduct() {
this.observer.next(new SharedState(MODES.CREATE));
}
}
Listing 23-4.Using an Observer in the table.component.ts File in the src/app/core Folder
该组件声明了对SHARED_STATE服务的依赖,该服务作为一个Observer<SharedState>对象被接收,这意味着一个Observer将发送使用SharedState对象描述的事件。editProduct和createProduct方法已经更新,因此它们调用观察器上的next方法来表示状态的变化。
理解主题
这两个组件都使用SHARED_STATE标记声明了对服务的依赖,但是每个组件都希望获得不同的类型:表格组件希望接收一个Observer<SharedState>对象,而表单组件希望接收一个Observable<SharedState>对象。
反应式扩展库提供了Subject类,它实现了Observer和Observable功能。这使得创建允许用单个对象产生和消费事件的服务变得容易。在清单 23-5 中,我已经修改了在@NgModule装饰者的providers属性中声明的服务,以使用一个Subject对象。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { ModelModule } from "../model/model.module";
import { TableComponent } from "./table.component";
import { FormComponent } from "./form.component";
import { SharedState, SHARED_STATE } from "./sharedState.model";
import { Subject } from "rxjs";
@NgModule({
imports: [BrowserModule, FormsModule, ModelModule],
declarations: [TableComponent, FormComponent],
exports: [ModelModule, TableComponent, FormComponent],
providers: [{ provide: SHARED_STATE, useValue: new Subject<SharedState>() }]
})
export class CoreModule { }
Listing 23-5.Changing the Service in the core.module.ts File in the src/app/core Folder
基于值的提供者告诉 Angular 使用一个Subject<SharedState>对象来解析对SHARED_STATE令牌的依赖,这将为组件提供它们协作所需的功能。
结果是,更改共享服务使其成为一个Subject允许表格组件发出不同的事件,这些事件由表单组件接收并用于更新其状态,而不需要依赖笨拙且昂贵的ngDoCheck方法。由于订阅了Observable的组件知道它接收到的所有事件必定源自于Observer,因此也不需要试图找出哪些变化是由本地组件生成的,哪些变化来自于其他地方。这意味着像不能编辑同一个产品两次这样的琐碎问题会消失,如图 23-4 所示。
图 23-4。
使用反应式扩展的效果
The Different Types of Subject
清单 23-5 使用了Subject类,这是创建既是Observer又是Observable的对象的最简单方法。它的主要限制是,当使用subscribe方法创建一个新订户时,直到下次调用next方法时,它才会收到一个事件。如果您正在动态地创建组件或指令的实例,并且希望它们一创建就有一些上下文数据,那么这可能是没有用的。
Reactive Extensions 库包含了一些专门的Subject类的实现,可以用来解决这个问题。BehaviorSubject类跟踪它处理的最后一个事件,并在新订阅者调用subscribe方法时将其发送给新订阅者。ReplaySubject类做了一些类似的事情,除了它跟踪它的所有事件,并把它们发送给新的订阅者,允许他们在订阅之前赶上发送的任何事件。
使用异步管道
Angular 包含了async管道,它可以用来在视图中直接使用Observable对象,选择从事件序列中接收到的最后一个对象。这是一个不纯的管道,如第十八章所述,因为它的变化是由使用它的视图之外驱动的,这意味着它的transform方法将被经常调用,即使没有从Observable接收到新的事件。清单 23-6 显示了将async管道添加到由表单组件管理的视图中。
<div class="bg-primary text-white p-2" [class.bg-warning]="editing">
<h5>{{editing ? "Edit" : "Create"}} Product</h5>
Last Event: {{ stateEvents | async | json }}
</div>
<form novalidate #form="ngForm" (ngSubmit)="submitForm(form)" (reset)="resetForm()">
...elements omitted for brevity...
</form>
Listing 23-6.Using the Async Pipe in the form.component.html File in the src/app/core Folder
字符串插值绑定表达式从组件(即Observable<SharedState>对象)获取stateEvents属性,并将其传递给async管道,后者跟踪最近接收到的事件。然后,async过滤器将事件传递给json管道,后者创建事件对象的 JSON 表示。结果是你可以跟踪表单组件接收到的事件,如图 23-5 所示。
图 23-5。
显示可观察的事件
这不是最有用的数据显示,但它确实提供了一些有用的调试见解。在这种情况下,最近的事件的mode值为 1,这对应于编辑模式,而id值为 4,这是角标志产品的 ID。
将异步管道与自定义管道一起使用
async管道可以与定制管道一起使用,以更加用户友好的方式呈现事件数据。为了演示,我在src/app/core文件夹中添加了一个名为state.pipe.ts的文件,并用它来定义清单 23-7 中所示的管道。
import { Pipe } from "@angular/core";
import { SharedState, MODES } from "./sharedState.model";
import { Model } from "../model/repository.model";
@Pipe({
name: "formatState",
pure: true
})
export class StatePipe {
constructor(private model: Model) { }
transform(value: any): string {
if (value instanceof SharedState) {
let state = value as SharedState;
return MODES[state.mode] + (state.id != undefined
? ` ${this.model.getProduct(state.id).name}` : "");
} else {
return "<No Data>"
}
}
}
Listing 23-7.The Contents of the state.pipe.ts File in the src/app/core Folder
在清单 23-8 中,我已经将管道添加到核心模块的声明集中。
Tip
TypeScript 枚举有一个有用的功能,通过它可以获得值的名称。因此,例如,表达式MODES[1]将返回EDIT,因为这是索引 1 处的MODES枚举值的名称。清单 23-7 中的管道使用这个特性向用户呈现状态更新。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { ModelModule } from "../model/model.module";
import { TableComponent } from "./table.component";
import { FormComponent } from "./form.component";
import { SharedState, SHARED_STATE } from "./sharedState.model";
import { Subject } from "rxjs";
import { StatePipe } from "./state.pipe";
@NgModule({
imports: [BrowserModule, FormsModule, ModelModule],
declarations: [TableComponent, FormComponent, StatePipe],
exports: [ModelModule, TableComponent, FormComponent],
providers: [{ provide: SHARED_STATE, useValue: new Subject<SharedState>() }]
})
export class CoreModule { }
Listing 23-8.Registering the Pipe in the core.module.ts File in the src/app/core Folder
清单 23-9 显示了用于替换表单组件管理的模板中内置json管道的新管道。
<div class="bg-primary text-white p-2" [class.bg-warning]="editing">
<h5>{{editing ? "Edit" : "Create"}} Product</h5>
Last Event: {{ stateEvents | async | formatState }}
</div>
<form novalidate #form="ngForm" (ngSubmit)="submitForm(form)" (reset)="resetForm()">
...elements omitted for brevity...
</form>
Listing 23-9.Applying a Custom Pipe in the form.component.html File in the src/app/core Folder
这个例子演示了从Observable对象接收的事件可以像任何其他对象一样被处理和转换,如图 23-6 所示,它说明了一个定制管道是如何建立在由async管道提供的核心功能之上的。
图 23-6。
格式化通过可观察序列接收的值
向上扩展应用功能模块
相同的反应式扩展构件可以在应用中的任何地方使用,并使构件之间的协作变得容易,即使反应式扩展的使用并没有暴露给应用的所有协作部分。作为示范,清单 23-10 展示了向MessageService类添加一个Subject来分发应该显示给用户的消息。
import { Injectable } from "@angular/core";
import { Message } from "./message.model";
import { Observable } from "rxjs";
import { Subject } from "rxjs";
@Injectable()
export class MessageService {
private subject = new Subject<Message>();
reportMessage(msg: Message) {
this.subject.next(msg);
}
get messages(): Observable<Message> {
return this.subject;
}
}
Listing 23-10.Using a Subject in the message.service.ts File in the src/app/messages Folder
以前的消息服务实现只支持应该向用户显示的消息的单个接收者。我本来可以添加管理多个接收者的代码,但是考虑到应用已经使用了反应式扩展,将这项工作委托给Subject类要简单得多,它可以很好地扩展,如果应用中有多个订阅者,不需要任何额外的代码或测试。
清单 23-11 显示了消息组件的相应变化,它将向用户显示最近的消息。
import { Component } from "@angular/core";
import { MessageService } from "./message.service";
import { Message } from "./message.model";
import { Observable } from "rxjs";
@Component({
selector: "paMessages",
templateUrl: "message.component.html",
})
export class MessageComponent {
lastMessage: Message;
constructor(messageService: MessageService) {
messageService.messages.subscribe(m => this.lastMessage = m);
}
}
Listing 23-11.Observing Messages in the message.component.ts File in the src/app/messages Folder
最后一步是生成一些要显示的消息。在清单 23-12 中,我修改了核心特性模块的配置,这样SHARED_STATE提供者使用一个工厂函数来创建用于分发状态变化事件的Subject,并添加一个订阅,将事件提供给消息服务。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { ModelModule } from "../model/model.module";
import { TableComponent } from "./table.component";
import { FormComponent } from "./form.component";
import { SharedState, SHARED_STATE } from "./sharedState.model";
import { Subject } from "rxjs";
import { StatePipe } from "./state.pipe";
import { MessageModule } from "../messages/message.module";
import { MessageService } from "../messages/message.service";
import { Message } from "../messages/message.model";
import { Model } from "../model/repository.model";
import { MODES } from "./sharedState.model";
@NgModule({
imports: [BrowserModule, FormsModule, ModelModule, MessageModule],
declarations: [TableComponent, FormComponent, StatePipe],
exports: [ModelModule, TableComponent, FormComponent],
providers: [{
provide: SHARED_STATE,
deps: [MessageService, Model],
useFactory: (messageService, model) => {
let subject = new Subject<SharedState>();
subject.subscribe(m => messageService.reportMessage(
new Message(MODES[m.mode] + (m.id != undefined
? ` ${model.getProduct(m.id).name}` : "")))
);
return subject;
}
}]
})
export class CoreModule { }
Listing 23-12.Feeding the Message Service in the core.module.ts File in the src/app/core Folder
代码有点乱,但结果是表格组件发送的每个状态变化事件都被消息组件显示出来,如图 23-7 。Reactive Extensions 使连接应用的各个部分变得很容易,清单中的代码如此密集的原因是它还使用了Model服务从数据模型中查找名称,以使事件更容易阅读。
图 23-7。
在消息服务中使用反应式扩展
超越基础
前面章节中的例子涵盖了Observable、Observer和Subject的基本用法。然而,在使用可用于高级或复杂应用的反应式扩展时,还有更多的功能可用。全套操作在 https://github.com/reactivex/rxjs 中描述,但在这一章中,我演示了一小部分在 Angular 应用中最可能需要的特性,如表 23-5 中所述。表格中描述的方法用于控制从Observable对象接收事件的方式。
表 23-5。
用于选择事件的有用的反应式扩展方法
|名字
|
描述
|
| --- | --- |
| filter | 该方法调用一个函数来评估从Observable接收的每个事件,并丢弃该函数返回的事件false。 |
| map | 该方法调用一个函数来转换从Observable接收的每个事件,并传递该函数返回的对象。 |
| distinctUntilChanged | 此方法禁止显示事件,直到事件对象发生变化。 |
| skipWhile | 此方法筛选事件,直到满足指定的条件,然后将事件转发给订阅者。 |
| takeWhile | 此方法将事件传递给订阅服务器,直到满足指定的条件,之后筛选事件。 |
过滤事件
并只选择那些需要的。清单 23-13 展示了如何使用filter方法过滤出与特定产品相关的事件。
import { Component, Inject } from "@angular/core";
import { NgForm } from "@angular/forms";
import { Product } from "../model/product.model";
import { Model } from "../model/repository.model";
import { MODES, SharedState, SHARED_STATE } from "./sharedState.model";
import { Observable } from "rxjs";
import { filter } from "rxjs/operators";
@Component({
selector: "paForm",
templateUrl: "form.component.html",
styleUrls: ["form.component.css"]
})
export class FormComponent {
product: Product = new Product();
constructor(private model: Model,
@Inject(SHARED_STATE) public stateEvents: Observable<SharedState>) {
stateEvents.pipe(filter(state => state.id != 3))
.subscribe((update) => {
this.product = new Product();
if (update.id != undefined) {
Object.assign(this.product, this.model.getProduct(update.id));
}
this.editing = update.mode == MODES.EDIT;
});
}
editing: boolean = false;
submitForm(form: NgForm) {
if (form.valid) {
this.model.saveProduct(this.product);
this.product = new Product();
form.reset();
}
}
resetForm() {
this.product = new Product();
}
}
Listing 23-13.Filtering Events in the form.component.ts File in the src/app/core Folder
要使用表 23-5 中描述的方法,需要一个rxjs/operators包的import语句,如下所示:
...
import { filter } from "rxjs/operators";
...
使用pipe方法将filter方法应用于Observable,如下所示:
...
stateEvents.pipe(filter(state => state.id != 3)).subscribe((update) => {
...
filter方法的参数是一个选择所需事件的语句,这些事件被传递给使用subscribe方法提供的函数。
您可以通过单击足球产品的 Edit 按钮来查看效果,该产品具有过滤器函数正在检查的 ID。async管道显示一个EDIT事件已经通过共享服务发送,但是filter方法阻止它被组件的subscribe函数接收。结果是表单没有反映状态的变化,也没有填充所选择的产品信息,如图 23-8 所示。
图 23-8。
过滤事件
转变事件
map方法用于转换从Observable接收的对象。您可以使用此方法以任何方式转换事件对象,方法的结果将替换事件对象。清单 23-14 使用map方法来改变事件对象属性的值。
import { Component, Inject } from "@angular/core";
import { NgForm } from "@angular/forms";
import { Product } from "../model/product.model";
import { Model } from "../model/repository.model";
import { MODES, SharedState, SHARED_STATE } from "./sharedState.model";
import { Observable } from "rxjs";
import { filter, map } from "rxjs/operators";
@Component({
selector: "paForm",
templateUrl: "form.component.html",
styleUrls: ["form.component.css"]
})
export class FormComponent {
product: Product = new Product();
constructor(private model: Model,
@Inject(SHARED_STATE) public stateEvents: Observable<SharedState>) {
stateEvents
.pipe(map(state => new SharedState(state.mode, state.id == 5
? 1 : state.id)))
.pipe(filter(state => state.id != 3))
.subscribe((update) => {
this.product = new Product();
if (update.id != undefined) {
Object.assign(this.product, this.model.getProduct(update.id));
}
this.editing = update.mode == MODES.EDIT;
});
}
editing: boolean = false;
submitForm(form: NgForm) {
if (form.valid) {
this.model.saveProduct(this.product);
this.product = new Product();
form.reset();
}
}
resetForm() {
this.product = new Product();
}
}
Listing 23-14.Transforming Events in the form.component.ts File in the src/app/core Folder
在这个例子中,传递给map方法的函数寻找id值为 5 的SharedState对象,当找到时,将值改为 1。结果是点击思维帽产品的编辑按钮选择 Kayak 产品进行编辑,如图 23-9 所示。
图 23-9。
转变事件
Caution
当使用map方法时,不要修改作为函数参数接收的对象。该对象依次传递给所有订阅者,您所做的任何更改都会影响后续的订阅者。这意味着一些订阅者将接收未修改的对象,一些订阅者将接收由map方法返回的对象。相反,创建一个新对象,如清单 23-14 所示。
注意,用于准备和创建对一个Observable对象的订阅的方法可以链接在一起。在这个例子中,map方法的结果通过管道传递给filter方法,后者的结果再传递给subscribe方法的函数。以这种方式将方法链接在一起允许为处理和接收事件的方式创建复杂的规则。
使用不同的事件对象
map方法可以用来产生任何对象,并且不限于改变它接收的对象的属性值。在清单 23-15 中,我使用了map方法来产生一个数字,它的值编码了操作和它所应用的对象。
...
constructor(private model: Model,
@Inject(SHARED_STATE) public stateEvents: Observable<SharedState>) {
stateEvents
.pipe(map(state => state.mode == MODES.EDIT ? state.id : -1))
.pipe(filter(id => id != 3))
.subscribe((id) => {
this.editing = id != -1;
this.product = new Product();
if (id != -1) {
Object.assign(this.product, this.model.getProduct(id))
}
});
}
...
Listing 23-15.Projecting a Different Type in the form.component.ts File in the src/app/core Folder
让一个简单的数据类型表示一个操作并指定它的目标没有什么好处。事实上,它通常会导致问题,因为这意味着组件假设模型中永远不会有一个对象的id属性为-1。但是作为一个简单的例子,它演示了map方法如何投射不同的类型,以及这些类型如何沿着反应式扩展方法链传递,这意味着由map方法产生的number值被接收为要由filter方法处理的值,并依次由subscribe方法处理,这两个方法的函数都被更新以处理新的数据值。
只接收不同的事件
distinctUntilChanged方法过滤事件序列,以便只将不同的值传递给订阅者。要查看可以用它来解决的问题,单击 Kayak 产品的 Edit 按钮并更改Category字段的值。不点击保存按钮,再次点击 Kayak 的编辑按钮,您将看到您的编辑被丢弃。在清单 23-16 中,我已经将distinctUntilChanged方法添加到方法链中,这样它就可以应用于由map方法产生的number值。只有不同的值将被转发给filter和subscribe方法。
import { Component, Inject } from "@angular/core";
import { NgForm } from "@angular/forms";
import { Product } from "../model/product.model";
import { Model } from "../model/repository.model";
import { MODES, SharedState, SHARED_STATE } from "./sharedState.model";
import { Observable } from "rxjs";
import { filter, map, distinctUntilChanged } from "rxjs/operators";
@Component({
selector: "paForm",
templateUrl: "form.component.html",
styleUrls: ["form.component.css"]
})
export class FormComponent {
product: Product = new Product();
constructor(private model: Model,
@Inject(SHARED_STATE) public stateEvents: Observable<SharedState>) {
stateEvents
.pipe(map(state => state.mode == MODES.EDIT ? state.id : -1))
.pipe(distinctUntilChanged())
.pipe(filter(id => id != 3))
.subscribe((id) => {
this.editing = id != -1;
this.product = new Product();
if (id != -1) {
Object.assign(this.product, this.model.getProduct(id))
}
});
}
editing: boolean = false;
submitForm(form: NgForm) {
if (form.valid) {
this.model.saveProduct(this.product);
this.product = new Product();
form.reset();
}
}
resetForm() {
this.product = new Product();
}
}
Listing 23-16.Preventing Duplicate Events in the form.component.ts File in the src/app/core Folder
如果您重复 Kayak 编辑过程,您将看到当您为正在编辑的产品单击编辑按钮时,更改不再被丢弃,因为这将产生与前一事件相同的值。编辑不同的产品将导致map方法发出不同的number值,该值将由distinctUntilChanged方法传递。
使用自定义等式检查器
distinctUntilChanged方法可以在像number这样的简单数据类型之间进行简单的比较,但是它不知道如何比较对象,并且会假设任意两个对象是不同的。为了解决这个问题,你可以指定一个比较函数来检查事件是否不同,如清单 23-17 所示。
import { Component, Inject } from "@angular/core";
import { NgForm } from "@angular/forms";
import { Product } from "../model/product.model";
import { Model } from "../model/repository.model";
import { MODES, SharedState, SHARED_STATE } from "./sharedState.model";
import { Observable } from "rxjs";
import { filter, map, distinctUntilChanged } from "rxjs/operators";
@Component({
selector: "paForm",
templateUrl: "form.component.html",
styleUrls: ["form.component.css"]
})
export class FormComponent {
product: Product = new Product();
constructor(private model: Model,
@Inject(SHARED_STATE) public stateEvents: Observable<SharedState>) {
stateEvents
.pipe(distinctUntilChanged((firstState, secondState) =>
firstState.mode == secondState.mode
&& firstState.id == secondState.id))
.subscribe(update => {
this.product = new Product();
if (update.id != undefined) {
Object.assign(this.product, this.model.getProduct(update.id));
}
this.editing = update.mode == MODES.EDIT;
});
}
editing: boolean = false;
submitForm(form: NgForm) {
if (form.valid) {
this.model.saveProduct(this.product);
this.product = new Product();
form.reset();
}
}
resetForm() {
this.product = new Product();
}
}
Listing 23-17.Using a Equality Checker in the form.component.ts File in the src/app/core Folder
这个清单删除了map和filter方法,并为distinctUntilChanged方法提供了一个函数,该函数通过比较SharedState对象的mode和id属性来比较它们。不同的对象被传递给提供给subscribe方法的函数。
接受和跳过事件
skipWhile和takeWhile方法用于指定导致事件被过滤或传递给订阅者的条件。必须小心使用这些方法,因为很容易指定将永久筛选来自订阅服务器的事件的条件。在清单 23-18 中,我使用了skipWhile方法来过滤事件,直到用户点击创建新产品按钮,之后事件将被传递。
import { Component, Inject } from "@angular/core";
import { NgForm } from "@angular/forms";
import { Product } from "../model/product.model";
import { Model } from "../model/repository.model";
import { MODES, SharedState, SHARED_STATE } from "./sharedState.model";
import { Observable } from "rxjs";
import { filter, map, distinctUntilChanged, skipWhile } from "rxjs/operators";
@Component({
selector: "paForm",
templateUrl: "form.component.html",
styleUrls: ["form.component.css"]
})
export class FormComponent {
product: Product = new Product();
constructor(private model: Model,
@Inject(SHARED_STATE) public stateEvents: Observable<SharedState>) {
stateEvents
.pipe(skipWhile(state => state.mode == MODES.EDIT))
.pipe(distinctUntilChanged((firstState, secondState) =>
firstState.mode == secondState.mode
&& firstState.id == secondState.id))
.subscribe(update => {
this.product = new Product();
if (update.id != undefined) {
Object.assign(this.product, this.model.getProduct(update.id));
}
this.editing = update.mode == MODES.EDIT;
});
}
editing: boolean = false;
submitForm(form: NgForm) {
if (form.valid) {
this.model.saveProduct(this.product);
this.product = new Product();
form.reset();
}
}
resetForm() {
this.product = new Product();
}
}
Listing 23-18.Skipping Events in the form.component.ts File in the src/app/core Folder
单击表中的编辑按钮仍然会生成事件,这些事件将由订阅到Subject的async管道显示,没有任何过滤或跳过。但是表单组件不接收那些事件,如图 23-10 所示,因为它的订阅被skipWhile方法过滤,直到接收到一个mode属性不是MODES.EDIT的事件。单击 Create New Product 按钮会生成一个结束跳过的事件,之后组件将接收所有事件。
图 23-10。
跳过事件
摘要
在这一章中,我介绍了 Reactive Extensions 包,并解释了如何用它来处理应用中不受 Angular 变化检测过程管理的部分的变化。我演示了如何使用Observable、Observer和Subject对象在应用中分发事件,向您展示了内置的async管道是如何工作的,并介绍了一些最有用的操作符,这些操作符是 Reactive Extensions 库为控制事件流向订户而提供的。在下一章,我将解释如何在 Angular 应用中进行异步 HTTP 请求,以及如何使用 RESTful web 服务。