Angular9-高级教程-八-

103 阅读1小时+

Angular9 高级教程(八)

原文:Pro Angular 9

协议:CC BY-NC-SA 4.0

十九、使用服务

服务是提供通用功能以支持应用中其他构件的对象,例如指令、组件和管道。关于服务,重要的是它们被使用的方式,这是通过一个叫做依赖注入的过程。使用服务可以增加 Angular 应用的灵活性和可伸缩性,但是依赖注入可能是一个很难理解的话题。为此,我慢慢地开始这一章,解释服务和依赖注入可以用来解决的问题,依赖注入如何工作,以及为什么你应该考虑在你自己的项目中使用服务。在第二十章中,我介绍了 Angular 为服务提供的一些更高级的特性。表 19-1 将服务放在上下文中。

表 19-1。

将服务置于环境中

|

问题

|

回答

| | --- | --- | | 它们是什么? | 服务是定义其他构建块(如组件或指令)所需功能的对象。服务与常规对象的区别在于,它们是由外部提供者提供给构建块的,而不是直接使用new关键字创建或由输入属性接收的。 | | 它们为什么有用? | 服务简化了应用的结构,使移动或重用功能变得更容易,并使隔离有效单元测试的构建块变得更容易。 | | 它们是如何使用的? | 类使用构造函数参数声明对服务的依赖,然后使用应用已配置的服务集解析这些参数。服务是已经应用了@Injectable装饰器的类。 | | 有什么陷阱或限制吗? | 依赖注入是一个有争议的话题,并不是所有的开发人员都喜欢使用它。如果您不执行单元测试,或者如果您的应用相对简单,实现依赖注入所需的额外工作不太可能带来任何长期回报。 | | 还有其他选择吗? | 服务和依赖注入是难以避免的,因为 Angular 使用它们来提供对内置功能的访问。但是如果您愿意的话,并不要求您为自己的定制功能定义服务。 |

表 19-2 总结了本章内容。

表 19-2。

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 避免手动分发共享对象的需要 | 使用服务 | 1–14, 21–28 | | 声明对服务的依赖 | 添加一个带有所需服务类型的构造函数参数 | 15–20 |

准备示例项目

我继续使用从第十一章开始的本章中的示例项目。为了准备本章,我用清单 19-1 中所示的元素替换了ProductTable组件模板的内容。

Tip

你可以从 https://github.com/Apress/pro-angular-9 下载本章以及本书其他章节的示例项目。如果在运行示例时遇到问题,请参见第一章获取帮助。

<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>{{item.price | 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>

Listing 19-1.Replacing the Contents of the productTable.component.html File in the src/app Folder

example文件夹中运行以下命令,启动 TypeScript 编译器和开发 HTTP 服务器:

ng serve

打开一个新的浏览器窗口并导航至http://localhost:4200以查看图 19-1 所示的内容。

img/421542_4_En_19_Fig1_HTML.jpg

图 19-1。

运行示例应用

理解对象分布问题

在第十七章中,我向项目中添加了组件,以帮助打破应用的整体结构。为此,我使用输入和输出属性来连接组件,使用主体元素来桥接 Angular 在父组件及其子组件之间强制实施的隔离。我还向您展示了如何查询视图子模板的内容,它补充了第十六章中描述的内容子特性。

如果谨慎应用,这些用于协调指令和组件的技术会非常强大和有用。但是它们也可能最终成为在整个应用中分发共享对象的通用工具,其结果是增加了应用的复杂性,并将组件紧密地绑定在一起。

演示问题

为了帮助演示这个问题,我将向项目添加一个共享对象和两个依赖于它的组件。我在src/app文件夹中创建了一个名为discount.service.ts的文件,并定义了清单 19-2 中所示的类。我将在本章的后面解释文件名的service部分的意义。

export class DiscountService {
    private discountValue: number = 10;

    public get discount(): number {
        return this.discountValue;
    }

    public set discount(newValue: number) {
        this.discountValue = newValue || 0;
    }

    public applyDiscount(price: number) {
        return Math.max(price - this.discountValue, 5);
    }
}

Listing 19-2.The Contents of the discount.service.ts File in the src/app Folder

DiscountService类定义了一个名为discountValue的私有属性,用于存储一个数字,该数字将用于降低数据模型中的产品价格。这个值是通过名为discount的 getters 和 setters 公开的,有一个名为applyDiscount的便利方法可以降低价格,同时确保价格不低于 5 美元。

对于第一个使用了DiscountService类的组件,我在src/app文件夹中添加了一个名为discountDisplay.component.ts的文件,并添加了清单 19-3 中所示的代码。

import { Component, Input } from "@angular/core";
import { DiscountService } from "./discount.service";

@Component({
    selector: "paDiscountDisplay",
    template: `<div class="bg-info text-white p-2">
                The discount is {{discounter.discount}}
               </div>`
})
export class PaDiscountDisplayComponent {

    @Input("discounter")
    discounter: DiscountService;
}

Listing 19-3.The Contents of the discountDisplay.component.ts File in the src/app Folder

DiscountDisplayComponent使用一个内嵌模板来显示折扣金额,该金额是从通过input属性接收的DiscountService对象中获得的。

对于使用DiscountService类的第二个组件,我在src/app文件夹中添加了一个名为discountEditor.component.ts的文件,并添加了清单 19-4 中所示的代码。

import { Component, Input } from "@angular/core";
import { DiscountService } from "./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 {

    @Input("discounter")
    discounter: DiscountService;
}

Listing 19-4.The Contents of the discountEditor.component.ts File in the src/app Folder

DiscountEditorComponent使用带有input元素的内嵌模板,允许编辑折扣金额。input元素在针对ngModel指令的DiscountService.discount属性上有一个双向绑定。清单 19-5 显示了 Angular 模块中启用的新组件。

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";

registerLocaleData(localeFr);

@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent],
  //providers: [{ provide: LOCALE_ID, useValue: "fr-FR" }],
  bootstrap: [ProductComponent]
})
export class AppModule { }

Listing 19-5.Enabling the Components in the app.module.ts File in the src/app Folder

为了让新组件工作,我将它们添加到父组件的模板中,将新内容放在列出产品的表格下面,这意味着我需要编辑productTable.component.html文件,如清单 19-6 所示。

