Angular-专家级编程-二-

45 阅读1小时+

Angular 专家级编程(二)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:使用组件

在本章中,我们将讨论使用 Angular 组件的不同技术和策略:

  • 初始化和配置组件

  • 构建组件

  • 组件生命周期

  • 数据共享和组件间通信

本章假设读者具有 JavaScript 和 TypeScript 编程基础以及网页开发的知识,并熟悉本书中的第一章*,* Angular 中的架构概述和构建简单应用的内容。本章中的所有示例都使用 TypeScript,并且也可以在 GitHub 上找到,网址为github.com/popalexandruvasile/mastering-angular2/tree/master/Chapter4

一个成功的开源项目的一个明显标志是出色的文档,Angular 也不例外。我强烈建议阅读来自angular.io/的所有可用文档,并在那里跟随可用的示例。作为一个一般规则,本章中的所有示例都遵循官方文档的格式和约定,我使用了来自github.com/angular/quickstart的 Angular 示例种子的简化版本作为示例。如果你想要尝试或玩自己的 Angular 创作,你可以使用本章代码中Example1文件夹的内容作为起点。

组件 101

组件是 Angular 应用程序的构建块,任何这样的应用程序在执行之前都需要至少定义一个称为根组件的组件。

基本根组件

在 Angular 中,组件被定义为一个具有特定元数据的类,将其与 HTML 模板和类似于 jQuery 的 HTML DOM 选择器相关联:

  • 组件模板可以绑定到属于组件类的任何属性或函数

  • 组件选择器(类似于 jQuery 选择器)可以针对定义组件插入点的元素标签、属性或样式类进行定位。

在 Angular 应用程序中执行时,组件通常会在特定页面位置呈现 HTML 片段,可以对用户输入做出反应并显示动态数据。

组件元数据表示为 TypeScript 装饰器,并支持本章中示例中将介绍的其他配置。

TypeScript装饰器在第一章中有介绍,Angular 中的架构概述和构建简单应用程序。它们对于理解组件如何配置至关重要,并且目前已经提议成为 JavaScript 规范(ECMAScript)的一部分。

本章的第一个示例是一个基本组件,也是一个根组件(任何 Angular 应用程序都至少需要一个根组件来初始化其组件树):

import { Component } from '@angular/core'; 
@Component({ 
    selector: 'my-app', 
    template: ` 
    <div class="container text-center"> 
      <div class="row"> 
        <div class="col-md-12"> 
          <div class="page-header"> 
            <h1>{{title}}</h1> 
          </div> 
          <p class="lead">{{description}}</p> 
        </div> 
      </div> 
      <div class="row"> 
        <div class="col-md-6"> 
          <p>A child component could go here</p> 
        </div> 
        <div class="col-md-6"> 
          <p>Another child component could go here</p> 
        </div> 
      </div>           
    </div>     
    ` 
}) 
export class AppComponent {  
  title: string; 
  description: string; 
  constructor(){ 
    this.title = 'Mastering Angular - Chapter 4, Example 1'; 
    this.description = 'This is a minimal example for an Angular 2   
    component with an element tag selector.'; 
  } 
} 

组件模板依赖于 Bootstrap 前端设计框架(getbootstrap.com/)进行样式设置,并且绑定到组件类的属性以检索一些显示的文本。它包含模板表达式,用于从组件类的属性中插值数据,例如{{title}}

根组件使用内联模板(模板内容与其组件在同一文件中)和一个元素选择器,该选择器将在index.html页面中呈现组件模板,替换高亮文本:

<!DOCTYPE html> 
<html> 
  <head> 
    <title>Mastering Angular example</title> 
    ... 
  </head> 
  <body> 
    <my-app>Loading...</my-app> 
  </body> 
</html>    

要查看示例的实际效果,您可以在本章的源代码中的Example1文件夹中运行以下命令行:

npm run start  

您可以在下一个截图中查看呈现的组件:

Angular 应用程序至少需要一个根模块,在main.ts文件中,我们正在为我们的示例引导这个模块:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 
import { AppModule } from './app.module'; 
platformBrowserDynamic().bootstrapModule(AppModule);  

我们使用app.module.ts模块文件来定义应用程序的根模块:

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

模块可以使用imports属性导入其他模块,并且模块可以在bootstrap属性下定义一个或多个根组件。在我们的示例中,每个这样的根组件都将初始化其自己的组件树,该组件树仅包含一个组件。在模块中使用任何组件、指令或管道之前,都需要将其添加到declarations属性中。

定义子组件

虽然根组件代表 Angular 应用程序的容器,但您还需要其他直接或间接是根组件后代的组件。当呈现根组件时,它还将呈现其所有子组件。

这些子组件可以从其父组件接收数据,也可以发送数据回去。让我们在一个更复杂的示例中看到这些概念的运作,这个示例是在前一个示例的基础上构建的。请注意,在Example1中,我们建议子组件可以插入到根组件模板中;这样的一个子组件定义如下:

import { Component, Input, Output, EventEmitter } from '@angular/core'; 
@Component({ 
    selector: 'div[my-child-comp]', 
    template: ` 
        <p>{{myText}}</p> 
        <button class="btn btn-default" type="button" (click)="onClick()">Send message</button>` 
}) 
export class ChildComponent {  
  private static instanceCount: number = 0; 
  instanceId: number; 
  @Input() myText: string; 
  @Output() onChildMessage = new EventEmitter<string>();   
  constructor(){ 
    ChildComponent.instanceCount += 1; 
    this.instanceId = ChildComponent.instanceCount; 
  } 
  onClick(){ 
    this.onChildMessage.emit(`Hello from ChildComponent with instance  
    id: ${this.instanceId}`); 
  } 
} 

第一个突出显示的代码片段显示了组件选择器使用自定义元素属性而不是自定义元素标记。在使用现有的 CSS 样式和 HTML 标记时,往往需要确保你的 Angular 组件与其上下文的外观和感觉自然地集成。这就是属性或 CSS 选择器真正有用的地方。

乍一看,组件类结构看起来与Example1中的类似--除了第二个突出显示的代码片段中的两个新装饰器。第一个装饰器是@Input(),应该应用于可以从父组件接收数据的任何组件属性。第二个装饰器是@Output(),应该应用于可以向父组件发送数据的任何属性。Angular 2 定义了一个EventEmitter类,它使用类似 Node.js EventEmitter或 jQuery 事件的方法来生成和消费事件。string类型的输出事件是在onClick()方法中生成的,任何父组件都可以订阅这个事件来从子组件接收数据。

EventEmitter 类扩展了 RxJS Subject 类,而 RxJS Subject 类又是 RxJS Observable 的一种特殊类型,允许多播。关于可观察对象、订阅者和其他响应式编程概念的更多细节可以在第七章 使用可观察对象进行异步编程中找到。

我们利用了 TypeScript 中的static类属性来生成一个唯一的实例标识符instanceId,该标识符在子组件通过onChildMessage输出属性发送的消息中使用。我们将使用这条消息来明显地表明每个子组件实例向其订阅者发送一个唯一的消息,这在我们的示例中是AppComponent根组件。

@Component({ 
    selector: 'div.container.my-app', 
    template: ` 
    <div class="container text-center"> 
      <div class="row"><div class="col-md-12"> 
          <div class="page-header"><h1>{{title}}</h1></div> 
          <p class="lead">{{description}}</p> 
      </div></div> 
      <div class="row"> 
        <div class="col-md-6" my-child-comp myText="A child component 
 goes here" (onChildMessage)="onChildMessageReceived($event)"> 
 </div>       
        <div class="col-md-6" my-child-comp 
 [myText]="secondComponentText" 
 (onChildMessage)="onChildMessageReceived($event)"></div>          
        </div> 
      <div class="row"><div class="col-md-12"><div class="well well-
       sm">          
            <p>Last message from child components: <strong> 
               {{lastMessage}}</strong>
            </p> 
           </div></div></div>           
    </div> 
}) 
export class AppComponent {  
  title: string; 
  description: string; 
  secondComponentText: string; 
  lastMessage: string; 
  constructor(){ 
    this.title = 'Mastering Angular - Chapter 4, Example 2'; 
    this.description = 'This is an example for an Angular 2 root   
    component with an element and class selector and a child component 
    with an element attribute selector.'; 
    this.secondComponentText = 'Another child component goes here'; 
  } 

  onChildMessageReceived($event: string) 
  { 
    this.lastMessage = $event; 
  } 
} 

突出显示的代码显示了根组件如何引用和绑定ChildComponent元素。onChildMessage输出属性绑定到AppComponent方法,使用与 Angular 2 用于绑定原生 HTML DOM 事件相同的括号表示法;例如,<button (click)="onClick($event)">

输入属性只是为第一个ChildComponent实例分配了一个静态值,并通过括号表示法绑定到AppComponentsecondComponentText属性。当我们仅分配固定值时,不需要使用括号表示法,Angular 2 在绑定到原生 HTML 元素属性时也会使用它;例如,<input type="text" [value]="myValue">

如果您还不熟悉 Angular 如何绑定到原生 HTML 元素属性和事件,您可以参考第六章,创建指令和实现变更检测,以供进一步参考。

对于两个ChildComponent实例,我们使用相同的AppComponentonChildMessageReceived方法,使用简单的事件处理方法绑定到onChildMessage事件,这将在应用程序页面上显示最后一个子组件消息。根组件选择器被更改为使用元素标签和 CSS 类选择器,这种方法导致index.html文件结构更简单。

我们必须修改AppModule的定义,以确保ChildComponent可以被AppComponent和同一模块中的任何其他组件引用:

@NgModule({ 
  imports:      [ BrowserModule ], 
  declarations: [ AppComponent, ChildComponent ], 
  bootstrap:    [ AppComponent ] 
}) 
export class AppModule { } 

您可以在本章的代码中的Example2文件夹中找到此示例。本文涵盖的概念,如组件属性和事件、组件数据流和组件组合,在构建相对复杂的应用程序方面可以发挥重要作用,我们将在本章中进一步探讨它们。

除了组件,Angular 还有指令的概念,这在 Angular 1 中也可以找到。每个 Angular 组件也是一个指令,我们可以粗略地将指令定义为没有任何模板的组件。@Component装饰器接口扩展了@Directive装饰器接口,我们将在第六章中更多地讨论指令,创建指令和实现变更检测

组件生命周期

Angular 渲染的每个组件都有自己的生命周期:初始化、检查变化和销毁(以及其他事件)。Angular 提供了一个hook方法,我们可以在其中插入应用代码以参与组件生命周期。这些方法通过 TypeScript 函数接口提供,可以选择性地由组件类实现,它们如下:

  • ngOnChanges:在数据绑定的组件属性在ngOnInit之前初始化一次,并且每次数据绑定的组件属性发生变化时都会被调用。它也是指令生命周期的一部分(约定是接口实现函数名加上ng前缀,例如ngOnInitOnInit)。

  • ngOnInit:在第一次ngOnChanges之后调用一次,当数据绑定的组件属性和输入属性都被初始化时调用。它也是指令生命周期的一部分。

  • ngDoCheck:作为 Angular 变化检测过程的一部分被调用,应用于执行自定义变化检测逻辑。它也是指令生命周期的一部分。

  • ngAfterContentInit:在第一次调用ngDoCheck之后调用一次,当组件模板完全初始化时调用。

  • ngAfterContentChecked:在ngAfterContentInit之后和每次ngDoCheck调用后都会被调用,用于验证组件内容。

  • ngAfterViewInit:在第一次ngAfterContentChecked之后调用一次,当所有组件视图及其子视图都被初始化时调用。

  • ngAfterViewChecked:在ngAfterViewInit之后和每次ngAfterContentChecked调用后都会被调用,用于验证所有组件视图及其子视图。

  • ngOnDestroy:当组件即将被销毁时调用,应用于清理操作;例如,取消订阅可观察对象和分离事件。

我们将调整我们之前的示例来展示一些这些生命周期hook,并且我们将使用一个父组件和一个子组件,它们要么显示要么记录所有它们的生命周期事件到控制台。直到组件完全加载的事件触发将被清晰地显示/记录,如下截图所示:

父组件的代码与子组件的代码非常相似,子组件有一个按钮,可以根据需要向父组件发送消息。当发送消息时,child组件和父组件都会响应由 Angular 的变更检测机制生成的生命周期事件。您可以在本章的源代码中的Example3文件夹中找到child.component.ts文件中的子组件代码。

import {Component, Input, Output, EventEmitter, OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked} from '@angular/core'; 
@Component({ 
  selector: 'div[my-child-comp]', 
  template: ` 
  <h2>These are the lifecycle events for a child component:</h2> 
  <p class="lead">Child component initial lifecycle events:</p> 
  <p>{{initialChildEvents}}</p> 
  <p class="lead">Child component continuous lifecycle events:</p> 
  <p>{{continuousChildEvents}}</p> 
  <button class="btn btn-default" type="button" (click)="onClick()">Send message from child to parent</button>` 
}) 
export class ChildComponent implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked { 
  initialChildEvents: string[]; 
  continuousChildEvents: string[]; 
  @Output() onChildMessage = new EventEmitter<string>(); 
  private hasInitialLifecycleFinished: boolean = false; 
  private ngAfterViewCheckedEventCount: number = 0; 
  constructor() { 
    this.initialChildEvents = []; 
    this.continuousChildEvents = []; 
  } 
  private logEvent(message: string) { 
        if (!this.hasInitialLifecycleFinished) { 
            this.initialChildEvents.push(message); 
        } else { 
            this.continuousChildEvents.push(message); 
        } 
    } 
  ngOnChanges(): void { 
    this.logEvent(` [${new Date().toLocaleTimeString()}]-ngOnChanges`); 
  } 
  ngOnInit(): void { 
    this.logEvent(` [${new Date().toLocaleTimeString()}]-ngOnInit`); 
  } 
  ngDoCheck(): void { 
    this.logEvent(` [${new Date().toLocaleTimeString()}]-ngDoCheck`); 
  } 
  ngAfterContentInit(): void { 
    this.logEvent(` [${new Date().toLocaleTimeString()}]-
    ngAfterContentInit`); 
  } 
  ngAfterContentChecked(): void { 
    this.logEvent(` [${new Date().toLocaleTimeString()}]-
    ngAfterContentChecked`); 
  } 
  ngAfterViewInit(): void { 
    console.log(`child: [${new Date().toLocaleTimeString()}]-
    ngAfterViewInit`); 
  } 
  ngAfterViewChecked(): void { 
    this.ngAfterViewCheckedEventCount += 1; 
    if (this.ngAfterViewCheckedEventCount === 2) { 
      this.hasInitialLifecycleFinished = true; 
    } 
    console.log(`child: [${new Date().toLocaleTimeString()}]-
    ngAfterViewChecked`); 
  } 
  onClick() { 
    this.onChildMessage.emit(`Hello from ChildComponent at: ${new 
    Date().toLocaleTimeString()}`); 
  } 
} 

ng开头的所有方法都是组件生命周期钩子,当触发时,大多数方法都会记录存储在组件中并通过数据绑定显示的事件(请参阅上一个代码清单中的突出显示的代码片段)。生命周期钩子中的两个--ngAfterViewInitngAfterViewChecked--会将事件记录到控制台,而不是将其存储为组件数据,因为在组件生命周期的那一点上组件状态的任何更改都会在 Angular 应用程序中生成异常。例如,让我们将ngAfterViewInit方法体更改为以下内容:

ngAfterViewInit(): void { 
    this.logEvent(` [${new Date().toLocaleTimeString()}]-
    ngAfterViewInit); 
} 

