Angular5-项目教程-三-

55 阅读42分钟

Angular5 项目教程(三)

原文:Angular 5 Projects

协议:CC BY-NC-SA 4.0

十二、更多组件

本章的目的是通过更高级的主题进一步增强您的组件知识。

组件和子组件

众所周知,组件是用户界面中的一个构建块。Angular 应用总是有一个应用(或根)组件。这个组件(像其他组件一样)在 HTML 中有一个标签,并且有 Angular 地引导到那个组件中。这个应用组件(像其他组件一样)可以包含其他(子)组件。

因此,组件可以包含其他组件。这就是所谓的构图。正如我在前面的章节中提到的,组件就像是 UI 的乐高积木。组合是用这些乐高积木组合成一个应用的艺术。我用一个例子介绍一下作文。

当您编写单页应用时,惯例是您有一个组件层次结构—一个组合。图 12-1 给出了一个例子。

A458962_1_En_12_Fig1_HTML.gif

图 12-1

Hierarchy of components

当你用一个组合编码时,你必须非常小心地把数据(称为状态)存储在正确的位置,这样它就不会重复(存储两次)。脸书大学的皮特·亨特在 ?? 写了一篇关于这个的精彩文章。这篇文章是关于 React 的,但是同样的规则也适用于 Angular。

数据向下流动

数据应该从较高级别的组件向下流到较低级别的组件。当您创建一个从外部接收数据的组件时,您必须使用@Input decorator 显式地告诉 Angular 期望该数据作为输入。您将@Input装饰器放在数据将从外部注入的实例变量旁边。

当您从外部将数据传递到组件中时,您使用输入属性将数据传递到组件中。

有时,您可能希望输入属性的名称不同于它将被注入到的实例变量的名称。这时您需要使用一个alias,它允许您指定输入属性名。在@Input装饰器中,alias可以在圆括号内指定。图 12-2 给出了一个例子。

A458962_1_En_12_Fig2_HTML.jpg

图 12-2

Passing data to car components