<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>{{item.price | 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 [discounter]="discounter"></paDiscountEditor>
<paDiscountDisplay [discounter]="discounter"></paDiscountDisplay>

Listing 19-6.Adding Component Elements in the productTable.component.html File in the src/app Folder

这些元素对应于清单 19-3 和清单 19-4 中组件的selector属性,并使用数据绑定来设置输入属性的值。最后一步是在父组件中创建一个对象,它将为数据绑定表达式提供值,如清单 19-7 所示。

import { Component, Input, ViewChildren, QueryList } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
import { DiscountService } from "./discount.service";

@Component({
    selector: "paProductTable",
    templateUrl: "productTable.component.html"
})
export class ProductTableComponent {
    discounter: DiscountService = new DiscountService();

    @Input("model")
    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 19-7.Creating the Shared Object in the productTable.component.ts File in the src/app Folder

图 19-2 显示了新组件的内容。由一个组件提供的对input元素中的值的更改将反映在由另一个组件呈现的内容中,反映了共享的DiscountService对象及其discount属性的使用。

img/421542_4_En_19_Fig2_HTML.jpg

图 19-2。

向示例应用添加组件

直到最后阶段,添加新组件和共享对象的过程都是简单明了且符合逻辑的。问题出现在我创建和分发共享对象的方式上:类的实例。

因为 Angular 将组件彼此隔离,我没有办法在DiscountEditorComponentDiscountDisplayComponent之间直接共享DiscountService对象。每个组件都可以创建自己的DiscountService对象,但是这意味着来自编辑器组件的更改不会显示在显示组件中。

这就是我在 product table 组件中创建DiscountService对象的原因,它是折扣编辑器和显示组件的第一个共享祖先。这允许我通过 product table 组件的模板分发DiscountService对象,确保需要它的两个组件共享一个对象。

但是有几个问题。首先,ProductTableComponent类实际上并不需要或使用一个DiscountService对象来交付它自己的功能。它恰好是需要该对象的组件的第一个共同祖先。在ProductTableComponent类中创建共享对象会使这个类稍微复杂一点,并且更难有效地测试。这是复杂性的适度增加,但它会发生在应用需要的每个共享对象上——一个复杂的应用可能依赖于许多共享对象,每个对象最终都是由组件创建的,而这些组件恰好是依赖它们的类的第一个公共祖先。

第二个问题由术语第一个共同祖先暗示。ProductTableComponent类恰好是依赖于DiscountService对象的两个类的父类,但是想想如果我想移动DiscountEditorComponent使其显示在表单下而不是表格下会发生什么。在这种情况下,我必须沿着组件树向上搜索,直到找到一个共同的祖先,这将成为根组件。然后,我必须沿着组件树添加输入属性和修改模板,以便每个中间组件可以从其父组件接收DiscountService对象,并将其传递给任何有需要它的后代的子组件。这同样适用于任何依赖于接收一个DiscountService对象的指令,其中任何其模板包含以该指令为目标的数据绑定的组件必须确保它们也是分发链的一部分。

结果是应用中的组件和指令紧密地绑定在一起。如果您需要在应用的不同部分移动或重用组件,并且输入属性和数据绑定的管理变得难以管理,则需要进行重大的重构。

使用依赖注入将对象作为服务分发

将对象分配给依赖它们的类有一个更好的方法,那就是使用依赖注入,对象从外部源提供给类。Angular 包含一个内置的依赖注入系统,并提供外部对象源,称为提供者。在接下来的小节中,我重新编写了示例应用来提供DiscountService对象,而不需要使用组件层次结构作为分发机制。

准备服务

通过依赖注入管理和分发的任何对象都被称为服务,这就是为什么我选择了名称DiscountService作为定义共享对象的类,以及为什么这个类被定义在一个名为discount.service.ts的文件中。Angular 使用@Injectable装饰器表示服务类,如清单 19-8 所示。@Injectable装饰器没有定义任何配置属性。

import { Injectable } from "@angular/core";

@Injectable()
export class DiscountService {
    private discountValue: number = 10;

    public get discount(): number {
        return this.discountValue;
    }

    public set discount(newValue: number) {
        this.discountValue = newValue || 0;
    }

    public applyDiscount(price: number) {
        return Math.max(price - this.discountValue, 5);
    }
}

Listing 19-8.Preparing a Class as a Service in the discount.service.ts File in the src/app Folder

Tip

严格地说,只有当一个类有自己的构造函数参数需要解析时,才需要使用@Injectable装饰器,但是无论如何应用它都是一个好主意,因为它提供了一个信号,表明这个类打算用作服务。

准备相关组件

类使用其构造函数声明依赖关系。当 Angular 需要创建一个类的实例时——比如当它找到一个与组件定义的selector属性匹配的元素时——它的构造函数会被检查,每个参数的类型也会被检查。Angular 然后使用已经定义的服务来尝试满足依赖性。术语依赖注入的出现是因为每个依赖都被注入到构造函数中以创建新的实例。

对于示例应用,这意味着依赖于DiscountService对象的组件不再需要输入属性,而是可以声明一个构造函数依赖。清单 19-9 显示了DiscountDisplayComponent类的变化。

import { Component, Input } from "@angular/core";
import { DiscountService } from "./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 19-9.Declaring a Dependency in the discountDisplay.component.ts File in the src/app Folder

同样的变化可以应用到DiscountEditorComponent类,用通过构造函数声明的依赖项替换输入属性,如清单 19-10 所示。

import { Component, Input } from "@angular/core";
import { DiscountService } from "./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 19-10.Declaring a Dependency in the discountEditor.component.ts File in the src/app Folder

这些都是很小的变化,但是它们避免了使用模板和输入属性来分发对象的需要,并且产生了更灵活的应用。我现在可以从产品表组件中移除DiscountService对象,如清单 19-11 所示。

import { Component, Input, ViewChildren, QueryList } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
import { DiscountService } from "./discount.service";

@Component({
    selector: "paProductTable",
    templateUrl: "productTable.component.html"
})
export class ProductTableComponent {
    //discounter: DiscountService = new DiscountService();

    @Input("model")
    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 19-11.Removing the Shared Object in the productTable.component.ts File in the src/app Folder

由于父组件不再通过数据绑定提供共享对象,我可以将它们从模板中移除,如清单 19-12 所示。

<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>{{item.price | 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 19-12.Removing the Data Bindings in the productTable.component.html File in the src/app Folder

注册服务

最后一个变化是配置依赖注入特性,这样它就可以向需要它们的组件提供DiscountService对象。为了使服务在整个应用中可用,它被注册在 Angular 模块中,如清单 19-13 所示。

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";

registerLocaleData(localeFr);

@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent],
  providers: [DiscountService],
  bootstrap: [ProductComponent]
})
export class AppModule { }

Listing 19-13.Registering a Service in the app.module.ts File in the src/app Folder

NgModule decorator 的providers属性被设置为将被用作服务的类的数组。目前只有一种服务,由DiscountService级提供。

当您保存对应用的更改时,不会有任何可见的更改,但是依赖注入特性将被用来为组件提供它们需要的DiscountService对象。

查看依赖注入更改

Angular 无缝地将依赖注入集成到它的特性集中。每当 Angular 遇到一个需要新构建块的元素,比如一个组件或一个管道,它就检查类构造函数来检查已经声明了哪些依赖项,并使用它的服务来尝试解决它们。用于解决依赖关系的服务集包括由应用定义的定制服务,例如在清单 19-13 中注册的DiscountService服务,以及一组由 Angular 提供的内置服务,将在后面的章节中描述。

上一节中引入依赖注入的更改并没有导致应用工作方式的巨大变化,或者说根本没有任何可见的变化。但是应用的组装方式有很大的不同,这使得它更加灵活和流畅。最好的演示是将需要DiscountService的组件添加到应用的不同部分,如清单 19-14 所示。

<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>

<paDiscountEditor></paDiscountEditor>
<paDiscountDisplay></paDiscountDisplay>

Listing 19-14.Adding Components in the productForm.component.html File in the src/app Folder

这些新元素复制了折扣显示和编辑器组件,因此它们出现在用于创建新产品的表单下方,如图 19-3 所示。

img/421542_4_En_19_Fig3_HTML.jpg

图 19-3。

复制具有从属关系的元件

有两点需要注意。首先,使用依赖注入使得向模板添加元素的过程变得简单,不需要修改祖先组件来使用输入属性提供一个DiscountService对象。

第二点需要注意的是,应用中所有声明依赖于DiscountService的组件都接收到了同一个对象。如果您编辑任一input元素中的值,更改将反映在另一个input元素和字符串插值绑定中,如图 19-4 所示。

img/421542_4_En_19_Fig4_HTML.jpg

图 19-4。

检查是否使用共享对象解决了依赖关系

在其他构建块中声明依赖项

不仅仅是组件可以声明构造函数依赖关系。一旦您定义了一个服务,您就可以更广泛地使用它,包括在应用的其他构建块中,比如管道和指令,如下面几节所演示的。

在管道中声明依赖关系

管道可以通过为每个必需的服务定义一个带有参数的构造函数来声明对服务的依赖。为了演示,我在src/app文件夹中添加了一个名为discount.pipe.ts的文件,并用它来定义清单 19-15 中所示的管道。

import { Pipe, Injectable } from "@angular/core";
import { DiscountService } from "./discount.service";

@Pipe({
    name: "discount",
    pure: false
})
export class PaDiscountPipe {

    constructor(private discount: DiscountService) { }

    transform(price: number): number {
        return this.discount.applyDiscount(price);
    }
}

Listing 19-15.The Contents of the discount.pipe.ts File in the src/app Folder

PaDiscountPipe类是一个接收price并通过调用DiscountService.applyDiscount方法生成结果的管道,其中服务通过构造函数接收。@Pipe装饰器中的pure属性是false,这意味着当DiscountService存储的值改变时,管道将被要求更新其结果,这不会被 Angular 变化检测过程识别。

Tip

正如第十八章中所解释的,这个特性应该小心使用,因为它意味着transform方法将在应用的每次更改后被调用,而不仅仅是在服务更改时。

清单 19-16 显示了在应用的 Angular 模块中注册的新管道。

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";

registerLocaleData(localeFr);

@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
    PaDiscountPipe],
  providers: [DiscountService],
  bootstrap: [ProductComponent]
})
export class AppModule { }

Listing 19-16.Registering a Pipe in the app.module.ts File in the src/app Folder

清单 19-17 显示了应用于产品表中Price列的新管道。