如果您查看应用程序页面浏览器控制台,在进行更改后,您应该会看到此错误消息:

表达在检查后已经改变。

在示例的初始运行中,ngDoCheckngAfterContentChecked方法(如果查看浏览器控制台输出,则还有ngAfterViewChecked)在任何用户交互之前已经为每个组件触发了两次。此外,每次按下示例按钮时,相同的三种方法都会被触发,每个组件一次。在实践中,除了编写更高级的组件或组件库之外,您可能很少使用这些生命周期钩子,除了ngOnChangesngOnInitngAfterViewInit。我们将在第六章中重新讨论这些核心生命周期钩子,创建指令和实现变更检测,因为它们在表单和其他交互式组件的上下文中非常有用。

在组件之间进行通信和共享数据

我们已经使用了最简单的方法来在组件之间通信和共享数据:InputOutput装饰器。使用Input装饰器装饰的属性通过传递数据来初始化组件,而Output装饰器可以用于分配事件监听器,以接收组件外部的数据。这种方法可以在本章源代码中的Example2文件夹中找到的组件中观察到。

从父组件引用子组件

我们可以通过模板引用变量或通过使用ViewChildViewChildren属性装饰器将目标组件注入到父组件中,来绕过声明性绑定到组件属性和事件。在这两种情况下,我们都可以获得对目标组件的引用,并且可以以编程方式分配其属性或调用其方法。为了演示这些功能的实际应用,我们将稍微修改Example2中的ChildComponent类,并确保myText属性具有默认文本设置。这可以在本章源代码中的Example4文件夹中找到的child.component.ts文件中的突出显示的代码片段中看到。

... 
export class ChildComponent {  
  private static instanceCount: number = 0;  
  instanceId: number; 
  @Input() myText: string; 
  @Output() onChildMessage = new EventEmitter<string>(); 

  constructor(){ 
    ChildComponent.instanceCount += 1; 
    this.instanceId = ChildComponent.instanceCount; 
    this.myText = 'This is the default child component text.'; 
  } 

  onClick(){ 
    this.onChildMessage.emit(`Hello from ChildComponent with instance 
    id: ${this.instanceId}`); 
  } 
} 

然后,我们将更改app.component.ts文件,以包括模板引用方法来处理第一个子组件和组件注入方法来处理第二个子组件:

import { Component, ViewChildren, OnInit, QueryList } from '@angular/core'; 
import { ChildComponent } from './child.component'; 
@Component({ 
    selector: 'div.container.my-app', 
    template: ` 
    <div class="container text-center"> 
      <div class="row"><div class="col-md-12"> 
          <div class="page-header"><h1>{{title}}</h1></div> 
          <p class="lead">{{description}}</p>           
      </div></div> 
      <div class="row"> 
        <div class="col-md-6"> 
          <button class="btn btn-default" type="button" 
 (click)="firstChildComponent.myText='First child component 
 goes here.'">Set first child component text</button> 
          <button class="btn btn-default" type="button" 
 (click)="firstChildComponent.onChildMessage.subscribe(onFirstChildComp
 onentMessageReceived)">Set first child component message 
 output</button> 
         </div>       
         <div class="col-md-6"> 
        <button class="btn btn-default" type="button" 
 (click)="setSecondChildComponentProperties()">Set second 
 child component properties</button> 
         </div>          
         </div>       
      <div class="row"> 
      <div class="col-md-6 well well-sm" my-child-comp 
 #firstChildComponent></div>       
        <div class="col-md-6 well well-sm" my-child-comp 
 id="secondChildComponent"></div>       
      </div> 
      <div class="row"><div class="col-md-12"><div class="well well-
      sm">          
            <p>Last message from child components: <strong>
            {{lastMessage}}</strong></p> 
      </div></div></div>           
    </div>` 
}) 
export class AppComponent {  
  title: string; 
  description: string; 
  lastMessage: string; 
  @ViewChildren(ChildComponent) childComponents: 
  QueryList<ChildComponent>; 
  constructor(){ 
    this.title = 'Mastering Angular - Chapter 4, Example 4'; 
    this.description = 'This is an example for how to reference 
    existing components from a parent component.'; 
    this.lastMessage = 'Waiting for child messages ...'; 
  } 
  onFirstChildComponentMessageReceived($event: string) 
  { 
    alert($event); 
  }   
  setSecondChildComponentProperties(){     
    this.childComponents.last.myText = "The second child component goes 
    here."; 
    this.childComponents.last.onChildMessage.subscribe( (message: 
    string) => {  
      this.lastMessage = message + ' (the message will be reset in 2 
      seconds)'; 
      setTimeout( ()=>{ this.lastMessage = 'Waiting for child messages 
      ...';}, 2000); 
    }); 
  } 
} 

首先,第三个突出显示的 HTML 片段中的两个子组件没有任何属性或事件绑定。第一个子组件有一个#firstChildComponent属性,它代表一个模板引用变量。

模板引用变量

模板引用变量可以在 Angular 模板中针对任何组件、指令或 DOM 元素进行设置,并且将该引用可用于当前模板。在前面示例中的第一个突出显示的 HTML 片段中,我们有两个按钮,它们使用内联 Angular 表达式来设置myText属性,并通过firstChildComponent模板引用变量绑定到onChildMessage事件。运行示例时,如果我们单击“设置第一个子组件文本”按钮,然后单击“设置第一个子组件消息输出”按钮,我们将通过模板引用变量直接操作第一个子组件,就像在之前示例中的第一个突出显示的 HTML 片段中所看到的那样。这种方法适用于初始化和读取组件属性,但在需要绑定到组件事件时,它被证明是繁琐的。

模板引用变量无法在组件类中访问;因此,我们的做法是绑定到第一个子组件事件。然而,在处理表单时,这种类型的变量将非常有用,我们将在第六章中重新讨论它们,创建指令和实现变更检测

注入子组件

对于第二个子组件,我们使用了一种基于在app.component.ts文件中的属性声明中注入组件的技术:

@ViewChildren(ChildComponent) childComponents: QueryList<ChildComponent>; 

ViewChildren装饰器采用了ChildComponent类型的选择器,该选择器将从父组件模板中识别和收集所有ChildComponent实例,并将其放入QueryList类型的专门列表中。这个列表允许迭代子组件实例,我们可以在AppComponent.setSecondChildComponentProperties()方法中使用QueryList.Last()调用来获取第二个子组件的引用。当运行本章源代码中Example4文件夹中找到的代码时,如果单击“设置第二个子组件属性”按钮,前一个代码清单中的第二个 HTML 片段将开始运行。

注入子组件是一种多才多艺的技术,我们可以以更高效的方式从父组件代码中访问引用的组件。

使用服务与组件

现在,我们将再次演变Example2,并将一些在组件级别定义的代码重构为 Angular 服务。

服务是一个 TypeScript 类,它有一个名为Injectable的装饰器,没有任何参数,允许服务成为 Angular 2 中依赖注入(DI)机制的一部分。DI 将确保每个应用程序只创建一个服务实例,并且该实例将被注入到任何声明它为依赖项的类的构造函数声明中。除了特定的装饰器之外,服务通常需要在模块定义中声明为提供者,但也可以在组件、指令或管道定义中声明。在跳转到本节的示例之前,您可以在第十二章中找到有关服务的更多信息,实现 Angular 服务

即使一个服务没有其他依赖,也最好确保它被装饰为可注入的,以防将来有依赖,并简化其在作为依赖项时的使用。

对于我们的示例,我们将在Example2代码的基础上构建一个新示例,该示例可以在本章的源代码中的Example4文件夹中找到。我们将首先将父组件和child组件的大部分逻辑提取到一个新的服务类中:

import {Injectable,EventEmitter} from '@angular/core'; 
@Injectable() 
export class AppService { 
  private componentDescriptions: string[]; 
  private componentMessages: string[]; 
  public appServiceMessage$ = new EventEmitter <string> (); 
  constructor() { 
    this.componentDescriptions = [ 
      'The first child component goes here', 
      'The second child component goes here' 
    ]; 
    this.componentMessages = []; 
  } 
  getComponentDescription(index: number): string { 
    return this.componentDescriptions[index]; 
  } 
  sendMessage(message: string): void { 
    this.componentMessages.push(message); 
    this.appServiceMessage$.emit(message); 
  } 
  getComponentMessages(): string[] { 
    return this.componentMessages; 
  } 
} 

该服务将用于存储componentDescriptions数组中由子组件使用的描述,并通过sendMessage()方法提供消息处理程序,该方法还将任何处理过的消息存储在AppService.componentMessages属性中。Example2child组件的onChildMessage属性现在移动到AppService.appServiceMessage$,并且可以供任何需要它的组件或服务使用。child组件的定义现在大大简化了。

import {Component, Input, Output, EventEmitter, OnInit} from '@angular/core'; 
import {AppService} from './app.service'; 

@Component({ 
  selector: 'div[my-child-comp]', 
  template: ` 
        <p>{{myText}}</p> 
        <button class="btn btn-default" type="button" 
        (click)="onClick()">Send message</button>` 
}) 
export class ChildComponent implements OnInit { 
  @Input() index: number; 
  myText: string; 
  constructor(private appService: AppService) {} 
  ngOnInit() { 
    this.myText = this.appService.getComponentDescription(this.index); 
  } 