该组件将数据从应用传递到汽车组件。这将是更多组件示例-ex100:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑 app 类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { ICar } from './icar';
    
    @Component({
      selector: 'app-root',
      template: `
      <car *ngFor="let car of cars" [theCar]="car"></car>
      `,
      styles: []
    })
    export class AppComponent {
      cars:Array<ICar> = [
        {make: 'bmw', model: 'm3'},
        {make: 'porsche', model: '911'},
        {make: 'bmw', model: 'm3'}
      ];
    }
    
    
  5. 创建ICar界面:使用以下命令:

    ng generate interface ICar
    
    
  6. 编辑 ICar 界面:编辑 icar.ts,更改为:

    export interface ICar {
        make: string,
        model: string
    }
    
    
  7. 创建Car类:使用下面的代码:

    ng generate component Car --inline-template --inline-style --flat
    
    
  8. 编辑Car类:编辑 car.component.ts,修改为:

    import { Component, Input } from '@angular/core';
    import { ICar } from './icar';
    
    @Component({
      selector: 'car',
      template: `
        <p>
          {{car.make}} : {{car.model}}
        </p>
      `,
      styles: []
    })
    export class CarComponent {
      @Input('theCar') car: ICar;
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 应用组件有一个三辆汽车的列表。我们使用ngFor指令遍历汽车列表,为每辆汽车生成一个汽车组件。我们使用theCar输入属性将汽车传递给汽车组件。
  • 我们有一个汽车组件来显示每辆汽车。在 car 组件中,我们使用theCar别名输入属性从外部接受 Car 实例变量。

Warning

您可以通过@Input()属性和@Component注释的inputs元素传递包含字段的对象。例如,您可以执行一个 HTTP 请求来获取一个包含名称和地址的 customer 对象,然后通过一个属性将它传递给一个子组件来显示它。这样做很好,但是请记住,在服务器返回响应之前,您传递的属性可能为空。因此,子组件可能试图显示像空对象的名称和地址这样的元素,这可能导致 Angular 抛出异常,并且在数据从服务器返回时不显示数据。这让我好几次措手不及。解决这个问题的方法是使用 Elvis 操作符。

向上流动的事件

有时您需要组合包含子组件的父组件并控制它们。父组件需要有响应子组件上发生的事情(事件)的代码。事件应该向上流动,从较低级别的组件向上发出,并由较高级别的组件响应。

下面是如何设置子组件以将自定义事件传递给父组件:

  1. 导入EventEmitter类。
  2. 通过使用@Component指令的events元素来指定您的组件将发出的定制事件。你一定要记得这样做!
  3. 在类中创建一个事件发射器作为实例变量。
  4. 当您想要发出一个事件时,调用事件发射器方法emit

下面是如何设置一个父组件来接收子组件的自定义事件:

  1. 将带有自定义事件的组件添加到另一个组件中。记得导入它并在@Component注释的directive元素中指定它。
  2. 将带有自定义事件的组件添加到另一个组件的模板标记中。编辑模板中的标记,使用圆括号中的事件名称和它将触发的模板语句来响应自定义事件,例如:(wordInput)="wordInputEvent($event)"。请注意,这使用了与非自定义事件相同的语法。

通过@Output()发出输出

您在子组件/指令中创建了一个类型为EventEmitter@Output()实例变量。您可以修改子组件/指令,以便在需要时使用该实例变量来发出事件。您可以修改父组件,用 template 语句绑定其模板中同名的事件属性。Angular 将从子组件/指令向父组件发出事件,并将调用模板语句。图 12-3 给出了一个例子。

A458962_1_En_12_Fig3_HTML.jpg

图 12-3

Emitting output

图 12-3 显示了事件如何从一个组件向上流向另一个组件。这将是更多组件示例-ex200:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex200 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑 app 类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { ICar } from './icar';
    
    @Component({
      selector: 'app-root',
      template: `
      <car *ngFor="let car of cars" (carDelete)="deleteCar(car)" [theCar]="car">
      </car>
      `,
      styles: []
    })
    
    export class AppComponent {
      cars:Array<ICar> = [
        {make: 'bmw', model: 'm3'},
        {make: 'porsche', model: '911'},
        {make: 'ford', model: 'mustang'}
      ];
    
      deleteCar(car: ICar){
        alert('Deleting car:' + JSON.stringify(car));
      }
    }
    
    
  5. 创建ICar接口:使用下面的代码:

    ng generate interface ICar
    
    
  6. 编辑ICar界面:编辑 icar.ts,更改为:

    export interface ICar {
        make: string,
        model: string
    }
    
    
  7. 创建Car类:使用下面的代码:

    ng generate component Car --inline-template --inline-style --flat
    
    
  8. 编辑Car类:编辑 car.component.ts,修改为:

    import { Component, Input, Output, EventEmitter } from '@angular/core';
    import { ICar } from './icar';
    
    @Component({
      selector: 'car',
      template: `
        <p>
          {{car.make}} : {{car.model}}
          <button (click)="delete(car)">Delete</button>
        </p>
      `,
      styles: []
    })
    export class CarComponent {
      @Input('theCar') car: ICar;
      @Output() carDelete = new EventEmitter();
    
      delete(car: ICar){
        this.carDelete.emit(car);
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 应用组件有一个包含三个Car的列表。它监听carDelete事件,当事件发生时触发deleteCar方法。
  • 我们有一个汽车组件来显示每辆汽车。它包含一个删除按钮,当用户单击它时会发出一个carDelete事件。

构图:示例

让我们使用 Angular CLI 创建一个包含其他组件的组件的简单示例。我们将编写一个包含客户列表的应用,该列表包含三个客户,如图 12-4 所示。

A458962_1_En_12_Fig4_HTML.jpg

图 12-4

Customer list with three customers

这将是更多组件示例-ex300:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex300 --inline-template --inline-style
    
    

    注意--inline-template--inline-style参数。这些命令告诉 CLI 将模板和样式合并到组件的类中,使组件定义成为一个文件,而不是三个文件——当您有很少样式的小模板时,这要容易得多。当您开始编写更大的组件时,您可能需要重新考虑这一点。当您使用这个命令(以及下面的命令)时,您可以添加--spec参数来告诉 CLI 不要为应用和组件创建. spec.ts 文件。我只是留下了spec文件单独生成。

  2. 开始ng serve:使用以下代码:

    cd more-components-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 创建客户列表组件:使用下面的代码:

    ng generate component customer-list --flat --inline-template --inline-style
    
    

    注意我们如何再次使用--inline-templateinline-style参数将组件合并到一个文件中。我们还使用参数--spec false告诉 CLI 不要生成. spec.ts 测试文件。

  5. 创建客户组件:使用下面的代码:

    ng generate component customer --flat --inline-template --inline-style
    
    

    我们再次使用--inline-templateinline-style参数将组件合并到一个文件中。我们使用参数--spec false告诉 CLI 不要生成. spec.ts 测试文件。

  6. 编辑 app 组件:将下面的代码复制粘贴到 app.component.ts:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <div class="app">
      [app]
      <app-customer-list>
      </app-customer-list>
      </div>
      `,
      styles: ['.app {background-color:#d5f4e6;margin:10px;padding:10px;}']
    })
    
    export class AppComponent {
    }
    
    
  7. 编辑客户列表组件:将下面的代码复制粘贴到 customer-list.component.ts:

    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'app-customer-list',
      template: `
        <div class="customerList">
        <p>
        [customer list]
        </p>
        <app-customer>
        </app-customer>
        <app-customer>
        </app-customer>
        <app-customer>
        </app-customer>
        </div>
      `,
      styles: ['.customerList {background-color:#80ced6;margin:10px;padding:10px;}']
    })
    export class CustomerListComponent implements OnInit {
    
      constructor() { }
    
      ngOnInit() {
      }
    
    }
    
    
  8. 编辑客户组件:将以下代码复制并粘贴到 customer.component.ts:

    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'app-customer',
      template: `
        <div class="customer">
          [customer]
        </div>
      `,
      styles: ['.customer {background-color:#fefbd8;margin:10px;padding:10px}']
    })
    export class CustomerComponent implements OnInit {
    
      constructor() { }
    
      ngOnInit() {
      }
    
    }
    
    

你的应用应该工作在本地主机:4200。您已经编写了一个由不同组件组成的应用。请注意以下几点:

  • 每个组件的顶部都有一个指定选择器的@Component指令。例如,customer 组件:

    @Component({
      selector: 'app-customer',
    
    

    当您需要将该组件包含在另一个组件中时,您可以使用选择器作为标记。例如,客户列表组件使用客户组件的标记三次将其包含在模板中:

    <app-customer>
    </app-customer>
    <app-customer>
    </app-customer>
    <app-customer>
    </app-customer>
    
    
  • 文件 app.module.ts 已被 CLI 修改。每个组件都作为一个声明添加到模块中(稍后将详细介绍模块)。

数据向下流动:示例

让我们修改示例 more-components-ex300,将数据从客户列表组件向下传递到客户组件,如图 12-5 所示。

A458962_1_En_12_Fig5_HTML.jpg

图 12-5

Customer list with three customers

这将是更多组件示例-ex400。

编辑客户组件

我们编辑客户组件以接受来自外部的输入数据:

  • 修改导入以包含来自 Angular core:

    import { Component, OnInit, Input } from '@angular/core';
    
    

    Input

  • 更改模板以包含实例变量customer : {{customer.name}}{{customer.city}}的字符串插值。这将输出customer实例变量

      template: `
        <div class="customer">
          {{customer.name}} | {{customer.city}}
        </div>
      `,
    
    

    namecity属性的内容

  • 将实例变量customer声明为输入变量:

    @Input() customer;
    
    

编辑客户列表组件

我们编辑客户列表组件,使用单向数据绑定将数据传递给客户组件(稍后将详细介绍):

  • 将以下标签

    <app-customer>
    </app-customer>
    <app-customer>
    </app-customer>
    <app-customer>
    </app-customer>
    
    

    替换为:

    <app-customer *ngFor="let customer of customerList" [customer]="customer">
    </app-customer>
    
    
  • 声明实例变量customerList并用数据填充它。在export之后、constructor之前添加代码:

      private customerList = [
        { name: 'Brian', city: 'Atlanta'},
        { name: 'Peter', city: 'San Francisco'},
        { name: 'Janet', city: 'Colorado'},
      ];
    
    

你的应用应该工作在本地主机:4200。您修改了组件列表以包含客户列表数据,并使用单向(向下)数据绑定将该数据向下传递给客户。请注意以下几点:

  • 客户列表组件将客户列表数据设置为实例变量,模板引用该变量。
  • 客户列表组件在模板中使用了一个ngFor。这允许模板遍历客户列表,为每个客户创建一个customer变量,并通过一个绑定属性将其传递给客户组件。
  • 客户组件声明了一个名为customer的实例变量。它使用注释@Input()告诉 Angular 从外部自动设置它的值。注意,Input类必须在客户组件类的顶部导入。
  • 客户组件使用模板中的{{customer.name}}{{customer.city}}输出名为customer的实例变量的namecity属性。

向上流动的事件:示例

让我们修改前面的示例,从客户组件向客户列表组件触发事件,如图 12-6 所示。这将是示例 ex300,它基于示例更多组件-ex500。

A458962_1_En_12_Fig6_HTML.jpg

图 12-6

Firing events

这将是示例 ex300。

编辑客户组件

我们编辑客户组件,以便在用户单击客户时输出一个单击事件:

  • 修改import以包括来自 Angular 核心的OutputEventEmitter类:

    import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
    
    
  • 修改模板以在用户点击客户:

    template: `
        <div class="customer"(click)="onClicked()">
          {{customer.name}} | {{customer.city}}
        </div>
      `,
    
    

    时调用方法onClicked

  • 将事件发射器的实例变量添加到 TypeScript 类:

    @Output() clicked: EventEmitter<String> = new EventEmitter<String>();
    
    
  • onClicked方法添加到 TypeScript 类:

    onClicked(){
      this.clicked.emit(this.customer.name);
    }
    
    

编辑客户列表组件

我们编辑客户列表组件来响应客户组件发出的clicked事件:

  • 修改模板以绑定到clicked事件,当事件发生时调用onCustomerClicked方法:

    <app-customer *ngFor="let customer of customerList" [customer]="customer" (clicked)="onCustomerClicked($event)">
    
    
  • 添加方法onCustomerClicked来接收事件数据,并显示一个带有客户名称:

    onCustomerClicked(customerName:String){
      alert('Customer Clicked:' + customerName);
    }
    
    

    的警告框

你的应用应该工作在本地主机:4200。当你点击一个客户时,应用现在应该显示一个警告框。请注意事件是如何从多个客户组件流向单个客户列表组件的。还要注意以下几点:

  • customer 组件为输出带有字符串数据的事件的事件发射器设置一个实例变量。它使用一个注释@Output()告诉 Angular 其他组件应该能够绑定到这个事件。
  • 客户组件模板包括 Angular 指令(click),用于监听和响应用户点击div。它触发一个使用事件发射器输出事件的方法。
  • 客户列表组件包括一个事件处理程序,用于监听和响应客户组件中的定制clicked事件。
  • 客户列表组件包含在onCustomerClicked方法中的代码,用于接收来自事件的数据并显示一个警告框。

模板参考变量

模板引用变量是对模板中一个或多个元素的引用。可以用ref-前缀代替#

一旦声明了模板引用变量,就可以在模板或代码中使用它。然而,你需要知道这个变量不是由 Angular 设置的,直到ngAfterViewInit生命周期方法已经完成(本书后面会有更多关于 Angular 生命周期的内容)。

ViewChild:示例

ViewChild声明对组件中子元素的引用。当声明实例变量时,在括号中指定一个选择器,用于将子元素绑定到实例变量。

该示例显示了文本(参见图 12-7 ),它看起来类似于 CLI 默认应用。这是更多组件示例-ex600。

A458962_1_En_12_Fig7_HTML.jpg

图 12-7

ViewChild example

如果您检查代码,您会看到它使用一个模板变量来引用h1元素:

<h1 #title></h1>

在组件加载完视图后,它有代码来设置它的内部 HTML:

  ngAfterViewInit(){
    this.title.nativeElement.innerHTML = 'app works differently!'
  }

还要注意,模板变量由模板中的一些插值引用:

The title is {{title.innerHTML}}

我们来看更多示例-组件-ex600:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex600 --inline-template
    
    

    记住--inline-template告诉 CLI 在生成新应用时使用内联模板。

  2. 开始ng serve:使用以下代码:

    cd more-components-ex600
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1 #title></h1>
      The title is {{title.innerHTML}}
      `,
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements AfterViewInit  {
      @ViewChild('title') title: ElementRef;
    
      ngAfterViewInit(){
        this.title.nativeElement.innerHTML = 'app works differently!'
      }
    }
    
    
  5. 查看应用:您应该看到“应用的工作方式不同!”

ViewChildren:示例

ViewChildren声明对组件中多个子元素的引用。当声明实例变量时,在括号中指定一个选择器,用于将子元素绑定到实例变量。

该选择器可以是子类型(即ChildAngular 元素的类)或模板引用(#name)。

本例使用ViewChildren来访问段落列表(见图 12-8 )。我们使用ViewChildren和一系列子引用名,用逗号分隔。这是更多组件示例-ex700。

A458962_1_En_12_Fig8_HTML.jpg

图 12-8

Accessing a list of paragraphs

我们来看更多示例-组件-ex700:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex700 --inline-template
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex700
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, ViewChildren, AfterViewInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <p #paragraph1>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. </p>
      <p #paragraph2>At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr,  sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p>
      <p *ngIf="note">{{note}}</p>
      `,
      styles: ['p { background-color: #FFE5CC; padding: 15px; text-align: center}']
    })
    export class AppComponent implements AfterViewInit{
      @ViewChildren('paragraph1, paragraph2') paragraphs;
      note: string = '';
    
      ngAfterViewInit(){
        setTimeout(_ => this.note = 'Number of Paragraphs:' + this.paragraphs.length);
      }
    }
    
    

你的应用应该工作在本地主机:4200。您应该会看到两段文本,下面有一个段落计数,如图 12-8 所示。

NgContent 和 Transclusion:示例

Transclusion 是将内容从组件标签内的区域包含和转移到组件的模板中。NgContent标签用于传输。它甚至有一个选择器,允许您选择要包含的内容。如果您在选择器中使用了一个[(如在[test]中),那么它可以用来选择具有该属性的内容(例如,<div test>hejwejgwegrhj</div>)。

这个例子非常简单,没有使用选择器。它只包括组件标签之间的文本。这是 more-components-ex800 的示例(见图 12-9 )。

A458962_1_En_12_Fig9_HTML.jpg

图 12-9

Text between component tags

我们来看更多示例-组件-ex800:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex800 --inline-template
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex800
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:这次我们将把两个Component类添加到同一个文件中。编辑 app.component.ts 并将其更改为以下内容:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'Paragraph',
      template: `
      <p><ng-content></ng-content></p>
      `,
      styles: ['p { border: 1px solid #c0c0c0; padding: 10px }']
    })
    export class Paragraph {
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <p>
      <Paragraph>Lorem ipsum dolor sit amet, consectetur adipiscing elit. </Paragraph>
      <Paragraph>Praesent eget ornare neque, vel consectetur eros. </Paragraph>
      </p>
      `,
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = "welcome to app!";
    }
    
    
  5. 编辑模块:我们在同一个文件中有两个Component类。我们需要确保两个Component都在模块定义中声明——否则,它们将不可用。编辑 app.module.ts 并将其更改为以下内容:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent, Paragraph } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        Paragraph
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    

你应该会看到两段文字,如图 12-9 所示。

ContentChild:示例

您可以使用ngContent来包含附加内容。Transclusion 指的是将内容注入 DOM 中的特定元素。您可以使用ContentChild来声明对被包含的附加内容中的child元素的引用。

这个例子与前一个例子相似,只是它使用了ContentChild来获取对被交叉包含的内容中的title元素的引用(参见图 12-10 )。然后,它包含来自该元素的内部 HTML。这是更多组件示例-ex900。

A458962_1_En_12_Fig10_HTML.jpg

图 12-10

Text between component tags

我们来看更多示例-组件-ex900:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex900 --inline-template
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex900
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts 并将其更改为:

    import { Component, ContentChild } from '@angular/core';
    
    @Component({
      selector: 'Paragraph',
      template: `
      <div>
      <b>{{title.nativeElement.innerHTML}}</b>
      <p><ng-content></ng-content></p>
      </div>
      `,
      styles: ['p { border: 1px solid #c0c0c0 }']
    })
    export class Paragraph {
      @ContentChild('title') title;
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <p>
      <Paragraph><title #title>Paragraph 1</title>Lorem ipsum dolor sit amet, consectetur adipiscing elit. In pulvinar egestas massa sit amet scelerisque.</Paragraph>
      <Paragraph><title #title>Paragraph 2</title>Praesent eget ornare neque, vel consectetur eros. Morbi gravida finibus arcu, vel mattis justo dictum a.</Paragraph>
      </p>
      `,
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'welcome to app!';
    }
    
    
  5. 编辑模块:我们在同一个文件中有两个Component类。我们需要确保两个Component都在模块定义中声明——否则,它们将不可用。编辑 app.module.ts 并将其更改为以下内容:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent, Paragraph } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        Paragraph
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    

您应该会看到两段文字。

内容儿童:示例

您可以使用ContentChildren在包含的附加内容中声明对多个子元素的引用。

这个例子与前一个例子相似,只是它使用ContentChild来获取对被交叉包含的内容中的title元素的引用(参见图 12-11 )。然后,它包含来自该元素的内部 HTML。这是更多组件示例-ex1000。

A458962_1_En_12_Fig11_HTML.jpg

图 12-11

List of people and people count

我们来看更多示例-组件-ex1000:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1000 --inline-template
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1000
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts 并将其更改为:

    import { Component, ContentChildren } from '@angular/core';
    
    @Component({
      selector: 'Person',
      template: `
      <div>&nbsp;-&nbsp;<ng-content></ng-content></div>
      `,
      styles: ['']
    })
    export class Person {
    }
    
    @Component({
      selector: 'Paragraph',
      template: `
      <div>
      <ng-content></ng-content>
      <p *ngIf="people">Number of people: {{people.length}}</p>
      </div>
      `,
      styles: ['div { border: 1px solid #c0c0c0; margin:10px; padding:10px }', 'p { margin: 5px 0 }']
    })
    export class Paragraph {
      @ContentChildren(Person) people;
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <Paragraph>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
        <Person>Albertus Falx</Person>
        <Person>Godefridus Turpilius</Person>
        <Person>Demipho Renatus</Person>
      </Paragraph>
      <Paragraph>Praesent eget ornare neque, vel consectetur eros.
        <Person>Hanno Grumio</Person>
        <Person>Lycus Auxilius</Person>
      </Paragraph>
        `,
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'welcome to app!';
    }
    
    
  5. 编辑模块:我们在同一个文件中有三个Component类。我们需要确保两个Component都在模块定义中声明——否则,它们将不可用。编辑 app.module.ts 并将其更改为以下内容:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent, Paragraph, Person } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        Paragraph,
        Person
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    

您应该会看到两段文字。每一段都要有一个人数列表,底部有一个人数,如图 12-11 。

组件类生命周期

像 AngularJS 一样,Angular 为您管理组件——当它创建组件、更新组件、销毁组件时,等等。每个组件都有所谓的生命周期事件:出生和生活事件,如变化和死亡。有时你需要添加额外的代码,当这些事件发生时,Angular 会为你触发这些代码。

构造函数与 OnInit

有时你需要设置你的组件并初始化它。这里有两种选择:可以使用构造函数或OnInit生命周期方法。当组件第一次初始化时,触发OnInit生命周期方法。

Tip

你应该使用哪一个取决于你,但是许多人遵循这个一般的经验法则。我们主要使用ngOnInit进行初始化/声明,避免在构造函数中工作。构造函数应该只用于初始化类成员,但不应该做实际的“工作”

一旦组件加载并可见,您可能需要添加一些代码来做一些事情。例如,将输入焦点放在第一个字段上,这样用户就可以开始输入了。您可能认为可以将这段代码添加到组件类构造函数中,但这是不正确的,因为构造函数是在组件可见之前触发的。事实上,您可能会在视图初始化之后将这段代码添加到ngAfterViewInit

接口

要挂钩到生命周期方法,组件的类应该实现所需的接口。然后,该接口将强制您实现相应的方法。

例如,要实现一个在视图初始化后触发的方法,您应该实现接口AfterViewInit,它需要方法ngAfterViewInit。表 12-1 有更多的细节。

表 12-1

Interfaces and Methods

| 连接 | 方法 | 描述 | | :-- | :-- | :-- | | `OnChanges` | `ngOnChanges` | 当输入或输出绑定值更改时调用 | | `OnInit` | `ngOnInit` | 第一个`ngOnChanges`之后 | | `DoCheck` | `ngDoCheck` | 开发人员的自定义变更检测 | | `AfterContentInit` | `ngAfterContentInit` | 组件内容初始化后 | | `AfterContentChecked` | `ngAfterContentChecked` | 每次检查成分含量后 | | `AfterViewInit` | `ngAfterViewInit` | 组件视图初始化后 | | `AfterViewChecked` | `ngAfterViewChecked` | 每次检查组件视图后 | | `OnDestroy` | `ngOnDestroy` | 就在指令被销毁之前 |

NgOnChanges:示例

当绑定属性的值更改时,将调用此回调。每当输入属性的值改变时,它就执行。它将接收一个包含绑定的当前和先前值的变更映射,该映射被封装在一个SimpleChange中(参见图 12-12 )。这是更多组件示例-ex1100。

A458962_1_En_12_Fig12_HTML.jpg

图 12-12

Component with a text box that lets you enter text

当您进行更改时,更改会记录在下面。我们来看更多示例-组件-ex1100:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts 并将其更改为:

    import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
    
    @Component({
      selector: 'name',
      template: `
      <p *ngFor="let change of changes">
      {{change}}
      </p>
      `,
      styles: []
    })
    export class NameComponent implements OnChanges{
      @Input('name') nm;
      changes: Array<string> = [''];
    
      ngOnChanges(changes: SimpleChanges){
        this.changes.push(JSON.stringify(changes));
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
      Change this field: <input [(ngModel)]="name" />
      <hr/>
      History
      <name [name]="name"></name>
      `,
      styles: []
    })
    export class AppComponent{
      name: string = '';
    }
    
    
  5. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent, NameComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        NameComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 这两个组件驻留在同一个文件中,但是它们必须在模块中分别导入和声明。
  • app 组件使用双向绑定到name实例变量。它将name实例变量传递给Name组件。
  • Name组件使用生命周期方法ngOnChanges来监听输入属性的变化(在本例中是name)。当这个方法被触发时,它使用 JSON.stringify 将一个表示更改的字符串转储到下面的更改列表中。

NgOnInit:示例

一旦 Angular 创建完组件并初始化它,这个回调就被调用。它在构造函数之后和第一次触发ngOnChange之后被直接调用。这是一个显示日志的组件,如图 12-13 所示。这将是更多组件示例-ex2000。

A458962_1_En_12_Fig13_HTML.jpg

图 12-13

Displaying logs

组件初始化和调用生命周期方法ngOnInit的日志。我们来看更多示例-组件-ex2000:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1200 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <p *ngFor="let log of logs">
      {{log}}
      </p>
      `,
      styles: []
    })
    export class AppComponent implements OnInit{
      logs: Array<string> = [ new Date()+''];
    
      constructor(){
        for (let i=0;i<1000;i++){
          console.log(i);
        }
      }
    
      ngOnInit(){
        this.logs.push(new Date()+'');
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 定义实例变量时,日志被初始化。
  • 构造函数有一些代码来减缓组件的创建。
  • 当 Angular 完成创建组件时,日志会增加。

NgDoCheck:示例

每次检查组件或指令的输入属性时,都会调用此回调。我们可以使用这个生命周期挂钩,用我们自己的定制检查逻辑来扩展检查。

这是一个让你创建一个数组的组件,它会计算出你改变了什么,如图 12-14 所示。这将是更多组件示例-ex1300。

A458962_1_En_12_Fig14_HTML.jpg

图 12-14

Creating an array and figuring out what you change

让我们做得更多-组件-ex1300:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1300 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts 并将其更改为:

    import { Component, Input, DoCheck, IterableDiffers } from '@angular/core';
    
    @Component({
      selector: 'numbers',
      template: `
      {{numbers}}
      <br/>
      <p *ngFor="let change of changes">
      {{change}}
      </p>
      `,
      styles: ['p{padding:0;margin:0}']
    })
    export class NumbersComponent implements DoCheck {
      @Input('numbers') numbersArray: Array<string>;
      changes: Array<string> = [];
      differ;
    
      constructor(private differs: IterableDiffers) {
        this.differ = differs.find([]).create(null);
      }
    
      ngDoCheck() {
        const differences = this.differ.diff(this.numbersArray);
        if (differences) {
          if (differences.forEachAddedItem) {
            differences.forEachAddedItem((item) => {
              if ((item) && (item.item)){
                this.changes.push('added ' + item.item);
    
              }
            });
          }
          if (differences.forEachRemovedItem) {
            differences.forEachRemovedItem((item) => {
              if ((item) && (item.item)){
                this.changes.push('removed ' + item.item);
              }
            });
          }
        }
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
      Enter Array (comma-separated): <input [(ngModel)]="numbers" (onModelChange)="onModelChange"/>
      <br/>
      <numbers [numbers]="numbers.split(',')"></numbers>
      `,
      styles: []
    })
    export class AppComponent {
      numbers = '';
    }
    
    
  5. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent, NumbersComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        NumbersComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule {}
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件将input字符串解析成一个数组,并将其传递给Numbers组件。
  • Numbers组件有一个通过构造函数注入的Iterable Differ,因此它可以是一个实例变量并在以后使用。
  • 当输入发生变化并且Numbers组件的input属性发生变化时,该组件使用differ来分析这些变化,并将每个变化添加到变化日志中。

NgAfterContentInit:示例

这个回调在 ngOnInit 之后被调用:当组件或指令的内容已经被初始化并且绑定已经被第一次检查时。

在本例中,app 组件声明了一个 crew 结构,其内容中包含成员,如图 12-15 所示。稍后,此生命周期回调用于选择列表中的第一个组员。这将是更多组件示例-ex1400。

A458962_1_En_12_Fig15_HTML.jpg

图 12-15

Declaring a crew structure and selecting a member

是时候做更多示例了-组件-ex1400:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1400 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1400
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts 并将其更改为:

    import { Component, Input, AfterContentInit, ContentChildren, QueryList } from '@angular/core';
    
    @Component({
      selector: 'member',
      template: `
      <p [style.backgroundColor]="getBackgroundColor()"><ng-content></ng-content></p>
      `,
      styles: ["p{padding: 5px}"]
    })
    export class MemberComponent {
    
      selected = false;
      getBackgroundColor(){
        return this.selected ? "#FFCCCC" : "#CCFFFF";
      }
    }
    
    @Component({
      selector: 'crew',
      template: `
      <p><ng-content></ng-content></p>
      `,
      styles: []
    })
    export class CrewComponent implements AfterContentInit {
      @ContentChildren(MemberComponent) members: QueryList<MemberComponent>;
    
      ngAfterContentInit() {
        this.members.first.selected = true;
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <crew>
        <member>Captain Kirk</member>
        <member>Spock</member>
        <member>Sulu</member>
        <member>Bones</member>
        <member>Checkov</member>
      </crew>
      `,
      styles: []
    })
    export class AppComponent {}
    
    
  5. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent, CrewComponent, MemberComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        CrewComponent,
        MemberComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件声明了一个成员在其内容中的 crew 结构。
  • Crew 组件声明实例变量members(使用@ContentChildren声明)以映射到内容中自己的crew标签内的船员列表。这个变量属于QueryList类型,因此可以更容易地查询它。
  • 一旦 Angular 为您设置了 members 的实例变量,Crew 内容就会使用生命周期方法ngAfterContentInit来访问它。然后,这个生命周期方法设置第一个memberselected实例变量,以便突出显示他或她。
  • 成员组件显示了member标签中的内容,并根据selected实例变量设置了组件的背景颜色。

NgAfterContentChecked:示例

每次检查完组件或指令的内容后,都会执行该回调,当检查完组件的所有绑定时,该回调会有效;即使他们没有改变。这个例子允许你挑选一张牌,如图 12-16 所示。这将是更多组件示例-ex1500。

A458962_1_En_12_Fig16_HTML.jpg

图 12-16

Picking a card

我们来看更多示例-组件-ex1500:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1500 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1500
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts 并将其更改为:

    import { Component, ContentChild, AfterContentChecked } from '@angular/core';
    
    @Component({
      selector: 'card',
      template: `
      <ng-content></ng-content>
      `,
      styles: []
    })
    export class CardComponent {
    }
    
    @Component({
    
      selector: 'app-root',
      template: `
      <card>{{card}}</card>
      <button (click)="pickCard($event)">Pick a Card</button>
      `,
      styles: []
    })
    export class AppComponent implements AfterContentChecked {
      card = CARD_ACE_OF_SPADES;
    
      @ContentChild(CardComponent) contentChild: CardComponent;
    
      ngAfterContentChecked() {
        console.log("content inside card has been checked: " + this.card);
      }
    
      pickCard() {
        this.card = this.card === CARD_ACE_OF_SPADES ? CARD_TEN_OF_CLUBS : CARD_ACE_OF_SPADES;
      }
    }
    
    const CARD_ACE_OF_SPADES = 'ace of spades';
    const CARD_TEN_OF_CLUBS = 'ten of clubs';
    
    
  5. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent, CardComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        CardComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件使用卡片组件来显示当前卡片。当前 card 是一个实例变量,其值作为内部内容放在 Card 组件中。
  • app 组件有一个按钮可以让你翻到另一张卡。它会更改实例变量的值。
  • app 组件有一个ngAfterContentChecked方法,当 Card 组件中的内容改变时会自动触发该方法。当当前卡片改变时触发。

NgAfterViewInit:示例

这个回调在组件视图及其子视图被创建并初始化后被调用。这对于执行组件初始化很有用。请注意,@ViewChild@ViewChildren实例变量此时已经设置好并可用(与组件生命周期的早期不同)。本示例向您展示如何设置初始输入焦点,如图 12-17 所示。这将是更多组件示例-ex1600。

A458962_1_En_12_Fig17_HTML.jpg

图 12-17

Setting initial inpur focus

我们来看更多示例-组件-ex1600:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1600 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1600
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, AfterViewInit, ViewChild } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
        First Input Field: <input #firstInput />
      `,
      styles: []
    })
    export class AppComponent implements AfterViewInit{
      @ViewChild('firstInput') firstInput;
    
      ngAfterViewInit(){
        // ViewChild variables are available in this method.
        // Set initial focus.
        this.firstInput.nativeElement.focus();
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件设置firstInput实例变量来引用声明为# firstInput的模板变量。
  • app 组件有一个ngAfterViewInit方法,一旦视图被初始化并且firstInput实例变量可用,就会触发该方法。此方法设置初始输入焦点。

NgAfterViewChecked:示例

每次检查组件视图后都会调用这个回调。它只适用于组件,当所有子指令的绑定都被检查时,即使它们没有改变。如果组件正在等待来自其子组件的东西,这可能会很有用。

不要在这里设置任何绑定到模板的变量。如果这样做,您将收到“检查后表达式已更改”错误。

该示例允许您输入一些内容,并显示一条消息,提示您输入的内容是否为数字,如图 12-18 所示。这将是更多组件示例-ex1700。

A458962_1_En_12_Fig18_HTML.jpg

图 12-18

Displaying whether input is numeric or not

我们来看更多示例-组件-ex1700:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1700 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1700
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, ViewChild, AfterViewChecked } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <input [(ngModel)]="input"/>
      <br/>
      {{input}}
      <br/>
      <div #message></div>
      `,
      styles: []
    })
    export class AppComponent implements AfterViewChecked {
      input: string = '';
    
      @ViewChild('message') message;
    
      ngAfterViewChecked(){
        console.log('AfterViewChecked');
        if (isNaN(parseInt(this.input))){
          this.message.nativeElement.innerHTML = "Input not numeric.";
        }else{
          this.message.nativeElement.innerHTML = "Input is numeric.";
        }
      }
    }
    
    
  5. 编辑类:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { AppComponent } from './app.component';
    import { FormsModule } from '@angular/forms';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        FormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件设置message实例变量来引用声明为# message的模板变量。
  • app 组件有一个ngAfterViewChecked方法,一旦检查了视图的绑定,就会触发该方法。这是我们检查输入并设置message来指示用户输入是否为数字的方法。