<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>{{item.price | discount | 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 19-17.Applying a Pipe in the productTable.component.html File in the src/app Folder

discount管道处理价格以应用折扣,然后将值传递给currency管道进行格式化。您可以通过更改折扣input元素之一的值来查看在管道中使用服务的效果,如图 19-5 所示。

img/421542_4_En_19_Fig5_HTML.jpg

图 19-5。

在管道中使用服务

在指令中声明依赖关系

指令也可以使用服务。正如我在第十七章中解释的,组件只是带有模板的指令,所以在组件中工作的任何东西也将在指令中工作。

为了演示如何在指令中使用服务,我在src/app文件夹中添加了一个名为discountAmount.directive.ts的文件,并用它来定义清单 19-18 中所示的指令。

import { Directive, HostBinding, Input,
    SimpleChange, KeyValueDiffer, KeyValueDiffers,
    ChangeDetectorRef } from "@angular/core";
import { DiscountService } from "./discount.service";

@Directive({
    selector: "td[pa-price]",
    exportAs: "discount"
})
export class PaDiscountAmountDirective {
    private differ: KeyValueDiffer<any, any>;

    constructor(private keyValueDiffers: KeyValueDiffers,
        private changeDetector: ChangeDetectorRef,
        private discount: DiscountService) { }

    @Input("pa-price")
    originalPrice: number;

    discountAmount: number;

    ngOnInit() {
        this.differ =
            this.keyValueDiffers.find(this.discount).create();
    }

    ngOnChanges(changes: { [property: string]: SimpleChange }) {
        if (changes["originalPrice"] != null) {
            this.updateValue();
        }
    }

    ngDoCheck() {
        if (this.differ.diff(this.discount) != null) {
            this.updateValue();
        }
    }

    private updateValue() {
        this.discountAmount = this.originalPrice
            - this.discount.applyDiscount(this.originalPrice);
    }
}

Listing 19-18.The Contents of the discountAmount.directive.ts File in the src/app Folder

指令没有与pipes使用的pure属性等价的属性,必须直接负责响应通过服务传播的变化。本指令显示产品的折扣金额。selector属性匹配具有pa-price属性的td元素,该属性也被用作输入属性来接收将要打折的价格。该指令使用exportAs属性导出其功能,并提供一个名为discountAmount的属性,其值设置为应用于产品的折扣。

关于这个指令还有另外两点需要注意。首先,DiscountService对象不是指令类中唯一的构造函数参数。

...
constructor(private keyValueDiffers: KeyValueDiffers,
            private changeDetector: ChangeDetectorRef,
            private discount: DiscountService) { }
...

KeyValueDiffersChangeDetectorRef参数也是 Angular 在创建 directive 类的新实例时必须解决的依赖关系。这些是 Angular 提供的内置服务的例子,它们提供了通常需要的功能。

第二点要注意的是指令如何处理它接收到的服务。使用DiscountService服务的组件和管道不必担心跟踪更新,因为 Angular 自动评估数据绑定的表达式,并在折扣率变化时更新它们(对于组件),或者因为应用中的任何变化都会触发更新(对于不纯的管道)。该指令的数据绑定在price属性上,如果被更改,该属性将触发更改。但是也存在对由DiscountService类定义的discount属性的依赖。使用通过构造函数接收的服务来检测discount属性的变化,这些服务类似于在第十六章中描述的用于跟踪可迭代序列变化的服务,但是它们操作于键值对对象,例如Map对象,或者定义属性的常规对象,例如DiscountService。当 Angular 调用ngDoCheck方法时,该指令使用键-值对 different 来查看是否发生了变化。(也可以通过跟踪 directive 类中以前的更新来处理这个更改方向,但是我想提供一个使用键值差异特性的示例。)

该指令还实现了ngOnChanges方法,以便它能够响应输入属性值的变化。对于这两种类型的更新,都调用了updateValue方法,该方法计算折扣价并将其分配给discountAmount属性。

清单 19-19 在应用的 Angular 模块中注册新指令。

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";

registerLocaleData(localeFr);

@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
    PaDiscountPipe, PaDiscountAmountDirective],
  providers: [DiscountService],
  bootstrap: [ProductComponent]
})
export class AppModule { }

Listing 19-19.Registering a Directive in the app.module.ts File in the src/app Folder

为了应用新的指令,清单 19-20 向表中添加了一个新列,使用字符串插值绑定来访问指令提供的属性,并将其传递给currency管道。

<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 19-20.Creating a New Column in the productTable.component.html File in the src/app Folder

该指令本来可以在textContent属性上创建一个主机绑定来设置其主机元素的内容,但是这会阻止使用currency管道。相反,该指令被分配给discount模板变量,然后在字符串插值绑定中使用该变量来访问并格式化discountAmount值。图 19-6 显示了结果。在折扣编辑器input元素中对折扣金额的更改将反映在新的表格列中。

img/421542_4_En_19_Fig6_HTML.jpg

图 19-6。

在指令中使用服务

理解测试隔离问题

示例应用包含一个相关的问题,服务和依赖注入可以用来解决这个问题。考虑如何在根组件中创建Model类。

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();

    addProduct(p: Product) {
        this.model.saveProduct(p);
    }
}

根组件被定义为ProductComponent类,它通过创建一个Model类的新实例为其model属性设置一个值。这是可行的——并且是创建对象的完全合法的方式——但是它使得有效地执行单元测试变得更加困难。

当您能够隔离应用的一小部分并集中精力执行测试时,单元测试效果最好。但是当您创建一个ProductComponent类的实例时,您也隐式地创建了一个Model类的实例。如果您要对根组件的addProduct方法运行测试并发现一个问题,您将无法知道这个问题是在ProductComponent还是Model类中。

使用服务和依赖注入隔离组件

潜在的问题是,ProductComponent类与Model类紧密绑定,而后者又与SimpleDataSource类紧密绑定。依赖注入可以用来分离应用中的构件,这样每个类都可以独立地被隔离和测试。在接下来的几节中,我将介绍分解这些紧密耦合的类的过程,基本上遵循与上一节相同的过程,但是会更深入地研究示例应用。

准备服务

@Injectable装饰器用来表示服务,就像前面的例子一样。清单 19-21 显示了应用于SimpleDataSource类的装饰器。

import { Injectable } from "@angular/core";
import { Product } from "./product.model";

@Injectable()
export class SimpleDataSource {
    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 19-21.Denoting a Service in the datasource.model.ts File in the src/app Folder

不需要其他更改。清单 19-22 显示了同样的装饰器被应用到数据仓库,由于这个类依赖于SimpleDataSource类,它声明它是一个构造函数依赖,而不是直接创建一个实例。

import { Injectable } from "@angular/core";
import { Product } from "./product.model";
import { SimpleDataSource } from "./datasource.model";

@Injectable()
export class Model {
    //private dataSource: SimpleDataSource;
    private products: Product[];
    private locator = (p:Product, id:number) => p.id == id;

    constructor(private dataSource: SimpleDataSource) {
        //this.dataSource = new SimpleDataSource();
        this.products = new Array<Product>();
        this.dataSource.getData().forEach(p => this.products.push(p));
    }

    // ...other members omitted for brevity...
}

Listing 19-22.Denoting a Service and Dependency in the repository.model.ts File in the src/app Folder

清单中需要注意的重要一点是,服务可以声明对其他服务的依赖。当 Angular 创建一个服务类的新实例时,它会检查构造函数,并尝试以处理组件或指令时相同的方式解析服务。

注册服务

这些服务必须被注册,以便 Angular 知道如何解析对它们的依赖,如清单 19-23 所示。

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";

registerLocaleData(localeFr);

@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
    PaDiscountPipe, PaDiscountAmountDirective],
    providers: [DiscountService, SimpleDataSource, Model],
  bootstrap: [ProductComponent]
})
export class AppModule { }

Listing 19-23.Registering the Services in the app.module.ts File in the src/app Folder

准备从属组件

不是直接创建一个Model对象,根组件可以声明一个构造函数依赖,Angular 将在应用启动时使用依赖注入来解析它,如清单 19-24 所示。

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: "template.html"
})
export class ProductComponent {
    //model: Model = new Model();

    constructor(public model: Model) { }

    addProduct(p: Product) {
        this.model.saveProduct(p);
    }
}

Listing 19-24.Declaring a Service Dependency in the component.ts File in the src/app Folder

现在 Angular 需要解决一系列的依赖关系。当应用启动时,Angular 模块指定ProductComponent类需要一个Model对象。Angular 检查了Model类,发现它需要一个SimpleDataSource对象。Angular 检查了SimpleDataSource对象,发现没有已声明的依赖关系,因此知道这是链的结尾。它创建一个SimpleDataSource对象,并将其作为参数传递给Model构造函数,以创建一个Model对象,然后可以将其传递给ProductComponent类构造函数,以创建将用作根组件的对象。所有这些都是自动发生的,基于每个类定义的构造函数和@Injectable装饰器的使用。

这些改变不会在应用的工作方式上产生任何可见的变化,但是它们允许一种完全不同的方式来执行单元测试。ProductComponent类要求提供一个Model对象作为构造函数参数,这允许使用模拟对象。

打破应用中类之间的直接依赖关系意味着它们中的每一个都可以出于单元测试的目的而被隔离,并通过它们的构造函数被提供给模拟对象,从而允许方法或一些其他特性的效果被一致且独立地评估。

完成服务的采用

一旦您开始在应用中使用服务,这个过程通常就有了自己的生命,并且您开始检查您创建的构建块之间的关系。你引入服务的程度是——至少部分是——个人偏好的问题。