  onClick() { 
    if (this.appService.getComponentMessages().length > 3) { 
      this.appService.sendMessage(`There are too many messages ...`); 
      return; 
    } 
    this.appService.sendMessage(`Hello from ChildComponent with index: 
    ${this.index}`); 
  } 
} 

Child组件的消息现在通过AppServicesendMessage()方法发送。此外,唯一的@Input()属性称为index,它存储了用于通过AppService.getComponentDescription()方法设置myText属性的组件索引。除了index属性之外,ChildComponent类完全依赖于AppService来读取和写入数据。

AppComponent类现在几乎没有逻辑,虽然它显示了AppService实例提供的所有消息,但它还在ngOnInit方法中注册了一个自定义订阅,用于存储最后接收到的消息。AppService.appServiceMessage$属性是EventEmitter类型,为任何对消费此事件感兴趣的其他 Angular 类提供了一个公共订阅:

import { Component, OnInit } from '@angular/core'; 
import { AppService } from './app.service'; 
@Component({ 
    selector: 'div.container.my-app', 
    template: `<div class="container text-center"> 
      <div class="row"><div class="col-md-12"> 
          <div class="page-header"><h1>{{title}}</h1></div> 
          <p class="lead">{{description}}</p> 
      </div></div> 
      <div class="row"> 
        <div class="col-md-6 well" my-child-comp index="0"></div>       
        <div class="col-md-6 well" my-child-comp index="1"></div>          
      </div> 
      <div class="row"><div class="col-md-12"><div class="well well-
       sm"> 
            <p><strong>Last message received:</strong> 
             {{lastMessageReceived}}</p> 
            <p><strong>Messages from child components:</strong> 
            {{appService.getComponentMessages()}}</p> 
       </div></div></div>           
    </div>` 
}) 
export class AppComponent implements OnInit {  
  title: string; 
  description: string; 
  lastMessageReceived: string; 
  constructor(private appService: AppService){ 
    this.title = 'Mastering Angular - Chapter 4, Example 4'; 
    this.description = 'This is an example of how to communicate and 
    share data between components via services.';     
  }  
  ngOnInit(){ 
    this.appService.appServiceMessage$.subscribe((message:string) => { 
      this.lastMessageReceived = message; 
    }); 
  } 
} 

在这个例子中,我们从一个依赖@Input()属性来获取所需数据的ChildComponent类开始;我们转而使用一个只需要一个键值来从服务类获取数据的类。编写组件的两种风格并不互斥,使用服务可以进一步支持编写模块化组件。

总结

在本章中,我们首先看了一个基本的组件示例,然后探讨了父子组件。对组件生命周期的了解之后,我们举例说明了如何在组件之间进行通信和共享数据。

第五章:实现 Angular 路由和导航

应用程序导航是任何网站或应用程序的核心功能之一。除了定义路由或路径之外,导航还帮助用户到达应用程序页面,探索功能,并且对于 SEO 目的也非常有用。

在本章中,您将学习有关 Angular 路由和导航的所有内容。以下是我们将在路由和导航中学习和实现的功能的详细列表。

您将学习以下路由和导航方面:

  • 导入和配置路由器

  • 在视图中启用路由出口、routerLinkrouterLinkActivebase href

  • 自定义组件路由和子路由

  • 具有内部子路由的自定义组件路由--同一页面加载

  • 演示应用程序的路由和导航

在本章结束时,我们将能够做到以下事情:

  • 为应用程序创建app.routes并设置所需的模块

  • 实现并启用RouterModule.forRoot

  • 定义路由出口和routerLink指令以绑定路由路径

  • 启用RouterLinkActivated以查找当前活动状态

  • 了解路由状态的工作原理

  • 了解并实现路由生命周期钩子

  • 创建自定义组件路由和子路由

  • 为我们的 Web 应用程序实现位置策略

  • 创建一个示例应用程序路由和导航

首先,让我们看一下我们将在本章开发的演示应用程序的路由和导航:

作为演示应用程序的一部分,我们将为“关于我们”、“服务”和“产品”组件开发路由。

服务组件将具有内部子路由。产品组件将使用ActivatedRoute来获取路由params。我们还将使用 JavaScript 事件onclick来实现导航。

导入和配置路由器

为了定义和实现导航策略,我们将使用路由器和RouterModule

我们需要更新我们的app.module.ts文件以执行以下操作:

  • 从 Angular 路由器模块导入RouterModule和路由

  • 导入应用程序组件

  • 定义具有路径和组件详细信息的路由

  • 导入RouterModule.forRootappRoutes

每个路由定义可以具有以下键:

  • path:我们希望在浏览器地址栏中显示的 URL。

  • component:将保存视图和应用程序逻辑的相应组件。

  • redirectTo(可选):这表示我们希望用户从此路径重定向的 URL。

  • pathMatch(可选):重定向路由需要pathMatch--它告诉路由器如何将 URL 与路由的路径匹配。pathMatch可以取fullprefix的值。

现在我们将在我们的NgModule中导入和配置路由器。看一下更新的app.module.ts文件,其中包含了路由器的完整实现:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';

import { AppComponent } from './app.component';
import { AboutComponent} from './about.component';
import { ServicesComponent} from './services.component';
import { ProductsComponent } from './products.component';

const appRoutes: Routes = [
 { path: 'about', component: AboutComponent },
 { path: 'services', component: ServicesComponent }, 
 { path: 'products', redirectTo:'/new-products', pathMatch:'full'},
 { path: '**', component: ErrorPageNotFoundComponent }
];

@NgModule({
 imports: [
 BrowserModule,
 FormsModule,
 RouterModule.forRoot(appRoutes)
 ],
 declarations: [
  AppComponent,
  AboutComponent,
  ServicesComponent,
  ProductsComponent,
 ],
 bootstrap: [ AppComponent ]
})
export class AppModule { }  

让我们分析上述代码片段:

  1. 我们从@angular/router导入RoutesRouterModule

  2. 我们从各自的 Angular 库中导入所需的模块NgModuleBrowserModuleFormsModule

  3. 我们正在导入自定义定义的组件--AboutServicesProducts

  4. 我们在appRoutes中定义了一个常量,其中我们为我们的组件指定了路径。

  5. 我们通过appRoutes创建我们的路由,并通过传递各种参数为各种 URL 路由链接定义自定义路径。

现在我们已经学会了如何导入和配置我们的NgModule来实现路由,在下一节中我们将学习路由器的构建模块。

路由器的构建模块

在本节中,您将学习路由器的重要构建模块。重要的构建模块包括base hrefRouter OutletrouterLinkrouterLinkActive

现在让我们分析路由器库的每个构建模块:

  • base href:我们必须在index.html页面中设置base指令。这是一个强制性步骤。没有base标签,浏览器可能无法在深度链接到应用程序时加载资源(图像、CSS 和脚本)。

在我们的应用程序中,我们需要在index.html文件的<head>标签中定义base href

<base href="/“>

  • 定义 router-outletrouter-outlet指令是包含视图加载数据的占位符。在router-outlet指令内,组件视图将被加载和显示。将该指令放在app.component.html模板中以呈现数据:
<router-outlet></router-outlet> 

  • 使用多个 router-outlet:在某些情况下,我们希望将数据加载到不同的视图容器而不是我们的router-outlet中。我们可以轻松地向页面添加多个 Router Outlets 并为它们分配名称,以便我们可以在其中呈现相应的数据:
<router-outlet></router-outlet> <router-outlet  name="content-farm"></router-outlet>

要加载视图数据到命名的router-outlet中,我们在定义路由时定义键:

 {   path:  'content', component: ContentFarmComponent, outlet:  'content- farm'
  }

  • 创建 RouterLink:这表示 URL 或链接地址可以直接从浏览器地址栏中到达。绑定并关联一个链接路径与锚点标签:例如,/about/products

绑定和关联锚点标签的一般语法如下:

<a [routerLink]="['/about']">About Us</a>
<a [routerLink]="['/products']">Products</a>
<a [routerLink]="['/services']">Services</a>

  • RouterLinkActive 用于活动状态链接routerLinkActive用于突出显示当前活动链接。使用routerLinkActive,我们可以轻松地突出显示当前活动的链接,以更好地适应我们应用程序的外观和感觉:
<a [routerLink]="['/about']" routerLinkActive = 
       “active-state">About Us</a>

在样式表中,添加我们的自定义样式类active-state

  • 构建动态 routerLink:我们可以通过将它们与routerLink指令绑定来传递动态值或参数以传递自定义数据。

通常,在大多数应用程序中,我们使用唯一标识符对数据进行分类--例如,http://hostname/product/10将被写成如下形式:

<a [routerLink]="['/product', 10]">Product 10</a>

同样的前面的代码可以在我们的模板视图中动态呈现:

<a [routerLink]="['/product', product.id]">Product 10</a>

  • 使用 routerLink 指令传递数组和数据集:我们可以通过routerLink传递数据数组。
 <a [routerLink]="['/contacts', { customerId: 10 }]">Crisis 
    Center</a>

关于路由器 LocationStrategy

我们需要定义应用程序的 URL 行为。根据应用程序的偏好,我们可以自定义 URL 应该如何呈现。

使用LocationStrategy,我们可以定义我们希望应用程序路由系统如何行为。

Angular 通过LocationStrategy提供了两种我们可以在应用程序中实现的路由策略。让我们了解一下我们可以在 Angular 应用程序中使用的不同路由策略选项:

  • PathLocationStrategy:这是默认的 HTML 样式路由机制。

应用PathLocationStrategy是常见的路由策略,它涉及在每次检测到更改时向服务器端发出请求/调用。实现此策略将允许我们创建清晰的 URL,并且也可以轻松地标记 URL。

使用PathLocationStrategy的路由示例如下:

http://hostname/about 

  • HashLocationStrategy: 这是哈希 URL 样式。在大多数现代 Web 应用程序中,我们看到哈希 URL 被使用。这有一个重大优势。

#后的信息发生变化时,客户端不会发出服务器调用或请求;因此服务器调用较少:

http://hostname/#/about

  • 在我们的应用程序中定义和设置LocationStrategy:在app.module.ts文件的providers下,我们需要传递LocationStrategy并告诉路由器使用HashLocationStrategy作为useClass

app.module.ts中,导入并使用LocationStrategy并说明我们要使用HashLocationStategy,如下所示:

@NgModule({
  imports: [
  BrowserModule,
  routing
 ],
 declarations: [
  AppComponent
 ],
 bootstrap: [
  AppComponent
 ],
 providers: [
  {provide: LocationStrategy, useClass: HashLocationStrategy }
 ]
})
export class AppModule { }

在上述代码中,我们在我们的提供者中注入了LocationStrategy,并明确告知 Angular 使用HashLocationStrategy

默认情况下,Angular 路由器实现PathLocationStrategy

处理错误状态-通配符路由

我们需要为找不到页面或 404 页面设置错误消息。我们可以使用ErrorPageNotFoundComponent组件来显示找不到页面或路由器未知路径的错误消息:

const appRoutes: Routes = [
 { path: 'about', component: AboutComponent },
 { path: 'services', component: ServicesComponent }, 
 { path: 'old-products', redirectTo:'/new-products', pathMatch:'full'},
 { path: '**', component: ErrorPageNotFoundComponent },
 { path:  'content', component: ContentFarmComponent, outlet:  'content-
    farm'  }
];

在这个阶段,有关如何使用路由器的各个方面的所有信息,让我们将它们全部添加到我们的app.component.ts文件中:

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

@Component({
 selector: 'my-app',
 template: `
 <h2>Angular2 Routing and Navigation</h2>
 <div class="">
 <p>
   <a routerLink="/about" routerLinkActive="active"> About Us</a> |
   <a routerLink="/services" routerLinkActive="active" > Services</a> |
   <a routerLink="/products" routerLinkActive="active"> Products</a>
 </p>
 <div class="app-data">
  <router-outlet></router-outlet>
 </div> 
 </div>`,
  styles: [`
    h4 { background-color:rgb(63,81,181);color:#fff; padding:3px;}
    h2 { background-color:rgb(255, 187, 0);color:#222}
    div {padding: 10px;}
    .app-data {border: 1px solid #b3b3b3;}
    .active {color:#222;text-decoration:none;}
    `
   ],
 encapsulation: ViewEncapsulation.None
})
export class AppComponent {
}

让我们分析上述代码并将其分解为关键功能:

  • 我们定义了routerLink属性,以便在用户点击锚链接时启用导航

  • 我们实现了routerLinkActive属性以突出显示当前/活动链接,也就是用户点击的链接

  • 我们为<router-outlet>定义了一个占位符,它将保存来自不同视图的数据--具体取决于点击了哪个链接

现在,当我们启动应用程序时,我们将看到以下结果输出:

太棒了!到目前为止,一切都很好。现在让我们添加路由守卫功能。

在下一节中,我们将学习如何集成路由守卫以在各个组件之间实现受控导航。

路由守卫

路由守卫让您控制路由导航的各个阶段。在从一个组件导航到另一个组件时,我们需要确保将要显示的数据对用户是经过授权的,如果不是,则取消导航。

路由守卫可以返回一个Observable<boolean>或一个Promise<boolean>,路由器将等待 Observable 解析为 true 或 false:

  • 如果路由守卫返回 true,它将继续导航并显示视图

  • 如果路由守卫返回 false,它将中止/取消导航

有各种路由守卫可以独立使用或组合使用。它们如下:

  • canActivate

  • canActivateChild

  • canDeactivate

  • Resolve

  • canLoad

守卫函数可以接受参数以做出更好的决定。我们可以传递的参数如下:

  • component:我们创建的自定义组件指令:例如ProductsServices等。

  • routeActivatedRouteSnapshot是如果守卫通过将要激活的未来路由。

  • stateRouterStateSnapshot是如果守卫通过将来的路由状态。

  • canActivate:这保护组件——将其视为一个类似于著名酒吧外面保镖的消毒函数。确保在激活路由之前满足所有必要的标准。我们需要从路由器导入canActivate模块,并在组件类中调用该函数。

以下是用于通用健全性服务check-credentials.ts文件的代码片段:

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';

@Injectable()
export class checkCredentials implements CanActivate {
  canActivate() {
   console.log('checking on user credential - user logged in: Passed');
   return true;
 }
}

如果您想要在没有任何验证或业务规则的情况下重定向用户,请使用导航函数而不是canActivate

  • canActivateChild:这保护子组件*——*在前一节中,我们创建了组件路由以及子路由?是的,现在我们也要确保保护它们。

  • canActivateChild函数类似于canActivate,但有一个关键区别,即此函数保护组件的子路由。

以下是在服务中使用canActivateChild函数的示例代码:

import {CanActivateChild} from "@angular/router";

@Injectable()
class checkCredentialsToken implements CanActivateChild {
 canActivateChild() {
 console.log("Checking for child routes inside components");
 return true;
 }
}

  • canDeactivate:这处理页面中的任何未保存更改*——*当用户尝试从具有未保存更改的页面导航时,我们需要通知用户有待更改,并确认用户是否要保存他们的工作或继续而不保存。

这就是canDeactivate的作用。以下是一个实现canDeactivate函数的服务的代码片段:

import { CanDeactivate } from '@angular/router';

@Injectable()
export class checkCredentials {
 canDeactivate() {
 console.log("Check for any unsaved changes or value length etc");
 return true;
 }
}

  • Resolve:这在路由激活之前执行路由数据检索——Resolve允许我们在激活路由和组件之前从服务中预取数据检索。

以下是我们如何使用Resolve函数并在激活路由之前从服务获取数据的代码片段:

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { UserService } from './shared/services/user.service';

@Injectable()
export class UsersResolve implements Resolve<any> {
  constructor(private service: UserService) {}
   resolve(route: ActivatedRouteSnapshot) {
   return this.service.getUsers();
  }
}

  • canLoad:这甚至在加载模块之前保护模块*——*使用canActivate,我们可以将未经授权的用户重定向到其他着陆页面,但在这些情况下,模块会被加载。

我们可以使用canLoad函数避免加载模块。

在下一节中,我们将学习为组件和子组件定义路由。我们将学习创建多级组件层次结构。

自定义组件路由和子路由

在之前的章节中,我们已经学习了路由的各种用法;现在是时候将我们的所有知识整合起来,使用所有的路由示例来创建一个样例演示应用程序。我们将创建一个自定义组件,并定义其带有子路由的路由文件。

我们将创建一个名为 Products 的项目列表,其中将包含子产品的链接列表项。点击相应的产品链接,用户将显示产品详情。

应用程序的导航计划如下:

在之前的章节中,我们已经学习了在NgModule中定义和创建路由。我们也可以选择在单独的app.route.ts文件中定义所有的路由细节。

创建app.route.ts文件,并将以下代码片段添加到文件中:

import { productRoutes } from './products/products.routes';

export const routes: Routes = [
 {
 path: '',
 redirectTo: '/',
 pathMatch: 'full'
 },
 ...aboutRoutes,
 ...servicesRoutes,
 ...productRoutes,
 { path: '**', component: PageNotFoundComponent }
];

export const routing: ModuleWithProviders = RouterModule.forRoot(routes);

我们将我们的组件导入到app.routes.ts文件中,然后使用productRoutes定义路由。

现在,我们要创建我们的product.routes.ts文件,其中包含我们产品的路径定义。以下是这样做的代码:

import { Routes } from '@angular/router';
import { ProductsComponent } from './products.component';
import { ProductsDetailsComponent } from './products-details.component';

export const productRoutes: Routes = [
 { path: 'products', component: ProductsComponent },
 { path: 'products/:id', component: ProductsDetailsComponent } 
];

让我们详细分析前述代码:

  1. 我们在products.routes.ts文件中定义了两个路径。

  2. 路径products将指向ProductsComponent

  3. 路径products/:id将被映射到ProductsDetailsComponent,对应的路径为products/10

现在,是时候创建我们的组件--ProductsComponentProductsDetailsComponent

让我们在products.components.ts文件中定义ProductsComponent类,并添加以下代码:

import { Component } from '@angular/core';
import { Routes, Router } from '@angular/router';

@Component({
 template: `
 <div class="container">
 <h4>Built with Angular2</h4>
 <p> select country specific website for more details </p>
 <ul>
 <li><a routerLink="10" routerLinkActive="disabled">Product #10</a>
   </li>
 <li><a routerLink="11" routerLinkActive="disabled">Product #11</a>
   </li>
 <li><a routerLink="12" routerLinkActive="disabled">Product #12</a>
   </li>
 </ul>

<button (click)="navigateToServices()">Navigate via Javascript event</button>

<router-outlet></router-outlet>

</div>`,
 styles: ['.container {background-color: #fff;}']
})
export class ProductsComponent {

   constructor(private router: Router) {}

   navigateToServices(){
     this.router.navigate(['/services']);
   }
}

让我们详细分析前述代码:

  • 我们已经使用routerLink指令创建了三个产品链接;点击这些链接将使我们映射到我们在products.route.ts文件中创建的路径。

  • 我们创建了一个按钮,它具有navigateToServices事件,在ProductsComponent类中,我们实现了导航到服务页面的方法。

  • 我们已经创建了一个routerLink来处理每个产品 ID,并且相应的数据将在<router-outlet>中加载。

现在,让我们在products文件夹下的products-details.components.ts中使用以下代码创建ProductsDetailsComponent

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ROUTER_DIRECTIVES, ActivatedRoute } from '@angular/router';

@Component({
 template: `
 <div class="container">
  <h4>Product Demo Information</h4>
  <p>This is a page navigation for child pages</p>
  showing product with Id: {{selectedId}}
  <p>
  <a routerLink="/products">All products</a>
  </p>
 </div>
 `,
 directives: [ROUTER_DIRECTIVES],
 styles: ['.container {background-color: #fff;}']
})

export class ProductsDetailsComponent implements OnInit {
  private selectedId: number;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
   this.sub = this.route.params.subscribe(params => {
   let id = params['id'];
   this.selectedId = id;
   console.log(id);
  });
 }
}

以下是前述代码的分析:

  • 当用户点击产品链接时,id将被映射,并显示相应的产品详情。

  • 我们从@angular/core库中导入所需的模块ComponentOnInit

  • 我们从angular/router库中导入所需的模块ROUTER_DIRECTIVESActivatedRoute

  • 我们正在导出ProductsDetailsComponent

  • 我们在构造方法中注入了ActivatedRoute

  • 我们正在定义ngOnInIt方法,该方法将在页面加载时调用

  • 我们正在使用ActivatedRoute服务,它提供了一个params Observable,我们可以订阅以获取路由参数

  • 我们使用this.route.params.subscribe来映射在 URL 中传递的参数

  • 参数具有所选/点击产品的id,我们将其分配给变量this.selectedId