NgOnDestroy:示例

当组件、指令、管道或服务被销毁时,将调用此回调。在这里添加代码以销毁任何可能作为实例变量保留的引用(也就是说,清理您的引用)。

这个例子使用一个时间间隔计时器来计数,当组件被破坏时,这个计时器就会被破坏,如图 12-19 所示。这是准备处理组件的最佳位置,例如,取消后台任务。这将是更多组件示例-ex1800。

A458962_1_En_12_Fig19_HTML.jpg

图 12-19

Counting up with an interval timer

现在我们来看更多示例-组件-ex1800:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1800 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1800
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, OnInit, OnDestroy } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>
        {{count}}
      </h1>
      `,
      styles: []
    })
    export class AppComponent implements OnInit, OnDestroy{
      interval;
      count = 0;
    
      ngOnInit(){
        this.interval = setInterval(() => {
          this.count++;
        })
      }
    
      ngOnDestroy(){
        clearInterval(this.interval);
        delete this.interval;
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件有一个ngOnInit方法,一旦加载就被触发。它初始化时间间隔,该时间间隔递增计数。
  • app 组件有一个在被销毁时触发的ngOnDestroy方法。它清除间隔,停止向上计数。

摘要

这是一个重要的章节,因为你将花费大部分时间编写组件。你需要知道你的用户界面是由一系列组件组成的,每一个组件从创建到销毁都有自己的生命周期。

注意您的数据是如何向下流动的,从高级组件向下到低级组件,这也是非常重要的。你需要知道事件如何向相反的方向发展。

在下一章,我们将讨论如何创建服务对象来执行非可视任务,并使用依赖注入将这些对象插入到组件中。

十三、依赖注入

在软件工程中,依赖注入是一种软件设计模式,它实现了用于解决依赖的控制反转。依赖是可以使用的对象(服务)。注入是将依赖关系传递给使用它的依赖对象(客户机)。在习惯了 Angular 之后,你会认为依赖注入是理所当然的,因为它太容易使用了。

例如,这段代码

var svc = new ShippingService(new ProductLocator(),
   new PricingService(), new InventoryService(),
   new TrackingRepository(new ConfigProvider()),
   new Logger(new EmailLogger(new ConfigProvider())));

可以被这样的东西代替:

var svc = container.Resolve<IShippingService>();

依赖注入的一些优点包括:

  • 你的代码更干净,可读性更强。
  • 对象是松散耦合的。
  • 有可能消除或至少减少组件不必要的依赖。
  • 减少组件的依赖性通常会使它更容易在不同的上下文中重用。
  • 提高组件的可测试性。
  • 将依赖关系移动到组件的接口,这样您就不用显式地引用依赖关系,而是通过接口来引用它们。

服务和供应器

Angular 提供的服务列于表 13-1 中。

表 13-1

Angular’s Provided Services

| 服务 | 描述 | | :-- | :-- | | 超文本传送协议(Hyper Text Transport Protocol 的缩写) | 对于与服务器的 HTTP 通信 | | 形式 | 表单处理程序代码 | | 路由器 | 页面导航代码 | | 动画 | 用户界面动画 | | 用户界面库 | 例如,`NgBootstrap` |

Tip

您可以从 www.ngmodules.org 下载其他服务。

您可能希望编写以下服务的特定实现:

  • 服务器通信
  • 安全
  • 审计
  • 记录
  • 会议

请记住,您的实现可以“包装”其他服务。例如,您的服务器通信服务本身可以使用 Angular Http 服务并添加更多的功能,实现不同的东西,或者只是具有不同的配置。

编写服务时,通常将它们编写为 TypeScript 类,每个类一个文件(filename.service.ts)。使用@Injectable()注释将这些类标记为可注入的是一个好主意。@Injectable()将一个类标记为可用于实例化的注入器。一般来说,当试图实例化一个没有标记为@Injectable()的类时,注入器会报告一个错误。

提供者用于注册类、函数或值,以便依赖注入可以使用它们。Injector类使用提供者来提供信息,这样它就可以创建一个对象的实例来注入另一个对象。因此,提供者基本上是如何创建对象实例的信息源。该信息包括令牌,即可能需要创建的对象的标识符。当你在 Angular 代码中看到provider()时,你看到的是一个 Angular 函数的调用,用来注册如何创建一个对象的信息。

有三种类型的提供者:类提供者、工厂提供者和值提供者。在这一章的后面,我将首先介绍类提供者,因为它们是最常用的。

每个组件都有自己的注入器,用于提供者为组件创建对象。当组件有子组件时,注射器为子组件创建子注射器。

当依赖注入需要将一个对象注入到一个组件中时,它会尝试使用get方法在本地注入器(即组件的注入器)中解析该对象。如果不能解决这个问题(换句话说,如果对象不存在于注入器中),它将尝试在父组件的注入器中解决该对象,依此类推,一直到应用组件。这确保了优先使用最近的(本地)注射器提供者,而不是更高级别的提供者。这就是所谓的隐藏,类似于同名的局部变量优先于同名的全局变量。

通常 Angular 会为您处理从属关系的解析和创建。然而,Injector类提供了您自己调用它的方法——例如,resolveAndCreate

创建服务:示例

这是一个简单的组件,它使用服务来提供关于汽车的信息。这将是依赖注入示例 ex100。它将在汽车组件中提供服务,如图 13-1 所示。

A458962_1_En_13_Fig1_HTML.gif

图 13-1

Component using a service to provide information

让我们试试这个练习:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 创建服务类:创建 car.service.ts 并将其更改为:

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class CarService {
        constructor(){
            console.log('CarService: constructor');
        }
        // Some dummy method.
        isSuperCharged(car: string){
            return car === 'Ford GT' ? 'yes' : 'no';
        }
    }
    
    
  5. 编辑类:编辑 app.component.ts,修改为:

    import { Component, OnInit, Input } from '@angular/core';
    import { CarService } from './car.service';
    
    @Component({
      selector: 'car',
      template: `
      <h3>
        {{name}} Is Supercharged: {{supercharged}}
      </h3>
      `,
      styles: [],
      providers: [CarService]
    })
    export class CarComponent implements OnInit{
      @Input() name;
      supercharged: string = '';
      constructor(private service: CarService){}
      ngOnInit(){
        this.supercharged = this.service.isSuperCharged(this.name);
      }
    }
    
    @Component({
    
      selector: 'app-root',
      template: `
      <car name="Ford GT"></car>
      <car name="Corvette Z06"></car>
      `,
      styles: []
    })
    export class AppComponent {
      title = 'app works!';
    }
    
    
  6. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent, CarComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent, CarComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

A458962_1_En_13_Fig2_HTML.jpg

图 13-2

Returning a yes or no

  • car服务在构造函数中输出一个日志。该服务包含一个方法isSuperCharged,该方法接收汽车名称作为参数,并相应地返回 yes 或 no,如图 13-2 所示。
  • app 组件有一个使用了两次的 car 组件。car 组件将car服务指定为提供者。汽车组件调用service方法isSuperCharged,方法ngOnInit. ngOnInit在组件初始化后被触发。

为什么会创建同一服务的多个实例?打开控制台,你会看到类似图 13-3 的东西。

A458962_1_En_13_Fig3_HTML.jpg

图 13-3

Constructor invoked twice

正如您所看到的,构造函数被调用了两次,因为服务被创建了两次。这是因为CarService是在汽车组件中提供的,并且汽车组件被创建了两次。以下是汽车部件的摘录:

@Component({
  selector: 'car',
  ...
  providers: [CarService]
})
export class CarComponent implements OnInit{
  ...
  constructor(private service: CarService){}
  ...
}

我们想要的是创建服务的单个实例,如图 13-4 所示。

A458962_1_En_13_Fig4_HTML.jpg

图 13-4

We want one instance of a service created

要做到这一点,我们只需将提供者转移到应用级别或只使用一次的类。在代码示例中,我们可以在

如果我们想要共享服务的单例,该怎么办?我们没有指定我们在Car对象上需要它,因为有多个Car。我们需要指定我们在应用级别的其他地方需要该服务。

让我们转换我们的应用来共享服务的一个实例。

转换应用以共享服务的一个实例:示例依赖项-注入-ex200

这是一个使用服务提供汽车信息的简单组件,如图 13-5 所示。

A458962_1_En_13_Fig5_HTML.jpg

图 13-5

Service providing information about cars

示例 dependency-injection-ex200 与 dependency-injection-ex100 相同,只是它在 app 组件中提供服务,因此只创建一个CarService实例,如图 13-6 所示。

A458962_1_En_13_Fig6_HTML.gif

图 13-6

One instance of CarService provided in the app component

让我们做练习依赖-注射-ex200:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex200 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 创建服务类:这与前面的例子相同。编辑 car.service.ts 并将其更改为以下内容:

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class CarService {
        constructor(){
            console.log('CarService: constructor');
        }
        // Some dummy method.
        isSuperCharged(car: string){
            return car === 'Ford GT' ? 'yes' : 'no';
        }
    }
    
    
  5. 编辑类:这与前面的例子不同。编辑 app.component.ts 并将其更改为以下内容:

    import { Component, OnInit, Input } from '@angular/core';
    import { CarService } from './car.service';
    
    @Component({
      selector: 'car',
      template: `
      <h3>
        {{name}} Is Supercharged: {{supercharged}}
      </h3>
      `,
      styles: [],
      providers: []
    })
    export class CarComponent implements OnInit{
      @Input() name;
      supercharged: string = '';
      constructor(private service: CarService){}
      ngOnInit(){
        this.supercharged = this.service.isSuperCharged(this.name);
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <car name="Ford GT"></car>
      <car name="Corvette Z06"></car>
      `,
      styles: [],
      providers: [CarService]
    })
    export class AppComponent {
      title = 'app works!';
    }
    
    
  6. 编辑模块:这与前面的例子相同。编辑 app.module.ts 并将其更改为以下内容:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent, CarComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent, CarComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    