一个很好的例子是在根组件中使用Model类。尽管该组件实现了一个使用Model对象的方法,但它这样做是因为它需要处理来自它的一个子组件的定制事件。根组件需要一个Model对象的另一个原因是使用输入属性通过它的模板将其传递给另一个子组件。

这种情况并不是一个大问题,您可能更喜欢在一个项目中拥有这些类型的关系。毕竟,对于单元测试来说,每一个组件都可以被隔离,而且它们之间的关系有一定的用途,尽管是有限的。组件之间的这种关系有助于理解应用提供的功能。

另一方面,使用服务越多,项目中的构建块就越能成为独立的和可重用的功能块,随着项目的成熟,这可以简化添加或更改功能的过程。

没有绝对的对错,你必须找到适合你、适合你的团队、最终适合你的用户和客户的平衡点。不是每个人都喜欢使用依赖注入,也不是每个人都执行单元测试。

我倾向于尽可能广泛地使用依赖注入。我发现当我开始一个新项目时,我的应用的最终结构可能与我期望的有很大不同,并且依赖注入提供的灵活性帮助我避免了重复的重构周期。所以,为了完成这一章,我将把Model服务的使用推进到应用的其余部分,打破根组件和它的直接子组件之间的耦合。

更新根组件和模板

我要做的第一个更改是从根组件中移除Model对象,以及使用它的方法和模板中的输入属性,该模板将模型分发给其中一个子组件。清单 19-25 显示了组件类的变化。

import { Component } from "@angular/core";
//import { Model } from "./repository.model";
//import { Product } from "./product.model";
//import { ProductFormGroup } from "./form.model";

@Component({
    selector: "app",
    templateUrl: "template.html"
})
export class ProductComponent {
    //model: Model = new Model();

    //constructor(public model: Model) { }

    //addProduct(p: Product) {
    //    this.model.saveProduct(p);
    //}
}

Listing 19-25.Removing the Model Object from the component.ts File in the src/app Folder

修改后的根组件类没有定义任何功能,现在只在其模板中提供顶级应用内容。清单 19-26 显示了根模板中的相应变化,删除了定制事件绑定和输入属性。

<div class="row m-2">
  <div class="col-4 p-2">
    <paProductForm></paProductForm>
  </div>
  <div class="col-8 p-2">
    <paProductTable></paProductTable>
  </div>
</div>

Listing 19-26.Removing the Data Bindings in the template.html File in the src/app Folder

更新子组件

为创建新的Product对象提供表单的组件依赖于根组件来处理它的定制事件和更新模型。如果没有这种支持,组件现在必须声明一个Model依赖并自己执行更新,如清单 19-27 所示。

import { Component, Output, EventEmitter, ViewEncapsulation } from "@angular/core";
import { Product } from "./product.model";
import { Model } from "./repository.model";

@Component({
    selector: "paProductForm",
    templateUrl: "productForm.component.html"
})
export class ProductFormComponent {
    newProduct: Product = new Product();

    constructor(private model: Model) { }

    // @Output("paNewProduct")
    // newProductEvent = new EventEmitter<Product>();

    submitForm(form: any) {
        //this.newProductEvent.emit(this.newProduct);
        this.model.saveProduct(this.newProduct);
        this.newProduct = new Product();
        form.reset();
    }
}

Listing 19-27.Working with the Model in the productForm.component.ts File in the src/app Folder

管理产品对象表的组件使用一个输入属性从其父对象接收一个Model对象,但是现在必须通过声明一个构造函数依赖来直接获得它,如清单 19-28 所示。

import { Component, Input, ViewChildren, QueryList } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
import { DiscountService } from "./discount.service";

@Component({
    selector: "paProductTable",
    templateUrl: "productTable.component.html"
})
export class ProductTableComponent {
    //discounter: DiscountService = new DiscountService();

    constructor(private dataModel: Model) { }

    // @Input("model")
    // 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 19-28.Declaring a Model Dependency in the productTable.component.ts File in the src/app Folder

当保存所有更改并且浏览器重新加载 Angular 应用时,您将在浏览器窗口中看到相同的功能,但是功能的连接方式发生了很大的变化,每个组件通过依赖注入特性获得它需要的共享对象,而不是依赖其父组件来提供。

摘要

在这一章中,我解释了依赖注入可以用来解决的问题,并演示了定义和消费服务的过程。我描述了如何使用服务来增加应用结构的灵活性,以及依赖注入如何使隔离构建块成为可能,从而可以有效地对它们进行单元测试。在下一章中,我将描述 Angular 为服务提供的高级特性。

二十、使用服务供应器

在前一章中,我介绍了服务,并解释了如何使用依赖注入来分发它们。当使用依赖注入时,用于解析依赖的对象由服务提供者创建,通常被称为提供者。在这一章中,我将解释提供者是如何工作的,描述不同类型的提供者,并演示如何在应用的不同部分创建提供者来改变服务的行为方式。表 20-1 将提供者放在上下文中。

表 20-1。

将服务供应器放在上下文中

|

问题

|

回答

| | --- | --- | | 它们是什么? | 提供者是在 Angular 第一次需要解决依赖关系时创建服务对象的类。 | | 它们为什么有用? | 提供者允许创建服务对象来满足应用的需求。最简单的提供者只是创建一个指定类的实例,但是也有其他提供者可以用来定制服务对象的创建和配置方式。 | | 它们是如何使用的? | 提供者是在 Angular 模块装饰器的providers属性中定义的。它们也可以由组件和指令来定义,以便向它们的子节点提供服务,如“使用本地提供者”一节中所述。 | | 有什么陷阱或限制吗? | 很容易产生意想不到的行为,尤其是在与本地供应器合作时。如果遇到问题,请检查您创建的本地提供程序的范围,并确保您的依赖项和提供程序使用相同的令牌。 | | 有其他选择吗? | 许多应用将只需要第十九章中描述的基本依赖注入特性。只有当您无法使用基本功能构建应用,并且对依赖注入有很好的理解时,才应该使用本章中的功能。 |

Why You Should Consider Skipping this Chapter

依赖注入在开发人员中激起了强烈的反应,并使观点两极分化。如果你是依赖注入的新手,还没有形成自己的观点,那么你可能想跳过这一章,只使用我在第十九章中描述的特性。这是因为像我在本章中描述的那些特性正是许多开发人员害怕使用依赖注入并强烈反对使用它的原因。

基本的 Angular 依赖注入特性很容易理解,并且在使应用更容易编写和维护方面有直接和明显的好处。本章中描述的特性提供了对依赖注入工作方式的细粒度控制,但是它们也可能急剧增加 Angular 应用的复杂性,并最终破坏基本特性提供的许多好处。

如果你决定要知道所有的细节,那就继续读下去。但是如果你是依赖注入领域的新手,你可能更愿意跳过这一章,直到你发现第十九章的基本特性没有提供你需要的功能。

表 20-2 总结了本章内容。

表 20-2。

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 改变服务的创建方式 | 使用服务供应器 | 1–3 | | 使用类指定服务 | 使用类提供程序 | 4–6, 10–13 | | 为服务定义任意令牌 | 使用InjectionToken类 | 7–9 | | 使用对象指定服务 | 使用值提供者 | 14–15 | | 使用函数指定服务 | 使用工厂提供者 | 16–18 | | 使用一个服务指定另一个服务 | 使用现有的服务供应器 | Nineteen | | 更改服务的范围 | 使用本地服务供应器 | 20–28 | | 控制依赖关系的解析 | 使用@Host@Optional@SkipSelf装饰器 | 29–30 |

准备示例项目

就像这本书这一部分的其他章节一样,我将继续处理在第十一章创建的项目,以及最近在第十九章修改的项目。为了准备本章,我在src/app文件夹中添加了一个名为log.service.ts的文件,并用它来定义清单 20-1 中所示的服务。

Tip

你可以从 https://github.com/Apress/pro-angular-9 下载本章以及本书其他章节的示例项目。如果在运行示例时遇到问题,请参见第一章获取帮助。

import { Injectable } from "@angular/core";

export enum LogLevel {
    DEBUG, INFO, ERROR
}

@Injectable()
export class LogService {
    minimumLevel: LogLevel = LogLevel.INFO;

    logInfoMessage(message: string) {
        this.logMessage(LogLevel.INFO, message);
    }

    logDebugMessage(message: string) {
        this.logMessage(LogLevel.DEBUG, message);
    }

    logErrorMessage(message: string) {
        this.logMessage(LogLevel.ERROR, message);
    }