到目前为止一切都准备好了吗?太棒了。

现在是时候用新组件和它们的声明更新我们的app.module.ts文件了。更新后的app.module.ts将如下所示:

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HashLocationStrategy, LocationStrategy } from "@angular/common";

import { AppComponent } from "./app.component";
import { routing } from "./app.routes";

import { ProductsComponent } from "./products/products.component";
import { ProductsDetailsComponent } from './products/products-
  details.component';

@NgModule({
  imports: [
      BrowserModule,
      routing
    ],
  declarations: [
     AppComponent,
     ProductsComponent,
     ProductsDetailsComponent
    ],
  bootstrap: [
     AppComponent
    ],
  providers: [
     {provide: LocationStrategy, useClass: HashLocationStrategy }
   ]
  })
export class AppModule { }

好的。现在,让我们测试一下我们迄今为止制作的应用程序。

以下图片显示了我们的应用在这个阶段应该如何运行:

以下图片显示了当用户点击任何特定产品时,应用程序将带用户到相应的产品列表:

具有内部子路由的自定义组件路由

在上面的示例中,当用户点击产品链接时,用户将被导航到新路径。在这个示例中,您将学习如何创建自定义组件和子路由,并在同一路径内显示视图;也就是说,内部子路由。

扩展相同的示例,看一下应用程序的导航计划:

让我们从在service.routes.ts文件中定义路由定义开始。请参考以下代码进行路由定义:

import { Routes } from '@angular/router';

import { ServicesComponent } from './services.component';
import { ServicesChildComponent } from "./services-child.component";
import { ServicesInnerChildComponent } from "./services-inner-
    child.component";

export const servicesRoutes: Routes = [
 {
    path: 'services',
    component: ServicesComponent,
    children: [
       {
         path: '', redirectTo: 'services', pathMatch: 'full'},
         {
           path: 'web-technologies',
           component: ServicesChildComponent,
           children: [
              { path: '', redirectTo: 'web-technologies', pathMatch: 
                'full'},
              { path: 'angular2', component: 
                  ServicesInnerChildComponent}
           ]
         }
     ]
   }
];

在上述代码片段中,我们正在创建路径服务,并在同一路径内创建多级子路由,这些子路由都属于同一 URL 层次结构。

组件导航路由定义如下所示:

  • /services

  • /services/web-technologies

  • /services/web-technologies/angular2

现在,让我们为我们的服务创建三个新的组件:

  • ServicesComponent

  • ServicesChildComponent

  • ServicesInnerChildComponent

请注意,在父视图中添加<router-outlet>指令是重要的;否则,它会抛出错误。

现在我们需要创建我们的服务组件。对于ServicesComponent,创建一个名为services.component.ts的新文件,并将以下代码片段添加到其中:

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

@Component({
 template: `
 <div class="container">
 <h4>Services offered</h4>
 <ul>
 <li><a routerLink="web-technologies" routerLinkActive="active">Web 
     Technologies Services</a></li>
 <li><a routerLink="#" routerLinkActive="disabled">Mobile Apps</a></li>
 <li><a routerLink="#" routerLinkActive="disabled">CRM Apps</a></li>
 <li><a routerLink="#" routerLinkActive="disabled">Enterprise Apps</a> 
  </li>
 </ul>
 </div>
 <router-outlet></router-outlet>
 `,
 styles: ['.container {background-color:#fff;}']
})

export class ServicesComponent {
}

接下来是对上述代码的快速说明:

  1. 我们在ServicesComponent模板中定义了一个无序列表<ul>和项目<li>

  2. 对于每个列表项,我们附加了routerLink属性来链接 URL。

  3. 在模板中,我们还添加了<router-outlet>--这将允许子组件视图模板放置在父组件视图中。

我们已经创建好了父组件ServicesComponent。现在是时候创建内部组件ServicesChildComponent了。

让我们创建一个名为services-child.component.ts的新文件,并将以下代码片段添加到文件中:

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

@Component({
 template: `
 <div class="container">
 <h4>Web Technologies</h4>
 <p>This is 1st level Inner Navigation</p>
 <a routerLink="angular2" routerLinkActive="active">Angular2 Services</a>
 </div>
<router-outlet></router-outlet> 
 `,
 styles: ['.container {background-color: #fff;}']
})

export class ServicesChildComponent {}

接下来是对上述代码的快速说明:

  1. 我们为标题和锚点标签<a>定义了routerLinkrouterLinkActive属性。

  2. 对于锚点标签,我们附加了routerLinkrouterLinkActive属性。

  3. 在模板中,我们还添加了<router-outlet>--这将允许内部子组件视图模板放置在子组件视图中。

看一下下面的层次结构图,它描述了组件结构:

到目前为止,我们已经创建了一个父组件ServicesComponent,以及它的子组件ServicesChildComponent,它们之间有父子关系的层次结构。

是时候创建第三级组件ServicesInnerChildComponent了。创建一个名为services-child.component.ts的新文件:

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

@Component({
 template: `
 <div class="container">
 <h4>Angular Services</h4>
 <p>This is 2nd level Inner Navigation</p>
 <a routerLink="/services" routerLinkActive="active">View All 
    Services</a>
 </div>
 `,
 styles: ['.container {background-color: #fff;}']
})

export class ServicesInnerChildComponent {}

好了,现在我们已经定义了所有的组件和子组件以及它们各自的路由定义,是时候看看它们的运行情况了。以下截图展示了服务组件和子组件的导航路由是如何工作的。

点击 Web Technologies 链接将显示用户子组件数据。

点击 Angular Services 链接将显示用户子组件数据。

我们的组件分别很好地工作。在下一节中,我们将把它们全部集成到一个单一的工作应用程序中。

将所有组件集成在一起

我们已经为各个组件AboutServicesProducts定义并实现了路由。

在本节中,我们将把它们全部集成到一个单一的NgModule中,这样我们就可以将所有路由作为一个单页面应用程序一起工作。

让我们将AboutServicesProducts组件的所有单独路由添加到我们的app.routes.ts中,更新后的app.route.ts文件如下:

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PageNotFoundComponent } from './not-found.component';

import { AboutComponent } from "./about/about.component";

import { ServicesComponent } from "./services/services.component";
import { ServicesChildComponent } from "./services/services-
  child.component";
import { ServicesInnerChildComponent } from "./services/services-inner-
  child.component";

import { ProductComponent } from "./products/products.component";
import { ProductsDetailsComponent } from './products/products-
  details.component';

import { aboutRoutes } from './about/about.routes';
import { servicesRoutes } from './services/services.routes';
import { productRoutes } from './products/products.routes';

export const routes: Routes = [
 {
   path: '',
   redirectTo: '/',
   pathMatch: 'full'
 },
 ...aboutRoutes,
 ...servicesRoutes,
 ...productRoutes,
 { 
  path: '**', component: PageNotFoundComponent }
];

export const routing: ModuleWithProviders = RouterModule.forRoot(routes);

我们已经更新了app.routes.ts文件,以包括所有组件以及子组件的路由。

现在是时候更新NgModule,导入所有组件以及更新的路由了。

更新后的app.module.ts文件如下:

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HashLocationStrategy, LocationStrategy } from "@angular/common";

import { AppComponent } from "./app.component";
import { routing } from "./app.routes";
import { PageNotFoundComponent } from './not-found.component';

import { AboutComponent } from "./about/about.component";
import { ServicesComponent } from "./services/services.component";
import { ServicesChildComponent } from "./services/services-
  child.component";
import { ServicesInnerChildComponent } from "./services/services-inner-
  child.component";

import { ProductsComponent } from "./products/products.component";
import { ProductsDetailsComponent } from './products/products-
  details.component';

@NgModule({
  imports: [
   BrowserModule,
   routing
    ],
  declarations: [
   AppComponent,
   ProductsComponent,
   ServicesComponent,
   AboutComponent,
   ProductsDetailsComponent,
   PageNotFoundComponent,
   ServicesChildComponent,
   ServicesInnerChildComponent
    ],
  bootstrap: [
   AppComponent
    ],
  providers: [
   {provide: LocationStrategy, useClass: HashLocationStrategy }
   ]
})
export class AppModule { }

在上述代码中需要注意的重要事项是:

  1. 我们导入了我们迄今为止创建的所有组件,即AboutServicesProducts

  2. 我们还在导入每个组件的app.routes.ts路由。

  3. 我们正在注入LocationStrategy并明确地将其指定为useClass HashLocationStrategy

我们已经了解了routerrouterModule以及 Angular 提供的用于实现应用程序路由机制的实用工具。我们了解了可以使用的不同类型的LocationStrategy来定义 URL 应该如何显示。

我们创建了具有路由路径和子组件路由路径的组件,并且我们也学会了如何使用 JavaScript 事件进行导航。

在接下来的部分,我们将把所有的代码组合在一起,制作我们的演示应用程序。

演示应用程序的路由和导航

我们已经在学习 Angular 路由方面走了很长的路。我们已经看到了如何使用路由模块的各种技巧和窍门。现在是时候将我们迄今学到的所有知识整合到一个整洁、干净的应用程序中了。

以下图片显示了我们最终的应用程序文件系统结构:

我们将在app.component.ts文件中添加主导航菜单和一些基本样式来为我们的应用程序增添活力:

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

@Component({
 selector: 'my-app',
 template: `
    <h2>Angular2 Routing and Navigation</h2>
    <div class="">
    <p>
      <a routerLink="/about" routerLinkActive="active">About Us</a>|
      <a routerLink="/services" routerLinkActive="active">Services</a>|
      <a routerLink="/products" routerLinkActive="active">Products</a>
    </p>
    <div class="app-data">
      <router-outlet></router-outlet>
    </div> 
   </div>`,
     styles: [`
       h4 { background-color:rgb(63,81,181);color:#fff; padding:3px;}
       h2 { background-color:rgb(255, 187, 0);color:#222}
       div {padding: 10px;}
       .app-data {border: 1px solid #b3b3b3;}
       .active {color:#222;text-decoration:none;}
      `
     ],
 encapsulation: ViewEncapsulation.None
})

export class AppComponent {
}

我们最终的app.routes.ts文件代码如下:

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PageNotFoundComponent } from './not-found.component';

import { AboutComponent } from "./about/about.component";
import { ServicesComponent } from "./services/services.component";
import { ServicesChildComponent } from "./services/services-
   child.component";
import { ServicesInnerChildComponent } from "./services/services-inner-
   child.component";

import { ProductComponent } from "./products/products.component";
import { ProductsDetailsComponent } from './products/products-
   details.component';

import { aboutRoutes } from './about/about.routes';
import { servicesRoutes } from './services/services.routes';
import { productRoutes } from './products/products.routes';

export const routes: Routes = [
   {
     path: '',
     redirectTo: '/',
     pathMatch: 'full'
   },
   ...aboutRoutes,
   ...servicesRoutes,
   ...productRoutes,
   { path: '**', component: PageNotFoundComponent }
  ];

export const routing: ModuleWithProviders =
           RouterModule.forRoot(routes);