你的应用应该工作在本地主机:4200。请注意,CarService构造函数在控制台中只记录一次。这是因为它只需要在 app 组件中创建一次,就可以被所有子组件使用。

转换应用以共享服务的一个实例:示例依赖项-注入-ex300

这是一个使用服务提供汽车信息的简单组件,如图 13-7 所示。

A458962_1_En_13_Fig7_HTML.jpg

图 13-7

Service providing information about cars

示例 dependency-injection-ex300 与 dependency-injection-ex200 相同,只是它在模块中提供服务,因此只创建了一个CarService实例,可以在应用中的任何地方使用,如图 13-8 所示。

A458962_1_En_13_Fig8_HTML.gif

图 13-8

One instance of CarService provided in the module

让我们做练习依赖-注射-ex300:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex300 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 创建服务类:这与前面的例子相同。编辑 car.service.ts 并将其更改为以下内容:

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class CarService {
        constructor(){
            console.log('CarService: constructor');
        }
        // Some dummy method.
        isSuperCharged(car: string){
            return car === 'Ford GT' ? 'yes' : 'no';
        }
    }
    
    
  5. 编辑类:这与前面的例子不同。编辑 app.component.ts 并将其更改为以下内容:

    import { Component, OnInit, Input } from '@angular/core';
    import { CarService } from './car.service';
    
    @Component({
      selector: 'car',
      template: `
      <h3>
        {{name}} Is Supercharged: {{supercharged}}
      </h3>
      `,
      styles: []
    })
    export class CarComponent implements OnInit{
    
      @Input() name;
      supercharged: string = '';
      constructor(private service: CarService){}
      ngOnInit(){
        this.supercharged = this.service.isSuperCharged(this.name);
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <car name="Ford GT"></car>
      <car name="Corvette Z06"></car>
      `,
      styles: []
    })
    export class AppComponent {
      title = 'app works!';
    }
    
    
  6. 编辑模块:这不同于前面的例子。编辑 app.module.ts 并将其更改为以下内容:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent, CarComponent } from './app.component';
    import { CarService } from './car.service';
    
    @NgModule({
      declarations: [
        AppComponent, CarComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      providers: [CarService],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    

你的应用应该工作在本地主机:4200。请注意,CarService构造函数在控制台中只记录一次。这是因为它只需要在 App 模块中创建一次,就可以被所有子组件使用。

类提供者:示例依赖注入 ex350

如前所述,有三种类型的提供者:类提供者、工厂提供者和值提供者。类提供者允许我们告诉提供者哪个类用于依赖项。

图 13-9 显示了一个依赖于Watch服务的组件。

A458962_1_En_13_Fig9_HTML.jpg

图 13-9

Component relying on a Watch service

让我们看看依赖注入示例 ex350:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex350 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex350
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    
    class Watch {
      getTime(): string {
        return new Date() + "";
      }
    }
    
    class Seiko extends Watch {
      getTime(): string{
        return "Seiko Time:" + super.getTime();
    
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>
        {{watch.getTime()}}
      </h1>
      `,
      styles: [],
      providers: [{
        provide: Watch,
        useClass: Seiko
      }]
    })
    export class AppComponent {
      constructor(private watch:Watch){}
    }
    
    