    logMessage(level: LogLevel, message: string) {
        if (level >= this.minimumLevel) {
            console.log(`Message (${LogLevel[level]}): ${message}`);
        }
    }
}

Listing 20-1.The Contents of the log.service.ts File in the src/app Folder

该服务将不同严重性级别的日志消息写入浏览器的 JavaScript 控制台。我将在本章的后面注册并使用这个服务。

创建服务并保存更改后,在example文件夹中运行以下命令,启动 Angular 开发工具:

ng serve

打开一个新的浏览器窗口,导航到http://localhost:4200查看应用,如图 20-1 所示。

img/421542_4_En_20_Fig1_HTML.jpg

图 20-1。

运行示例应用

使用服务供应器

正如我在前面的章节中解释的,类使用它们的构造函数参数来声明对服务的依赖。当 Angular 需要创建该类的新实例时,它会检查构造函数,并使用内置和自定义服务的组合来解析每个参数。清单 20-2 更新了DiscountService类,使其依赖于前一节中创建的LogService类。

import { Injectable } from "@angular/core";
import { LogService } from "./log.service";

@Injectable()
export class DiscountService {
    private discountValue: number = 10;

    constructor(private logger: LogService) { }

    public get discount(): number {
        return this.discountValue;
    }

    public set discount(newValue: number) {
        this.discountValue = newValue || 0;
    }

    public applyDiscount(price: number) {
        this.logger.logInfoMessage(`Discount ${this.discount}`
            + ` applied to price: ${price}`);
        return Math.max(price - this.discountValue, 5);
    }
}

Listing 20-2.Creating a Dependency in the discount.service.ts File in the src/app Folder

清单 20-2 中的变化阻止了应用的运行。Angular 处理 HTML 文档并开始创建组件的层次结构,每个组件都有需要指令和数据绑定的模板,它遇到依赖于DiscountService类的类。但是它不能创建DiscountService的实例,因为它的构造函数需要一个LogService对象,而且它不知道如何处理这个类。

当您保存清单 20-2 中的更改时,您将在浏览器的 JavaScript 控制台中看到类似这样的错误:

NullInjectorError: No provider for LogService!

Angular 将创建依赖注入所需对象的责任委托给提供者,每个提供者管理一种类型的依赖。当它需要创建一个DiscountService类的实例时,它会寻找一个合适的提供者来解析LogService依赖关系。由于没有这样的提供者,Angular 无法创建启动应用所需的对象并报告错误。

创建提供者最简单的方法是将服务类添加到分配给 Angular 模块的providers属性的数组中,如清单 20-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 } from "./log.service";

registerLocaleData(localeFr);

@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
    PaDiscountPipe, PaDiscountAmountDirective],
    providers: [DiscountService, SimpleDataSource, Model, LogService],
  bootstrap: [ProductComponent]
})
export class AppModule { }

Listing 20-3.Creating a Provider in the app.module.ts File in the src/app Folder

当您保存更改时,您将已经定义了 Angular 处理LogService依赖项所需的提供者,并且您将在浏览器的 JavaScript 控制台中看到如下所示的消息:

Message (INFO): Discount 10 applied to price: 16

您可能想知道为什么清单 20-3 中的配置步骤是必需的。毕竟,Angular 可以假设它应该在第一次需要时创建一个新的LogService对象。

事实上,Angular 提供了一系列不同的提供者,每个提供者都以不同的方式创建对象,让您控制服务创建过程。表 20-3 描述了一组可用的提供者,这些提供者将在下面的章节中描述。

表 20-3。

Angular 提供者

|

名字

|

描述

| | --- | --- | | 类别提供者 | 此提供程序是使用类配置的。对服务的依赖由 Angular 创建的类的实例来解决。 | | 价值提供者 | 此提供程序是使用对象配置的,该对象用于解析对服务的依赖关系。 | | 工厂供应商 | 此提供程序是使用函数配置的。使用通过调用函数创建的对象来解析对服务的依赖。 | | 现有服务供应器 | 此提供程序是使用另一个服务的名称配置的,并允许为服务创建别名。 |

使用类提供程序

这个提供者是最常用的,也是我通过在清单 20-3 中向模块的providers属性添加类名而应用的。这个清单展示了速记语法,还有一个文字语法可以达到同样的结果,如清单 20-4 所示。

...
@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
    PaDiscountPipe, PaDiscountAmountDirective],
    providers: [DiscountService, SimpleDataSource, Model,
      { provide: LogService, useClass: LogService }],
  bootstrap: [ProductComponent]
})
...

Listing 20-4.Using the Class Provider Literal Syntax in the app.module.ts File in the src/app Folder

提供者被定义为类,但是可以使用 JavaScript 对象文字格式来指定和配置它们,如下所示:

...
{ provide: LogService, useClass: LogService }
...

类提供者支持三个属性,这些属性在表 20-4 中描述,并在下面的章节中解释。

表 20-4。

类提供程序的属性

|

名字

|

描述

| | --- | --- | | provide | 此属性用于指定标记,该标记用于标识将被解析的提供程序和依赖项。请参见“理解令牌”一节。 | | useClass | 此属性用于指定将由提供程序实例化以解析依赖关系的类。请参见“了解 useClass 属性”一节。 | | multi | 该属性可用于提供一组服务对象来解析依赖关系。请参阅“解析多个对象的依赖关系”一节。 |

了解令牌

所有提供者都依赖于一个令牌,Angular 使用这个令牌来标识提供者可以解析的依赖关系。最简单的方法是使用一个类作为令牌,这就是我在清单 20-4 中所做的。但是,您可以使用任何对象作为令牌,这允许将依赖项和对象的类型分开。这有助于增加依赖注入配置的灵活性,因为它允许提供程序提供不同类型的对象,这对于本章后面介绍的一些更高级的提供程序很有用。举个简单的例子,清单 20-5 使用类提供者来注册在本章开始时创建的日志服务,使用一个字符串作为令牌,而不是一个类。

...
@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
    PaDiscountPipe, PaDiscountAmountDirective],
    providers: [DiscountService, SimpleDataSource, Model,
      { provide: "logger", useClass: LogService }],
  bootstrap: [ProductComponent]
})
...

Listing 20-5.Registering a Service with a Token in the app.module.ts File in the src/app Folder

在清单中,新提供者的provide属性被设置为logger。Angular 将自动匹配其令牌是一个类的提供者,但是它需要一些其他令牌类型的额外帮助。清单 20-6 显示了更新后的DiscountService类对日志服务的依赖,使用logger令牌访问。

import { Injectable, Inject } from "@angular/core";
import { LogService } from "./log.service";

@Injectable()
export class DiscountService {
    private discountValue: number = 10;

    constructor(@Inject("logger") private logger: LogService) { }

    public get discount(): number {
        return this.discountValue;
    }

    public set discount(newValue: number) {
        this.discountValue = newValue || 0;
    }

    public applyDiscount(price: number) {
        this.logger.logInfoMessage(`Discount ${this.discount}`
            + ` applied to price: ${price}`);
        return Math.max(price - this.discountValue, 5);
    }
}

Listing 20-6.Using a String Provider Token in the discount.service.ts File in the src/app Folder

@Inject decorator 应用于构造函数参数,用于指定应该用来解析依赖关系的令牌。当 Angular 需要创建一个DiscountService类的实例时,它将检查构造函数并使用@Inject装饰器参数来选择将用于解析依赖关系的提供者,解析对LogService类的依赖关系。

使用不透明令牌

当使用简单类型作为提供者标记时,应用的两个不同部分可能会尝试使用同一个标记来标识不同的服务,这意味着可能会使用错误的对象类型来解析依赖关系并导致错误。

为了帮助解决这个问题,Angular 提供了InjectionToken类,该类提供了一个围绕string值的对象包装器,可以用来创建唯一的令牌值。在清单 20-7 中,我使用了InjectionToken类来创建一个令牌,该令牌将用于标识对LogService类的依赖。

import { Injectable, InjectionToken } from "@angular/core";

export const LOG_SERVICE = new InjectionToken("logger");

export enum LogLevel {
    DEBUG, INFO, ERROR
}

@Injectable()
export class LogService {
    minimumLevel: LogLevel = LogLevel.INFO;

    // ...methods omitted for brevity...
}

Listing 20-7.Using the InjectionToken Class in the log.service.ts File in the src/app Folder

InjectionToken类的构造函数接受一个描述服务的string值,但是将成为令牌的是InjectionToken对象。依赖项必须在用于在模块中创建提供程序的同一个InjectionToken上声明;这就是使用const关键字创建令牌的原因,它可以防止对象被修改。清单 20-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 } from "./log.service";

registerLocaleData(localeFr);

@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
    PaDiscountPipe, PaDiscountAmountDirective],
    providers: [DiscountService, SimpleDataSource, Model,
      { provide: LOG_SERVICE, useClass: LogService }],
  bootstrap: [ProductComponent]
})
export class AppModule { }

Listing 20-8.Creating a Provider Using an InjectionToken in the app.module.ts File in the src/app Folder

最后,清单 20-9 展示了更新后的DiscountService类,使用InjectionToken而不是string来声明一个依赖项。

import { Injectable, Inject } from "@angular/core";
import { LogService, LOG_SERVICE } from "./log.service";