我们的app.module.ts文件代码如下:

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HashLocationStrategy, LocationStrategy } from 
     "@angular/common";
import { AppComponent } from "./app.component";
import { routing } from "./app.routes";

import { PageNotFoundComponent } from './not-found.component';
import { AboutComponent } from "./about/about.component";

import { ServicesComponent } from "./services/services.component";
import { ServicesChildComponent } from "./services/services-
   child.component";
import { ServicesInnerChildComponent } from "./services/services-inner-
    child.component";

import { ProductsComponent } from "./products/products.component";
import { ProductsDetailsComponent } from './products/products-
    details.component';

@NgModule({
 imports: [
   BrowserModule,
   routing
   ],
 declarations: [
   AppComponent,
   ProductsComponent,
   ServicesComponent,
   AboutComponent,
   ProductsDetailsComponent,
   PageNotFoundComponent,
   ServicesChildComponent,
   ServicesInnerChildComponent
 ],
 bootstrap: [
    AppComponent
 ],
 providers: [
   { provide: LocationStrategy, useClass: HashLocationStrategy }
 ]
})
export class AppModule { }

我们的应用程序已经准备好进行大规模演示了。

在以下的屏幕截图中,我们展示了应用程序的行为。

当我们启动页面时,我们会看到登陆页面。登陆页面的截图如下:

登陆页面

现在让我们点击 Services 链接。routerLink/services将被激活,并且应该显示以下屏幕:

Services 页面。

好的,现在我们在服务页面。现在,点击子组件,Web 技术服务。应显示以下屏幕截图:

服务子页面--Web 技术。

事情在这里发展得非常顺利。

我们现在已经在子组件--Web 技术服务中,现在我们再点击一级。让我们点击 Angular2 服务。应显示以下屏幕截图:

Web 技术内部子路由--Angular2。

好的,现在点击“产品”链接。应显示以下屏幕截图:

产品页面。

好的,现在我们在产品页面。现在,点击“所有产品”链接,导航到服务页面。

但是,导航是使用 JavaScript 事件而不是routerLink发生的。

产品详情页面。

总结

Angular 路由是任何 Web 应用程序的核心功能之一。在本章中,我们详细讨论、设计和实现了我们的 Angular 路由。我们还讨论了如何实现和启用RouterModule.forRoot。此外,我们定义了 Router Outlet 和routerLink指令来绑定路由路径,并启用了RouterLinkActivated来查找当前活动状态。

我们重点关注路由状态的工作原理,并了解并实现了路由生命周期钩子。我们概述了如何创建自定义组件路由和子路由,以及如何为我们的 Web 应用程序实现位置策略。最后,我们创建了一个实现路由和导航的示例应用程序。

在下一章中,您将学习如何创建指令并实现变更检测。您还将了解 Angular 提供的不同类型的指令,并创建自定义用户定义的指令。

您将深入学习 Angular 如何处理变更检测以及如何在我们的应用程序中利用变更检测。

第六章:创建指令和实现变更检测

在本章中,我们将学习和理解关于 Angular 指令和变更检测的所有内容。

我们将学习 Angular 提供的不同类型的指令,并创建一些自定义用户定义的指令。我们将深入学习 Angular 如何处理变更检测以及如何在我们的应用程序中利用变更检测。

在本章结束时,您将能够做到以下几点:

  • 理解 Angular 指令

  • 理解并实现内置组件指令

  • 理解并实现内置结构指令

  • 理解并实现内置属性指令

  • 创建自定义属性指令

  • 理解 Angular 中的变更检测工作原理

Angular 指令

指令允许我们扩展元素的行为。我们可以使用不同类型的指令定义来操纵 HTML 页面的文档对象模型DOM)。

Angular 使用@Directive元数据来告诉应用程序它们具有的指令类型以及每个指令定义的功能能力。

以下图表显示了不同类型的指令:

主要有三种类型的 Angular 指令:

  • 组件指令:我们可以将其定义为用户定义的指令,类似于 Angular 1.x 中的自定义指令

  • 结构指令:在运行时改变或转换 DOM 元素(一个或多个)的指令

  • 属性指令:扩展元素的行为或外观

在 Angular 1.x 中,我们有 A(属性)、E(元素)、C(类)、M(匹配注释)指令。

Angular 带有许多内置指令,我们将在前面提到的类别中对其进行分类。

Angular 使用使用ng的指令,因此避免在自定义指令中使用ng;这可能会导致未知问题。例如,ng-changeColor是一个不好的样式实例。

组件指令

组件指令是用户定义的指令,用于扩展功能并创建小型可重用功能。

将组件指令视为附加了模板的指令,因为组件指令具有自己的视图或模板定义。

在之前的章节中,我们创建了许多组件。如果您已经掌握了创建组件并在布局中使用它们的艺术,您将已经知道如何创建组件指令。

关于 Angular 组件的快速回顾:组件是可以在整个应用程序中重复使用的小型代码片段。

在以下代码片段中,我们将看到组件的基本语法。创建一个名为my-template.component.ts的文件:

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

@Component({
 selector: 'my-app',
 template: `<h2>{{ title }}</h2>`
})

export class MyTemplateComponent {
 title = 'Learning Angular!!!'
}

导入新创建的组件:

import  {MyTemplate}  from  "./my-app.component"  

然后,在我们的index.html文件中调用组件指令*:*

  <my-app>Loading...</my-app>

以下是您将看到的最简单和最简单的组件示例;就是这么简单:

因此,到目前为止我们创建的所有组件都是组件指令。如果您想深入学习更多并创建组件,请参考第四章,使用组件。

结构指令

顾名思义,结构指令通过在运行时添加、附加或删除 DOM 元素来改变 DOM 结构。

Angular 结构指令在指令名称之前显示为(*)星号符号。

一些常用的结构指令如下:

  • ngFor:重复器指令通常用于循环并显示元素列表。

  • ngIf:根据表达式评估的结果显示或隐藏 DOM 元素;结果要么是 true,要么是 false。

  • ngSwitch:如果匹配表达式的值与开关表达式的值匹配,则返回。返回的结果可以是任何值;匹配值进行条件检查。

每个元素只允许一个结构指令。

让我们详细了解每个结构指令,并使用它们创建一些示例:

ngFor 指令

ngFor指令将帮助我们迭代项目并在运行时将它们附加到列表中。

我们需要在StructureDirectiveComponent类中声明一个数组,然后使用ngFor来循环这些值并在模板中显示它们。

列表<li>元素会在运行时附加到<ul>元素上。

以下是ngFor指令用法的组件片段:

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

@Component({
   selector: 'my-app',
   template: `

   <h4>{{title}}</h4>

   <strong>Using ngFor directive</strong>
   <ul>
<li *ngFor="let language of languages">{{ language.name }}</li>
</ul>
   `
 })
export class StructureDirectiveComponent {
  title = 'Structural Directives';

 public languages = [
  { name: "PHP"},
  { name: "JavaScript"},
  { name: "Ruby"},
  { name: "Java"},
  { name: "HTML5"}
];

}

上述组件的输出如下所示:

ngIf 指令

ngIf指令帮助我们根据条件评估表达式,非常类似于任何编程语言中的if语句。

通用语法如下代码片段所示:

 <div *ngIf="!isLoggedIn">
   <p>Hello Guest user</p>
 </div>

前面的代码片段有一个*ngIf条件;如果isLoggedIntrue,指令将渲染内部的语句;否则,它将跳过并继续。

让我们创建一个示例,同时使用*ngFor*ngIf语句,如下所示:

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

@Component({
  selector: 'my-app',
  template: `
    <h4>{{title}}</h4>
    <strong>Using ngIf directive</strong>
    <div *ngIf="isLoggedIn">
      <p>Hello Packt Author</p>
    </div>

   <div *ngIf="!isLoggedIn">
      <p>Hello Guest user</p>
   </div>

  <strong>Using ngFor directive - Programming Languages </strong>

  <ul>
    <li *ngFor="let language of languages">{{ language.name }}</li>
  </ul>
`
})

export class StructureDirectiveComponent {
 title = 'Structural Directives';
 isLoggedIn= true;

 public languages = [
  { name: "PHP"},
  { name: "JavaScript"},
  { name: "Ruby"},
  { name: "Java"},
  { name: "HTML5"}
];

}

让我们详细分析前面的代码片段:

  1. 我们在view模板中使用了*ngFor*ngIf

  2. 在组件类中,我们使用布尔值定义了一个isLoggedIn变量。

  3. 我们创建了一个团队名称列表的数组,我们将迭代并在视图中显示。

运行应用程序,我们应该看到如下截图所示的输出:

ngSwitch 指令

当我们需要根据多个值来评估表达式时,我们使用ngSwitchngSwitch的示例如下代码片段所示:

<div [ngSwitch]="taxRate">
  <p *ngSwitchCase="'state'">State Tax</p>
  <p *ngSwitchCase="'fedral'">Fedral Tax</p>
  <p *ngSwitchCase="'medical'">Medical Tax</p>
  <p *ngSwitchDefault>Default</p>
</div>

根据taxRate的值,我们的应用程序将决定显示哪个元素。让我们更新我们的示例并添加一个*ngSwitch语句。

更新后的示例代码如下所示:

import {Component} from "@angular/core";
@Component({
    selector: 'structure-directive',
    templateUrl: 'structure-directive.component.html'
})

export class StructureDirectiveComponent {
 title = 'Structural Directives';

 username = "Sridhar Rao";
 taxRate = "state";
 isLoggedIn= true;

 public languages = [
  { name: "PHP"},
  { name: "JavaScript"},
  { name: "Ruby"},
  { name: "Java"},
  { name: "HTML5"}
 ];
}

前面代码示例的输出如下:

属性指令

属性指令扩展了给定元素的行为或外观。属性指令与 HTML 属性非常相似,与元素一起定义。

属性指令可以分为两种类型:

  • 内置属性指令

  • 自定义或用户定义的属性指令

现在让我们在以下章节中详细查看它们。

内置属性指令

如前所述,属性是页面中元素的属性。HTML 元素的属性示例包括 class、style 等。

同样,Angular 提供了几个内置的属性指令。这些指令包括ngModelngClassngStyle等等。

让我们通过创建一些示例来了解每个属性指令,如下所示:

  • ngModel:使用ngModel,我们可以实现双向数据绑定。要了解更多关于数据绑定和模板语法的内容,请参考第八章,模板和数据绑定语法

ngModel指令写在带有事件绑定[()]的括号内。

记得从 Angular 表单中导入表单模块,否则你会收到错误消息。

ngModel属性指令的一个示例如下:

<input [(ngModel)]="username">
<p>Hello {{username}}!</p>

  • ngClass:当我们想要向 DOM 元素添加或移除任何 CSS 类时,最好使用ngClass属性指令。我们可以以不同的方式为ngClass分配类名。

我们可以使用stringobject或组件method来分配类名

ngClass属性指令的一个示例如下:

//passing string to assign class name
<p [ngClass]="'warning'" >Sample warning message</p>
 //passing array to assign class name
<p [ngClass]="['error', 'success']" > Message </p>

//Passing object to assign class name
<p [ngClass]="{'error': true, 'success': false }"> Message</p>

//Passing component method to assign class name
<p [ngClass]="getClassName('error')"> </p> 

记得将 CSS 类名用单引号括起来;否则,你将看不到样式。