你的应用应该工作在本地主机:4200。注意,当我们使用@Component注释的Provider元素来创建依赖关系时,我们指定了Watch的子类(一个精工)。

工厂提供者:示例依赖项-注入-ex400

工厂提供程序使用函数为 Angular 提供对象的实例。当您需要基于某些数据动态地更改您想要创建的对象时,这很有用。

这是一个使用日志服务的简单组件:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex400 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex400
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 创建服务类:这与前面的例子相同。创建 logging.service.ts 并将其更改为以下内容:

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class LoggingService {
        constructor(private dateAndTime: boolean){
            console.log('LoggingService: constructor');
        }
        log(message){
            console.log((this.dateAndTime ? new Date() + ': ' : '') + message);
        }
    }
    
    
  5. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { LoggingService } from './logging.service';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>
        {{title}}
      </h1>
      `,
      styles: [],
      providers: [provideLoggingService()]
    })
    export class AppComponent {
      constructor(private logging: LoggingService){
        logging.log('test log');
      }
      title = 'app works!';
    }
    export const LOGGING_USE_DATE = false;
    export function provideLoggingService() {
      return {
        provide: LoggingService,
        useFactory: () => new LoggingService(LOGGING_USE_DATE)
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意,该日志服务可以选择包含日志日期和时间。您可以使用日志记录服务的构造函数进行设置。工厂提供者用于提供日志服务的实例。

图 13-10 显示了测井包含的日期;

A458962_1_En_13_Fig10_HTML.jpg

图 13-10

Date included in logging

export const LOGGING_USE_DATE = true;
export function provideLoggingService() {
  return {
    provide: LoggingService,
    useFactory: () => new LoggingService(LOGGING_USE_DATE)
  }
}

图 13-10 显示了不包括在测井中的日期;

A458962_1_En_13_Fig11_HTML.jpg

图 13-11

Date not included in logging

export const LOGGING_USE_DATE = false;
export function provideLoggingService() {
  return {
    provide: LoggingService,
    useFactory: () => new LoggingService(LOGGING_USE_DATE)
  }
}

工厂提供者:示例依赖注入 ex500

这是一个显示扑克牌的简单组件,如图 13-12 所示。

A458962_1_En_13_Fig12_HTML.jpg

图 13-12

Displaying a playing card

让我们以依赖注入 ex500 为例:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex500 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex500
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 创建Card类:这与前面的例子相同。编辑 card.ts 并将其更改为以下内容:

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class Card {
        constructor(public suite: string, public rank: string) {}
        toString(): string {
            return "Card is " + this.rank + " of " + this.suite;
        }
    }
    
    
  5. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { Card } from './card';
    @Component({
      selector: 'app-root',
      template: `
      <h1>
        {{title}}
      </h1>
      `,
      styles: [],
      providers: [{
        provide: Card,
        useFactory: () => {
          const suite: number = Math.floor(Math.random() * 4);
          const suiteName: string =
            suite == 0 ? "Clubs" :
            suite == 1 ? "Diamonds" :
            suite == 2 ? "Hearts" : "Spades";
          const rank: number = Math.floor(Math.random() * 15);
          const rankName: string =
            rank == 0 ? "Ace" :
            rank == 1 ? "Joker" :
            rank == 2 ? "King" :
            rank == 3 ? "Queen" :
            (rank - 3).toString();
          return new Card(suiteName, rankName);
        }
      }]
    })
    
    export class AppComponent {
      title = 'app works!';
      constructor(card:Card){
        this.title = card.toString();
    
      }
    }
    
    

价值提供者:示例依赖关系-注入-ex600

您已经看到了类提供者和工厂提供者的代码和示例。现在让我们看看价值提供者。值提供者只是提供一个对象的值,如图 13-13 所示。

A458962_1_En_13_Fig13_HTML.jpg

图 13-13

Value of an object

让我们看一下依赖注入示例 ex600:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex600 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex600
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, Injector } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>
        {{title}}
      </h1>
      `,
      styles: [],
      providers: [{
        provide: 'language',
        useValue: 'en'
      }]
    })
    export class AppComponent {
      title: string = '';
      constructor(private injector: Injector){
        this.title = 'Language is: ' + injector.get('language');
      }
     }
    
    