@Injectable()
export class DiscountService {
    private discountValue: number = 10;

    constructor( @Inject(LOG_SERVICE) private logger: LogService) { }

    public get discount(): number {
        return this.discountValue;
    }

    public set discount(newValue: number) {
        this.discountValue = newValue || 0;
    }

    public applyDiscount(price: number) {
        this.logger.logInfoMessage(`Discount ${this.discount}`
            + ` applied to price: ${price}`);
        return Math.max(price - this.discountValue, 5);
    }
}

Listing 20-9.Declaring a Dependency in the discount.service.ts File in the src/app Folder

应用提供的功能没有区别,但是使用InjectionToken意味着服务之间不会混淆。

了解使用类别属性

类提供者的useClass属性指定了将被实例化以解析依赖关系的类。提供者可以配置任何类,这意味着您可以通过更改提供者配置来更改服务的实现。应谨慎使用此功能,因为服务对象的接收方需要特定的类型,并且在应用在浏览器中运行之前,不匹配不会导致错误。(TypeScript 类型强制对依赖项注入没有任何影响,因为它发生在运行时类型批注被 TypeScript 编译器处理之后。)

改变类最常见的方法是使用不同的子类。在清单 20-10 中,我扩展了LogService类来创建一个服务,该服务在浏览器的 JavaScript 控制台中编写不同格式的消息。

import { Injectable, InjectionToken } from "@angular/core";

export const LOG_SERVICE = new InjectionToken("logger");

export enum LogLevel {
    DEBUG, INFO, ERROR
}

@Injectable()
export class LogService {
    minimumLevel: LogLevel = LogLevel.INFO;

    logInfoMessage(message: string) {
        this.logMessage(LogLevel.INFO, message);
    }

    logDebugMessage(message: string) {
        this.logMessage(LogLevel.DEBUG, message);
    }

    logErrorMessage(message: string) {
        this.logMessage(LogLevel.ERROR, message);
    }

    logMessage(level: LogLevel, message: string) {
        if (level >= this.minimumLevel) {
            console.log(`Message (${LogLevel[level]}): ${message}`);
        }
    }
}

@Injectable()
export class SpecialLogService extends LogService {

    constructor() {
        super()
        this.minimumLevel = LogLevel.DEBUG;
    }

    logMessage(level: LogLevel, message: string) {
        if (level >= this.minimumLevel) {
            console.log(`Special Message (${LogLevel[level]}): ${message}`);
        }
    }
}

Listing 20-10.Creating a Subclassed Service in the log.service.ts File in the src/app Folder

SpecialLogService类扩展了LogService并提供了自己的logMessage方法的实现。清单 20-11 更新了提供者配置,因此useClass属性指定了新服务。

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 } from "./log.service";

registerLocaleData(localeFr);

@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
    PaDiscountPipe, PaDiscountAmountDirective],
    providers: [DiscountService, SimpleDataSource, Model,
      { provide: LOG_SERVICE, useClass: SpecialLogService }],
  bootstrap: [ProductComponent]
})
export class AppModule { }

Listing 20-11.Configuring the Provider in the app.module.ts File in the src/app Folder

令牌和类的组合意味着对LOG_SERVICE不透明令牌的依赖将使用SpecialLogService对象来解析。保存更改时,您将在浏览器的 JavaScript 控制台中看到类似这样的消息,表明派生服务已被使用:

Special Message (INFO): Discount 10 applied to price: 275

当设置useClass属性来指定依赖类期望的类型时,必须小心。指定子类是最安全的选择,因为基类的功能保证可用。

解析具有多个对象的依赖关系

可以将类提供程序配置为提供一组对象来解决依赖关系,如果您希望提供一组配置方式不同的相关服务,这将非常有用。为了提供一个数组,使用同一个令牌配置多个类提供者,并将multi属性设置为true,如清单 20-12 所示。

...
@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
    PaDiscountPipe, PaDiscountAmountDirective],
    providers: [DiscountService, SimpleDataSource, Model,
      { provide: LOG_SERVICE, useClass: LogService, multi: true },
      { provide: LOG_SERVICE, useClass: SpecialLogService, multi: true }],
  bootstrap: [ProductComponent]
})
...

Listing 20-12.Configuring Multiple Service Objects in the app.module.ts File in the src/app Folder

Angular 依赖注入系统将通过创建LogServiceSpecialLogService对象,将它们放在一个数组中,并将它们传递给依赖类的构造函数,来解析对LOG_SERVICE标记的依赖。接收服务的类必须期待一个数组,如清单 20-13 所示。

import { Injectable, Inject } from "@angular/core";
import { LogService, LOG_SERVICE, LogLevel } from "./log.service";

@Injectable()
export class DiscountService {
    private discountValue: number = 10;
    private logger: LogService;

    constructor( @Inject(LOG_SERVICE) loggers: LogService[]) {
        this.logger = loggers.find(l => l.minimumLevel == LogLevel.DEBUG);
    }

    public get discount(): number {
        return this.discountValue;
    }

    public set discount(newValue: number) {
        this.discountValue = newValue || 0;
    }

    public applyDiscount(price: number) {
        this.logger.logInfoMessage(`Discount ${this.discount}`
            + ` applied to price: ${price}`);
        return Math.max(price - this.discountValue, 5);
    }
}

Listing 20-13.Receiving Multiple Services in the discount.service.ts File in the src/app Folder

构造函数以数组的形式接收服务,它使用数组的find方法定位第一个minimumLevel属性为LogLevel.Debug的记录器,并将其分配给logger属性。applyDiscount方法调用服务的logDebugMessage方法,这导致类似这样的消息显示在浏览器的 JavaScript 控制台中:

Special Message (INFO): Discount 10 applied to price: 275

使用值提供者

当您想自己负责创建服务对象,而不是将它留给类提供者时,可以使用值提供者。当服务是简单类型时,例如stringnumber值,这也是有用的,这是提供对公共配置设置的访问的有用方式。值提供者可使用文字对象应用,并支持表 20-5 中描述的属性。

表 20-5。

值提供者属性

|

名字

|

描述

| | --- | --- | | provide | 该属性定义服务令牌,如本章前面的“理解令牌”一节所述。 | | useValue | 此属性指定将用于解析依赖关系的对象。 | | multi | 此属性用于允许组合多个提供程序来提供一个对象数组,这些对象将用于解析对令牌的依赖关系。有关示例,请参见本章前面的“解决多个对象的依赖关系”一节。 |

值提供程序的工作方式与类提供程序相同,只是它是用对象而不是类型配置的。清单 20-14 展示了如何使用值提供者来创建一个配置了特定属性值的LogService类的实例。

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 } from "./log.service";

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],
    providers: [DiscountService, SimpleDataSource, Model,
      { provide: LogService, useValue: logger }],
  bootstrap: [ProductComponent]
})
export class AppModule { }

Listing 20-14.Using the Value Provider in the app.module.ts File in the src/app Folder

这个值提供者被配置为解析在模块类之外创建和配置的特定对象对LogService标记的依赖性。

值提供者——实际上是所有的提供者——可以使用任何对象作为标记,如前一节所述,但我还是回到了使用类型作为标记,因为这是最常用的技术,而且它与 TypeScript 构造函数参数类型化配合得非常好。清单 20-15 显示了对DiscountService的相应更改,它使用类型化的构造函数参数声明了一个依赖项。

import { Injectable, Inject } from "@angular/core";
import { LogService, LOG_SERVICE, LogLevel } from "./log.service";

@Injectable()
export class DiscountService {
    private discountValue: number = 10;

    constructor(private logger: LogService) { }

    public get discount(): number {
        return this.discountValue;
    }

    public set discount(newValue: number) {
        this.discountValue = newValue || 0;
    }

    public applyDiscount(price: number) {
        this.logger.logInfoMessage(`Discount ${this.discount}`
            + ` applied to price: ${price}`);
        return Math.max(price - this.discountValue, 5);
    }
}

Listing 20-15.Declaring a Dependency Using a Type in the discount.service.ts File in the src/app Folder

使用工厂提供程序

工厂提供者使用函数来创建解析依赖关系所需的对象。该提供程序支持表 20-6 中描述的属性。

表 20-6。

工厂提供者属性

|

名字

|

描述

| | --- | --- | | provide | 该属性定义服务令牌,如本章前面的“理解令牌”一节所述。 | | deps | 该属性指定了一个提供者标记数组,该数组将被解析并传递给由useFactory属性指定的函数。 | | useFactory | 此属性指定将创建服务对象的函数。解析由deps属性指定的令牌所产生的对象将作为参数传递给函数。函数返回的结果将被用作服务对象。 | | multi | 此属性用于允许组合多个提供程序来提供一个对象数组,这些对象将用于解析对令牌的依赖关系。有关示例,请参见本章前面的“解决多个对象的依赖关系”一节。 |