记得在index.html或你的相应组件中包含样式表。

  • ngStyle:当我们想要操纵任何 DOM 元素的一些样式属性时,我们可以使用ngStyle。你可以将其与 CSS 世界中的内联样式相关联。

ngStyle属性指令的一个示例如下:

<p [ngStyle]="{ 'font-size': '13px', 'background-color':'#c5e1a5'}" >Sample success message</p>

好了,现在我们已经了解了内置属性指令,让我们在一个示例中将它们全部放在一起。

以下是一个使用ngModelngClassngStyle的代码示例:

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

@Component({
 selector: 'my-app',
 styleUrls: ['./attribute-directive.component.css'],
 template:`
 <h4>Welcome to Built-In {{title}}</h4>

 <strong>using ngModel</strong>
 <div><label for="username">Enter username</label>
 <input type="text" [(ngModel)]="username" placeholder="enter username" 
    id="username">
 <p>username is: {{username}}</p>
 </div>

<strong>Notification example using ngStyle</strong>
 <p [ngStyle]="{ 'font-size': '13px', 'background-color':'#c5e1a5'}" 
>Sample success message</p>

<strong>Notification example using ngClass</strong>
    <p [ngClass]="'warning'" >Sample warning message</p>
    <p [ngClass]="'error'" >Sample error message</p>
   `
})
export class AttributeDirectiveComponent {
 title = 'Attribute Directive';
 public username="Packt Author";
}

查看以下屏幕截图,显示了前面代码示例的输出:

创建自定义指令-结构和属性指令

到目前为止,在之前的章节中,我们已经学习并实现了 Angular 提供的内置指令。

通过创建自定义用户定义的指令,Angular 允许我们定义和扩展页面中元素的行为和功能。

要创建自定义指令,我们必须使用@Directive装饰器并在类定义中实现逻辑。

我们可以创建自定义组件、结构和属性指令。

任何用户定义的 HTML 标记都是组件属性(例如,<my-app>)。在本书的每一章中,我们一直在创建自定义组件。

Angular CLI-生成指令

我们将使用 Angular CLI 工具为我们的示例生成指令。

导航到项目目录并运行以下ng命令:

ng generate directive highlightDirective

我们应该看到以下屏幕截图中显示的输出:

正如你在前面的屏幕截图中看到的,新生成的指令highlightDirective已经创建,并且app.module.ts文件已经更新。

在继续实现我们的指令之前,让我们快速回顾一下结构和属性指令:

  • 结构指令:顾名思义,结构属性影响 HTML 布局的结构,因为它塑造或重塑了 DOM 结构。它可以影响页面中的一个或多个元素。

  • 属性指令:定义并扩展页面中元素的外观或行为。

我们学会了使用 Angular CLI 生成自定义指令,现在我们清楚地知道结构指令和属性指令是如何工作的。

现在是时候创建我们自己的自定义指令了。继续阅读。

创建自定义属性指令

我们将从创建自定义属性指令开始。我们将继续使用前一节中创建的示例highlightDirective

顾名思义,我们将使用这个指令来突出显示附加到这个属性的元素的更改文本颜色。

现在是时候定义我们的指令highlightDirective的功能和行为了。

highlight-directive.ts文件中,添加以下代码行:

import { Directive, ElementRef } from '@angular/core';

@Directive({
 selector: '[appHighlightDirective]'
})
export class HighlightDirectiveDirective{

 constructor(private elRef: ElementRef) { 
  this.elRef.nativeElement.style.color = 'orange';
 }
} 

让我们详细分析前面的代码片段:

  1. 我们需要导入 Angular 提供的必要工具来处理指令。

  2. 我们将从@angular/core中导入DirectiveElementRefAfterViewInit

  3. 如前所述,我们使用@Directive装饰器定义指令,并在元数据选择器中传递名称appHighlightDirective

  4. 我们正在导出appHighlightDirective类。

  5. 如前所述,属性指令特定于一个元素,因此我们需要创建ElementRef变量elRef的实例,我们将使用它来定位和更新附加到我们自定义指令的元素。

  6. constructor中,我们使用nativeElement方法来定位特定的元素,并使用一个值orange更新style属性color

现在我们已经创建了我们的指令,我们需要将其应用到应用程序组件模板app.component.html文件中的元素上:

<div appHighlightDirective> Custom Directive </div>

现在运行应用程序,我们应该看到如下截图所示的输出:

看看创建自定义属性指令是多么简单和容易。

如果你仔细观察,它是一个非常基本的属性,可以改变文本的颜色。现在,如果我们想要动态地传递颜色的值而不是静态地传递呢?

我们必须使我们的属性能够传递值。让我们看看我们需要对我们的指令进行哪些更改,使其成为更合适的候选者。

让我们首先在我们的组件app.component.html模板中进行编辑,我们想要使用该属性的地方:

<div appHighlightDirective highlightColor="green">Custom 
    Directive</div>

您会看到,我们现在通过highlightColor变量为我们的属性appHighlightDirective传递了一个值green

现在更新我们的highlight-directive.ts文件,并向其中添加以下代码行:

import { Directive, ElementRef, Input, AfterViewInit } from '@angular/core';

@Directive({
 selector: '[appHighlightDirective]'
})
export class HighlightDirectiveDirective{

 @Input() highlightColor : string;

 constructor(private elRef: ElementRef) { 
   this.elRef.nativeElement.style.color = 'orange';
 }

 ngAfterViewInit(): void {
   this.elRef.nativeElement.style.color = this.highlightColor;
 }
}

让我们看看我们在highlight-directive.ts文件中所做的更改:

  1. 此外,我们还从@angular/core库中导入了InputAfterViewInit模块。

  2. 我们使用@Input装饰器告诉 Angular 我们希望通过定义为highlightColor的变量动态传递值。

  3. ngAfterViewInit方法中,我们使用ElementRef实例elRef创建了元素的对象实例,并使用nativeElement方法来更新元素的style属性color

  4. 文本的color将更改为通过元素的appHighlightDirective属性的值传递的highlightColor

运行应用程序,我们应该看到以下截图中显示的输出:

好吧,到目前为止还不错。我们的属性正在变得非常完善。

让我们看看您在实现我们的自定义指令方面取得的进展:

  • 我们创建了一个自定义属性指令highlightDirective

  • 我们学会了使用highlightColor变量将值传递给自定义属性指令

这是很好的东西。但是,如果我们想要将Javascript事件(如mouseovermouseoutclick等)绑定到我们的属性呢?

让我们进行必要的更改,以实现与我们的属性附加的事件。为此,我们将需要一张漂亮的图片,并将附加一些事件以及自定义属性指令。

让我们在组件app.component.html文件模板中添加一张图片:

<img [src]="imageUrl" width="100" height="100" appHighlightDirective 
    showOpacity="0.5" hideOpacity="1">

关于前面代码片段的重要说明:

  1. 我们已将我们的自定义属性组件appHighlightDirective添加到元素中。

  2. 此外,我们添加了两个属性,showOpacityhideOpacity,它们将具有元素的不透明度样式属性。

  3. 我们将为这些属性附加onmouseoveronmouseout事件,并动态更改图像的不透明度。

现在我们已经将图像添加到组件视图模板中,更新后的输出如下截图所示:

让我们转到自定义指令highlight-directive.directive.ts文件:

import { Directive, ElementRef, Input, HostListener, AfterViewInit } 
  from '@angular/core';

@Directive({
 selector: '[appHighlightDirective]'
})
export class HighlightDirectiveDirective{
 @Input() highlightColor : string;
 @Input() showOpacity : string;
 @Input() hideOpacity : string;

 constructor(private elRef: ElementRef) { 
   this.elRef.nativeElement.style.color = 'orange';
 }
 ngAfterViewInit(): void {
   this.elRef.nativeElement.style.color = this.highlightColor;
 }

@HostListener('mouseover') onmouseover() {
  this.elRef.nativeElement.style.opacity = this.hideOpacity;
 }

@HostListener('mouseout') onmouseout() {
  this.elRef.nativeElement.style.opacity = this.showOpacity;
 }
}

让我们分析我们在前面的代码中所做的更新:

  1. 我们从@angular/core中导入了所需的模块DirectiveElementRefInputHostListenerAfterViewInit

  2. 请注意,为了将事件绑定和实现到元素上,我们特别需要导入HostListener

  3. 使用@HostListener装饰器,我们将mouseovermouseout事件绑定到我们附加自定义属性的元素上。

  4. 请注意,当我们使用this.elRef.nativeElement时,我们是在引用附加了自定义属性的元素。

  5. 当用户将鼠标悬停在元素上时,我们为this.hideOpacity变量赋值。

  6. 当用户将鼠标移出元素时,我们为this.showOpacity变量赋值。

现在运行应用程序,您应该看到以下截图中显示的输出:

太棒了。现在让我们看看您在实现我们的自定义指令方面取得的进展:

  • 我们已经创建了一个自定义属性指令highlightDirective

  • 我们学会了使用highlightColor变量向自定义属性指令传递值

  • 我们已经学会了将诸如mouseovermouseout这样的事件附加到我们的自定义属性highlightDirective上。

在本节中,您已经学会了创建和使用具有属性和方法的自定义属性指令。

在下一节中,您将学习创建自定义结构型指令。

创建自定义结构型指令

到目前为止,您已经学习并实现了内置指令--组件、结构型和属性指令。

我们还学会了在Angular CLI - 生成指令部分使用 Angular CLI 生成自定义指令。

在上一节中,我们学习并实现了自定义属性指令。在本节中,我们将学习创建结构型指令。

让我们使用 Angular CLI 创建一个新的指令:

ng generate directive custom-structural

您应该看到前面命令的输出,如下截图所示:

运行前面的ng命令,我们应该看到指令已创建,并且app.module.ts已更新为新创建的指令。

好了,是时候创建和实现我们的自定义结构型指令了。以下是我们将使用自定义结构型指令构建的用例:

  1. 我们将使用我们的结构指令来循环遍历产品列表。

  2. 该指令应该只显示isAvailable设置为true的元素。

首先让我们在app.component.ts文件中定义我们的产品 JSON:

public products = [{
 productName: 'Shoes',
 isAvailable : true
 },
 {
 productName: 'Belts',
 isAvailable : true
 },
 {
 productName: 'Watches',
 isAvailable : false
 }]

我们刚刚创建了一个产品的 JSON 列表,其中包含productNameisAvailable两个键。

还没有超级英雄般的事情,还不是时候!

是时候使用*ngFor循环并在app.component.html文件中显示产品列表了:

<ul *ngFor="let product of products">
  <li *appCustomStructural="product">{{product.productName}}</li>
</ul>

让我们快速分析前面的代码

  1. 我们正在使用内置的结构指令*ngFor来循环遍历产品列表,并使用键product.productName显示名称。

  2. 我们正在定义我们自定义的结构指令appCustomStructural,并传递product对象进行分析。

  3. 由于我们将整个产品对象传递给我们的属性,我们现在可以在appCustomStructural中定义我们的自定义逻辑,并根据我们的应用程序需求进行转换。

在我们的指令custom-structural.directive.ts文件中进行一些超级英雄般的工作:

import { Directive, Input, TemplateRef, ViewContainerRef, AfterViewInit 
    } from '@angular/core';

@Directive({
 selector: '[appCustomStructural]'
})
export class CustomStructuralDirective {
 @Input()
 set appCustomStructural(product){
  if(product.isAvailable == true)
  {
    this.viewContainerRef.createEmbeddedView(this.templateRef );
  }
 }