注射器 API

您还不需要非常详细地了解注射器 API。如果你在本章的这一点上感到不知所措,请随意跳到下一章,稍后再回到这一章。

然而,如果您想要对创建依赖关系有更多的控制,您可以直接访问Injector对象。Injector是角芯包中的一类。它是一个依赖注入容器,用于实例化对象和解析依赖关系。

如果您试图解析和创建(使用Injector)的类本身有依赖关系,Injector会自动尝试为您解析和创建这些依赖关系。你也可以使用Provider类中的附加选项。

这里有一个例子:

import { Injector } from '@angular/core';

const injector = Injector.resolveAndCreate([Car, Engine, Tires, Doors]);

const car = injector.get(Car);

另一个例子是:

import { Injector } from '@angular/core';

const injector = Injector.resolveAndCreate(
  [
    provide(Car, useClass: Car)),
    provide(Engine, useClass: Engine)),
    provide(Tires, useClass: Tires)),
    provide(Doors, useClass: Doors))
  ]
);

const car = injector.get(Car);

摘要

本章涵盖了很多内容,从依赖注入的概念一直到Injector本身。此时,您不需要了解关于注射器 API 的一切。

您需要知道的是 Angular 中依赖注入的基础——如何设置您的提供者和使用构造器注入。我希望您遵循了这些示例,并且它们帮助您理解依赖注入是如何工作的。

通过依赖注入来使用服务是你会一直用到的。大多数情况下,您的应用将使用每个服务的一个实例。每个服务有一个实例非常有用,因为您可以使用这些服务来保存多个组件访问的状态信息(例如客户列表)。此外,有时您将使用单实例服务作为组件之间的“通信”桥梁。

在下一章,我们将开始讨论第三方部件库。当大公司为你提供经过良好测试和设计精美的 UI 小部件库时,为什么要花费宝贵的时间来开发定制的外观呢?