这是在如何创建服务对象方面提供最大灵活性的提供者,因为您可以定义适合您的应用需求的函数。清单 20-16 显示了一个创建LogService对象的工厂函数。

...
@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
    PaDiscountPipe, PaDiscountAmountDirective],
    providers: [DiscountService, SimpleDataSource, Model,
      {
          provide: LogService, useFactory: () => {
              let logger = new LogService();
              logger.minimumLevel = LogLevel.DEBUG;
              return logger;
          }
      }],
  bootstrap: [ProductComponent]
})
...

Listing 20-16.Using the Factory Provider in the app.module.ts File in the src/app Folder

这个例子中的函数很简单:它不接收任何参数,只创建一个新的LogService对象。当使用deps属性时,这个提供者真正的灵活性就来了,它允许在其他服务上创建依赖关系。在清单 20-17 中,我定义了一个指定调试级别的令牌。

import { Injectable, InjectionToken } from "@angular/core";

export const LOG_SERVICE = new InjectionToken("logger");
export const LOG_LEVEL = new InjectionToken("log_level");

export enum LogLevel {
    DEBUG, INFO, ERROR
}

@Injectable()
export class LogService {
    minimumLevel: LogLevel = LogLevel.INFO;

    // ...methods omitted for brevity...
}

@Injectable()
export class SpecialLogService extends LogService {

    // ...methods omitted for brevity...
}

Listing 20-17.Defining a Logging-Level Service in the log.service.ts File in the src/app Folder

在清单 20-18 中,我定义了一个使用LOG_LEVEL令牌创建服务的值提供者,并在创建LogService对象的工厂函数中使用该服务。

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";

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],
    providers: [DiscountService, SimpleDataSource, Model,
      { provide: LOG_LEVEL, useValue: LogLevel.DEBUG },
      { provide: LogService,
        deps: [LOG_LEVEL],
        useFactory: (level) => {
          let logger = new LogService();
          logger.minimumLevel = level;
          return logger;
       }}],
  bootstrap: [ProductComponent]
})
export class AppModule { }

Listing 20-18.Using Factory Dependencies in the app.module.ts File in the src/app Folder

值提供者使用LOG_LEVEL标记将简单值定义为服务。工厂提供者在其deps数组中指定该令牌,依赖注入系统解析该令牌并将其作为参数提供给工厂函数,工厂函数使用它来设置新LogService对象的minimumLevel属性。

使用现有的服务供应器

该提供程序用于为服务创建别名,以便可以使用多个令牌将它们作为目标,使用表 20-7 中描述的属性。

表 20-7。

现有的提供程序属性

|

名字

|

描述

| | --- | --- | | provide | 该属性定义服务令牌,如本章前面的“理解令牌”一节所述。 | | useExisting | 此属性用于指定另一个提供程序的令牌,该提供程序的服务对象将用于解析对此服务的依赖关系。 | | multi | 此属性用于允许组合多个提供程序来提供一个对象数组,这些对象将用于解析对令牌的依赖关系。有关示例,请参见本章前面的“解决多个对象的依赖关系”一节。 |

当您想要重构一组提供程序,但不想消除所有过时的标记以避免重构应用的其余部分时,此提供程序会很有用。清单 20-19 展示了这个提供者的用法。

...
@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
    PaDiscountPipe, PaDiscountAmountDirective],
    providers: [DiscountService, SimpleDataSource, Model,
      { provide: LOG_LEVEL, useValue: LogLevel.DEBUG },
      { provide: "debugLevel", useExisting: LOG_LEVEL },
      { provide: LogService,
        deps: ["debugLevel"],
        useFactory: (level) => {
          let logger = new LogService();
          logger.minimumLevel = level;
          return logger;
       }}],
  bootstrap: [ProductComponent]
})
...

Listing 20-19.Creating a Service Alias in the app.module.ts File in the src/app Folder

新服务的令牌是字符串debugLevel,它用LOG_LEVEL令牌作为提供者的别名。使用任何一个标记都将导致依赖项被解析为相同的值。

使用本地供应器

当 Angular 创建一个类的新实例时,它使用一个注入器来解析任何依赖关系。它是一个注入器,负责检查类的构造函数,以确定已经声明了哪些依赖项,并使用可用的提供程序来解析它们。

到目前为止,所有的依赖注入示例都依赖于在应用的 Angular 模块中配置的提供者。但是 Angular 依赖注入系统更复杂:有一个对应于应用的组件和指令树的注入器层次结构。每个组件和指令都可以有自己的注入器,每个注入器都可以配置自己的一组提供者,称为本地提供者

当存在要解决的从属关系时,Angular 将注射器用于最近的组件或指令。注入器首先尝试使用自己的一组本地提供者来解决依赖关系。如果没有设置本地提供程序,或者没有可用于解析此特定依赖关系的提供程序,则注入器会咨询父组件的注入器。重复这个过程——父组件的注入器试图使用它自己的一组本地提供者来解决依赖关系。如果有合适的提供者可用,则使用它来提供解决依赖性所需的服务对象。如果没有合适的提供者,那么请求将被传递到层次结构中的下一级,传递给原始注入者的祖父级。层次结构的顶部是根 Angular 模块,其提供者是报告错误之前的最后手段。

在 Angular 模块中定义提供者意味着应用中某个令牌的所有依赖项都将使用同一个对象来解析。正如我在下面几节中解释的那样,在注入器层次结构的更底层注册提供者可以改变这种行为,并改变创建和使用服务的方式。

了解单个服务对象的局限性

使用单个服务对象可能是一个强大的工具,允许应用不同部分中的构建块共享数据和响应用户交互。但有些服务并不适合如此广泛地共享。举个简单的例子,清单 20-20 向第十八章中创建的管道之一添加了对LogService的依赖。

import { Pipe, Injectable } from "@angular/core";
import { DiscountService } from "./discount.service";
import { LogService } from "./log.service";

@Pipe({
    name: "discount",
    pure: false
})
export class PaDiscountPipe {

    constructor(private discount: DiscountService,
                private logger: LogService) { }

    transform(price: number): number {
        if (price > 100) {
            this.logger.logInfoMessage(`Large price discounted: ${price}`);
        }
        return this.discount.applyDiscount(price);
    }
}

Listing 20-20.Adding a Service Dependency in the discount.pipe.ts File in the src/app Folder

管道的转换方法使用作为构造函数参数接收的LogService对象,当它转换的price值大于 100 时,生成日志消息。

问题是这些日志消息被由DiscountService对象生成的消息淹没了,每次应用折扣时它都会创建一条消息。显而易见的是,当模块提供者的工厂函数创建LogService对象时,要改变它的最低级别,如清单 20-21 所示。

...
@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ProductComponent, PaAttrDirective, PaModel,
    PaStructureDirective, PaIteratorDirective,
    PaCellColor, PaCellColorSwitcher, ProductTableComponent,
    ProductFormComponent, PaToggleView, PaAddTaxPipe,
    PaCategoryFilterPipe, PaDiscountDisplayComponent, PaDiscountEditorComponent,
    PaDiscountPipe, PaDiscountAmountDirective],
    providers: [DiscountService, SimpleDataSource, Model,
      { provide: LOG_LEVEL, useValue: LogLevel.ERROR },
      { provide: "debugLevel", useExisting: LOG_LEVEL },
      { provide: LogService,
        deps: ["debugLevel"],
        useFactory: (level) => {
          let logger = new LogService();
          logger.minimumLevel = level;
          return logger;
       }}],
  bootstrap: [ProductComponent]
})
...

Listing 20-21.Changing the Logging Level in the app.module.ts File in the src/app Folder

当然,这并没有达到预期的效果,因为在整个应用中使用了相同的LogService对象,过滤DiscountService消息意味着管道消息也被过滤。

我可以增强LogService类,这样每个日志消息源都有不同的过滤器,但是这很快就变得复杂了。相反,我将通过创建一个本地提供者来解决这个问题,以便在应用中有多个LogService对象,每个对象都可以单独配置。

在组件中创建本地提供程序

组件可以定义本地提供者,这允许应用的一部分创建和使用单独的服务器。组件支持两个装饰器属性来创建本地提供者,如表 20-8 所述。

表 20-8。

本地提供程序的组件装饰器属性

|

名字

|

描述

| | --- | --- | | providers | 此属性用于创建用于解析视图和内容子级的依赖关系的提供程序。 | | viewProviders | 此属性用于创建一个提供程序,该提供程序用于解析视图子级的依赖关系。 |

解决我的LogService问题的最简单方法是使用providers属性建立一个本地提供者,如清单 20-22 所示。

import { Component, Input, ViewChildren, QueryList } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
import { DiscountService } from "./discount.service";
import { LogService } from "./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 20-22.Creating a Local Provider in the productTable.component.ts File in the src/app Folder