 constructor(
   private templateRef : TemplateRef<any>,
   private viewContainerRef : ViewContainerRef
 ) { }
}

让我们详细分析前面的代码:

  1. 我们从@angular/core中导入所需的模块DirectiveInputTemplateRefViewContainerRefAfterViewInit

  2. 我们正在为我们的自定义结构指令appCustomStructural定义 CSS selector

  3. 通过使用@Input装饰器,我们明确告诉 Angular 我们的自定义指令将通过appCustomStructural获得输入。

  4. 在构造函数中,我们注入了TemplateRef<any>ViewContainerRef的实例。

  5. 使用TemplateRef<any>,我们指定这是一个嵌入式模板,可以用于实例化嵌入式视图。

  6. 由于结构指令涉及在页面中塑造或重塑 DOM 结构,我们正在注入ViewContainerRef

  7. 我们正在检查product.isAvailable的值是否等于true

  8. 如果product.isAvailable的值为 true,则使用ViewContainerRef的实例--一个可以附加一个或多个视图的容器,通过使用createEmbeddedView方法--我们将元素附加到视图中。

运行应用程序,我们应该看到如下截图所示的输出:

我们只看到鞋子和腰带被显示,因为只有这些产品的isAvailable键设置为 true。尝试改变其他产品的值并查看输出显示。

在本节中,我们学习了自定义结构指令。我们学习了 Angular 提供的重要工具--createEmbeddedViewViewContainerRefTemplateRef

迄今为止,我们已经学习和实现了自定义指令的一些要点。

我们创建了一个自定义属性指令 highlightDirective。我们学会了使用 highlightColor 变量向自定义属性指令传递值。我们学会了将事件(如 mouseovermouseout)附加到我们的自定义属性 highlightDirective

我们已经学会了创建一个自定义结构指令 appCustomStructural。我们已经学会了使用 createEmbeddedViewViewContainerRefTemplateRef

在下一节中,我们将学习变化检测,这是 Angular 框架内部工作的一个重要方面,我们还将学习如何在我们的应用程序中使用它。

在 Angular 中实现变化检测

变化检测是检测模型或组件类中的任何内部状态变化,然后将其反映到视图中的过程,主要是通过操作 DOM。

变化检测是从 Angular 1.x 到 2.x 中最重要的变化之一。

应用程序状态的变化发生在模型到视图或视图到模型之间。为了更好地理解,看一下下面的图表:

应用程序状态的变化可以以两种方式发生:

  • 从模型到视图模板(DOM)

  • 从视图(DOM)到模型(组件类)

既然我们知道状态变化发生在模型或 DOM 中,让我们分析一下是什么触发了变化检测。

变化检测是由以下触发的:

  • JavaScript 事件(clickmouseoverkeyup 等)

  • setTimeout()setInterval()

  • 异步请求

请注意,前面列出的三种方式都是异步过程。因此可以说,在 Angular 中,每当我们有异步方法/请求时,变化检测就会发生。

在我们深入了解变化检测的更多内容之前--它是如何工作的,如何处理的等等--让我们快速创建一个示例来理解是什么触发了变化检测。

看一下下面的代码片段:

import { Component} from '@angular/core';
@Component({
  selector: 'my-app',
  template:`
  <h4>Learning Angular {{title}}</h4>

  <button (click)="toggleUser()"> Toggle User </button>
  <div *ngIf="isLoggedIn">
    <b>Hello Packt Author</b>
  </div>

  <div *ngIf="!isLoggedIn">
    <b>Hello Guest user</b>
  </div>
 `
 ]
})
export class AppComponent {
 title = 'Change Detection';
 isLoggedIn = true;
 toggleUser(){
 if (this.isLoggedIn)
   this.isLoggedIn = false
 else
   this.isLoggedIn = true
 }
}

上述代码片段可以解释如下:

  1. 我们创建了一个按钮,点击事件调用了 toggleUser 方法。

  2. toggleUser 的点击事件中,变量 isLoggedIn 的值被设置为 truefalse

  3. 根据变量,在视图中isLoggedIn的值会更新。如果值为true,则显示“Hello Packt Author”,如果值为 false,则显示Hello Guest user

在下一节中,我们将学习 Angular 如何在内部处理变化检测以及 Angular 提供的工具来帮助我们实现更好的变化检测。

变化检测 - Angular 和 ZoneJS

ZoneJS 的官方网站描述了该库如下:

Zone 是一个跨异步任务持续存在的执行上下文。

Angular 使用 ZoneJS 来检测变化,然后调用这些事件的监听方法。

Angular 利用 zone 来处理所有内部状态变化和变化检测。Zone 理解异步操作和状态变化的上下文。

Angular 内置了ngZone,用于跟踪所有已完成的异步操作,并通过onTurnDone事件通知它们。每个组件都有一个变化检测器,它在树形结构中跟踪组件上附加的所有绑定。

我们不再有像在以前版本的 Angular 中的$scope.apply$digest

默认情况下,Angular 变化检测总是会检查值是否发生了变化。变化检测总是从根组件顶部到树形结构中的内部组件执行相同的操作。

这是通过变化检测器对象为所有组件执行的操作。

使用ngZones,Angular 应用的性能大大提高了。

变化检测策略

默认情况下,Angular 为我们应用中的每个组件定义了一个变化检测策略--这意味着每当模板发生任何变化时,它会遍历到树形结构中的最后一个组件,检查是否有任何变化,并进行必要的更新。

这会带来性能损耗!

因此,Angular 为我们提供了明确定义我们想要为组件实现的变化检测策略的选项。

Angular 提供了一个ChangeDetectionStrategy模块,通过它我们可以定义我们想要使用的变化检测策略。

ChangeDetectionStrategy有两个不同的值:

  • Default

  • OnPush

让我们详细分析每个选项,以了解它们的工作原理。

ChangeDetectionStrategy - 默认

这是 Angular 实现的默认机制--变化由事件触发,变化的传播从视图模板到模型。根据实现的逻辑,DOM 结构会更新。

这里需要注意的一点是,使用这种策略时,每次 Angular 都会遍历所有组件,从根组件到最后一个组件,以检查是否需要更新所有属性。

参考我们在前面部分创建的示例,在 Angular 中实现变更检测。我们正在更新属性,Angular 默认使用Default值的ChangeDetectionStrategy

ChangeDetectionStrategy - OnPush

我们使用OnPush来提高我们的 Angular 应用程序的性能。我们必须明确指出我们要使用ChangeDetectionStrategyOnPush值。

更改由事件触发,更改的传播适用于在view模板中呈现的整个对象,而不是每个属性。

当我们使用OnPush值时,我们强制 Angular 仅依赖于输入。我们通过@Input装饰器传递对象,只有完整的对象及其属性会受到影响,而不会影响任何单个属性的更改。

ChangeDetectionStrategy - OnPush 的优势

在前面的部分中,您已经学习了使用defaultOnPush选项的 ChangeDetectionStrategy。

使用OnPush选项而不是default的一些优势包括:

  1. 它有助于提高我们的 Angular 应用程序的性能。

  2. Angular 不必遍历整个组件树结构以检测属性的单个更改。

  3. 当输入属性不发生变化时,Angular 内部可以跳过嵌套的树结构。

为了更好地理解它,让我们创建一个用例。首先,我们需要使用 Angular CLI ng命令创建一个名为change-detect的新组件。

组件创建后,您应该看到如下截图所示的输出:

让我们在user.ts文件中创建一个class用户,并具有userNameuserId属性:

export class User {
 constructor(
 public userName: string,
 public userId: number) {}
}

现在让我们编辑我们生成的Component类,并添加以下代码片段:

import { Component, Input, ChangeDetectionStrategy  } from '@angular/core';
import { User } from '../shared/user';

@Component({
 selector: 'app-change-detect',
 template: `
 <h3>{{ title }}</h3>
 <p>
 <label>User:</label>
 <span>{{user.userName}} {{user.userId}}</span>
</p>`,
 changeDetection: ChangeDetectionStrategy.OnPush,
 styleUrls: ['./change-detect.component.css']
})

export class ChangeDetectComponent{
 title = "Change Detection";
 @Input() user: User;
 constructor() { }
} 

让我们详细分析前面的代码:

  1. 我们从@angular/core库中导入了InputComponentChangeDetectionStrategy所需的模块。

  2. 我们将新创建的User类导入到组件类中。

  3. 我们明确指定changeDetection的值为ChangeDetectionStrategy.OnPush

  4. 我们使用 CSS 的selector app-change-detect,在那里我们将显示组件的输出。

  5. 由于我们告诉 Angular 使用OnPush选项,我们需要使用@Input并传递在我们的情况下是User的对象。

  6. 根据模板部分,我们在view模板中绑定了用户属性userNameuserId

很好。到目前为止,我们已经创建了我们的组件,并明确指出,每当检测到变化时,应更新整个对象,即user对象,而不仅仅是单个属性。

现在是时候创建方法来测试我们的逻辑了。因此,在AppComponent类中,添加以下代码:

 changeDetectionDefault(): void {
   this.user.userName = 'Packt Publications';
   this.user.userId = 10;
 }

 changeDetectionOnPush(): void {
   this.user = new User('Mike', 10);
 }

对于我们的组件,我们已经指定了要使用的selectorapp-change-detect。我们需要在模板app.component.html文件中使用该组件。

我们还指定了该组件将以user作为输入,因此我们将用户对象传递给该组件。

将以下代码行添加到app.component.html模板文件中的app-change-detect组件中。

<button type="button" (click)="changeDetectionDefault()">
  Change Detection: Default
 </button>
 <button type="button" (click)="changeDetectionOnPush()">
 Change Detection: OnPush
 </button>

<app-change-detect [user]="user"></app-change-detect>

好了,一切都准备就绪。运行应用程序,您应该看到如下屏幕截图中显示的输出:

应用程序功能可以总结如下:

  1. app-change-detect组件加载到AppComponent模板中。

  2. 默认值传递给对象在view模板中显示。

  3. 单击Change Detection: OnPush按钮,我们会看到更新后的用户加载到视图中。

  4. 当我们点击Change Detection: Default时,与我们之前创建的示例不同,我们不会看到任何属性发生变化。这是因为我们明确指出,任何变化检测都应通过对象而不是属性传递,使用ChangeDetectionStrategyOnPush选项。

在本节中,我们已经了解了 Angular 提供的变化检测策略。我们已经探讨了如何通过使用OnPush选项来改善应用程序的性能,强制 Angular 仅检查作为输入传递的对象而不是单个属性。

更新属性将告诉 Angular 遍历整个应用程序组件树结构,并对性能造成影响。

摘要

在本章中,我们学习了指令,以及不同类型的指令,即组件指令、结构指令和属性指令。

我们实现了自定义用户指令,以了解如何扩展指令并更有效地使用它们。

我们简要了解了 ZoneJS,以及区域如何帮助我们处理现代应用程序框架中的“异步”任务。

最后,我们了解了 Angular 如何处理变化检测,以及如何使用变化检测方法来提高整体应用程序性能。

在下一章中,我们将学习使用 Observables 进行异步编程。在本章中,我们将学习如何利用 Observable 和 Promises 在 Angular 中利用异步编程。

此外,我们将学习如何构建一个基本但可扩展的异步 JSON API,用于查询漫威电影宇宙。