当 Angular 需要创建一个新的管道对象时,它会检测对LogService的依赖,并开始沿着应用层次向上工作,检查它找到的每个组件,以确定它们是否有可用于解决依赖的提供者。ProductTableComponent确实有一个LogService提供者,用于创建解析管道依赖性的服务。这意味着现在应用中有两个LogService对象,每个都可以单独配置,如图 20-2 所示。

img/421542_4_En_20_Fig2_HTML.jpg

图 20-2。

创建本地提供程序

由组件提供者创建的LogService对象使用其minimumLevel属性的默认值,并将显示LogLevel.INFO消息。模块创建的LogService对象将用于解析应用中的所有其他依赖项,包括由DiscountService类声明的依赖项,该对象被配置为只显示LogLevel.ERROR消息。当您保存更改时,您将看到来自管道(从组件接收服务)的日志消息,而不是来自DiscountService(从模块接收服务)的日志消息。

了解供应商备选方案

如表 20-8 所述,有两个属性可用于创建本地提供者。为了演示这些属性的不同,我在src/app文件夹中添加了一个名为valueDisplay.directive.ts的文件,并用它来定义清单 20-23 中所示的指令。

import { Directive, InjectionToken, Inject, HostBinding} from "@angular/core";

export const VALUE_SERVICE = new InjectionToken("value_service");

@Directive({
    selector: "[paDisplayValue]"
})
export class PaDisplayValueDirective {

    constructor( @Inject(VALUE_SERVICE) serviceValue: string) {
        this.elementContent = serviceValue;
    }

    @HostBinding("textContent")
    elementContent: string;
}

Listing 20-23.The Contents of the valueDisplay.directive.ts File in the src/app Folder

VALUE_SERVICE opaque 令牌将用于定义基于值的服务,该清单中的指令声明了对该服务的依赖,以便可以在主机元素的内容中显示该服务。清单 20-24 显示了正在定义的服务和在 Angular 模块中注册的指令。为了简洁起见,我还简化了模块中的LogService提供者。

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 20-24.Registering the Directive and Service in the app.module.ts File in the src/app Folder

提供者为VALUE_SERVICE服务设置一个值Apples。下一步是应用新的指令,这样一个实例是组件的视图子级,另一个是内容子级。清单 20-25 设置内容子实例。

<div class="row m-2">
  <div class="col-4 p-2">
    <paProductForm>
      <span paDisplayValue></span>
    </paProductForm>
  </div>
  <div class="col-8 p-2">
    <paProductTable></paProductTable>
  </div>
</div>

Listing 20-25.Applying a Content Child Directive in the template.html File in the src/app Folder

清单 20-26 投射主机元素的内容,并添加新指令的视图子实例。

<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 20-26.Adding Directives in the productForm.component.html File in the src/app Folder

当您保存更改时,您将看到新的元素,如图 20-3 所示,两者显示相同的值,因为VALUE_SERVICE的唯一提供者是在模块中定义的。

img/421542_4_En_20_Fig3_HTML.jpg

图 20-3。

查看和内容子指令

为所有孩子创建本地提供程序

@Component decorator 的providers属性用于定义提供者,这些提供者将用于解析所有子元素的服务依赖关系,而不管它们是在模板中定义的(视图子元素)还是从主机元素中投影的(内容子元素)。清单 20-27 在两个新指令实例的父组件中定义了一个VALUE_SERVICE提供者。

import { Component, Output, EventEmitter, ViewEncapsulation } from "@angular/core";
import { Product } from "./product.model";
import { Model } from "./repository.model";
import { VALUE_SERVICE } from "./valueDisplay.directive";

@Component({
    selector: "paProductForm",
    templateUrl: "productForm.component.html",
    providers: [{ provide: VALUE_SERVICE, useValue: "Oranges" }]
})
export class ProductFormComponent {
    newProduct: Product = new Product();

    constructor(private model: Model) { }

    submitForm(form: any) {
        this.model.saveProduct(this.newProduct);
        this.newProduct = new Product();
        form.reset();
    }
}

Listing 20-27.Defining a Provider in the productForm.component.ts File in the src/app Folder

新的供应器改变了服务价值。当 Angular 开始创建新指令的实例时,它通过沿着应用层次向上搜索来开始搜索提供者,并找到清单 20-27 中定义的VALUE_SERVICE提供者。服务值被指令的两个实例使用,如图 20-4 所示。

img/421542_4_En_20_Fig4_HTML.jpg

图 20-4。

为组件中的所有子组件定义提供程序

为视图子级创建提供程序

viewProviders属性定义了用于解析视图子级而非内容子级的依赖关系的提供程序。清单 20-28 使用viewProviders属性为VALUE_SERVICE定义一个提供者。

import { Component, Output, EventEmitter, ViewEncapsulation } from "@angular/core";
import { Product } from "./product.model";
import { Model } from "./repository.model";
import { VALUE_SERVICE } from "./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) { }

    submitForm(form: any) {
        this.model.saveProduct(this.newProduct);
        this.newProduct = new Product();
        form.reset();
    }
}

Listing 20-28.Defining a View Child Provider in the productForm.component.ts File in the src/app Folder

Angular 在解析视图子级而不是内容子级的依赖关系时使用提供程序。这意味着子内容的依赖关系在应用的层次结构中向上引用,就好像组件没有定义提供者一样。在本例中,这意味着视图子节点将接收组件提供者创建的服务,内容子节点将接收模块提供者创建的服务,如图 20-5 所示。

img/421542_4_En_20_Fig5_HTML.jpg

图 20-5。

为视图子级定义提供程序

Caution

不支持使用providersviewProviders属性为同一服务定义提供者。如果这样做,视图和内容的孩子都将收到由viewProviders提供者创建的服务。

控制依赖关系解析

Angular 提供了三个装饰器,可以用来提供关于如何解决依赖关系的指令。这些装饰器在表 20-9 中描述,并在以下章节中演示。

表 20-9。

依赖关系解析装饰器

|

名字

|

描述

| | --- | --- | | @Host | 这个装饰器将对提供者的搜索限制在最近的组件上。 | | @Optional | 如果不能解决依赖关系,这个装饰器会阻止 Angular 报告错误。 | | @SkipSelf | 这个装饰器排除了依赖关系被解析的组件/指令所定义的提供者。 |

限制供应器搜索

@Host decorator 限制对合适的提供者的搜索,以便一旦到达最近的组件就停止搜索。装饰器通常与@Optional结合使用,这样可以防止 Angular 在无法解决服务依赖时抛出异常。清单 20-29 展示了在示例中向指令添加两个装饰器。

import { Directive, InjectionToken, Inject,
         HostBinding, Host, Optional} from "@angular/core";

export const VALUE_SERVICE = new InjectionToken("value_service");

@Directive({
    selector: "[paDisplayValue]"
})
export class PaDisplayValueDirective {

    constructor( @Inject(VALUE_SERVICE) @Host() @Optional() serviceValue: string) {
        this.elementContent = serviceValue || "No Value";
    }

    @HostBinding("textContent")
    elementContent: string;
}

Listing 20-29.Adding Dependency Decorators in the valueDisplay.directive.ts File in the src/app Folder

当使用@Optional decorator 时,您必须确保如果服务不能被解析,类能够运行,在这种情况下,服务的构造函数参数是undefined。最近的组件为它的视图子组件而不是内容子组件定义了一个服务,这意味着该指令的一个实例将接收一个服务对象,而另一个不会,如图 20-6 所示。

img/421542_4_En_20_Fig6_HTML.jpg

图 20-6。

控制如何解决依赖关系

跳过自定义提供程序

默认情况下,组件定义的提供程序用于解析其依赖关系。可以将@SkipSelf decorator 应用于构造函数参数,告诉 Angular 忽略本地提供者,并在应用层次结构的下一级开始搜索,这意味着本地提供者将仅用于解析子元素的依赖关系。在清单 20-30 中,我添加了对用@SkipSelf修饰的VALUE_SERVICE提供者的依赖。

import { Component, Output, EventEmitter, ViewEncapsulation,
    Inject, SkipSelf } from "@angular/core";
import { Product } from "./product.model";
import { Model } from "./repository.model";
import { VALUE_SERVICE } from "./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 20-30.Skipping Local Providers in the productForm.component.ts File in the src/app Folder

当您保存更改且浏览器重新加载页面时,您将在浏览器的 JavaScript 控制台中看到以下消息,显示本地定义的服务值(Oranges)已被跳过,并允许 Angular 模块解析相关性:

Service Value:pples

摘要

在这一章中,我解释了提供者在依赖注入中扮演的角色,并解释了如何使用它们来改变服务解决依赖的方式。我描述了可用于创建服务对象的不同类型的提供者,并演示了指令和组件如何定义它们自己的提供者来解析它们自己及其子对象的依赖关系。在下一章中,我将描述模块,它是 Angular 应用的最终构建模块。