Angular-专家级编程-全-

Angular 专家级编程(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

学习如何使用 Angular 框架为任何部署目标(移动、桌面或原生应用)构建出色的应用程序。

本书涵盖了编写现代、直观和响应式应用程序所需的一切。

本书涵盖了概念和基础知识,以及详细的代码片段和示例,这将帮助您通过学习 Angular 框架来快速启动并开启新的想法。

本书的章节涵盖了任何开发人员轻松掌握 Angular 编程技能的主题。与此同时,经验丰富的开发人员将学会掌握从现有的 AngularJS 框架迁移的技能,并学习本书中涵盖的高级技术和最佳实践。

除了出色的功能,任何应用程序都严重依赖于设计方面。本书将向您介绍并帮助您通过 Material Design 和 Bootstrap CSS 来提高您的设计技能。

学习如何编写和创建可重用、可测试和可维护的服务、表单、管道、异步编程、动画、路由等等。

我们说过可测试吗?当然。本书向您介绍了 Jasmine 和 Protractor 框架。在学习这些框架的同时,我们将学会使用 Jasmine 编写单元测试脚本,以及使用 Protractor 框架编写端到端测试脚本。

学习和掌握 Angular 技能的旅程将是有趣的、发人深省的,最重要的是简单的。逐步指南帮助用户在其应用程序和项目中实现这些概念。

本书内容包括

第一章,“Angular 的架构概述和构建简单应用”,解释了 Angular 的架构、TypeScript 的基础知识,以及如何创建一个简单的 Angular 应用。

第二章,“将 AngularJS 应用迁移到 Angular 应用”,展示了如何将 AngularJS 应用迁移到 Angular 4,并讨论了迁移应用的最佳实践。

第三章,“使用 Angular CLI 生成最佳实践的 Angular 应用”,展示了如何使用 Angular 命令行界面为 Angular 应用生成样板代码。

第四章,“使用组件”,讨论了组件的生命周期。我们将学习如何实现多个和容器组件,以及不同组件之间的交互。

第五章,“实现 Angular 路由和导航”,展示了如何为我们的 Angular 应用程序创建路由策略和路由。我们将学习路由的构建模块,创建路由、子路由,并使用路由守卫来保护路由。状态是路由中的一个重要方面。我们将实现状态以创建安全的、多状态的应用程序路由。

第六章,“创建指令和实现变更检测”,解释了指令,Angular 提供的不同类型的指令,以及如何创建自定义用户定义的指令。我们将深入学习 Angular 如何处理变更检测,以及如何在我们的应用程序中利用变更检测。

第七章,“使用 Observables 进行异步编程”,展示了如何利用 Angular 的 Observable 和 Promises 来实现异步编程。此外,我们还将学习如何构建一个基本但可扩展的异步 JSON API,用于查询漫威电影宇宙。

第八章,“模板和数据绑定语法”,讨论了用于编写表达式、运算符、属性和将事件附加到元素的模板语法。数据绑定是允许数据从数据源到视图目标以及反之的关键功能之一。此外,我们还将学习不同的数据绑定方式,并创建许多示例。

第九章,“Angular 中的高级表单”,解释了如何使用和掌握响应式表单。我们通过强调 HTML 模型与 NgModels 之间的关系来解决响应式表单的响应式部分,以便在给定表单上的每次更改都传播到模型。

第十章,“Angular 中的 Material Design”,讨论了 Material Design,这是关于设计的新热潮。在本章中,我们将学习如何将 Material Design 与 Angular 集成。此外,我们还将学习如何使用诸如网格和按钮之类的有用组件。

第十一章《实现 Angular 管道》解释了在视图中转换数据是我们在应用程序中必须做的最常见的仪式之一。我们将学习如何使用各种内置管道来转换值,并创建我们自己的管道。此外,我们还将学习如何传递参数并根据需要自定义管道。

第十二章《实现 Angular 服务》讨论了服务和工厂,创建 Angular 服务,使用服务从组件中访问数据以及创建异步服务。

第十三章《应用依赖注入》解释了如何创建可用作各种组件之间共享资源的可注入对象、服务和提供者类。此外,我们还将学习如何使用 Inject、Provider、useClassuseValue动态创建对象并及时使用。

第十四章《处理 Angular 动画》展示了动画对于设计和构建具有平滑过渡和效果的美观用户体验至关重要。我们将学习并实施使用动画、过渡、状态和关键帧的示例。

第十五章《将 Bootstrap 集成到 Angular 应用程序中》讨论了 Bootstrap,这可能是目前最流行的前端框架,在本章中,我们将了解拥有 Angular x Bootstrap 应用程序意味着什么。

第十六章《使用 Jasmine 和 Protractor 框架测试 Angular 应用程序》教授了软件开发过程中可能最重要的方面——使用 Jasmine 和 Protractor 框架测试 Angular 应用程序。我们将首先概述每个框架,然后转向 Angular 提供的测试工具。我们还将创建用于测试 Angular 组件和服务的示例测试脚本。

第十七章《Angular 中的设计模式》讨论了 TypeScript,这是一种面向对象的编程语言,我们可以利用数十年关于面向对象架构的知识。在本章中,我们将探索一些最有用的面向对象设计模式,并学习如何在 Angular 中应用它们。

你需要为这本书做好准备

您将需要以下软件清单:

  • NodeJS 6.10 或更高版本

  • NPM 3.10 或更高版本

  • 良好的编辑器,如 Visual Studio Code 或 Sublime Text

  • 浏览器,如 Chrome 或 Firefox 或 Edge

  • 互联网连接以下载和安装节点包

这本书是为谁准备的

这本书是为具有一定 Angular 先前经验的 JavaScript 开发人员准备的。我们假设您对 HTML、CSS 和 JavaScript 有一定的了解。

约定

在本书中,您将找到一些文本样式,用于区分不同类型的信息。以下是一些示例以及它们的含义解释。文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“然后,我们进入advanced-forms文件夹并删除不在chap7/angular-promise子目录中的所有内容。”

代码块设置如下:

@Component({ selector: 'app-root',
 templateUrl: './app.component.html', 
 styleUrls: ['./app.component.css']
  }) 

任何命令行输入或输出都按照以下方式编写:

npm install -g typescript
tsc mytypescriptcodefile.ts

警告或重要提示显示在这样的框中。提示和技巧显示如下。

第一章:Angular 的架构概述和构建简单应用

无论您是新手还是对 AngularJS 或 Angular 都不熟悉。如果您想快速开发具有丰富 UI 和 Angular 组件、模板和服务功能的优秀 Web 应用程序,您需要掌握 Angular,这本书就是为您准备的。

Angular 是一个 JavaScript 框架,使开发人员能够构建 Web 和移动应用程序。使用 Angular 构建的应用程序可以针对任何设备,如手机、平板电脑和台式电脑。Angular 不是 AngularJS 的增量版本。它完全重写了改进的依赖注入、动态加载和更简单的路由,并建议开发人员使用 TypeScript 并利用面向对象编程、静态类型、泛型和 lambda。

在本章中,我们将涵盖以下主题:

  • Angular 架构

  • TypeScript 的基础知识

  • 构建一个简单的应用程序

Angular 架构

在讨论架构之前,让我们看看 Angular 的新功能。Angular 的主要重点是移动设备,因为重要的是要考虑应用程序在手机上的性能和加载时间。许多模块已经从 Angular 核心中解耦,只留下了绝对核心的模块;从 Angular 核心中移除不需要的模块可以提高性能。

Angular 的目标是 ES6,并利用 TypeScript 作为开发脚本语言,可以在编译时对类型进行检查,而不是在运行时。TypeScript 在实例化类时提供了关于类的额外信息,通过为类注释元数据。您也可以使用 ES5 和 Dart 作为开发语言。有一个改进的依赖注入版本,支持子注入器和实例范围。路由器被完全重写,引入了组件路由器。Angular 支持组件指令、装饰器指令和模板指令。$scope 已经完全从 Angular 中移除。

Angular 的架构包括模块组件模板元数据指令服务

NgModules

Angular 框架有各种库,这些库被分组为模块,以构建应用程序。Angular 应用程序具有模块化的特性,并通过组装各种模块来构建。模块可能包含组件、服务、函数和/或值。一些模块可能包含其他模块的集合,被称为库模块。

Angular 包,如corecommonhttprouter,它们以@angular为前缀,包含许多模块。我们从这些库模块中导入我们的应用程序需要的内容,如下所示:

import {Http, Response} from @angular/http'; 

在这里,我们从库模块@angular/http中导入HttpResponse@angular/http指的是 Angular 包中的一个文件夹。可以通过引用模块的文件名将任何定义为导出的模块导入到另一个模块中。

注意:这个导入语句是在 ES2015 中引入的,用于导入从其他模块或脚本导出的对象或函数

但是,我们也可以像我们引用@angular/http一样引用文件夹。这可以通过在文件夹中添加一个index.ts文件并添加代码来从文件夹中导出模块来实现。这是 Angular 风格指南建议的最佳实践,称为桶技术:

export * from './http'; 

这是在@angular/http中找到的index.ts中的导出语句。该语句意味着它导出 HTTP 中的所有模块,并且它们可以在我们的应用程序中根据需要导入。

当我们编写一个 Angular 应用程序时,我们首先定义一个AppComponent(不一定要使用相同的名称)并导出它。

组件

组件是一个具有属性和方法的类,用于在视图中使用。这些暴露给视图的属性和方法使视图能够与组件交互。我们在组件类中编写支持视图的逻辑:

例如,接下来是一个组件类 book,它具有properties标题和作者以及一个getPubName方法,该方法返回书的名称:

export class BookComponent { 
  title: string; 
  author: string; 
  constructor() { 
      this.title = 'Learning Angular for .Net Developers'; 
      this.author = 'Rajesh Gunasundaram'; 
  } 
  getPubName() : string { 
    return 'Packt Publishing'; 
  } 
} 

注意:在本书的所有示例中,我们将使用 TypeScript。

组件的生命周期由 Angular 根据用户与应用程序的交互来管理。我们还可以添加一个根据组件状态变化触发的event方法。这些event方法称为生命周期钩子,是可选的。

我们将在第五章中详细了解组件,“实现 Angular 路由和导航”。

模板

模板可以被视为根据应用程序的 UI/UX 需求可视化的组件的表示。一个组件将有一个与之关联的模板。模板负责根据用户事件显示和更新数据:

这是一个简单的模板,用于显示书籍的标题和作者:

<h1>Book Details</h1> 
<p>Title of the Book: {{title}}</p> 
<p>Author Name : {{author}}</p> 

在这里,用花括号括起来的标题和作者值将由相关组件实例提供。

我们将在第八章中详细讨论模板及其语法,模板和数据绑定语法

元数据

通过使用@Component对类进行注释并传递必要的元数据,如selectortemplatetemplateUrl,可以将类转换为组件。只有在向类附加元数据后,Angular 才会将其视为组件:

让我们重新访问一下我们之前定义的BookComponent类。除非我们对其进行注释,否则 Angular 不会将此类视为组件。TypeScript 利用 ES7 功能,提供了一种用元数据装饰类的方法,如下所示:

@Component({ 
  selector:    'book-detail', 
  templateUrl: 'app/book.component.html' 
}) 
export class BookComponent { ... } 

在这里,我们用@Component装饰了BookComponent类,并附加了选择器和templateUrl的元数据。这意味着,无论在视图中的哪里,Angular 都会看到特殊的<book-detail/>标签,并创建一个BookComponent实例,并呈现分配给templateUrl的视图,即book.component.html

TypeScript 提供的装饰器是一个函数,它接受配置参数,这些参数由 Angular 用于创建组件实例并呈现相关视图。配置参数还可能包含有关指令和提供者的信息,在创建组件时,Angular 将使其可用。

数据绑定

数据绑定是开发人员在编写代码时的核心责任之一,用于将数据绑定到用户界面,并根据用户与用户界面的交互更新变化的数据。Angular 减轻了编写大量代码来处理数据绑定的负担:

Angular 通过与模板和组件协调来处理数据绑定。模板向 Angular 提供了如何以及绑定什么的指令。在 Angular 中有两种类型的绑定:全局单向数据绑定和双向数据绑定。单向数据绑定处理从组件到 DOM 或从 DOM 到组件的数据绑定。双向数据绑定处理通信的双方,即组件到 DOM 和 DOM 到组件。

<div>Title: {{book.title}}<br/> 
  Enter Author Name: <input [(ngModel)]="book.author"> 
</div> 

在这里,book.title用双大括号包裹,处理单向数据绑定。如果组件实例中有书名的值,它将显示在视图中。book.author赋给输入元素的ngModel属性,处理双向数据绑定。如果组件实例中的作者属性有值,它将被赋给输入元素,如果用户在输入控件中更改了值,更新后的值将在组件实例中可用。

我们将在第八章中详细学习数据绑定,模板和数据绑定语法

指令

指令是用于渲染模板的指令或指导方针。一个带有@Directive装饰的类附加了元数据,被称为指令。Angular 支持三种类型的指令,即组件指令、结构指令和属性指令:

组件是带有模板的指令的一种形式,它被装饰为@Component:实际上它是一个带有模板特性的扩展@Directive

<book-detail></book-detail> 

结构指令通过添加、删除和替换 DOM 元素来操作 DOM 元素并改变它们的结构。以下代码片段使用了两个结构指令:

<ul> 
<li *ngFor="let book of books"> 
    {{book.title}} 
</li> 
</ul> 

在这里,div元素有一个*ngFor指令,它遍历 books 集合对象并替换每本书的标题。

属性指令有助于更新元素的行为或外观。让我们使用属性指令来设置段落的字体大小。以下代码片段显示了一个使用属性指令实现的 HTML 语句:

<p [myFontsize]>Fontsize is sixteen</p> 

我们需要实现一个带有@Directive注解的类,以及指令的选择器。这个类应该包含指令行为的指令:

import { Directive, ElementRef, Input } from '@angular/core'; 
@Directive({ selector: '[myFontsize]' }) 
export class FontsizeDirective { 
    constructor(el: ElementRef) { 
       el.nativeElement.style.fontSize = 16; 
    } 
} 

在这里,Angular 将查找带有[myFontsize]指令的元素,并将字体大小设置为16

需要将myFontSize指令传递给@NgModule的 declarations 元数据,如下所示:

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

我们将在第六章中详细讨论指令,创建指令和实现变更检测

服务

服务是用户定义的用于解决问题的类。Angular 建议只在组件中包含特定于模板的代码。组件的责任是丰富 Angular 应用程序中的 UI/UX,并将业务逻辑委托给服务。组件是服务的消费者:

应用程序特定或业务逻辑,如持久化应用程序数据、记录错误和文件存储,应该委托给服务,组件应该消费相应的服务来处理适当的业务或应用程序特定逻辑:

例如,我们可以有一个名为BookService的服务,用于插入新书籍,编辑或删除现有书籍,并获取所有可用书籍的列表。

我们将在第十一章中更多地了解服务,实现 Angular 管道

依赖注入

当创建类的实例时,为其正常运行提供所需的依赖项称为依赖注入。Angular 提供了依赖注入的现代和改进版本:

在 Angular 中,注入器维护容器来保存依赖项的实例,并在需要时提供它们。如果依赖项的实例在容器中不可用,则注入器将创建依赖项的实例并提供它:

如前所述,组件具有与模板相关的逻辑,并且大多数情况下消费服务以执行业务逻辑。因此,组件依赖于服务。当我们为组件编写代码时,我们创建一个带有服务作为参数的构造函数。这意味着创建组件的实例取决于构造函数中的服务参数。Angular 要求注入器在组件的构造函数参数中提供服务的实例。如果可用,注入器将提供所请求服务的实例;否则,它将创建一个新的实例并提供它:

export class BookComponent { 
  constructor(private service: BookService) { } 
} 

在此代码片段中,: 符号来自 TypeScript,并不是 Angular 语法糖。private 关键字也来自 TypeScript,并且可以自动将传递的构造函数分配给类实例。类型信息用于推断要注入的类型。BookComponent 依赖于 BookService 并在构造函数中注入。因此,当创建 BookComponent 的实例时,Angular 也会确保 BookService 的实例对于 BookComponent 实例来说是可用的。

注射器知道要从提供程序创建的依赖项,并在引导应用程序或装饰组件时配置所需的依赖项类型,如下所示:

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

前面的代码片段将 BookService 添加为引导函数的提供程序。注射器将创建 BookService 的实例,并在整个应用程序中保持其可用性,以便在请求时注入:

@Component({ 
  providers:   [BookService] 
}) 
export class BookComponent { ... } 

前面的代码片段将 BookService 添加为组件的元数据提供程序。当遇到创建 BookComponent 实例的请求时,注射器将创建 BookService 的实例。

我们将在第十二章中详细讨论依赖注入和分层依赖注入,实现 Angular 服务

TypeScript 的基础知识

TypeScript 是 JavaScript 的超集,是由 Microsoft 开发的开源语言。用 TypeScript 编写的代码将被编译为 JavaScript,并在运行 Node.js 的任何浏览器或服务器上执行。TypeScript 实际上是 JavaScript 的一种类型。TypeScript 有助于提高您在 JavaScript 中编写的代码的质量。如果我们使用外部库,我们需要使用导入库的类型定义文件。类型定义文件提供 JavaScript 工具支持,并通过推断代码结构来启用编译时检查、代码重构和变量重命名支持。TypeScript 正在不断发展,并不断添加与 ES2016 规范和以后对齐的其他功能。

市场上有各种编辑器可以编写 TypeScript 代码,并使用 TypeScript 编译器进行编译。这些编辑器负责将您的 TypeScript 编译为 JavaScript。这里显示了一些流行的编辑器:

  • Visual Studio

  • Visual Studio Code

  • Sublime text

  • Atom

  • Eclipse

  • Emacs

  • WebStorm

  • Vim

您还可以通过在 Node.js 命令行工具中执行以下命令来将 TypeScript 作为Node.js包下载到全局:

npm install -g typescript

要将 TypeScript 代码转译为 JavaScript,您可以在命令行工具中执行以下命令:

tsc mytypescriptcodefile.ts

在这里,tsc是 TypeScript 编译器,它将 TypeScript 文件转换为 JavaScript 文件。mytypescriptfile是您的 TypeScript 代码文件的名称,.ts是 TypeScript 文件的扩展名。执行tsc命令时,它会生成一个与.ts源文件同名的.js文件。

在本章中,我们将使用 Visual Studio Code 编辑器进行示例代码演示。让我们看看 TypeScript 的基本特性,并举例说明。

基本类型

让我们探索 TypeScript 中一些基本类型以及如何使用它们。基本类型包括原始类型,如数字、字符串、布尔和数组。JavaScript 只在运行时验证类型,但 TypeScript 在编译时验证变量类型,并大大减少了运行时类型转换问题的可能性。

数字类型

数字类型表示浮点值。它可以保存十进制、二进制、十六进制和八进制文字等值:

let decimal: number = 6; 
let hex: number = 0xf00d; 
let binary: number = 0b1010; 
let octal: number = 0o744; 

布尔类型

布尔类型是一个非常简单的类型,可以保存两个值中的任一个,true 或 false。这种布尔类型用于在变量中维护状态:

let isSaved: Boolean; 
isSaved = true; 

在这里,布尔类型的isSaved变量被赋值为 true。

字符串

字符串数据类型可以保存一系列字符。声明和初始化字符串变量非常简单,如下所示:

var authorName: string = "Rajesh Gunasundaram"; 

在这里,我们声明了一个名为authorName的变量,类型为字符串,并赋值为Rajesh Gunasundaram。TypeScript 支持用双引号(")或单引号(')括起字符串值。

数组

数组数据类型旨在保存特定类型的值的集合。在 TypeScript 中,我们可以以以下两种方式定义数组:

var even:number[] = [2, 4, 6, 8, 10]; 

此语句使用方括号([])在数据类型数字后声明了一个数字类型的数组变量,并将其赋值为从 2 到 10 的一系列偶数。定义数组的第二种方式如下:

var even:Array<number> = [2, 4, 6, 8, 10]; 

此语句使用了通用数组类型,它使用 Array 关键字后跟尖括号(<>)来包裹数字数据类型。

枚举

枚举数据类型将具有一组命名的值。我们使用枚举器为标识某些值的常量提供用户友好的名称:

enum Day {Mon, Tue, Wed, Thu, Fri, Sat, Sun}; 
var firstDay: Day = Day.Mon; 

这里,我们有Day枚举变量,它保存了代表每周每天的一系列值。第二个语句展示了如何访问特定的枚举值,并将其赋值给另一个变量。

任意

任意数据类型是一个动态数据类型,可以容纳任何值。如果将字符串变量赋给整数变量,TypeScript 会抛出编译时错误。如果不确定一个变量将要容纳什么值,并且希望在赋值时退出编译器对类型的检查,可以使用任意数据类型:

var mixedList:any[] = [1, "I am string", false]; 
mixedList [2] = "no you are not"; 

这里,我们使用了任意类型的数组,以便它可以容纳任何类型,比如数字、字符串和布尔值。

Void

Void 实际上什么都不是。它可以用作函数的返回类型,声明这个函数不会返回任何值:

function alertMessage(): void { 
    alert("This function does not return any value"); 
} 

类是一个可扩展的模板,用于创建具有成员变量以保存对象状态和成员函数以处理对象行为的对象。

JavaScript 只支持基于函数和基于原型的继承来构建可重用的组件。ECMAScript 6 提供了使用类的语法糖来支持面向对象编程。然而,并非所有浏览器都理解 ES6,我们需要转译器,比如 TypeScript,将代码编译成 JavaScript 并针对 ES5,这与所有浏览器和平台兼容:

class Customer { 
    name: string; 
    constructor(name: string) { 
        this.name = name; 
   } 
    logCustomer() { 
        console.log('customer name is ' + this.name; 
   } 
} 

var customer = new Customer("Rajesh Gunasundaram"); 

这个Customer类有三个成员:一个 name 属性,一个构造函数和一个logCustomer方法。在 customer 类外部的最后一个语句使用new关键字创建了一个 customer 类的实例。

接口

接口是定义类行为的抽象类型。接口是抽象实现的契约。接口为可以在客户端之间交换的对象提供了类型定义。这使得客户端只能交换符合接口类型定义的对象。否则,我们会得到一个编译时错误。

在 TypeScript 中,接口定义了代码内部和项目外部对象的契约。让我们看一个使用 TypeScript 的例子:

function addCustomer(customerObj: {name: string}) { 
  console.log(customerObj.name); 
} 

var customer = {id: 101, name: "Rajesh Gunasundaram"}; 
addCustomer(customer); 

类型检查器验证了addCustomer方法调用并检查了它的参数。addCustomer期望一个具有字符串类型的 name 属性的对象。但调用addCustomer的客户端传递了一个具有两个参数idname的对象。

然而,编译器不会检查id属性,因为它不在addCustomer方法的参数类型中。对于编译器来说,只要求的属性存在即可。

让我们重写应用interface作为参数类型的方法如下:

interface Customer { 
  name: string; 
} 

function addCustomer(customerObj: Customer) { 
  console.log(customerObj.name); 
} 
var customer = {id: 101, name: "Rajesh Gunasundaram"}; 
addCustomer(customer); 

在这里,我们用Customer接口声明了name参数,并修改了addCustomer签名以接受Customer接口类型的参数。其余语句与前面的代码片段相同。编译器只检查对象的形状,因为 TypeScript 实现了结构类型系统。它不会检查我们传递的对象是否实现了Customer接口。它只查找参数中string类型的name属性,然后允许它存在。

使用接口的可选属性

在某些情况下,我们可能只想为最小的参数传递值。在这种情况下,我们可以将接口中的属性定义为可选属性,如下所示:

interface Customer { 
  id: number; 
  name: string; 
  bonus?: number; 
} 

function addCustomer(customer: Customer) { 
  if (customer.bonus) { 
    console.log(customer.bonus); 
  } 
} 

addCustomer({id: 101, name: "Rajesh Gunasundaram"}); 

在这里,通过在name属性末尾添加问号(?),将bonus属性定义为可选属性。

函数类型接口

我们刚刚看到如何在接口中定义属性。类似地,我们也可以在接口中定义函数类型。我们可以通过给出函数的签名和返回类型来在接口中定义函数类型。请注意,在下面的代码片段中,我们没有添加函数名:

interface AddCustomerFunc { 
  (firstName: string, lastName: string): string; 
} 

现在,我们有了AddCustomerFunc。让我们定义一个名为AddCustomerFunc的接口变量,并将一个具有相同签名的函数分配给它,如下所示:

var addCustomer: AddCustomerFunc; 
addCustomer = function(firstName: string, lastName: string) { 
  console.log('Full Name: ' + firstName + ' ' + lastName); 
  return firstName + ' ' + lastName; 
} 

函数签名中的参数名称可以变化,但数据类型不能变化。例如,我们可以修改字符串类型的fnln函数参数如下:

addCustomer = function(fn: string, ln: string) { 
  console.log('Full Name: ' + fn + ' ' + ln); 
} 

因此,如果我们在这里改变参数的数据类型或函数的返回类型,编译器将抛出关于参数不匹配或返回类型与AddCustomerFunc接口不匹配的错误。

数组类型接口

我们还可以为数组类型定义一个接口。我们可以指定索引数组的数据类型和数组项的数据类型如下:

interface CutomerNameArray { 
  [index: number]: string; 
} 

var customerNameList: CutomerNameArray; 
customerNameList = ["Rajesh", "Gunasundaram"]; 

TypeScript 支持两种索引类型:数字和字符串。这种数组类型接口还规定了数组的返回类型应与声明相匹配。

类类型接口

类类型接口定义了类的契约。实现接口的类应该满足接口的要求:

interface CustomerInterface { 
    id: number; 
    firstName: string; 
    lastName: string; 
    addCustomer(firstName: string, lastName: string); 
    getCustomer(id: number): Customer; 
} 

class Customer implements CustomerInterface { 
    id: number; 
    firstName: string; 
    lastName: string; 
    constructor() { } 
    addCustomer(firstName: string, lastName: string) { 
        // code to add customer 
   } 
    getCustomer(id: number): Customer { 
        return this; 
    } 
} 

类类型接口只处理类的公共成员。因此,不可能向接口添加私有成员。

扩展接口

接口可以被扩展。扩展接口使其共享另一个接口的属性,如下所示:

interface Manager { 
    hasPower: boolean; 
} 

interface Employee extends Manager { 
    name: string; 
} 

var employee = <Employee>{}; 
employee.name = "Rajesh Gunasundaram"; 
employee.hasPower = true; 

在这里,Employee接口扩展了Manager接口,并与Employee接口共享其hasPower

混合类型接口

混合类型接口用于当我们希望将对象既用作函数又用作对象时。如果实现了混合类型接口,我们可以像调用函数一样调用对象,或者我们可以将其用作对象并访问其属性。这种类型的接口使您能够将接口用作对象和函数,如下所示:

interface Customer { 
    (name: string); 
    name: string; 
    deleteCustomer(id: number): void; 
} 

var c: Customer; 
c('Rajesh Gunasundaram'); 
c.name = 'Rajesh Gunasundaram'; 
c.deleteCustomer(101); 

继承

继承是从另一个类或对象继承行为的概念。它有助于实现代码的重用性,并建立类或对象之间的关系层次结构。此外,继承帮助您转换类似的类。

JavaScript 以 ES5 为目标,不支持类,因此无法实现类继承。但是,我们可以实现原型继承而不是类继承。让我们通过示例来探索 ES5 中的继承。

首先,创建一个名为Animal的函数如下:

var Animal = function() { 

    this.sleep = function() { 
       console.log('sleeping'); 
   } 

    this.eat = function() { 
       console.log('eating'); 
   } 
} 

在这里,我们创建了一个名为Animal的函数,其中包含两个方法:sleepeat。现在,让我们使用原型扩展这个Animal函数,如下所示:

Animal.prototype.bark = function() { 
    console.log('barking'); 
} 

现在,我们可以创建一个Animal实例,并调用扩展函数bark,如下所示:

var a = new Animal(); 
a.bark(); 

我们可以使用Object.Create方法克隆父级的原型并创建一个子对象。然后,我们可以通过添加方法来扩展子对象。让我们创建一个名为Dog的对象,并从Animal继承它:

var Dog = function() { 
    this.bark = new function() { 
       console.log('barking'); 
   } 
} 

现在,让我们克隆Animal的原型,并继承Dog函数中的所有行为。然后,我们可以使用Dog实例调用Animal方法,如下所示:

Dog.prototype = Object.create(animal.prototype); 
var d = new Dog(); 
d.sleep(); 
d.eat(); 

TypeScript 中的继承

我们刚刚看到了如何使用原型在 JavaScript 中实现继承。现在,我们将看到如何在 TypeScript 中实现继承,这基本上是 ES6 继承。

在 TypeScript 中,类似于扩展接口,我们也可以通过继承另一个类来扩展类,如下所示:

class SimpleCalculator { 
   z: number; 
    constructor() { } 
   addition(x: number, y: number) { 
        this.z = this.x + this.y; 
   } 
    subtraction(x: number, y: number) { 
        this.z = this.x - this.y; 
   } 
} 

class ComplexCalculator extends SimpleCalculator { 
    constructor() { super(); } 
   multiplication(x: number, y: number) { 
        this.z = x * y; 
   } 
    division(x: number, y: number) { 
        this.z = x / y; 
   } 
} 
var calculator = new ComplexCalculator(); 
calculator.addition(10, 20); 
calculator.Substraction(20, 10); 
calculator.multiplication(10, 20); 
calculator.division(20, 10); 

在这里,我们可以使用ComplexCalculator的实例来访问SimpleCalculator的方法,因为它扩展了SimpleCalculator

私有和公共修饰符

在 TypeScript 中,类中的所有成员默认都是public的。我们必须显式添加private关键字来控制成员的可见性,而这个有用的特性在 JavaScript 中是不可用的。

class SimpleCalculator { 
    private x: number; 
    private y: number; 
    z: number; 
    constructor(x: number, y: number) { 
       this.x = x; 
       this.y = y; 
    } 
    addition() { 
        this.z = this.x + this.y; 
   } 
   subtraction() { 
        this.z = this.x - this.y; 
   } 
} 

class ComplexCalculator { 
    z: number; 
    constructor(private x: number, private y: number) { } 
    multiplication() { 
       this.z = this.x * this.y;  
   } 
    division() { 
        this.z = this.x / this.y; 
   } 
} 

请注意,在SimpleCalculator类中,我们将xy定义为私有属性,这些属性在类外部不可见。在ComplexCalculator中,我们使用参数属性定义了xy。这些参数属性将使我们能够在一个语句中创建和初始化成员。在这里,xy在构造函数中创建和初始化,而不需要在其中编写任何进一步的语句。

访问器

我们还可以实现对属性的 getter 和 setter,以控制从客户端访问它们。我们可以在设置属性变量的值之前或获取属性变量的值之前拦截一个过程:

var updateCustomerNameAllowed = true; 
class Customer { 
    private _name: string; 
    get name: string { 
          return this._name; 
   } 
    set name(newName: string) { 
          if (updateCustomerNameAllowed == true) { 
               this._name = newName; 
          } 
          else { 
               alert("Error: Updating Customer name not allowed!"); 
          } 
   } 
} 

在这里,name属性的 setter 确保客户名称可以更新。否则,它会显示一个警报消息,说明这是不可能的。

静态属性

这些属性不是特定于实例的,并且通过类名而不是使用this关键字来访问:

class Customer { 
     static bonusPercentage = 20; 
     constructor(public salary: number) {  } 

   calculateBonus() { 
          return this.salary * Customer.bonusPercentage/100; 
     } 
} 
var customer = new Customer(10000); 
var bonus = customer.calculateBonus(); 

在这里,我们声明了一个名为bonusPercentage的静态变量,它在calculateBonus方法中使用Customer类名进行访问。这个bonusPercentage属性不是特定于实例的。

模块

JavaScript 是一种强大而动态的语言。使用 JavaScript 进行动态编程时,我们需要结构化和组织代码,以使其易于维护,并且还能够轻松地找到特定功能的代码。我们可以通过应用模块化模式来组织代码。代码可以分成各种模块,并且相关的代码可以放在每个模块中。

TypeScript 通过使用模块关键字更容易实现模块化编程。模块使您能够控制变量的范围、代码的可重用性和封装性。TypeScript 支持两种类型的模块:内部模块和外部模块。

命名空间

我们可以使用 namespace 关键字在 TypeScript 中创建命名空间。在命名空间下定义的所有类都将在此命名空间下进行作用域限定,并且不会附加到全局范围:

namespace Inventory { 
      class Product { 
             constructor (public name: string, public quantity:  
                  number) {   } 
      } 
      // product is accessible 
      var p = new Product('mobile', 101); 
} 

// Product class is not accessible outside namespace 
var p = new Inventory.Product('mobile', 101); 

为了使Product类在namespace之外可用,我们需要在定义Product类时添加export关键字,如下所示:

module Inventory { 
      export class Product { 
             constructor (public name: string, public quantity: number) {   } 
      } 
} 

// Product class is now accessible outside namespace 
var p = new Inventory.Product('mobile', 101); 

我们还可以通过在引用文件的开头添加引用语句来跨文件共享命名空间,如下所示:

/// <reference path="Inventory.ts" /> 

模块

TypeScript 还支持模块,因为我们处理大量外部 JavaScript 库,这种模块化将帮助我们组织我们的代码。使用 import 语句,我们可以导入模块,如下所示:

Import { inv } from "./Inventory"; 
var p = new inv.Product('mobile', 101); 

在这里,我们刚刚导入了先前创建的模块 Inventory,创建了Product的一个实例并将其分配给变量p

函数

遵循 ES5 规范的 JavaScript 不支持类和模块。但是,我们尝试使用 JavaScript 中的函数式编程来限定变量和模块化。函数是 JavaScript 应用程序的构建块。

尽管 TypeScript 支持类和模块,但函数在定义特定逻辑方面起着关键作用。我们可以在 JavaScript 中定义命名函数和匿名函数,如下所示:

//Named function 
function multiply(a, b) { 
    return a * b; 
} 

//Anonymous function 
var result = function(a, b) { return a * b; }; 

在 TypeScript 中,我们使用函数箭头表示法定义参数的类型和返回类型,这也是 ES6 中支持的,如下所示:

var multiply:(a: number, b: number) => number = 
          function(a: number, b: number): number { return a * b; }; 

可选和默认参数

例如,我们有一个带有三个参数的函数,有时我们可能只在函数中传递前两个参数的值。在 TypeScript 中,我们可以使用可选参数来处理这种情况。我们可以将前两个参数定义为正常参数,将第三个参数定义为可选参数,如下面的代码片段所示:

function CustomerName(firstName: string, lastName: string, middleName?: string) { 
    if (middleName) 
        return firstName + " " + middleName + " " + lastName; 
    else 
        return firstName + " " + lastName; 
} 
//ignored optional parameter middleName 
var customer1 = customerName("Rajesh", "Gunasundaram"); 
//error, supplied too many parameters 
var customer2 = customerName("Scott", "Tiger", "Lion", "King");  
//supplied values for all 
var customer3 = customerName("Scott", "Tiger", "Lion");  

在这里,middleName是可选参数,当调用function时可以忽略它。

现在,让我们看看如何在函数中设置默认参数。如果在函数中没有提供参数的值,我们可以定义它以采用配置的默认值:

function CustomerName(firstName: string, lastName: string, middleName: 
     string = 'No Middle Name') { 
    if (middleName) 
        return firstName + " " + middleName + " " + lastName; 
    else 
        return firstName + " " + lastName; 
} 

在这里,middleName是默认参数,如果调用者没有提供值,它将默认为No Middle Name

剩余参数

使用剩余参数,您可以将值数组传递给函数。这可以用于您不确定将向函数提供多少值的情况:

function clientName(firstClient: string, ...restOfClient: string[]) { 
   console.log(firstClient + " " + restOfClient.join(" ")); 
} 
clientName ("Scott", "Steve", "Bill", "Sergey", "Larry"); 

在这里,请注意restOfClient剩余参数前面带有省略号(...),它可以保存一个字符串数组。在函数的调用者中,只有提供的第一个参数的值将被赋给firstClient参数,其余的值将被赋给restOfClient作为数组值。

泛型

泛型对于开发可重用的组件非常有用,可以针对任何数据类型进行操作。因此,消费该组件的客户端将决定它应该对哪种类型的数据进行操作。让我们创建一个简单的函数,返回传递给它的任何数据:

function returnNumberReceived(arg: number): number { 
    return arg; 
} 
unction returnStringReceived(arg: string): string { 
    return arg; 
} 

正如你所看到的,我们需要单独的方法来处理每种数据类型。我们可以使用任意数据类型在一个函数中实现它们,如下所示:

function returnAnythingReceived (arg: any): any { 
    return arg; 
} 

这与泛型类似。但是,我们无法控制返回类型。如果我们传递一个数字,我们无法预测函数是否会返回该数字,返回类型可以是任何类型。

泛型提供了一个特殊的T类型变量。将这种类型应用于函数,使客户端能够传递他们希望这个函数处理的数据类型:

function returnWhatReceived<T>(arg: T): T { 
    return arg; 
} 

因此,客户端可以按照以下方式调用这个函数来处理各种数据类型:

var stringOutput = returnWhatReceived<string>("return this");  
// type of output will be 'string' 
var numberOutput = returnWhatReceived<number>(101);  
// type of output will be number 

请注意,在函数调用中,要处理的数据类型是通过尖括号(<>)包裹传递的。

泛型接口

我们还可以使用T类型变量定义泛型接口,如下所示:

interface GenericFunc<T> { 
    (arg: T): T; 
} 
function func<T>(arg: T): T { 
    return arg; 
} 
var myFunc: GenericFunc<number> = func; 

在这里,我们定义了一个泛型接口和GenericFunc类型的myFunc变量,将数字数据类型传递给T类型变量。然后,将这个变量赋值给一个名为func的函数。

泛型类

与泛型接口类似,我们也可以定义泛型类。我们使用尖括号(<>)定义带有泛型类型的类,如下所示:

class GenericClass<T> { 
    add: (a: T, b: T) => T; 
}

var myGenericClass = new GenericClass<number>(); 
myGenericClass.add = function(a, b) { return a + b; }; 

在这里,通过传递数字作为泛型数据类型来实例化泛型类。因此,add 函数将处理并添加作为参数传递的两个数字类型的变量。

装饰器

装饰器使我们能够通过添加行为来扩展类或对象,而无需修改代码。装饰器为类添加额外功能。装饰器可以附加到类、属性、方法、参数和访问器上。在 ECMAScript 2016 中,装饰器被提议用于修改类的行为。装饰器以@符号和在运行时调用的函数解析为装饰器名称。

以下代码片段显示了授权函数,并且它可以作为@authorize装饰器应用于任何其他类:

function authorize(target) { 
    // check the authorization of the use to access the "target" 
} 

类装饰器

类装饰器在类声明之前声明。类装饰器可以观察、修改和替换被其应用于的类的定义,通过应用于该类的构造函数。TypeScript 中ClassDecorator的签名如下:

declare type ClassDecorator = <TFunction extends Function>(target:  
      TFunction) => TFunction | void; 

考虑一个Customer类;我们希望该类被冻结。其现有属性不应被移除,也不应添加新属性。

我们可以创建一个单独的类,可以接受任何对象并将其冻结。然后我们可以用@freezed装饰客户类,以防止向类添加新属性或删除现有属性:

@freezed 
class Customer { 

  public firstName: string; 
  public lastName: string; 

  constructor(firstName : string, lastName : string) { 
    this.firstName = firstName; 
    this.lastName = lastName; 
  } 
} 

前面的类在firstnamelastname构造函数中接受四个参数。以下是为@freezed装饰器编写的函数的代码片段:

function freezed(target: any) { 
    Object.freeze(target); 
} 

在这里,freezed 装饰器接受target,即被装饰的Customer类,并在执行时将其冻结。

方法装饰器

方法装饰器在方法声明之前声明。此装饰器用于修改、观察或替换方法定义,并应用于方法的属性描述符。以下代码片段显示了一个简单的类,其中应用了方法装饰器:

class Hello { 
    @logging 
    increment(n: number) { 
        return n++; 
   } 
} 

Hello类具有increment方法,该方法递增其参数提供的数字。请注意,increment方法使用@logging装饰器进行装饰,以记录递增方法的输入和输出。以下是logging函数的代码片段:

function logging(target: Object, key: string, value: any) { 

        value.value = function (...args: any[]) { 
            var result = value.apply(this, args); 
            console.log(JSON.stringify(args)); 
            return result; 
        } 
    }; 
} 

方法装饰器函数接受三个参数:targetkeyvaluetarget保存被装饰的方法;key保存被装饰方法的名称;value是对象上存在的指定属性的属性描述符。

当调用递增方法时,logging 方法被调用,并将值记录到控制台。

访问器装饰器

访问器装饰器在访问器声明之前加上前缀。这些装饰器用于观察、修改或替换访问器定义,并应用于属性描述符。以下代码片段显示了一个简单的类,其中应用了访问器装饰器:

class Customer { 
  private _firstname: string; 
  private _lastname: string; 

  constructor(firstname: string, lastname: string) { 
        this._firstname = firstname; 
        this._lastname = lastname; 
  } 

  @logging(false) 
  get firstname() { return this._firstname; } 

  @logging(false) 
  get lastname() { return this._lastname; } 
} 

在这个类中,我们使用@logging装饰器修饰了firstnamelastname的获取器,并传递了boolean来启用或禁用日志记录。以下代码片段显示了@logging装饰器的函数:

function logging(value: boolean) { 
    return function (target: any, propertyKey: string, descriptor: 
                PropertyDescriptor) { 
        descriptor.logging = value; 
    }; 
} 

logging函数将布尔值设置为日志属性描述符。

属性装饰器

属性装饰器是前缀到属性声明的。它们实际上通过添加额外的行为来重新定义被装饰的属性。在 TypeScript 源代码中,PropertyDecorator的签名如下:

declare type PropertyDecorator = (target: Object, propertyKey: string | 
    symbol) => void; 

以下是一个类的代码片段,其中应用了属性装饰器:

class Customer { 
  @hashify 
  public firstname: string; 
  public lastname: string; 

  constructor(firstname : string, lastname : string) { 
    this.firstname = firstname; 
    this.lastname = lastname; 
  } 
} 

在这段代码中,firstname属性被@hashify属性装饰器修饰。现在,我们将看到@hashify属性装饰器函数的代码片段:

function hashify(target: any, key: string) { 
  var _value = this[key]; 

  var getter = function () { 
        return '#' + _value; 
  }; 

  var setter = function (newValue) { 
    _value = newValue; 
  }; 

  if (delete this[key]) { 
    Object.defineProperty(target, key, { 
      get: getter, 
      set: setter, 
      enumerable: true, 
      configurable: true 
    }); 
  } 
} 

_value保存了被装饰属性的值。获取器和设置器函数都可以访问变量_value,在这里我们可以通过添加额外的行为来操纵_value。我已经在获取器中连接了#来返回带有哈希标记的firstname。然后我们使用delete运算符从类原型中删除原始属性。将创建一个带有原始属性名称和额外行为的新属性。

参数装饰器

参数装饰器是前缀到参数声明的,并且它们应用于类构造函数或方法声明的函数。ParameterDecorator的签名如下:

declare type ParameterDecorator = (target: Object, propertyKey:  
   string | symbol, parameterIndex: number) => void; 

现在,让我们定义Customer类,并使用参数装饰器来修饰参数,以使其成为必需参数,并验证值是否已被提供:

class Customer { 
    constructor() {  } 

    getName(@logging name: string) { 
        return name; 
   } 
} 

在这里,名称参数已被@logging修饰。参数装饰器隐式接受三个输入,即具有此装饰器的类的prototype,具有此装饰器的方法的name,以及被装饰的参数的index。参数装饰器的logging函数实现如下:

function logging(target: any, key : string, index : number) { 

  console.log(target); 
  console.log(key); 
  console.log(index); 
} 

在这里,target是具有装饰器的类,key是函数名称,index包含参数索引。这段代码只是将targetkeyindex记录到控制台。

构建一个简单的应用程序

我假设您已经安装了 Node.js、npm 和 Visual Studio Code,并准备好用它们进行开发。现在让我们通过克隆 Git 存储库并执行以下步骤来创建一个 Angular 应用程序:

  1. 打开Node.Js命令提示符并执行以下命令:
 **git clone https://github.com/angular/quickstart my-angular** 

使用 Visual Studio Code 打开克隆的my-angular应用程序。此命令将克隆 Angular 快速启动存储库,并为您创建一个名为 my-angular 的 Angular 应用程序,其中包含所需的所有样板代码。

my-angular 应用程序的文件夹结构。

文件夹结构和样板代码按照官方样式指南angular.io/docs/ts/latest/guide/style-guide.html进行组织。src文件夹包含与应用程序逻辑相关的代码文件,e2e文件夹包含与端到端测试相关的文件。现在不要担心应用程序中的其他文件。让我们现在专注于package.json

  1. 点击package.json文件,它将包含有关元数据和项目依赖项配置的信息。以下是package.json文件的内容:
{   
   "name":"angular-quickstart",   
   "version":"1.0.0",   
   "description":"QuickStart   package.json from the documentation, 
         supplemented with testing support",   
   "scripts":{   
      "build":"tsc   -p src/",   
      "build:watch":"tsc   -p src/ -w",   
      "build:e2e":"tsc   -p e2e/",   
      "serve":"lite-server   -c=bs-config.json",   
      "serve:e2e":"lite-server   -c=bs-config.e2e.json",   
      "prestart":"npm   run build",   
      "start":"concurrently   \"npm run build:watch\" \"npm run 
            serve\"",   
      "pree2e":"npm   run build:e2e",   
      "e2e":"concurrently   \"npm run serve:e2e\" \"npm run 
            protractor\" --kill-others   --success first",   
      "preprotractor":"webdriver-manager   update",   
      "protractor":"protractor   protractor.config.js",   
      "pretest":"npm   run build",   
      "test":"concurrently   \"npm run build:watch\" \"karma start 
           karma.conf.js\"",   
      "pretest:once":"npm   run build",   
      "test:once":"karma   start karma.conf.js --single-run",   
      "lint":"tslint   ./src/**/*.ts -t verbose"   
   },   
   "keywords":[   

   ],   
   "author":"",   
   "license":"MIT",   
   "dependencies":{   
      "@angular/common":"~4.0.0",   
      "@angular/compiler":"~4.0.0",   
      "@angular/core":"~4.0.0",   
      "@angular/forms":"~4.0.0",   
      "@angular/http":"~4.0.0",   
      "@angular/platform-browser":"~4.0.0",   
      "@angular/platform-browser-dynamic":"~4.0.0",   
      "@angular/router":"~4.0.0",   
      "angular-in-memory-web-api":"~0.3.0",   
      "systemjs":"0.19.40",   
      "core-js":"².4.1",   
      "rxjs":"5.0.1",   
      "zone.js":"⁰.8.4"   
   },   
   "devDependencies":{   
      "concurrently":"³.2.0",   
      "lite-server":"².2.2",   
      "typescript":"~2.1.0",   
      "canonical-path":"0.0.2",   
      "tslint":"³.15.1",   
      "lodash":"⁴.16.4",   
      "jasmine-core":"~2.4.1",   
      "karma":"¹.3.0",   
      "karma-chrome-launcher":"².0.0",   
      "karma-cli":"¹.0.1",   
      "karma-jasmine":"¹.0.2",   
      "karma-jasmine-html-reporter":"⁰.2.2",   
      "protractor":"~4.0.14",   
      "rimraf":"².5.4",   
      "@types/node":"⁶.0.46",   
      "@types/jasmine":"2.5.36"   
   },   
   "repository":{   

   }   
}   

  1. 现在,我们需要在命令窗口中运行npm install命令,导航到application文件夹中,以安装package.json中指定的所需依赖项:

执行 npm 命令以安装package.json中指定的依赖项。

现在,您将在node_modules文件夹下添加所有依赖项,如此屏幕截图所示:

node_modules文件夹下的依赖项。

  1. 现在,让我们运行这个应用程序。要运行它,在命令窗口中执行以下命令:
          npm start

运行此命令将构建应用程序,启动 lite 服务器,并将应用程序托管到其中。

打开任何浏览器,导航到http://localhost:3000/;您将看到以下页面显示,这是通过我们的 Angular 应用程序呈现的:

在 Visual Studio Code 中激活调试窗口。

现在让我们浏览index.html的内容。以下是index.html的内容:

<!DOCTYPE html>
<html>
<head>
<title>Hello Angular 4</title>
<base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,                  initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-   
                js/client/shim.min.js">   
      </script>
<script    
         src="node_modules/zone.js/dist/zone.js">   
      </script>
<script    
         src="node_modules/systemjs/dist/system.src.js">   
      </script>
<script src="systemjs.config.js"></script>
<script>
            System.import('main.js').catch(function(err){               
              console.error(err); });
         </script>

</head>
<body>
<my-app>My first Angular 4   app for Packt    
               Publishing...</my-app>
</body>
</html>

请注意,脚本是使用System.js加载的。System.js是在运行时加载模块的模块加载器。

哇!终于,我们的第一个 Angular 应用程序已经启动运行。到目前为止,我们已经看到了如何通过从 GitHub 克隆官方快速启动存储库来创建 Angular 应用程序。我们运行了应用程序,并成功在浏览器中看到了它。

总结

哇!这是一个很棒的介绍,不是吗?我们从学习 Angular 的架构开始。我们讨论了 Angular 架构的各种构件。然后我们深入了解了 TypeScript 的基础知识。我们已经看到了一些基本类型和示例。我们还学习了如何编写类,使用接口,并在类中实现它们。我们还学习了继承。

我们已经学习了通过使用模块和命名空间来构建我们的代码。我们还涵盖了一些 TypeScript 的高级主题,如修饰符、访问器、静态属性、泛型和装饰器。

最后,我们使用 Angular 和 TypeScript 创建了一个简单的应用程序。本章为您提供了使用 TypeScript 开发 Angular 应用程序所需的知识,使用了它提供的语法糖。

在下一章中,我们将讨论将 AngularJS 应用迁移到 Angular。

第二章:将 AngularJS 应用程序迁移到 Angular 应用程序

我们都知道 Angular 有很多改进,并且是从头开始设计的。因此,Angular 开发人员中最令人困扰的问题之一是如何将现有的 AngularJS 应用程序迁移到 Angular。在本章中,我们将讨论成功迁移现有 AngularJS 应用程序所推荐的最佳实践、方法和工具。

在本章中,我们将涵盖以下主题:

  • 迁移过程

  • 语法差异

  • 升级到 Angular 的好处

  • 升级到 Angular 的规则

  • 使用 UpgradeAdapter 进行增量升级

  • 组件迁移

  • 从 AngularJS 到 Angular 的路线图

迁移过程

AngularJS 和 Angular 在语法和概念上有所不同。因此,迁移过程不仅涉及在语法层面上的代码更改,还涉及实现层面的更改。Angular 团队通过在 Angular 中提供内置工具,使开发人员更容易将 AngularJS 应用程序迁移到 Angular。在开始迁移过程之前,我们的现有 AngularJS 应用程序中有一些初步过程要做。

初步过程涉及解耦现有代码并使现有代码可维护。这个初步过程不仅为升级代码做好准备,还将改善现有的 AngularJS 应用程序的性能。

我们可以通过在同一个应用程序中同时运行 AngularJS 和 Angular,并逐个启动迁移过程,从组件开始逐步迁移。这种方法有助于迁移大型应用程序,将业务与任何影响隔离开,并在一段时间内完成升级。这种方法可以使用 Angular 升级模块实现。

Angular 和 AngularJS 之间的语法差异

Angular 在许多方面与 AngularJS 的语法不同。让我们在这里看一些。

模板中的本地变量和绑定

模板是处理应用程序的 UI 部分的视图,使用 HTML 编写。首先,我们将看到单向数据绑定的语法差异。

AngularJS:

<h1>Book Details:</h1> 
<p>{{vm.bookName}}</p> 
<p>{{vm.authorName}}</p> 

Angular:

<h1>Book Details:</h1> 
<p>{{bookName}}</p> 
<p>{{authorName}}</p> 

这两个代码片段都显示了单向数据绑定,将书籍和作者名称绑定到 UI,使用双大括号。然而,AngularJS 在引用控制器的属性以绑定到模板时会加上控制器的别名前缀,而 Angular 不会使用别名前缀,因为视图或模板默认与组件关联。

模板中的过滤器和管道

AngularJS 过滤器现在在 Angular 中被称为管道。在 AngularJS 中,过滤器在管道字符(|)之后使用,在 Angular 中没有语法上的变化。然而,Angular 将过滤器称为管道。

AngularJS:

<h1>Book Details:</h1> 
<p>{{vm.bookName}}</p> 
<p>{{vm.releaseDate | date }}</p> 

Angular:

<h1>Book Details:</h1> 
<p>{{bookName}}</p> 
<p>{{releaseDate | date }}</p> 

请注意,我们已经将日期管道或过滤器应用于releaseDate,在 AngularJS 和 Angular 之间没有语法上的变化。

模板中的本地变量

让我们看看在 AngularJS 和 Angular 中分别使用本地变量在ng-repeatngFor中的示例。

AngularJS:

<tr ng-repeat="book in vm.books"> 
  <td>{{book.name}}</td> 
</tr> 

Angular:

<tr *ngFor="let book of books"> 
  <td>{{book.name}}</td> 
</tr> 

请注意,在 AngularJS 中,本地变量 book 是隐式声明的,在 Angular 中,使用 let 关键字来定义本地变量 book。

Angular 应用程序指令

AngularJS 允许使用ng-app指令声明性地引导应用程序。但是,Angular 不支持声明性引导。它只支持通过调用引导函数并传递应用程序的根组件来显式引导应用程序。

AngularJS:

<body ng-app="packtPub"> 

Angular:

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

请注意,在 AngularJS 中,Angular 模块名称packtPub已分配给ng-app指令。然而,在 Angular 中,根据执行环境,我们将AppModule传递给引导模块。请注意,AppModuleNgModule类,它是我们刚刚根据执行环境引导的应用程序的根模块。

处理 CSS 类

AngularJS 提供了ng-class指令来包含或排除 CSS 类。同样,Angular 有ngClass指令根据表达式应用或移除 CSS 类。类绑定是 Angular 提供的另一个更好的选项,用于添加或移除 CSS 类。

AngularJS:

<div ng-class="{active: isActive}"> 

Angular:

<div [ngClass]="{active: isActive}"> 
<div [class.active]="isActive"> 

请注意,在 Angular 中将类绑定应用于第二个div

绑定点击事件

AngularJS 提供了基于事件的指令ng-click,可以将click事件绑定到关联控制器中的方法。Angular 通过使用可以使用( )语法来定位本机 DOM 元素来实现相同的功能,并通过将单向数据绑定与event绑定相结合来实现这一点。

AngularJS:

<button ng-click="vm.showBook()"> 
<button ng-click="vm.showBook($event)"> 

Angular:

<button (click)="showBook()"> 
<button (click)="showBook($event)"> 

请注意,在 Angular 中,目标事件 click 是在括号内定义的,并且组件中的方法在引号中指定。

模板中的控制器和组件

AngularJS 提供ng-controller指令来将控制器附加到视图,并将视图与该视图相关的控制器绑定。Angular 不支持控制器和ng-controller指令将控制器与视图关联。组件同意其关联的视图或模板,而不是反过来。

AngularJS:

<div ng-controller="PacktBooksCtrl as vm"> 

Angular:

@Component({ 
  selector: 'packt-books', 
  templateUrl:'app/packtbooks.component.html' 
})

在 AngularJS 中,我们使用立即调用的函数表达式IIFE)来定义控制器。在 Angular 中,我们使用装饰有@Component的 TypeScript 类来定义组件,提供元数据,如selectortemplateUrl等。

AngularJS:

(function () { 
  ... 
}()); 

Angular:

@Component({ 
  selector: 'packt-books', 
  templateUrl:'app/packtbooks.component.html' 
}) 
export class PacktBooks { 
} 

升级到 Angular 的好处

让我们看看升级到 Angular 的一些好处:

  • 更好的性能:Angular 支持更快的变更检测、更快的引导时间、视图缓存、模板预编译等。

  • 服务器端渲染:Angular 已经分成了一个应用层和一个渲染层。这使我们能够在 Web 工作者或者除了浏览器之外的服务器上运行 Angular。

  • 更强大的模板:Angular 引入了新的模板语法,去除了许多指令,并与 Web 组件和其他元素更好地集成。

  • 更好的生态系统:Angular 生态系统将来会变得更好,更有趣。

升级到 Angular 的策略

有不同的升级策略可用于迁移到 Angular。它们如下:

  • 一次性:替换整个 AngularJS 应用程序,从一个点开始重写代码为 Angular。

  • 增量:逐个服务或组件升级现有应用程序,同时运行 AngularJS 和 Angular。

如果 AngularJS 应用程序很小,那么一次性重写可能是升级的最简单和最快的方式。如果 AngularJS 应用程序较大,无法一次性重写整个代码,我们需要逐步重写,逐个组件,逐个服务。这被称为增量升级。然而,同时运行ng1ng2会对性能产生影响。

增量升级到 Angular 的规则

如果我们遵循以下一套规则,逐步升级将会更容易:

  • 每个文件实现一个组件;这有助于隔离组件并逐个迁移它们。

  • 应用模块化编程并按功能排列文件夹;这将使开发人员能够集中精力逐步迁移一个功能。

  • 使用模块加载器;遵循前面的规则,您将在项目中得到大量的文件。这会带来组织文件和在 HTML 页面中正确引用它们的麻烦。当您使用诸如SystemJSWebpackBrowserify之类的模块加载器时,它使我们能够使用 TypeScript 内置的模块系统。这使开发人员能够明确地导入或导出功能,并在应用程序的各个部分之间共享它们的代码中使用。

  • 首先安装 TypeScript;在开始实际升级过程之前,最好先引入 TypeScript 编译器。这可以通过简单的安装 TypeScript 编译器来实现。

  • 使用组件指令;最好使用组件指令而不是在 AngularJS 应用程序中使用ng-controllerng-include,这样在 Angular 中迁移组件指令将比迁移控制器更容易。

使用 UpgradeAdapter 进行增量升级

可以使用UpgradeAdapter无缝进行增量升级。UpgradeAdapter是一个可以引导和管理同时支持 Angular 和 AngularJS 代码的混合应用程序的服务。UpgradeAdapter使您能够同时运行 AngularJS 和 Angular 代码。UpgradeAdapter促进了从一个框架到另一个框架的组件和服务之间的互操作性。UpgradeAdapter将负责依赖注入、DOM 和变更检测的互操作性。

将 AngularJS 依赖注入到 Angular 中

我们可能会遇到这样的情况,即将 AngularJS 服务上的业务逻辑或任何内置服务(如$location$timeout)注入到 Angular 代码中。这可以通过将 AngularJS 提供者升级到 Angular 并在需要的地方将其注入到 Angular 代码中来处理。

将 Angular 依赖注入到 AngularJS 中

有时可能需要将 Angular 依赖项降级,以便在 AngularJS 代码中使用它们。当我们需要将现有服务迁移到 Angular 或在 Angular 中创建新服务时,这是必要的,因为这些服务在 AngularJS 中编写的组件依赖于它们。

组件迁移

将 AngularJS 应用程序设计为以组件为中心的做法比以控制器为中心的设计更好。如果您按照这种做法开发了应用程序,那么迁移将会更容易。AngularJS 中的组件指令将具有与 Angular 组件类似的模板、控制器和绑定。但请确保您的 AngularJS 应用程序组件指令没有使用 compile、replace、priority 和 terminal 等属性。如果您的应用程序实现了具有这些属性的组件指令,那么它就不符合 Angular 架构。如果您的 AngularJS 应用程序是使用 AngularJS 1.5 开发的,并且组件是使用组件 API 实现的,那么您可能已经注意到了与 Angular 组件的相似之处。

从 AngularJS 到 Angular 的路线图

在将 AngularJS 迁移到 Angular 的过程中,遵循这个路线图是很好的:

  • JavaScript 转换为 TypeScript

  • 安装 Angular 包

  • 创建 AppModule

  • 引导您的应用程序

  • 升级您的应用程序服务

  • 升级您的应用程序组件

  • 添加 Angular 路由器

让我们在以下部分详细讨论它们。

JavaScript 转换为 TypeScript

通过引入 TypeScript 开始迁移过程,因为您将在 Angular 中使用 TypeScript 编写代码。将 TypeScript 安装到您的 Angular 应用程序中非常容易。运行以下命令,从npm安装 TypeScript 到您的应用程序,并将包信息保存到package.json中:

npm i typescript --save-dev

注意:由于 Angular 包仅在 npm 上可用,我们将从 npm 安装任何新包,并逐渐淘汰 Bower 包管理器

我们还需要配置 TypeScript,指示它将 TypeScript 代码转译为tsconfig.json文件中的 ES5 代码。

最后,我们需要在package.json的 scripts 部分下添加以下命令,以在后台以监视模式运行 TypeScript 编译器,这样当您进行更改时,代码将被重新编译:

"script": {   
  "tsc": "tsc",   
  "tsc:w": "tsc -w",   
}   

安装 Angular 包

我们需要安装 Angular 以及SystemJS模块加载器。最快的方法是从 GitHub 克隆quickstart应用程序到您的开发系统。然后将与 Angular 相关的依赖项从package.json复制到您的应用程序package.json中,并将SystemJS配置文件systemjs.config.js复制到您的应用程序根目录。完成所有这些后,然后运行以下命令来安装我们刚刚在package.json中添加的软件包:

npm install

将以下语句添加到index.html文件中。这将帮助相对 URL 从app文件夹中提供服务。这很重要,因为我们需要将index.html文件从app文件夹移动到应用程序的root文件夹中:

<base href="/app/">   

现在,让我们添加 JavaScript 文件引用并通过SystemJS加载 Angular。最后,使用System.import语句加载实际应用程序:

<script src="/node_modules/core-js/client/shim.min.js"></script>   
<script src="/node_modules/zone.js/dist/zone.js"></script>   
<script src="/node_modules/systemjs/dist/system.src.js"></script>   
<script src="/systemjs.config.js"></script>   
<script>   
  System.import('/app');   
</script>   

创建 AppModule

我们需要为您的应用程序创建一个AppModule。以下AppModule类定义了最小的NgModule

import { NgModule } from '@angular/core';   
import { BrowserModule } from '@angular/platform-browser';   

@NgModule({   
  imports: [   
    BrowserModule,   
  ],   
})   
export class AppModule {   
}

在这里,我们只是从@angular/core导入了一个NgModule和从@angular/platform-browser导入了BrowserModule。任何简单的基于浏览器的 Angular 应用程序都会有这样一个简单的AppModule

引导您的应用程序

通过将ng-app指令附加到<html>元素来引导 AngularJS 应用程序。这在 Angular 中将不再起作用,因为引导 Angular 应用程序是不同的。

通过运行以下命令安装 Angular 升级包,并将映射添加到system.config.js

npm install @angular/upgrade --save

该语句还会更新package.json,引用了@angular/upgrade。更新后的systemjs.config.js如下所示:

System.config({   
    paths: {   
      'npm:': '/node_modules/'   
    },   
    map: {   
      'ng-loader': '../src/systemjs-angular-loader.js',   
      app: '/app',   
      '@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js'   
    }   
})   

现在从index.html文件中的<html>元素中删除ng-app属性。然后我们需要将UpgradeModule导入到AppModule中。为了以 Angular 方式引导我们的 AngularJS 应用程序,我们需要在AppModule中重写ngDoBootstrap函数如下:

import { UpgradeModule } from '@angular/upgrade/static';   

@NgModule({   
  imports: [   
    BrowserModule,   
    UpgradeModule,   
  ],   
})   
export class AppModule {   
  constructor(private upgrade:   UpgradeModule) { }   
  ngDoBootstrap() {   
      this.upgrade.bootstrap(document.documentElement, [yourApp']);   
  }   
}   

最后,我们需要在main.ts中引导AppModule,该文件在system.config.js中配置为应用程序的入口点。main.ts的代码片段如下所示:

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

升级应用程序服务

在 Angular 应用程序中,服务主要用于在整个应用程序中提供数据,并且这些数据将从任何服务中获取。在 AngularJS 中,我们一直在使用ngResource%http来与服务通信和处理数据。

作为迁移的一部分,我们需要在我们使用ngResource$http的地方使用 Angular HTTP 模块。要使用 Angular HTTP 模块,我们首先需要导入HttpModule并将其添加到AppModuleNgModule指令的导入数组中,如下所示:

import { HttpModule } from '@angular/http';   

@NgModule({   
  imports: [   
    BrowserModule,   
    UpgradeModule,   
    HttpModule,   
  ],   
})   
export class AppModule {   
  constructor(private upgrade:   UpgradeModule) { }   
  ngDoBootstrap() {   
      this.upgrade.bootstrap(document.documentElement, ['yourApp']);   
  }   
}   

接下来,用装饰有@Injectable指令的新 TypeScript 类替换应用程序中基于ngResource$http的服务的代码片段,如下所示:

@Injectable()   
export class BookService {   
/* . . . */   
}   

装饰器@Injectable将向BookService类添加特定于依赖注入的元数据,以便 Angular 知道哪些类已准备好进行依赖注入。我们需要将 HTTP 服务注入到BookService的构造函数中,并且注入的 HTTP 服务将用于访问books.json中的数据以获取书籍列表,如下所示:

@Injectable()   
export class BookService {   
  constructor(private http: Http) { }   
  books(): Observable<Book[]> {   
    return   this.http.get(`data/books.json`)   
      .map((res: Response) =>   res.json());   
  }   
}   

以下是可以作为书籍模型类型的Book接口:

export interface PhoneData {   
  title: string;   
  author: string;   
  publication: string;   
}   

这个 Angular 服务与 AngularJS 不兼容,不能直接注入。因此,我们需要将injectable方法降级以将我们的BookService插入到 AngularJS 代码中。为此,我们需要在@angular/upgrade/static中使用一个名为downgradeInjectable的方法:

declare var angular:   angular.IAngularStatic;   
import { downgradeInjectable } from '@angular/upgrade/static';   

@Injectable()   
export class BookService {   
}   

angular.module('core.lib')   
  .factory('core.lib',   downgradeInjectable(BookService));   

BookService的完整代码片段如下所示:

import { Injectable } from '@angular/core';   
import { Http, Response } from '@angular/http';   
import { Observable } from 'rxjs/Rx';   

declare var angular:   angular.IAngularStatic;   
import { downgradeInjectable } from '@angular/upgrade/static';   

import 'rxjs/add/operator/map';   

export interface Book {   
  title: string;   
  author: string;   
  publication: string;   
}   

@Injectable()   
export class BookService {   
  constructor(private http: Http) { }   
  books(): Observable<Book[]> {   
    return   this.http.get(`data/books.json`)   
      .map((res: Response) =>   res.json());   
  }   
}   

angular.module('core.lib')   
  .factory('phone',   downgradeInjectable(BookService));   

最后,我们需要在NgModule下注册BookService作为提供者,以便 Angular 将BookService的实例保持准备好在整个应用程序中注入。app.module.ts的更新代码片段如下所示:

import { BookService } from './book.service';   

@NgModule({   
  imports: [   
    BrowserModule,   
    UpgradeModule,   
    HttpModule,   
  ],   
  providers: [   
    BookService,   
  ]   
})   
export class AppModule {   
  constructor(private upgrade:   UpgradeModule) { }   
  ngDoBootstrap() {   
    this.upgrade.bootstrap(document.documentElement,   [yourApp']);   
  }   
}   

升级你的应用程序组件

作为升级组件的一部分,我们需要创建一个降级的 Angular 组件,以便它可以被 AngularJS 代码消耗。以下是降级的 Angular 组件的代码片段:

declare var angular:   angular.IAngularStatic;   
import { downgradeComponent } from '@angular/upgrade/static';   

@Component({   
  selector: 'book-list',   
  templateUrl: './book-list.template.html'   
})   
export class BookListComponent {   
}   

angular.module('bookList')   
  .directive(   
    'bookList',   
    downgradeComponent({component:   BookListComponent}) as 
           angular.IDirectiveFactory   
);   

在这里,我们向 TypeScript 编译器指示directive工厂是从downgradeComponent返回的。现在我们需要通过将其添加到AppModuleentryComponents来注册downgradeComponent,如下所示:

import { BookListComponent } from './components/book-list.component';   

@NgModule({   
  imports: [   
    BrowserModule,   
    UpgradeModule,   
    HttpModule   
  ],   
  declarations: [   
    BookListComponent,   
  ],   
  entryComponents: [   
    BookListComponent,   
})   
export class AppModule {   
  constructor(private upgrade:   UpgradeModule) { }   
  ngDoBootstrap() {   
      this.upgrade.bootstrap(document.documentElement, ['yourApp']);   
  }   
}   

phone-list.template.html的更新模板如下所示:

<ul>   
    <li *ngFor="let book of books">   
      {{book.title}}   
    </li>   
  </ul>   

这里ng-repeats已被替换为*ngFor

添加 Angular 路由器

Angular 已经完全重新定义了路由器。逐模块升级路由器模块是一个好的做法。Angular 有一个特殊的标签<router-outlet>,用于显示或加载路由视图。这应该在根组件的模板中。所以对于你的应用程序,我们需要创建一个名为AppComponent的根组件:

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

@Component({   
  selector: 'your-app',   
  template: '<router-outlet></router-outlet>'   
})   
export class AppComponent { }   

这是一个指令,如果在网页中找到<your-app>,就将根组件加载到其中。因此,让我们用应用程序元素<your-app>替换index.html中的ng-view指令:

<body>   
  <your-app></your-app>   
</body>   

我们需要为路由创建另一个NgModule,代码片段如下所示:

import { NgModule } from '@angular/core';   
import { Routes, RouterModule } from '@angular/router';   
import { HashLocationStrategy,   LocationStrategy } from '@angular/common';   

import { BookListComponent }   from './components/book-list.component';   

const routes: Routes = [   
  { path: '', redirectTo: 'books',   pathMatch: 'full' },   
  { path: 'books',          component:   BookListComponent }   
];   

@NgModule({   
  imports: [ RouterModule.forRoot(routes)   ],   
  exports: [ RouterModule ],   
  providers: [   
    { provide: LocationStrategy,   useClass: HashLocationStrategy },   
  ]   
})   
export class AppRoutingModule { }   

在路由对象中定义了单个路由,还为应用程序的空路径或根路径设置了默认路由。然后,我们将路由对象传递给RouterModule.forRoot,以便RouterModule来处理它。我们使用HashLocationStrategy来指示RouterModule在 URL 的片段中使用一个哈希(#)。

最后,让我们更新AppModule来导入AppRoutingModule,并且我们已经到了一个阶段,可以移除ngDoBootstrap,因为现在一切都是 Angular。以下是AppModule的更新代码片段:

import { NgModule } from '@angular/core';   
import { BrowserModule } from '@angular/platform-browser';   
import { HttpModule } from '@angular/http';   
import { AppRoutingModule } from './app-routing.module';   
import { AppComponent }  from './app.component';   
import { BookService }   from './services/book.service';   
import { BookListComponent }  from './components/book-list.component';   

@NgModule({   
  imports: [   
    BrowserModule,   
    HttpModule,   
    AppRoutingModule   
  ],   
  declarations: [   
    AppComponent,   
    BookListComponent   
  ],   
  providers: [   
    BookService   
  ],   
  bootstrap: [ AppComponent ]   
})   
export class AppModule {}   

请注意,我们将AppRoutingModule添加到NgModule属性的导入集合中,以便应用程序路由将在AppModule中注册。

总结

干得好!好多东西,不是吗?!我们开始学习在 Angular 中进行迁移。

然后,我们看到了将 AngularJS 迁移到 Angular 应用程序的各种方法和最佳实践。

接下来,我们讨论了使用升级适配器进行增量升级。

最后,我们详细了解了从 AngularJS 迁移到 Angular 的路线图。

在下一章中,我们将讨论 Angular CLI,这是 Angular 的命令行界面。

第三章:使用 Angular CLI 生成遵循最佳实践的 Angular 应用程序

Angular CLI 是用于 Angular 的命令行界面,它可以帮助您使用遵循所有必要最佳实践的样板代码快速启动应用程序开发。通过在 Angular CLI 中执行命令,您可以为应用程序生成服务、组件、路由和管道。

在本章中,我们将涵盖以下主题:

  • 介绍 Angular CLI

  • 安装和设置 Angular CLI

  • 为新应用程序生成代码

  • 生成组件和路由

  • 生成服务

  • 生成指令和管道

  • 创建针对各种环境的构建

  • 运行应用程序的测试

  • 更新 Angular CLI

介绍 Angular CLI

Angular CLI 是一个作为节点包可用的命令行界面。Angular CLI 是与 Angular 一起推出的,它通过为新应用程序生成样板代码并向现有应用程序添加服务、管道、组件和指令等功能,帮助您更快地开发应用程序。Angular CLI 在轻松搭建应用程序方面非常强大和方便。借助 Angular CLI 的帮助,我们可以创建、构建、测试和运行我们的应用程序,这将极大地减轻开发人员的负担。

Angular CLI 在 node 下运行,并依赖于许多包。

安装和设置 Angular CLI

要安装 Angular CLI,我们必须在系统中安装最新版本的 node 和 npm。确保所需的包已经安装,并开始全局安装 Angular CLI。最低要求的 npm 版本是 3.x.x,node 版本是 4.x.x。有时,在安装 Angular CLI 时可能会出现错误。在这种情况下,请确保您已安装了最新版本的 node.js。我们可以通过执行以下命令验证 node 的版本:

node --version

我们可以通过执行以下命令检查 npm 的版本:

npm --version  

现在,我们知道了在我们的开发机器上安装的 node 和 npm 的版本。让我们通过执行以下命令全局安装 Angular CLI:

npm install -g angular-cli 

Angular CLI 已安装并可全局在我们的开发机器上使用。

为新应用程序生成代码

现在我们已经准备好使用 Angular CLI 了。让我们为一个显示书籍列表的 Angular 应用程序生成样板代码。我们将应用程序的名称命名为BookList。在 node.js 命令中执行以下命令:

ng new BookList

此命令将创建一个名为BookList的文件夹,并生成样板代码,以便开始使用 Angular 应用程序。以下图显示了生成代码中组织的文件结构:

为了确保生成的代码正常工作,让我们通过执行以下命令来运行应用程序。首先通过执行此语句导航到应用程序文件夹:

cd BookList

然后,执行以下代码来在开发服务器中启动应用程序:

ng serve

现在,让我们浏览到http://localhost:4200/,如果生成的代码正确,浏览器将呈现以下页面的默认文本。如果出现错误,请确保防火墙没有阻止端口 4200,并且在生成样板代码时 Angular CLI 没有抛出任何错误:

生成组件和路由

组件是功能、视图和样式的逻辑组合,适用于视图和与组件相关的处理这些构件的类。组件负责根据业务逻辑要求呈现视图。

我们可以使用 Angular CLI 生成组件的代码。这个工具在搭建组件时非常方便。让我们通过执行以下语句为我们的应用程序生成一个名为booklist的组件。通过执行以下命令导航到 Angular 项目文件夹:

cd BookList

然后,执行以下 Angular CLI 命令来生成组件Booklist

ng generate component booklist

执行上述语句会创建booklist.component.cssbooklist.component.htmlbooklist.component.spec.tsbooklist.component.ts,如下图所示:

booklist.component.ts文件负责根据业务逻辑需求呈现相关视图。书籍组件生成的代码片段如下:

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

@Component({   
  selector: 'app-booklist',   
  templateUrl: './booklist.component.html',   
  styleUrls: ['./booklist.component.css']   
})   
export class BooklistComponent implements   OnInit {   

  constructor() { }   

  ngOnInit() {   
  }   

}   

请注意,BooklistComponent类使用@Component指令进行修饰,以及选择器、templateUrlstyleUrls等元数据。元数据选择器使得 Angular 在遇到app-booklist选择器时实例化组件BooklistComponent

Angular CLI 还生成了模板文件booklist.component.html,内容如下。Angular 将根据组件中给定的指令解析和呈现此内容:

<p>   
  booklist works!   
</p>   

我们还可以在生成的文件booklist.component.css中添加特定于此模板的样式,组件将会应用这些样式,因为元数据styleUrlsbooklist.component.css的路径进行了映射。

生成booklist.component.spec.ts以添加测试方法来断言BooklistComponent的功能。booklist.component.spec.ts的代码片段如下所示:

/* tslint:disable:no-unused-variable */   

import { TestBed, async } from '@angular/core/testing';   
import { BooklistComponent } from './booklist.component';   

describe('Component: Booklist', () =>   {   
  it('should create an instance', ()   => {   
    let component = new   BooklistComponent();   
    expect(component).toBeTruthy();   
  });   
});   

路由

路由指示 Angular 导航应用程序。路由使得 Angular 能够仅加载特定路由的视图,而无需重新加载整个页面或应用程序。在撰写本章时,使用 Angular CLI 生成路由被禁用,但将很快启用。

生成服务

服务是用户定义的类,用于解决一些目的。Angular 建议在组件中只有特定于模板的代码。组件的责任是丰富 Angular 应用程序中的 UI/UX,并将业务逻辑委托给服务。组件是服务的消费者。

我们已经有了帮助渲染Booklist模板的组件。现在,让我们运行一个 CLI 命令来生成一个服务,以提供书籍列表。执行以下命令生成booklist.services.tsbooklist.services.spec.ts

生成的booklist.service.ts的代码片段如下所示:

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

@Injectable()   
export class BooklistService {   

  constructor() { }   

}   

请注意,BooklistService被装饰为@Injectible,以便该书单服务将可用于组件。还有一个警告消息,服务已生成但未提供,必须提供才能使用。这意味着要使用BooklistService,它需要提供给将要使用它的组件。Angular 中的提供者将在第十三章中详细讨论,应用依赖注入

Angular CLI 还生成了一个文件,用于编写测试方法来断言BooklistServicebooklist.service.spec.ts的代码片段如下所示:

/* tslint:disable:no-unused-variable */   

import { TestBed, async, inject } from '@angular/core/testing';   
import { BooklistService } from './booklist.service';   

describe('Service: Booklist', () => {   
  beforeEach(() => {   
    TestBed.configureTestingModule({   
      providers: [BooklistService]   
    });   
  });   

  it('should ...',   inject([BooklistService], (service: 
      BooklistService) => {   
           expect(service).toBeTruthy();   
  }));   
});   

生成指令和管道

一个使用@Directive装饰的类来附加元数据被称为指令。它是一个渲染模板的指示或指导方针。

我们已经看到了生成组件和服务。现在,让我们使用 Angular CLI 生成指令和管道。我们将从创建一个名为 book 的指令开始。运行以下命令生成指令:

ng generate directive book       

执行命令的结果如下所示:

执行此命令会分别创建两个文件,即book.directive.spec.tsbook.directive.ts。以下是book.directive.ts的代码片段:

import { Directive } from '@angular/core';
 @Directive({
    selector: '[appBookish]' 
   }) 
  export class BookishDirective { 
      constructor() { } 
  } 

book.directive.spec.ts的代码片段如下所示:

/* tslint:disable:no-unused-variable */ 
import { TestBed, async } from '@angular/core/testing';
import { BookDirective } from './book.directive'; 

describe('Directive: Book', () => {
   it('should create an instance', () => 
     { let directive = new BookDirective();   
        expect(directive).toBeTruthy();
    }); 
  }); 

管道

管道指示 Angular 在过滤或渲染输入数据时的操作。管道根据管道中给定的逻辑转换输入数据。

现在,让我们通过执行以下语句使用 Angular CLI 生成一个管道:

在这里,我使用 Angular CLI 创建了一个名为bookfilter的管道。请注意,它还创建了一个名为bookfilter.pipe.spec.ts的测试文件,用于编写测试方法来断言管道。bookfilter.pipe.ts的代码片段如下所示:

import { Pipe, PipeTransform } from '@angular/core'; 
 @Pipe({ 
    name: 'bookfilter'
    }) 
 export class BookfilterPipe implements PipeTransform { 
        transform(value: any, args?: any): any {
    return null; 
  } 
} 

为测试文件bookfilter.pipe.spec.ts生成的代码片段如下所示:

/* tslint:disable:no-unused-variable */ 
import { TestBed, async } from '@angular/core/testing'; 
import { BookfilterPipe } from './bookfilter.pipe'; 
  describe('Pipe: Bookfilter', () => { 
   it('create an instance', () => {
         let pipe = new BookfilterPipe(); 
         expect(pipe).toBeTruthy(); 
   }); 
 }); 

创建针对各种环境的构建

使用 Angular CLI,我们还可以为应用程序创建针对各种环境的构建,例如开发和生产。应用程序将根据环境进行特定配置。例如,应用程序可以配置为在开发或暂存环境中使用暂存 API 的 URL,并在 LIVE 或生产环境中配置 API 的生产 URL。开发人员将手动根据应用程序构建的环境更新 URL 的配置。Angular 可以简化通过针对各种环境创建构建的过程。

在名为environment.ts的文件中维护了一个常量变量环境。此文件将帮助根据执行构建命令时传递的参数来覆盖默认值。

要使用生产文件,我们需要执行以下命令:

 ng build --env=prod 

此命令将使用environment.prod.ts中的设置。用于识别环境文件的映射在angular-cli.json中指定,如下所示:

"environments": { 
  "source": "environments/environment.ts", 
   "dev": "environments/environment.ts", 
   "prod": "environments/environment.prod.ts" 
  } 

为您的应用程序运行测试

在将应用程序移至生产环境之前,测试应用程序是必不可少的过程。开发人员可以编写测试来断言应用程序的行为。编写适当的测试将保护应用程序免受偏离要求的影响。

Jasmine 是一个测试框架,它方便编写测试来断言应用程序的行为,并使用 HTML 测试运行器在浏览器中执行测试。Karma 是一个测试运行器,它使开发人员能够在开发阶段同时编写单元测试。一旦构建过程完成,将使用 Karma 执行测试。Protractor 可以用于运行端到端测试,以断言应用程序的工作流程,就像最终用户的体验一样。

以下命令在应用程序中运行测试:

ng test 

端到端测试可以通过在此处运行命令来执行,并且只有在应用程序由命令 ng serve 提供服务时才能成功运行。这个端到端测试是由 Protractor 运行的:

ng e2e 

我不会详细介绍每个生成的文件的内容,因为有章节会详细解释它们。

更新 Angular CLI

我们可以在全局包和本地项目中更新 Angular CLI 版本。要全局更新 Angular CLI 包,请运行以下命令:

npm uninstall -g @angular/cli npm cache clean npm install -g @angular/cli@latest 

要在本地项目文件夹中更新 CLI,请运行此命令:

rm -rf node_modules dist # use rmdir /S/Q node_modules dist in Windows 
  Command Prompt; use rm -r -fo node_modules,dist in Windows PowerShell npm install --save-dev @angular/cli@latest npm install 

总结

那很顺利和简单,不是吗?Angular CLI 通过为 Angular 应用程序的各种构件生成样板代码,使开发人员的生活更加轻松。您开始学习强大的工具 Angular CLI 以及它如何帮助您使用样板代码启动应用程序。然后,您学会了使用 Angular 命令行界面生成组件、指令、管道、路由和服务。最后,您还了解了如何使用 Angular CLI 构建 Angular 应用程序。在下一章中,我们将讨论如何使用 Angular 组件。

第四章:使用组件

在本章中,我们将讨论使用 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,用于查询漫威电影宇宙。

使用 Observable 进行异步编程

如果我们仔细想想,没有什么是瞬间的。 实时不是一个事物。 我刚才失去了理智吗? 现在,你可以按下一个按钮,感觉像是瞬间的,但是,在你内心深处,作为程序员的你知道那不是真的。 信息已经传输,代码已被执行,数据库已被获取,等等。 在这段时间里,无论它是多短还是多长,你都在等待。 更准确地说,你的代码让用户等待。 您宁愿有一个基于异步概念构建的代码库,并且可以在等待时执行其他任务,或者至少警告用户我们正在等待某件事情发生吗? 这就是这一章的基本思想。 这一章将帮助您理解异步编程的概念,并在 Angular 中使用 Observable 实现相同的功能。

在这一章中,我们将涵盖以下主题:

  • 观察者模式

  • 异步编程

  • HTTP 客户端

  • 订阅可观察对象

  • 承诺

观察者模式

Observable 模式允许一个称为subject的对象跟踪对其状态感兴趣的其他对象,称为observers。 当主题状态改变时,它会通知观察者。 其背后的机制非常简单。

TypeScript 可观察对象

让我们看一下纯 TypeScript 中的以下Observer/Subject实现(即没有任何 Angular 或任何框架,只有 TypeScript)。

首先,我定义了一个Observer接口,任何具体实现都必须实现:

export interface Observer{ 

   notify(); 
} 

这个接口只定义了notify()方法。 当其状态改变时,该方法将被主题(即被Observer观察的Object)调用。

然后,我有一个名为HumanObserver的这个接口的实现:

export class HumanObserver implements Observer{ 

   constructor(private name:string){} 

   notify(){ 

               console.log(this.name, 'Notified'); 
   } 
} 

这个实现利用了typescript属性构造函数,其中你可以在构造函数内定义你的类的属性。 这种表示法与以下表示法完全等效,而且更短:

private name: string; 

constructor(name:string){  
      this.name = name; 
} 

根据Observer接口和HumanObserver的定义,我们可以继续进行主题的定义。 我定义了一个管理observersSubject类。 这个类有三个方法:attachObserverdetachObservernotifyObservers

export class Subject{ 

   private observers:Observer[] = []; 

   /** 
   * Adding an observer to the list of observers 
   */ 
   attachObserver(observer:Observer):void{ 

               this.observers.push(observer); 
   } 

   /** 
   * Detaching an observer 
   */ 
   detachObserver(observer:Observer):void{
      let index:number = this.observers.indexOf(observer);
      if(index > -1){
          this.observers.splice(index, 1);
        }
      else{
       throw "Unknown observer";
       }
} 

   /** 
   * Notify all the observers in this.observers 
   */ 
   protected notifyObservers(){
   for (var i = 0; i < this.observers.length; ++i) {  
      this.observers[i].notify();
       } 
    } 
} 

attachObserver方法将新的观察者推送到观察者属性中,而detachObserver则将它们移除。

主题实现通常带有附加/分离,订阅/取消订阅或添加/删除前缀。

最后一个方法是notifyObservers,它遍历观察者并调用它们的notify()方法。 允许我们展示Observable机制的最后一个类是IMDB,它扩展了Subject。 当添加电影时,它将通知观察者:

export class IMDB extends Subject{ 

   private movies:string[] = []; 

   public addMovie(movie:string){ 

         this.movies.push(movie); 
         this.notifyObservers(); 
   } 
} 

要使各个部分相互通信,我们必须:

  • 创建一个Subject

  • 创建一个Observer

  • Observer附加到Subject

  • 通过addMovie方法改变Subject的状态

更具体地说,这是先前列表的一个实现:

let imdb:IMDB = new IMDB(); 
let mathieu:HumanObserver = new HumanObserver("Mathieu"); 
imbd.attachObserver(mathieu); 
imbd.addMovie("Jaws"); 

为了加快我们的开发流程,我们将安装ts-node。这个节点包将把typescript文件转换成 JavaScript,并解决这些文件之间的依赖关系。

要快速编译和执行typescript应用程序,我推荐使用优秀的ts-node包。这个包将转换以下命令:

$ npm install -g  typescript ts-node
$ ts-node myFile.ts

输出是Mathieu Notified。我们可以测试将mathieu分离并添加另一个电影:

imdb.detachObserver(mathieu); 
imdb.addMovie("Die Hard"); 

输出仍然是Mathieu Notified,这发生在我们添加了大白鲨电影之后。第二部电影的添加(也就是《虎胆龙威》)并没有触发控制台打印Mathieu Notified,因为它已经分离了。

具有参数的 TypeScript Observable

因此,这是观察者模式的一个基本实现。然而,它并不完整,因为HumanObserver只知道它观察的主题中出现了一些变化。因此,它必须迭代它所观察到的所有主题,并检查它们的先前状态与当前状态来识别发生了什么变化以及在哪里发生了变化。更好的做法是修改Observer的通知,使其包含更多的信息。例如,我们可以添加可选参数,如下:

export interface Observer{ 

   notify(value?:any, subject?:Subject); 
} 

export class HumanObserver implements Observer{ 

   constructor(private name:string){} 

   notify(value?:any, subject?:Subject){ 

         console.log(this.name, 'received', value, 'from', subject); 
   } 
} 

notify()方法现在接受一个可选的值参数,该参数表征了Subject对象的新状态。我们还可以接收到Subject对象本身的引用。如果观察者观察了许多主题,这是很有用的。在这种情况下,我们需要能够区分它们。

因此,我们必须稍微改变SubjectIMDB,使它们使用新的notify

export class Subject{ 

   private observers:Observer[] = []; 

   attachObserver(oberver:Observer):void{ 

         this.obervers.push(oberver); 
   } 

   detachObserver(observer:Observer):void{ 
         let index:number = this.obervers.indexOf(observer); 
         if(index > -1){ 
         this.observers.splice(index, 1); 

         }else{ 

         throw "Unknown observer"; 
         } 
   } 

   protected notifyObservers(value?:any){ 

         for (var i = 0; i < this.obervers.length; ++i) { 

         this.observers[i].notify(value, this); 
         } 
   } 
} 

export class IMDB extends Subject{ 

   private movies:string[] = []; 

   public addMovie(movie:string){ 

         this.movies.push(movie); 
         this.notifyObservers(movie); 
   } 
} 

最后,输出如下:

 Mathieu received Jaws from IMDB { 

  observers: [ HumanObserver { name: 'Mathieu' } ], 
  movies: [ 'Jaws' ] } 

这比Mathieu Notified更有表现力。

现在,当我们习惯于用于异步编程的Observer模式时,我们真正的意思是,我们要求某事,并且在其处理过程中我们不想等待去做任何事情。相反,我们订阅响应事件以在响应到达时得到通知。

在接下来的章节中,我们将使用相同的模式和机制在 Angular2 中。

此观察者实现的代码位于bit.ly/mastering-angular2-chap7-part1

观察 HTTP 响应

在本节中,我们将构建一个返回根据搜索参数返回电影的 JSON API。我们将利用观察者设计模式的力量,而不是简单地等待 HTTP 查询完成,让用户知道我们正在等待,并且如果需要的话,执行其他流程。

首先要做的是:为我们的 IMDB 类似应用程序准备一个数据源。构建和部署一个能够解释 HTTP 查询并相应地发送结果的服务器端应用程序现在相对简单。然而,这超出了本书的范围。相反,我们将获取托管在bit.ly/mastering-angular2-marvel的静态 JSON 文件。该文件包含漫威电影宇宙最新电影的一些信息。它包含一个描述十四部电影的 JSON 对象的 JSON 数组。以下是第一部电影:

{ 
     "movie_id" : 1, 
     "title" : "Iron Man", 
     "phase" : "Phase One: Avengers Assembled", 
     "category_name" : "Action", 
     "release_year" : 2015, 
     "running_time" : 126, 
     "rating_name" : "PG-13", 
     "disc_format_name" : "Blu-ray", 
     "number_discs" : 1, 
     "viewing_format_name" : "Widescreen", 
     "aspect_ratio_name" : " 2.35:1", 
     "status" : 1, 
     "release_date" : "May 2, 2008", 
     "budget" : "140,000,000", 
     "gross" : "318,298,180", 
     "time_stamp" : "2015-05-03" 
}, 

您可以找到类似 IMDB 的应用程序提供的经典信息,例如发行年份、播放时间等。我们的目标是设计一个异步的 JSON API,使这些字段中的每一个都可以搜索。

因为我们要获取一个静态的 JSON 文件(也就是说我们不会插入、更新或删除任何元素),可接受的 API 调用将是以下内容:

IMDBAPI.fetchOneById(1); 
IMDBAPI.fetchByFields(MovieFields.release_date, 2015); 

第一个调用只是获取movie_id: 1的电影;第二个调用是一个更通用的调用,在任何字段中都适用。为了防止 API 消费者请求我们电影中不存在的字段,我们使用在Movie类内部定义的枚举器限制field的值。

现在,这里的重要部分是这些调用的实际返回。事实上,它们将触发一个Observable机制,调用方将自己附加到Observable HTTP 调用上。然后,当 HTTP 调用完成并根据查询参数过滤结果后,被调用方将通知调用方关于响应。因此,调用方不必等待被调用方(IMDBAPI);它将在请求完成时收到通知。

实施

让我们深入了解实现。首先,我们需要使用 Angular CLI 创建一个新的 Angular2 项目:

    ng new angular-observable
    ng init
    ng serve

然后,为了确保一切顺利,您可以浏览到localhost:4200并查看是否得到类似以下内容:

接下来,我们需要一个代表电影概念的模型。我们将使用命令行ng g class models/Movie生成这个类。然后,我们可以添加一个构造函数,定义Movie模型的所有私有字段,以及相同的 getters 和 setters:

export class Movie { 

   public constructor( 
         private _movie_id:number, 
         private _title: string, 
         private _phase: string, 
         private _category_name: string, 
         private _release_year: number, 
         private _running_time: number, 
         private _rating_name: string, 
         private _disc_format_name: string, 
         private _number_discs: number, 
         private _viewing_format_name: string, 
         private _aspect_ratio_name: string, 
         private _status: string, 
         private _release_date: string, 
         private _budget: number, 
         private _gross: number, 
         private _time_stamp:Date){ 
   } 

   public toString = () : string => { 

         return `Movie (movie_id: ${this._movie_id}, 
         title: ${this._title}, 
         phase: ${this._phase}, 
         category_name: ${this._category_name}, 
         release_year: ${this._release_year}, 
         running_time: ${this._running_time}, 
         rating_name: ${this._rating_name}, 
         disc_format_name: ${this._disc_format_name}, 
          number_discs: ${this._number_discs}, 
         viewing_format_name: ${this._viewing_format_name}, 
         aspect_ratio_name: ${this._aspect_ratio_name}, 
         status: ${this._status}, 
         release_date: ${this._release_date}, 
         budget: ${this._budget}, 
         gross: ${this._gross}, 
         time_stamp: ${this._time_stamp})`; 

   } 
   //GETTER 
   //SETTER 
} 

export enum MovieFields{ 
   movie_id, 
   title, 
   phase, 
   category_name, 
   release_year, 
   running_time, 
   rating_name, 
   disc_format_name, 
   number_discs, 
   viewing_format_name, 
   aspect_ratio_name, 
   status, 
   release_date, 
   budget, 
   gross, 
   time_stamp 
} 

在这里,电影 JSON 定义的每个字段都使用 typescript 的构造函数属性声明映射到 Movie 类的私有成员中。此外,我们重写了toString方法,所以它会打印出每个字段。在toString方法中,我们利用了反引号提供的多行字符串。

IMDBAPI.fetchOneById(1);

IMDBAPI.fetchByFields(MovieFields.release_date, 2015);


Here's `IMDBAPIService` with the `fetchOneById` method:

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

import { Http } from '@angular/http';

import { Movie, MovieFields } from '../models/movie';

import { Observable } from 'rxjs/Rx';

import 'rxjs/Rx';

@Injectable()

导出类 IMDBAPIService {

私有 moviesUrl:string = "assets/marvel-cinematic-universe.json";

constructor(private http: Http) { }

/**

  • Return an Observable to a Movie matching id

  • @param {number} id

  • @return {Observable}

*/

public fetchOneById(id:number):Observable{

console.log('fetchOneById', id);

    return this.http.get(this.moviesUrl)

    /**

    * Transforms the result of the http get, which is observable

    * into one observable by item.

    */

.flatMap(res => res.json().movies)

    /**

    * Filters movies by their movie_id

    */

    .filter((movie:any)=>{

    console.log("filter", movie);

    return (movie.movie_id === id)

    })

    /**

    * Map the JSON movie item to the Movie Model

    */

    .map((movie:any) => {

    console.log("map", movie);

    return new Movie(

        movie.movie_id,

        movie.title,

        movie.phase,

        movie.category_name,

        movie.release_year,

        movie.running_time,

        movie.rating_name,

        movie.disc_format_name,

        movie.number_discs,

        movie.viewing_format_name,

        movie.aspect_ratio_name,

        movie.status,

        movie.release_date,

        movie.budget,

        movie.gross,

        movie.time_stamp

    );

    });

}

}


# Understanding the implementation

Let's break it down chunk by chunk. First, the declaration of the service is pretty standard:

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

import { Http } from '@angular/http';

import { Movie, MovieFields } from '../models/movie';

import { Observable } from 'rxjs/Rx';

import 'rxjs/Rx';

@Injectable()

export class IMDBAPIService {

private moviesUrl:string = "app/marvel-cinematic-universe.json";

constructor(private http: Http) { }


Services are injectable. Consequently, we need to import and add the `@Injectable` annotation. We also import `Http`, `Movie`, `MovieFields`, `Observable`, and the operators of RxJS. **RxJS** stands for **Reactive Extensions for JavaScript**. It is an API to do `Observer`, iterator, and functional programming. When it comes to asynchronism in Angular2, you rely on RxJS for the most part.

One important thing to note is that we use RxJS 5.0, which is a complete rewrite, based on the same concept as RxJS 4.0.

The `IMDBAPIService` also has a reference to the path of our JSON file and a constructor to receive an injection of the `http` service. On the implementation of the `fetchOneById` method, we can see four distinct operations chained with each other: `get`, `flatMap`, `filter`, and `map`.

*   Get returns an `Observable` onto the body of the `http` request.
*   `flatMap` transforms the get `Observable` by applying a function that you specify to each item emitted by the source `Observable`, where that function returns an `Observable` that itself emits items. `flatMap` then merges the emissions of these resulting observables, emitting these merged results as its sequence. In our case, it means that we will apply the next two operations (that is `filter` and `map`) on all the items received from the `http` get.
*   `filter` checks if the ID of the current movie is the one we are looking for
*   `map` transforms the JSON representation of a movie into the `typescript` representation of a movie (that is the `Movie` class).

This last operation, while counter-intuitive, is mandatory. Indeed, one could think that the JSON representation and the `typescript` representation are identical, as they own the same fields. However, the `typescript` representation, also to its properties, defines functions such as `toString`, the getters, and the setters. Removing the map would return an `Object` instance--containing all the fields of a `Movie` without being one. Also, a typecast will not help you there. Indeed, the `typescript` transpiler will allow you to cast an `Object` into a `Movie`, but it still won't have the methods defined in the `Movie` class, as the concept of static typing disappears when the `typescript` is transpiled in JavaScript. The following would transpile fail at execution time with; `movie.movie_id(25)` TypeError: `movie.movie_id` is not a function at `Object.<anonymous>`.

movie:Movie = JSON.parse(`{

        "movie_id" : 1,

        "title" : "Iron Man",

        "phase" : "Phase One: Avengers Assembled",

        "category_name" : "Action",

        "release_year" : 2015,

        "running_time" : 126,

        "rating_name" : "PG-13",

        "disc_format_name" : "Blu-ray",

        "number_discs" : 1,

        "viewing_format_name" : "Widescreen",

        "aspect_ratio_name" : " 2.35:1",

        "status" : 1,

        "release_date" : "May 2, 2008",

        "budget" : "140,000,000",

        "gross" : "318,298,180",

        "time_stamp" : "2015-05-03"

    }`);

Console.log(movie.movie_id(25));


Now, if we want to use our `IMDB` service, further modifications are required in the code that was generated by the Angular CLI. First, we need to modify `app.module.ts` so it looks like this:

import { BrowserModule } from '@angular/platform-browser';

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

import { FormsModule } from '@angular/forms';

import { HttpModule } from '@angular/http';

import { IMDBAPIService } from './services/imdbapi.service';

import { AppComponent } from './app.component';

@NgModule({

declarations: [

AppComponent

],

imports: [

BrowserModule,

FormsModule,

HttpModule

],

providers: [IMDBAPIService],

bootstrap: [AppComponent]

})

export class AppModule { }


The lines in bold represent what have been added. We import our `IMDBAPIService` and `HTTP_PROVIDERS`. Both providers are declared at the application level, meaning that the instance that will be injected in the component or directive will always be the same.

Then, we modify the `app.component.ts` file that was generated and add the following:

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

import { IMDBAPIService } from './services/imdbapi.service';

import { Movie } from './models/movie';

@Component({

selector: 'app-root',

templateUrl: './app.component.html',

styleUrls: ['./app.component.css']

})

export class AngularObservableAppComponent {

title = 'angular-observable works!';

private movies:Movie[] = []; private error:boolean = false; private finished:boolean = false;

constructor(private IMDBAPI:IMDBAPIService){

this.IMDBAPI.fetchOneById(1).subscribe( value => {this.movies.push(value); console.log("Component",

    value)}, error => this.error = true, () => this.finished = true )  }

}


Once again, the bold lines of code represent the lines that were added in comparison to the generated file. We have added several properties to the `AppComponent`: movies, error, and finished. The first property is an array of Movie that will store the result of our queries, the second and the third flag for error and termination. In the constructor, we have an injection of the `IMDBAPIService`, and we subscribe to the result of the `fetchOneById` method. The `subscribe` method expects three callbacks:

*   `Observer`: Receive the value yielded by the `Observer` method. It is the RxJs equivalent of the `notify()` method we saw earlier in this chapter.
*   `onError` (Optional): Triggered if the `Observer` object yields an error.
*   `onComplete` (Optional): Triggered on completion.

Finally, we can modify the `app.component.html` file to map the movies property of the `AppComponent` array:

{{title}}

  • {{movie}}

The produced output of our code is:

![](https://gitee.com/OpenDocCN/freelearn-angular-zh/raw/master/docs/exp-ng/img/4ad5912d-0990-441d-badb-f71764f9cf50.png)

We can see that the first movie item has been correctly inserted into our `ul`/`li` HTML structure. What's really interesting about this code is the order in which things execute. Analyzing the log helps us to grasp the true power of asynchronism in Angular with RxJs. Here's what the console looks like after the execution of our code:

fetchOneById 1

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:34 映射对象

:4200/app/angular-observable.component.js:21 组件

电影宽高比名称:" 2.35:1"_ 预算:

"140,000,000"_ 类别名称:"动作"_ 光盘格式名称:"蓝光-

光盘"总收入:"318,298,180"_ 电影 id:1_ 光盘数量:1_ 阶段:"第

一个:复仇者联盟成立"_ 评分名称:"PG-13"_ 发布日期:"5 月 2 日,

2008"_ 发布年份: 2015_ 播放时长: 126_ 状态: 1_ 时间戳:

"2015-05-03"_ 标题:"钢铁侠"_ 查看格式名称:

"宽屏"宽高比名称:(...)预算:(...)类别名称:

(...)光盘格式名称:(...)总收入:(...)电影 id:(...)光盘数量:

(...)阶段:(...)评分名称:(...)发布日期:(...)发布年份:

(...)播放时长:(...)状态:(...)时间戳:(...)标题:

(...)ToString:()查看格式名称:(...)proto:对象

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:30 过滤对象

:4200/app/services/imdbapi.service.js:30 过滤对象


As you can see, `AngularObservableAppComponent` was notified that a movie matching the query was found before the `filter` function had analyzed all the items. As a reminder, the order to the operations inside the `fetchOneById` by `id` was: `get`, `flatMap`, `filter`, `map`, and we have logging statements in the `filter` and `map` methods. So, here, the filter operation analyzes the first item, which happens to be the one we are looking for `(movie_id===1)` and forwards it to the map operation that transforms it into a `Movie`. This `Movie` is sent right away to `AngularObservableAppComponent`. We clearly see that the received object in the `AngularObservableAppComponent` component is from type movie, as the console gives us our overriding of the `toString` method. Then, the `filter` operation continues with the rest of the items. None of them match; consequently, we do not have any more notifications.

Let's test this further with a second method: `IMDBAPI.fetchByField`:

public fetchByField(field:电影字段, value:任意){

console.log('fetchByField', field, value);

返回 this.http.get(this.moviesUrl)

    .flatMap(res => res.json().movies)

    /**

    * 根据它们的字段过滤电影

    */

    .filter((电影:任意)=>{

    console.log("过滤", 电影);

    返回 (电影[电影字段[field]] === value)

    })

    /**

    * 将 JSON 电影项目映射到电影模型

    */

    .map((电影:任意) => {

    console.log("映射", 电影);

    返回新电影(

        电影电影 id,

        电影标题,

        电影阶段,

        电影类别名称,

        电影发布年份,

        电影播放时长,

        电影评分名称,

        电影光盘格式名称,

        电影光盘数量,

        电影查看格式名称,

        电影宽高比名称,

        电影状态,

        电影发布日期,

        电影预算,

        电影总收入,

        电影时间戳

    );

    });

}


For the `fetchByField` method, we use the same mechanisms as `fetchById`. Unsurprisingly, the operations stay the same: `get`, `flatMap`, `filter`, and `map`. The only change is in the `filter` operation, where we now have to `filter` on a `field` received as parameter `return (movie[MovieFields[field]] === value)`.

This statement can be a bit overwhelming to the TypeScript or JavaScript newcomer. First, the `MovieFields[field]` part is explained by the fact that `enum` will be transpiled into the following JavaScript function:

(function (电影字段) {

电影字段[电影字段["电影 id"] = 0] = "电影 id";

电影字段[电影字段["标题"] = 1] = "标题";

电影字段[电影字段["阶段"] = 2] = "阶段";

电影字段[电影字段["类别名称"] = 3] = "类别名称";

电影字段[电影字段["发布年份"] = 4] = "发布年份";

电影字段[电影字段["播放时长"] = 5] = "播放时长";

电影字段[电影字段["评分名称"] = 6] = "评分名称";

电影字段[电影字段["光盘格式名称"] = 7] =

    "光盘格式名称";

电影字段[电影字段["光盘数量"] = 8] = "光盘数量";

电影字段[电影字段["查看格式名称"] = 9] =

    "查看格式名称";

电影字段[电影字段["宽高比名称"] = 10] =

    "aspect_ratio_name";

MovieFields[MovieFields["status"] = 11] = "status";

MovieFields[MovieFields["release_date"] = 12] = "release_date";

MovieFields[MovieFields["budget"] = 13] = "budget";

MovieFields[MovieFields["gross"] = 14] = "gross";

MovieFields[MovieFields["time_stamp"] = 15] = "time_stamp";

})

(exports.MovieFields || (exports.MovieFields = {}));

var MovieFields = exports.MovieFields;


Consequently, the value of `MovieFields.release_year` is, in fact, `4` and `MovieFields` is a static array. So, `movie[MovieFields[field]]` is interpreted as a `movie["release_year is"]` in our current example.

The next subtlety is that every object in JavaScript is represented as an associative array, where the variable name acts as a key. Therefore, `movie["release_year"]` is equivalent to `movie.release_year`. This allows us to retrieve the value of any property only by knowing its name. Modify the constructor of `AngularObservableAppComponent` to look like the following:

constructor(private IMDBAPI:IMDBAPIService){

this.IMDBAPI.fetchByField(MovieFields.release_year, 2015).subscribe(

    value => {this.movies.push(value); console.log("组件",

    value)},

    error => this.error = true,

    () => this.finished = true

)

}


This will have the impact shown in the following screenshot:

![](https://gitee.com/OpenDocCN/freelearn-angular-zh/raw/master/docs/exp-ng/img/295a388f-1911-4c40-9913-a485d0bf1989.png)

Now we have five matches instead of one. On the analysis of the console, we can see that the notifications still come as soon as a suitable object is found, and not when they have all been filtered:

fetchByField 4 2015

imdbapi.service.js:43 过滤对象 {movie_id: 1, title: "钢铁侠",

phase: "第一阶段: 复仇者联盟组装", category_name: "动作",

release_year: 2015...}

imdbapi.service.js:47 映射对象 {movie_id: 1, title: "钢铁侠",

phase: "第一阶段: 复仇者联盟组装", category_name: "动作",

release_year: 2015...}

angular-observable.component.js:22 组件 电影 {_movie_id: 1,

_title: "钢铁侠", _phase: "第一阶段: 复仇者联盟组装",

_category_name: "动作", _release_year: 2015...}

imdbapi.service.js:43 过滤对象 {movie_id: 2, title: "The

难以置信的绿巨人", phase: "第一阶段: 复仇者联盟组装",

category_name: "动作", release_year: 2008...}

imdbapi.service.js:43 过滤对象 {movie_id: 3, title: "钢铁侠 2",

phase: "第一阶段: 复仇者联盟组装", category_name: "动作",

release_year: 2015...}

imdbapi.service.js:47 映射对象 {movie_id: 3, title: "钢铁侠 2",

phase: "第一阶段: 复仇者联盟组装", category_name: "动作",

release_year: 2015...}

angular-observable.component.js:22 组件 电影 {_movie_id: 3,

_title: "钢铁侠 2", _phase: "第一阶段: 复仇者联盟组装",

_category_name: "动作", _release_year: 2015...}

imdbapi.service.js:43 过滤对象 {movie_id: 4, title: "雷神", phase:

"第一阶段: 复仇者联盟组装", category_name: "动作",

release_year: 2011...}

imdbapi.service.js:43 过滤对象 {movie_id: 5, title: "Captain

America", phase: "第一阶段: 复仇者联盟组装", category_name:

"动作", release_year: 2011...}

imdbapi.service.js:43 过滤对象 {movie_id: 6, title: "复仇者联盟,

The", phase: "第一阶段: 复仇者联盟组装", category_name: "科幻

Fiction", release_year: 2012...}

imdbapi.service.js:43 过滤对象 {movie_id: 7, title: "钢铁侠 3",

phase: "第二阶段", category_name: "动作", release_year: 2015...}

imdbapi.service.js:47 映射对象 {movie_id: 7, title: "钢铁侠 3",

phase: "第二阶段", category_name: "动作", release_year: 2015...}

angular-observable.component.js:22 组件 电影 {_movie_id: 7,

_title: "钢铁侠 3", _phase: "第二阶段", _category_name: "动作",

_release_year: 2015...}

imdbapi.service.js:43 过滤对象 {movie_id: 8, title: "雷神: 黑暗世界",

黑暗世界", phase: "第二阶段", category_name: "科幻",

release_year: 2013...}

imdbapi.service.js:43 过滤对象 {movie_id: 9, title: "Captain

发布年份:"2015..."}

派两:复仇者联盟",类别名称:"动作",发布年份:"2014..."}

imdbapi.service.js:43 过滤对象{电影 _ID: 10,标题:"银河护卫队"

Fiction", _release_year: 2015...}

let imdbSubscription =

imdbapi.service.js:43filter Object {movie_id: 1, title: "Iron Man",

阶段:"第一阶段:复仇者联盟",类别名称:"动作"

阶段:"第一阶段:复仇者联盟",类别名称:"动作"

imdbSubscription.unsubscribe();

_title: "复仇者联盟:奥创时代",_phase: "第二阶段"

}

constructor(private IMDBAPI:IMDBAPIService){

imdbapi.service.js:43 过滤对象{电影 _ID: 12,标题:"蚁人"

imdbapi.service.js:43 过滤对象{电影 _ID: 11,标题:"复仇者联盟

angular-observable.component.js:24 组件电影{_ 电影 ID: 3,

"动作",发布年份:"2014..."}

imdbapi.service.js:43 过滤对象{电影 _ID: 14,标题:"奇异博士

_ 标题:"钢铁侠",_ 阶段:"第一阶段:复仇者联盟"

阶段:"第二阶段",类别名称:"科幻",发布年份:"2015..."

console.log("组件", value)

Age of Ultron",阶段:"第二阶段",类别名称:"科幻"

America: The Winter Soldier", phase: "Phase Two", category_name:

_title: "Ant-Man", _phase: "Phase Two", _category_name: "Science

发布年份:"2015..."

"美国队长:内战",阶段:"第三阶段",类别名称:"科学"

category_name: "Action", release_year: 2008...}

发布年份:"2014..."}

angular-observable.component.js:22 组件电影{_ 电影 ID: 11,

value => {


Now, the other strength of this design pattern is the ability to unsubscribe yourself. To do so, you only have to acquire a reference to your subscription and call the `unsubscribe()` method, shown as follows:

发布年份:"2015..."}

imdbapi.service.js:43 过滤对象{电影 _ID: 2,标题:"银河护卫队"

this.movies.push(value);

2015..."}

_ 标题:"钢铁侠 2",_ 阶段:"第一阶段:复仇者联盟"

    发布年份:"2015..."

    imdbapi.service.js:49 映射对象{电影 _ID: 3,标题:"钢铁侠 2"

    发布年份:"2015..."

        subscribe(

    imdbapi.service.js:47 映射对象{电影 _ID: 11,标题:"复仇者联盟:奥创时代

    阶段:"第一阶段:复仇者联盟",类别名称:"动作"

_ 类别名称:"动作",_ 发布年份:"2015..."}

error => this.error = true,

angular-observable.component.js:22 组件电影{_ 电影 ID: 12,

Fiction", release_year: 2016...}


Here, we unsubscribe after the third notification. To add to all this, the `Observable` object will even detect that nobody's observing anymore and will stop whatever it was doing. Indeed, the previous code with `unsubscribe` produces:

}

the Galaxy", phase: "Phase Two", category_name: "Science Fiction",

2015..."}

angular-observable.component.js:24 组件电影{_movie_id: 1,

if(this.movies.length > 2){

Strange",阶段:"第二阶段",类别名称:"科幻"

imdbapi.service.js:43 过滤对象{电影 _ID: 3,标题:"钢铁侠 2"

阶段:"第一阶段:复仇者联盟",类别名称:"动作"

);

_ 类别名称:"科幻",_ 发布年份:"2015..."}

},

"神奇的浩克",阶段:"第一阶段:复仇者联盟"

phase: "Phase Two", category_name: "Science Fiction", release_year:

发布年份:"2016..."}

imdbapi.service.js:49 映射对象{电影 _ID: 1,标题:"钢铁侠"

imdbapi.service.js:47 映射对象{电影 _ID: 12,标题:"蚁人"

() => this.finished = true

of Ultron",阶段:"第二阶段",类别名称:"科幻"

this.IMDBAPI.fetchByField(MovieFields.release_year, 2015).

imdbapi.service.js:43 过滤对象{电影 _ID: 13,标题:"美国队长

fetchByField 4 2015

_category_name: "动作", _release_year: 2015...}

imdbapi.service.js:43 filter Object {movie_id: 4, title: "雷神", phase:

"第一阶段:复仇者联盟", category_name: "动作",

release_year: 2011...}

imdbapi.service.js:43 filter Object {movie_id: 5, title: "美国队长

America", phase: "第一阶段:复仇者联盟", category_name:

"动作", release_year: 2011...}

imdbapi.service.js:43 filter Object {movie_id: 6, title: "复仇者联盟

The", phase: "第一阶段:复仇者联盟", category_name: "科幻

Fiction", release_year: 2012...}

imdbapi.service.js:43 filter Object {movie_id: 7, title: "钢铁侠 3",

phase: "第二阶段", category_name: "动作", release_year: 2015...}

imdbapi.service.js:49 map Object {movie_id: 7, title: "钢铁侠 3",

phase: "第二阶段", category_name: "动作", release_year: 2015...}

angular-observable.component.js:24 组件 电影 {_movie_id: 7,

_title: "钢铁侠 3", _phase: "第二阶段", _category_name: "动作",

_release_year: 2015...}


Everything stops after the third notification.

The code for this `Observer` implementation is at [`bit.ly/mastering-angular2-chap7-part2`](http://bit.ly/mastering-angular2-chap7-part2).

# Promises

Promises are another useful asynchronous concept available in Angular. Conceptually, promises implement a totally different pattern. A `Promise` is a value that will be resolved or rejected in the future. Like the `Observer` pattern, they can be used to manage async programming. So, why bother to have two concepts to do the same thing? Well, the verbosity of `Observer` allows one thing that `Promise` does not: unsubscribe. The main difference that may lead to a decision about which one to use is the ability of `Observable` to catch many subsequent asynchronous events, while `Promise` can manage a single asynchronous event. To emphasise the differences between `Observer` and `Promise`, we will take the same example as before, fetching movies from a JSON API.

The `AngularObservableAppComponent` component will make an asynchronous call to the `IMDBAPIService` and, upon the answer, will update the HTML view.

Here's the `fetchOneById` method using `Promise` instead of `Observable`:

/** ,

  • 返回匹配 id 的电影的 Promise

  • @param {number} id

  • @return {Promise}

*/

public fetchOneById(id:number):Promise{

console.log('fecthOneById', id);

    return this.http.get(this.moviesUrl)

    /**

    * 转换 http get 的结果,它是 observable

    * 转换成一个 observable

    */

    .flatMap(res => res.json().movies)

    /**

    * 通过它们的 movie_id 来过滤电影

    */

    .filter((movie:any)=>{

    console.log("filter", movie);

    return (movie.movie_id === id)

    })

    .toPromise()

    /**

    * 将 JSON 电影项映射到电影模型

    */

    .then((movie:any) => {

    console.log("map", movie);

    return new Movie(

        movie.movie_id,

        movie.title,

        movie.phase,

        movie.category_name,

        movie.release_year,

        movie.running_time,

        movie.rating_name,

        movie.disc_format_name,

        movie.number_discs,

        movie.viewing_format_name,

        movie.aspect_ratio_name,

        movie.status,

        movie.release_date,

        movie.budget,

        movie.gross,

        movie.time_stamp

    )

    });

}


As shown by this code, we went from `flatMap`, `filter`, `map` to `flatMap`, `filter`, `toPromise`, and `then`. The new operations, `toPromise` and `then` create a `Promise` object that will contain the result of the `filter` operation and, on completion of the `filter` operation, the `then` operation will be executed. The `then` operation can be thought of as a map; it does the same thing. To use this code, we also have to change the way we call `IMDBAPIService` in `AngularObservableAppComponent`, to the following:

this.IMDBAPI.fetchOneById(1).then(

    value => {

        this.movies.push(value);

        console.log("Component", value)

    },

    error => this.error = true

);


Once again, we can see a `then` operation that will be executed when the promise from `IMDBAPIService.FetchOneById` has completed. The `then` operation accepts two callbacks: `onCompletion` and `onError`. The second callback, `onError`, is optional.

Now, `onCompletion` callback will only be executed once `Promise` has completed, as shown in the console:

imdbapi.service.js:30 filter Object {movie_id: 2, title: "复仇者联盟"

狂野的绿巨人", phase: "第一阶段:复仇者联盟",

category_name: "动作", release_year: 2008...}

imdbapi.service.js:30 filter Object {movie_id: 3, title: "钢铁侠 2",

phase: "第一阶段:复仇者联盟", category_name: "动作",

release_year: 2015...}

imdbapi.service.js:30 filter Object {movie_id: 4, title: "雷神", phase:

"第一阶段:复仇者联盟", category_name: "动作",

release_year: 2011...}

imdbapi.service.js:30 filter Object {movie_id: 5, title: "美国队长"

America", phase: "第一阶段:复仇者联盟", category_name:

"动作", release_year: 2011...}

imdbapi.service.js:30 filter Object {movie_id: 6, title: "复仇者联盟

The", phase: "第一阶段:复仇者联盟", category_name: "科幻

Fiction", release_year: 2012...}

imdbapi.service.js:30 filter Object {movie_id: 7, title: "钢铁侠 3",

phase: "第二阶段", category_name: "动作", release_year: 2015...}

imdbapi.service.js:30 filter Object {movie_id: 8, title: "Thor: The

Dark World", phase: "Phase Two", category_name: "Science Fiction",

release_year: 2013...}

imdbapi.service.js:30 filter Object {movie_id: 9, title: "Captain

America: The Winter Soldier", phase: "Phase Two", category_name:

"Action", release_year: 2014...}

imdbapi.service.js:30 filter Object {movie_id: 10, title: "Guardians of

the Galaxy", phase: "Phase Two", category_name: "Science Fiction",

release_year: 2014...}

imdbapi.service.js:30 filter Object {movie_id: 11, title: "Avengers:

Age of Ultron", phase: "Phase Two", category_name: "Science Fiction",

release_year: 2015...}

imdbapi.service.js:30 filter Object {movie_id: 12, title: "Ant-Man",

phase: "Phase Two", category_name: "Science Fiction", release_year:

2015...}

imdbapi.service.js:30 filter Object {movie_id: 13, title: "Captain

America: Civil War", phase: "Phase Three", category_name: "Science

Fiction", release_year: 2016...}

imdbapi.service.js:30 filter Object {movie_id: 14, title: "Doctor

Strange", phase: "Phase Two", category_name: "Science Fiction",

release_year: 2016...}

imdbapi.service.js:35 map Object {movie_id: 1, title: "Iron Man",

phase: "Phase One: Avengers Assembled", category_name: "Action",

release_year: 2015...}

angular-observable.component.js:23 Component Movie {_movie_id: 1,

_title: "Iron Man", _phase: "Phase One: Avengers Assembled",

_category_name: "Action", _release_year: 2015...}


While the modification of `IMDBAPIService` was minimal for the `fetchOneById` method, we will have to change `fetchByField` more consequently. Indeed, `onComplete` callback will only be executed once, so we need to return an array of `Movie`, and not only one `Movie`. Here's the implementation of the `fetchByField` method:

public fetchByField(field:MovieFields, value:any):Promise<Movie[]>{

console.log('fetchByField', field, value);

return this.http.get(this.moviesUrl)

    .map(res => res.json().movies.filter(

    (movie)=>{

        return (movie[MovieFields[field]] === value)

    })

    )

.toPromise()

    /**

    * Map the JSON movie items to the Movie Model

    */

    .then((jsonMovies:any[]) => {

    console.log("map", jsonMovies);

    let movies:Movie[] = [];

    for (var i = 0; i < jsonMovies.length; i++) {

        movies.push(

                new Movie(

                jsonMovies[i].movie_id,

                jsonMovies[i].title,

                jsonMovies[i].phase,

                jsonMovies[i].category_name,

                jsonMovies[i].release_year,

                jsonMovies[i].running_time,

                jsonMovies[i].rating_name,

                jsonMovies[i].disc_format_name,

                jsonMovies[i].number_discs,

                jsonMovies[i].viewing_format_name,

                jsonMovies[i].aspect_ratio_name,

                jsonMovies[i].status,

                jsonMovies[i].release_date,

                jsonMovies[i].budget,

                jsonMovies[i].gross,

                jsonMovies[i].time_stamp

                )

        )

    }

    return movies;

    });

}


To implement this, I trade `flatMap` for a classical map as the first operation. In the map, I acquire the reference to the JSON movie array directly and apply the `field` filter. The result is transformed into a promise and processed in the `then` operation. The `then` operation receives an array of JSON movies and transforms it into an array of `Movie`. This produces an array of `Movie` which is returned, as the promised result, to the caller. The call in `AngularObservableAppComponent` is also a bit different, as we now expect an array:

this.IMDBAPI.fetchByField(MovieFields.release_year, 2015).then(

    value => {

        this.movies = value;

        console.log("Component", value)

    },

    error => this.error = true

)


Another way to use `Promise` is through the `fork`/`join` paradigm. Indeed, it is possible to launch many processes (`fork`) and wait for all the promises to complete before sending the aggregated result to the caller (`join`). It is therefore relatively easy to supercharge the `fetchByField` method, as it can run in many fields with logic or.

Here are the three very short methods we need to implement to the logic or:

/**

  • Private member storing pending promises

*/

private promises:Promise<Movie[]>[] = [];

/**

  • Register one promise for field/value. Returns this

  • for chaining that is

  • byField(Y, X)

  • .or(...)

  • .fetch()

  • @param {MovieFields} field

  • @param {any} value

  • @return

*/

public byField(field:MovieFields, value:any):IMDBAPIService{

this.promises.push(this.fetchByField(field, value));

return this;

}

/**

  • 便于使调用更容易阅读的方便方法,即

  • byField(Y, X)

  • .or(...)

  • .fetch()

  • instead of

  • byField(Y, X)

  • .byField(...)

  • .fetch()

  • @param {MovieFields} field

  • @param {any} value

  • @return

*/

public or(field:MovieFields, value:any):IMDBAPIService{

return this.byField(field, value);

}

/**

  • 加入所有的 promises 并返回聚合结果。

  • @return

*/

public fetch():Promise<Movie[]>{

return Promise.all(this.promises).then((results:any) => {

    //result is an array of movie arrays. One array per

    //promise. We need to flatten it.

    return [].concat.apply([], results);

});

}


Here, I provide two convenient methods, `byfield` and/or that take a `MovieField` and a value as an argument and create a new `Promise`. They both return this for chaining. The `fetch` method joins all the `Promise` together and merges their respective results. In `AngularObservableAppComponent`, we can now have the following:

this.IMDBAPI.byField(MovieFields.release_year, 2015)

        .or(MovieFields.release_year, 2014)

        .or(MovieFields.phase, "Phase Two")

        .fetch()

        .then(

                value => {

                this.movies = value;

                console.log("Component", value)

                },

                error => this.error = true

        );

这个非常简单易懂,保持了 Angular 的所有异步能力。

针对 promises 的代码可在此处找到:[`bit.ly/mastering-angular2-chap7-part3`](http://bit.ly/mastering-angular2-chap7-part3)。

# Summary

在这一章节中,我们利用了 Angular 的异步编程,使用了`Observable`和`Promise`。

更具体地,我们学习了如何在`typescript`中实现`Observable`模式,然后利用了 Angular 框架,同时仍然使用`Observable`的特性。

同样,我们看到如何在 Angular 中利用`Promises`,并构建了一个基本但可扩展的用于查询漫威电影宇宙的 JSON API。

在下一章中,我们将在此示例的基础上构建高级表单。实际上,我们将创建添加、删除和更新漫威电影宇宙电影所需的内容。此外,我们还将学习关于 FormBuilder,控制组和自定义验证。


# 第七章:模板和数据绑定语法

在本章中,您将学习 Angular 框架提供的模板语法和数据绑定。模板语法和数据绑定主要关注应用程序的 UI 或视图方面;因此,这是一个非常重要和关键的功能。

在本章中,您将学习有关模板语法和在我们的组件中包含模板的不同方式。您还将学习创建组件,包括子组件,并在视图模板中使用表达式和运算符。您还将专注于如何在模板中附加事件、属性和实现指令。

数据绑定是 Angular 的关键特性之一,它允许我们将数据从源映射到视图目标,反之亦然。您将学习不同的数据绑定方式。

在本章中,您将学习如何在学习过程中创建示例的帮助下,包含视图模板并在模板中定义数据绑定。

您将在本章中学习并实现以下内容:

+   模板语法

+   包含模板语法的各种方式

+   Angular 中的模板表达式

+   数据绑定语法

+   Angular 双向数据绑定

+   模板中的属性绑定

+   在模板中将事件附加到视图

+   模板中的表达式和语句

+   模板中的指令

# 学习模板语法

组件的视图是使用模板定义的,告诉 Angular 如何呈现外观。在模板中,我们定义数据应该如何显示,并使用数据绑定附加事件。

大多数 HTML 标签都可以在 Angular 模板中使用。我们可以使用和定义用户自定义指令。

为组件定义模板的一般语法如下:

```ts
import {Component, View} from "@angular/core";

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

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

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

  1. 我们定义了一个组件,MyTemplateComponent

  2. 我们使用template定义了组件视图。

  3. 在模板中,我们定义了一个<h2>标签。

  4. 我们定义了一个title变量并赋予了一个值。

  5. 使用{{ }}插值,我们将变量绑定到模板上。

运行应用程序,您应该看到以下输出:

在下一节中,您将详细了解包含模板的各种方式,以及插值的相关内容。

包含模板语法的各种方式

在本节中,您将学习有关在组件视图中包含模板的不同方法。在组件语法中包含模板语法有两种方式:

  • 我们可以在component装饰器内定义视图模板。使用template,我们可以在组件装饰器内联包含模板。

  • 我们也可以使用templateURL包含模板。使用templateUrl,我们将模板元素写在一个单独的文件中,并将模板的路径提供给组件。

templateURL是一个更受欢迎的方式,因为它允许我们以逻辑方式分离代码,更有效地组织代码。

使用内联模板语法

我们讨论了在组件中以不同方式包含模板。让我们学习如何在组件内定义我们的模板。

在组件装饰器内使用模板的语法如下:

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

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

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

在前面的代码片段中需要注意的最重要的事情如下:

  1. 我们在@component装饰器内定义模板。

  2. 组件class定义和模板在同一个文件中定义。

使用 templateURL 来包含一个模板

在前面的代码片段中,我们在同一个文件中创建了模板和组件类。然而,当组件类的复杂性在模板元素和类定义中增加时,将很难维护它。

我们需要分离逻辑类和视图,这样更容易维护和理解。现在,让我们看另一种使用templateURL为组件定义视图模板的方式。

使用templateURL进行查看的语法如下;让我们创建一个名为app-template.component.ts的文件:

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

@Component({
 selector: 'app-data-binding',
 templateUrl: './data-binding.component.html',
 styleUrls: ['./data-binding.component.css']
})
export class DataBindingComponent {
}

如果我们使用上述任何一种方式来使用模板,将不会有视觉上的区别。为 HTML、CSS 和组件类创建单独的文件是有意义的,因为这样可以更好地组织代码,并在代码增加时最终有助于维护代码库。

在下一节中,您将学习 Angular 框架为数据和模板绑定提供的功能。

模板中的插值

双大括号{{ }}是 Angular 中的插值。它们是一种将大括号之间的文本映射到组件属性的方式。我们已经在整个章节中的各种示例中使用和实现了插值。

在我们将要编写的模板中,值写在双大括号内,如下所示:

{{ test_value }}

让我们快速创建一个简单的例子来理解插值。在app.component.ts文件中,让我们定义一个名为title的变量:

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

@Component({
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.css']
})

export class AppComponent {
  constructor() { }
  title = "Data Binding";
}

现在,我们需要在模板中显示title的值。更新app.component.html文件,并添加以下代码行:

<p> {{ title }} </p>

现在,尝试更改类中title的值;我们将看到模板中自动反映出更新后的值。这就是插值,这是我们在 Angular 中喜爱的一个关键特性。

现在我们知道如何使用插值,接下来我们将处理如何在模板中添加表达式。

Angular 中的模板表达式

我们可以在模板中使用表达式;表达式执行并产生一个值。

就像在 JavaScript 中一样,我们可以使用表达式语句,但不能使用赋值、new 和链式操作符。

让我们看一些模板表达式的例子:

<p> {{ tax+10 }} </p> // Using plus operator

<p> {{( tax*50)-10 }} </p>

在前面的代码片段中,我们正在使用变量tax进行算术运算。

如果您使用过任何编程语言,很可能会发现本节非常简单。就像在任何其他语言中一样,我们可以使用算术运算符。

让我们快速创建一个示例。更新app.component.html文件,并添加以下代码:

<h4>Template Expressions</h4>

<p> Expression with (+) Operator: <strong>{{ tax+ 10 }}</strong></p>

<p> Expression with (+ and *) Operator: <strong>{{ (tax*50) +10 }} 
   </strong></p>

在前面的代码片段中,我们在模板中使用了表达式。我们对tax变量进行了加法和算术运算。

在更新的app.component.ts文件中,添加以下代码片段:

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

@Component({
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.css']
})

export class AppComponent {
 constructor() { }

 title = "Data Binding";
 tax = 10;
}

我们正在创建一个AppComponent类并声明两个变量,titletax。我们为titletax分配了初始值。

运行应用程序,我们应该看到前面代码片段的输出,如下截图所示:

到目前为止,您已经学习了如何在视图中使用模板、插值以及在模板中使用表达式。现在,让我们学习如何在模板中附加事件并实现指令。

在模板中将事件附加到视图

在前面的部分中,我们介绍了如何在组件中定义和包含模板以及在模板中使用插值和表达式。

在本节中,您将学习如何将事件附加到模板中的元素。

事件是基于用户操作触发的常规 JavaScript 方法,例如onclickonmouseover。方法是一组定义为执行特定任务的语句。

附加事件的一般语法如下:

<button (click)= function_name()> Update Tax</button>

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

  1. 我们在模板中创建了一个button

  2. 我们正在将click事件附加到按钮上。

  3. 通过click事件,我们绑定了function_name()方法。

现在,让我们用上述代码更新我们的component文件,并看看它的运行情况。

我们将首先更新我们的app.component.html文件,并添加以下代码片段:

<p> {{ title }} </p>
<p> {{ tax+ 10 }}</p>
<p> {{ (tax*50) +10 }} </p>
<button (click)= updateTax()> Update Tax </button>

关于上述代码片段的一些快速注释:

  1. 我们在模板中添加了button

  2. 我们在click事件的按钮上附加了一个名为updateTax的事件。

现在,是时候用以下代码更新我们的app.component.ts文件了:

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

@Component({
 templateUrl: './data-binding.component.html',
 styleUrls: ['./data-binding.component.css']
})

export class DataBindingComponent {
 constructor() { }

 title = "Data Binding and Template Syntax";
 tax = 10;

 updateTax() {
  this.tax = 20;
 }
}

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

  1. 我们正在定义和创建一个组件--AppComponent

  2. 我们已经定义了两个变量,titletax,并为它们分配了一些值。

  3. 我们正在定义和创建一个updateTax方法,当调用时将更新tax变量。

  4. 更新后的tax值将显示在模板中。

现在,运行应用程序,我们应该看到如下截图所示的输出;点击“更新税收”按钮,您应该看到模板中的数据得到更新:

太棒了!所以,在本节中,您学会了在模板中附加事件,还学会了在组件类中定义方法来更新组件的属性。在下一节中,您将学会在模板中实现指令。

在模板中实现指令

我们在学习模板语法方面取得了良好的进展。我们讨论了如何包含模板语法,如何在模板中使用插值,并附加事件。

就像事件一样,我们也可以在模板中实现指令和属性。在本节中,我们将解释如何在模板中实现指令。

看一下以下代码片段:

<list-products></list-products>

上述代码看起来是否类似于在早期版本的 Angular 中定义自定义指令的方式?没错。在 Angular 框架中,自定义指令现在被称为组件。

我们可以根据应用程序的要求创建和定义自定义指令或标签。

目录结构和子组件之间没有关系,但作为一个良好的实践,始终将逻辑上的父子关系组件放在一个目录下;这有助于更好地组织代码。

我们将使用我们在上一节中创建的组件。我们创建了一个组件--data-binding.component.ts。现在我们将创建一个新的组件,list-products,我们将能够将其绑定到data-binding组件。

将以下代码片段添加到list-products.component.ts文件中:

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

@Component({
 selector: 'list-products',
 templateUrl: './list-products.component.html',
 styleUrls: ['./list-products.component.css']
})

export class ListProductsComponent {
 constructor() { }
}

让我们分析前面的代码:

  1. 我们创建了一个新的组件,即list-products组件。

  2. 在组件定义中,我们将selector命名为list-products

  3. @Component装饰器为组件提供了 Angular 元数据。使用 CSS selector,我们可以在list-products标签内显示模板或视图的输出。

  4. 我们可以为selector指定任何名称,但确保在父组件中也使用相同的名称进行调用。

现在我们已经告诉 Angular 我们需要将list-products组件的输出放在自定义标签list-products中,我们需要在父组件模板中放置子组件标签。

我们需要使用选择器标签来在模板data-binding.component.html文件中识别list-products组件:

<list-products></list-products>

我们已经准备好了。现在运行应用程序,我们应该看到前面的代码和子组件的输出以及data-binding.component.html模板视图一起显示出来:

太棒了!所以,你现在学会了如何在模板中包含子组件。任何一个 Angular 应用程序如果没有使用其中任何一个都很少完整。我们将在下一节继续学习和构建更多示例,在那里你将学习如何在模板中使用属性绑定。

模板中的绑定

在这一部分,我们将扩展在前一部分创建的示例。我们将介绍如何在模板中使用属性绑定。属性是模板中元素的属性,例如 class、ID 等。

HTML 属性的一般语法如下:

<button class="myBtn" [disabled]="state=='texas'"  .
  (click)="updateTax()"></button>

前面代码片段中需要注意的重点如下:

  1. 我们使用button标签定义了一个html元素。

  2. 我们向button标签添加了class属性。

  3. 我们附加了一个click事件,调用了一个名为updateTax的方法到按钮上。

  4. 我们有一个disabled属性;如果state的值是texas,按钮元素将显示在页面上并且将被disabled。如果不是,它将显示一个启用的按钮。

使用属性绑定,我们可以动态更改disabled的属性值;当组件类中的值更新或更改时,视图也会更新。

让我们更新app.component.html文件,并将属性添加到模板中的元素:

<button (click)= updateTax() [disabled]="state=='texas'"> Update Tax 
  </button>

仔细观察,你会发现我们已经添加了disabled属性;根据state的值,按钮将被启用或禁用。

现在,在app.component.ts文件中,让我们定义一个名为state的属性变量并为其赋值:

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

@Component({
 templateUrl: './data-binding.component.html',
 styleUrls: ['./data-binding.component.css']
})
export class DataBindingComponent {

 constructor() { }

 title = "Data Binding and Template Syntax";

 tax = 10;
 state = 'texas';

 updateTax() {
  this.tax = 20;
 }
}

在前面的代码中,我们只是定义了一个名为state的新变量,并为其赋值。根据state的值——分配或更新——按钮将被启用或禁用。

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

太棒了!你学会了如何在 Angular 组件中使用模板。

我们讨论了如何编写模板语法,不同的包含模板语法的方式,如何将事件附加到元素,将属性附加到元素,以及如何在模板中实现指令。

在下一节中,您将了解数据绑定——这是 Angular 最重要和最突出的特性之一,也是最常与模板语法一起使用的特性之一。

Angular 数据绑定

Angular 提供了一种在同一视图和模型之间轻松共享数据的机制。我们可以将一个值关联和赋值给一个类组件,并在视图中使用它。它提供了许多种数据绑定。我们将首先了解各种可用的数据绑定,然后继续创建一些示例。

数据绑定可以分为三个主要类别:

  1. 单向数据绑定,即从数据源到视图。

  2. 单向数据绑定,即从视图到数据源。

  3. 双向数据绑定,即从视图目标到数据源和从数据源到视图。

单向数据绑定 - 数据源到视图

在本节中,您将学习从数据源到视图目标的单向数据绑定。在下一节中,您将学习从模板到数据源的单向数据绑定。

在 Angular 中,单向数据绑定指的是从数据源到视图的数据流。换句话说,我们可以说每当值和数据更新时,它们会反映在视图目标中。

单向数据绑定从数据源到视图目标应用于以下 HTML 元素属性:

  • 插值

  • 属性

  • 属性

  • 样式

现在我们知道了单向数据绑定从数据源到目标应用于哪些属性和元素,让我们学习如何在我们的代码中使用它们。

让我们来看一下从数据源到视图模板的单向数据绑定的一般语法。

{{ value_to_display }} // Using Interpolation  [attribute] = "expression" // Attribute binding

让我们详细分析先前定义的语法:

  • 插值是在双大括号中写入的值,就像上面的代码中所示的那样。

  • 大括号{{ }}之间的文本通常是组件属性的名称。Angular 会用相应组件属性的字符串值替换该名称。

  • 我们可以通过在方括号[]中写入来定义属性属性的单向数据绑定。

  • value_to_displayexpression属性是在组件类中定义的。

一些开发人员还喜欢使用规范形式,通过在属性后添加前缀。

<a bind-href = "value"> Link 1</a>

使用bind前缀与元素的定义一起,绑定属性或属性。

现在我们知道了写单向数据绑定的语法,是时候为此编写示例了:

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

<div [style.color]="colorVal">Updating element Style CSS 
    Attributes</div>
<p>
  <div [className]="'special'" >I am Div with className directive</div>
<p>
  <div [ngClass]="{'specialClass': true, 'specialClass2': true}" >I am 
        Div with ngClass directive</div>
<p>
<img [src]="imageUrl" width="100" height="100">

让我们快速分析一下上述代码片段中的一些关键点:

  1. 我们正在使用插值--双大括号{{ }}中的值--来显示来自数据源到模板的值。属性title将在组件模型中设置。

  2. 我们通过将值动态绑定到组件类中定义的变量colorVal,来定义style属性color

  3. 我们正在定义ngClass属性,并且根据条件,无论是specialClass还是specialClass2属性中的哪一个被设置为 true,相应的类都将被分配。

  4. 我们通过将组件类中的属性imageUrl绑定到src属性值,动态地提供了图片的src属性值。

让我们快速在组件类one-way.component.ts文件中定义我们的变量:

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

@Component({
  selector: 'app-one-way',
  templateUrl: './one-way.component.html',
  styleUrls: ['./one-way.component.css']
})
export class OneWayComponent {
 constructor() { }

 title = 'One way data bindings';

  state = 'california';
  colorVal = 'red';
  specialClass : true;
  imageUrl = '././././assets/images/angular.jpeg';
  tax = 20;
}

在上述代码片段中,我们已经定义了我们的colorValisStyleVisibleimageUrl变量。

现在,让我们运行上述代码,你应该会看到以下截图中显示的输出:

如果你仔细注意,在所有上述的代码片段中,我们只是单向绑定数据,也就是说,只从数据源到视图目标。

因此,从本质上讲,这是给你的最终用户的只读数据。在下一节中,我们将学习有关从视图模板到数据源的单向数据绑定。

考虑以下的实践练习:尝试创建更多的变量并将它们映射到视图中。

单向数据绑定 - 视图模板到数据源

在前面的部分中,我们学习了从数据源到视图模板的单向数据绑定。

在本节中,我们将学习从视图模板到数据源的单向数据绑定。

从视图模板到数据源的单向数据绑定主要用于事件。

创建绑定的一般语法如下:

(target)="statement"

从视图到数据源的绑定主要用于调用方法或捕获事件交互。

下面给出了从视图模板到数据源的单向绑定示例

<button (click)="updateTax()"></button>

我们附加了click事件,当按钮被点击时,将调用updateTax方法。

我们学习了从数据源到模板以及从视图模板到数据源的单向数据绑定。

在下一节中,您将学习双向数据绑定,显示数据属性以及在对元素的属性进行更改时更新这些属性。

Angular 双向数据绑定

双向数据绑定必须是 Angular 中最重要的功能之一。双向数据绑定帮助使用ngModel指令将输入和输出绑定表达为单个符号。

双向数据绑定是一种机制,可以直接将数据从模型映射到视图,反之亦然。这种机制允许我们在视图和模型之间保持数据同步,即从数据源到视图使用[],从视图到数据源使用()

在 Angular 中,我们使用ngModel实现双向数据绑定。

双向数据绑定的一般语法如下:

<input [(ngModel)]="sample_value" />

在上述语法中,请注意以下内容:

  • 我们使用ngModel写在[()]内绑定元素

  • 我们为input元素关联了双向数据绑定

不要忘记从@angular/forms导入FormsModule,否则会出错。ngModel从领域模型创建一个FormControl实例,并将其绑定到表单控件元素。

现在,让我们使用ngModel创建一个示例:

<div> {{sample_value}}</div>

<input [(ngModel)]="sample_value" />

我们添加了一个div元素,并使用数据绑定,将输入元素的值映射到ngModel。使用ngModel有助于跟踪控件的值、用户交互和验证状态,并保持视图与模型同步。

现在,当我们开始在类型为文本的input元素中输入时,我们看到我们输入的内容被复制到我们的div元素中作为value

太棒了!就数据绑定和模板而言,我们在这一章中取得了相当大的进展。凭借我们在整个章节中获得的所有知识,我们可以创建出优雅而强大的应用程序界面。

总结

模板语法和数据绑定是 Angular 应用程序的骨架和灵魂。我们介绍了模板:如何以不同的方式包含它们并在视图模板中使用表达式。然后,我们通过将事件和属性附加到模板来遍历模板。

我们探讨了 Angular 应用程序中数据绑定的方面,并专注于如何为模板内的值实现数据绑定。

在数据绑定中,我们深入探讨了它的广泛类别。我们探索了数据绑定的可用方式:单向数据绑定和双向数据绑定。

利用数据绑定和模板结合在一起,我们几乎可以为我们的 Angular 应用程序创建模拟的功能屏幕:这就是数据绑定和模板语法的力量。

所以,继续发挥你的创造力吧!祝你好运。

在下一章中,您将学习有关 Angular 中的高级表单,学习如何使用和掌握响应式表单。我们通过强调您的 html 模型和您的NgModel之间的关系来解决响应式表单的响应部分,因此给定表单上的每个更改都会传播到模型

第八章:Angular 中的高级表单

在第七章 异步编程 使用 Observables中,我们使用 Observables 构建了一个简单但易于扩展的 JSON API 来查询漫威电影宇宙。在本章中,我们将构建表单,以更加用户友好的方式查询我们的 API。这些表单将帮助我们不仅从漫威电影宇宙中检索电影,还可以添加电影。除了表单本身,我们显然需要在我们的 API 上进行构建,以支持添加和修改电影。

在本章中,我们将详细介绍以下主题:

  • 响应式表单

  • 控件和控件组

  • 表单指令

  • 使用 FormBuilder

  • 添加验证

  • 自定义验证

开始

正如本章介绍中所述,我们将在第七章 异步编程 使用 Observables中构建我们的漫威电影宇宙的 JSON API。更准确地说,我们将改进基于 Promise 的版本。为什么使用 Promise 而不是纯观察者?嗯,Promise 是一个非常强大的工具,在我迄今为止看到的大多数 Angular/Typescript 项目中都在使用。因此,多练习一下 Promise 不会有太大的坏处。

您可以在这里找到 Promises 部分的代码bit.ly/mastering-angular2-chap7-part3

要将此代码克隆到名为advanced-forms的新存储库中,请使用以下命令:

$ git clone --depth one https://github.com/MathieuNls/mastering-
   angular2 advanced-forms
$ cd advanced-forms
$ git filter-branch --prune-empty --subdirectory-filter chap7/angular-
   promise HEAD
$ npm install

这些命令将最新版本的 GitHub 存储库中包含本书代码的文件夹命名为advanced-forms。然后,我们进入advanced-forms文件夹,并清除不在chap7/angular-promise子目录中的所有内容。神奇的是,Git 会重写存储库的历史,只保留在chap7/angular-promise子目录中的文件。最后,npm install将准备好所有我们的依赖项。

因此,您将在名为 advanced-forms 的新项目中实现我们在《第七章》使用可观察对象进行异步编程中实现的行为(例如从漫威电影宇宙查询电影)。现在,如果我们使用表单来创建、读取、更新和删除漫威电影宇宙中的电影,并且这些更改不反映在查询部分,那将不会有太多乐趣。提醒一下,我们在《第七章》使用可观察对象进行异步编程中构建的查询 API 是一个静态的 JSON 文件作为后端模拟。为了保存来自我们表单的更改,我们将不得不修改 JSON 文件。虽然这是可能的,但这意味着我们将为我们的模拟构建一个全新的功能(即编辑文件)只是为了这个目的。这个新功能在我们继续使用真正的后端时将毫无帮助。因此,我们将使用漫威电影宇宙中的电影的内存引用。

app.component.ts文件如下所示:

import { Component } from '@angular/core';
import { IMDBAPIService } from './services/imdbapi.service';
import { Movie, MovieFields } from './models/movie'; 

@Component({ 
  selector: 'app-root', 
  templateUrl: './app.component.html', 
  styleUrls: ['./app.component.css'] 
}) 
export class AppComponent { 
  title = 'app works!';

  private movies:Movie[] = [];
  private error:boolean = false;
  private finished:boolean = false;

  constructor(private IMDBAPI:IMDBAPIService){

      this.IMDBAPI.fecthOneById(1).then(
        value => {
            this.movies.push(value); 
            console.log("Component", value)
        },
        error => this.error = true
      );

      this.IMDBAPI.fetchByField(MovieFields.release_year, 2015).then(
        value => {
            this.movies = value; 
            console.log("Component", value)
        },
        error => this.error = true
      )

      this.IMDBAPI.byField(MovieFields.release_year, 2015)
        .or(MovieFields.release_year, 2014)
        .or(MovieFields.phase, "Phase Two")
        .fetch()
        .then(
          value => {
              this.movies = value; 
              console.log("Component", value)
          },
          error => this.error = true
        );
     }
} 

相关的 HTML 模板如下:

<h1>
  {{title}}
</h1>

<ul>
    <li *ngFor="let movie of movies">{{movie}}</li> 
</ul> 

IMDBAPIService与《第七章》使用可观察对象进行异步编程中的内容相同,执行ng start将得到以下结果:

在《第七章》使用可观察对象进行异步编程结束时的状态。

响应式表单

在《第八章》模板和数据绑定语法中,我们学习了如何在 Angular 中利用数据绑定和模板化。在这里,我们将把这些新概念与表单结合起来。任何有两个小时 HTML 经验的人都知道<form>的含义以及如何使用它们。在您掌握了几个小时的 HTML 之后,您就知道如何识别表单中的不同信息,并选择一种方法(即GETPOSTPUTDELETE)将所有内容发送到您选择的后端。

然而,在这个示例中,我们将使用命令式的 TypeScript 代码构建表单,而不是传统的 HTML。你可能会问,为什么?嗯,这样可以让我们在不依赖生成 DOM 的端到端测试的情况下测试我们的表单。使用响应式表单,我们可以像在第十六章中描述的那样,使用经典的单元测试来测试我们的表单。

让我们从为表单构建基础的 HTML 结构开始,旨在向漫威电影宇宙添加一部新电影,如下所示:

<form [formGroup]="movieForm">
        <label>movie_id</label>
        <input type="text" formControlName="movie_id"><br/>
        <label>title</label>
        <input type="text" formControlName="title"><br/>
        <label>phase</label>
        <input type="text" formControlName="phase"><br/>
        <label>category_name</label>
        <input type="text" formControlName="category_name"><br/>
        <label>release_year</label>
        <input type="text" formControlName="release_year"><br/>
        <label>running_time</label>
        <input type="text" formControlName="running_time"><br/>
        <label>rating_name</label>
        <input type="text" formControlName="rating_name"><br/>
        <label>disc_format_name</label>
        <input type="text" formControlName="disc_format_name"><br/>
        <label>number_discs</label>
        <input type="text" formControlName="number_discs"><br/>
        <label>viewing_format_name</label>
        <input type="text" formControlName="viewing_format_name"><br/>
        <label>aspect_ratio_name</label>
        <input type="text" formControlName="aspect_ratio_name"><br/>
        <label>status</label>
        <input type="text" formControlName="status"><br/>
        <label>release_date</label>
        <input type="text" formControlName="release_date"><br/>
        <label>budget</label>
        <input type="text" formControlName="budget"><br/>
        <label>gross</label>
        <input type="text" formControlName="gross"><br/>
        <label>time_stamp</label>
        <input type="text" formControlName="time_stamp"><br/>
</form> 

在上述表单中,我们为Movie模型的每个属性都有一个标签-输入对。现在,这个表单中有一些明显不是纯 HTML 的指令。即[formGroup]="movieForm"formControlName=""。第一个指令([formGroup]="movieForm")用于将这个特定表单与FormGroup的实例绑定。然后,formControlName指的是FormControl类的实例,它包括FormGroup。换句话说,movieForm是由FormControlFormGroupFormControl组成的,@angular/forms包中都有。因此,我们需要在app.component.ts文件中导入这个包:import { FormGroup, FormControl } from @angular/forms;在更新了app.component.html文件后。此外,我们需要导入ReactiveFormsModule并将其添加到我们的应用程序模块中。

如果你现在就启动你的应用程序,它会毫无问题地转译。然而,在运行时,它会抱怨,因为movieForm表单组在你的组件中还不存在。让我们创建它:

 private movieForm:FormGroup =  new FormGroup({
    movie_id: new FormControl(),
    title: new FormControl(),
    phase: new FormControl(),
    category_name: new FormControl(),
    release_year: new FormControl(),
    running_time: new FormControl(),
    rating_name: new FormControl(),
    disc_format_name: new FormControl(),
    number_discs: new FormControl(),
    viewing_format_name: new FormControl(),
    aspect_ratio_name: new FormControl(),
    status: new FormControl(),
    release_date: new FormControl(),
    budget: new FormControl(),
    gross: new FormControl(),
    time_stamp: new FormControl()
}); 

正如你所看到的,AppComponent组件有一个FormGroup的私有成员实例。这个FormGroup实例由许多FormControl实例组成,每个字段都是精确的一个。

此外,每个字段的值可以通过this.movieForm.value.my_field来访问。因此,如果我们在表单中添加一个提交按钮:

<button (click)="submit()" type="submit">SUBMIT</button> 

然后,在AppComponent组件中对应的submit()函数,然后我们可以显示每个字段的值。

  private submit(){
    console.log(
      "Form Values",
      this.movieForm.value.movie_id,
      this.movieForm.value.title,
      this.movieForm.value.phase,
      this.movieForm.value.category_name,
      this.movieForm.value.release_year,
      this.movieForm.value.running_time,
      this.movieForm.value.rating_name,
      this.movieForm.value.disc_format_name,
      this.movieForm.value.number_discs,
      this.movieForm.value.viewing_format_name,
      this.movieForm.value.aspect_ratio_name,
      this.movieForm.value.status,
      this.movieForm.value.release_date,
      this.movieForm.value.budget,
      this.movieForm.value.gross,
      this.movieForm.value.time_stamp
    );
  } 

就是这么简单;我们在 HTML 模板和组件之间建立了通信:

显示相当粗糙的 HTML 表单和提交函数的控制台输出。

然后,我们可以创建Movie模型的实例并将其发送到IMDBAPI进行持久化。唯一缺少的是一个可用的后端。

private submit(){
    console.log(
      "Form Values",
      this.movieForm.value.movie_id,
      this.movieForm.value.title,
      this.movieForm.value.phase,
      this.movieForm.value.category_name,
      this.movieForm.value.release_year,
      this.movieForm.value.running_time,
      this.movieForm.value.rating_name,
      this.movieForm.value.disc_format_name,
      this.movieForm.value.number_discs,
      this.movieForm.value.viewing_format_name,
      this.movieForm.value.aspect_ratio_name,
      this.movieForm.value.status,
      this.movieForm.value.release_date,
      this.movieForm.value.budget,
      this.movieForm.value.gross,
      this.movieForm.value.time_stamp
    );

    let movie:Movie = new Movie(
      this.movieForm.value.movie_id,
      this.movieForm.value.title,
      this.movieForm.value.phase,
      this.movieForm.value.category_name,
      this.movieForm.value.release_year,
      this.movieForm.value.running_time,
      this.movieForm.value.rating_name,
      this.movieForm.value.disc_format_name,
      this.movieForm.value.number_discs,
      this.movieForm.value.viewing_format_name,
      this.movieForm.value.aspect_ratio_name,
      this.movieForm.value.status,
      this.movieForm.value.release_date,
      this.movieForm.value.budget,
      this.movieForm.value.gross,
      this.movieForm.value.time_stamp
     );

    console.log(movie);

    //Persist movie

  } 

在下面的截图中,我们可以看到显示的 HTML 表单和改进的submit函数的控制台输出:

现在很好;我们已经从 HTML 表单中检索到了值,并在应用程序的组件端创建了一个可以移动和持久化的Movie对象。这个表单中至少有两个不同的改进之处:

  • 表单创建的冗长(new FormControl()太多了?)

  • 对不同输入的验证

使用 FormBuilder

FormBuilder是 Angular 的@angular/forms包中的可注入辅助类。这个类有助于减少表单创建的冗长,如下面的代码所示:

this.movieForm = this.formBuilder.group({
   movie_id: '',
   title: '',
   phase: '',
   category_name: '',
   release_year: '',
   running_time: '',
   rating_name: '',
   disc_format_name: '',
   number_discs: '',
   viewing_format_name: '',
   aspect_ratio_name: '',
   status: '',
   release_date: '',
   budget: '',
   gross: '',
   time_stamp: ''
}); 

正如你所看到的,使用FormBuilder类的group方法,FormGroupFormControl的声明现在是隐式的。我们只需要有字段名称,后面跟着它的默认值。在这里,所有的默认值都是空白的。

要使用FormBuilder类,我们首先必须导入它:

Import { FormGroup, FormControl, FormBuilder } from '@angular/forms'; 

然后我们在AppComponent组件的构造函数中注入它:

 constructor(private IMDBAPI:IMDBAPIService, private formBuilder: FormBuilder) 

请注意,我们仍然从第七章注入了IMDBAPIService使用可观察对象进行异步编程

因此,AppComponent现在看起来像下面这样:


import { Component } from '@angular/core';
import { IMDBAPIService } from './services/imdbapi.service';
import { Movie, MovieFields } from './models/movie';

import { FormGroup, FormControl, FormBuilder } from '@angular/forms';

@Component({ 
  selector: 'app-root', 
  templateUrl: './app.component.html', 
  styleUrls: ['./app.component.css'] 
})
export class AppComponent {
  title = 'app works!';

  private movies:Movie[] = [];
  private error:boolean = false;
  private finished:boolean = false;
  private movieForm:FormGroup;

  constructor(private IMDBAPI:IMDBAPIService, private formBuilder: 
     FormBuilder){

      this.movieForm =  this.formBuilder.group({
        movie_id: '',
        title: '',
        phase: '',
        category_name: '',
        release_year: '',
        running_time: '',
        rating_name: '',
        disc_format_name: '',
        number_discs: '',
        viewing_format_name: '',
        aspect_ratio_name: '',
        status: '',
        release_date: '',
        budget: '',
        gross: '',
        time_stamp: ''
      });

      // IMDB queries have been removed for simplicity
    }

    private submit(){
        // submit body has been removed for simplicity
    }    

我们解决了我们两个问题中的第一个:表单创建的冗长。在下一节中,我们将解决本章的验证部分,学习如何验证传入的输入。

添加验证

处理表单对开发人员来说通常是一种痛苦,因为显然你不能信任用户提供的输入。这要么是因为他们只是没有注意到你在表单中期望的内容,要么是因为他们想要破坏事情。验证来自表单的输入在每种语言中都是痛苦的,无论是服务器端还是客户端。

现在,Angular 团队提出了一种相当简单的方法,通过在表单创建时定义对每个字段的期望来验证输入,使用Validators。Angular 包含以下内置的Validators,我们可以使用:

  • required: 要求非空值

  • minLength(minLength: number): 要求控件值的最小长度为minLength

  • maxLength(maxLength: number): 要求控件值的最大长度为maxLength

  • pattern(pattern: string): 要求控件值与提供的模式匹配

向我们的表单添加这些内置的validators很简单:


//In AppComponent

import { FormGroup, FormControl, FormBuilder, Validators } from '@angular/forms';

//[...]

constructor(private IMDBAPI:IMDBAPIService, private formBuilder: FormBuilder){

      this.movieForm =  this.formBuilder.group({
        movie_id: ['', Validators.required],
        title: ['', Validators.required],
        phase: ['', Validators.required],
        category_name: ['', Validators.required],
        release_year: ['', Validators.required],
        running_time: ['', Validators.required],
        rating_name: ['', Validators.required],
        disc_format_name: ['', Validators.required],
        number_discs: ['', Validators.required],
        viewing_format_name: ['', Validators.required],
        aspect_ratio_name: ['', Validators.required],
        status: ['', Validators.required],
        release_date: ['', Validators.required],
        budget: ['', Validators.required],
        gross: ['', Validators.required],
        time_stamp: ['', Validators.required]
      });
}

//[...] 

除了每个字段的空白默认值之外,我们还添加了必需的validator,这是@angular/forms包中包含的Validators类的静态属性。我们可以使用FormGroup的 valid 属性来读取表单的有效性(即,如果所有验证器都通过了):

 private submit(){
    console.log(
      "Form Values",
      this.movieForm.value.movie_id,
      this.movieForm.value.title,
      this.movieForm.value.phase,
      this.movieForm.value.category_name,
      this.movieForm.value.release_year,
      this.movieForm.value.running_time,
      this.movieForm.value.rating_name,
      this.movieForm.value.disc_format_name,
      this.movieForm.value.number_discs,
      this.movieForm.value.viewing_format_name,
      this.movieForm.value.aspect_ratio_name,
      this.movieForm.value.status,
      this.movieForm.value.release_date,
      this.movieForm.value.budget,
      this.movieForm.value.gross,
      this.movieForm.value.time_stamp
    );

    if(this.movieForm.valid){
      let movie:Movie = new Movie(
        this.movieForm.value.movie_id,
        this.movieForm.value.title,
        this.movieForm.value.phase,
        this.movieForm.value.category_name,
        this.movieForm.value.release_year,
        this.movieForm.value.running_time,
        this.movieForm.value.rating_name,
        this.movieForm.value.disc_format_name,
        this.movieForm.value.number_discs,
        this.movieForm.value.viewing_format_name,
        this.movieForm.value.aspect_ratio_name,
        this.movieForm.value.status,
        this.movieForm.value.release_date,
        this.movieForm.value.budget,
        this.movieForm.value.gross,
        this.movieForm.value.time_stamp
       );

      console.log(movie);
      //Persist movie
    }else{
      console.error("Form not valid");
    }
} 

submit方法的上一个修改中,如果用户没有填写其中一个字段,则Movie对象将不会被创建。此外,我们将显示console.error("表单无效");如果我们添加一个条件<p></p>块,并附带一些基本的 CSS,我们可以为用户提供一些反馈。

<p class='error' *ngIf=!movieForm.valid>Error</p> 
/*app.component.css*/
.error{
    color:red;
} 

在以下屏幕截图中,我们可以看到显示的 HTML 表单,并对表单的有效性进行了反馈。

我们可以再进一步,为每个字段提供可视化反馈。通过每个子FormControlvalid属性可以访问每个字段的状态。

<form [formGroup]="movieForm">

        <p class='error' *ngIf=!movieForm.valid>Error</p>
        <label>movie_id</label>
        <p class='error' *ngIf=!movieForm.controls.movie_id.valid>This 
               field is required</p>
        <input type="text" formControlName="movie_id"><br/>
        <label>title</label>
        <p class='error' *ngIf=!movieForm.controls.title.valid>This 
               field is required</p>
        <input type="text" formControlName="title"><br/>
        <label>phase</label>
        <p class='error' *ngIf=!movieForm.controls.phase.valid>This 
               field is required</p>
        <input type="text" formControlName="phase"><br/>
        <label>category_name</label>
        <p class='error' 
              *ngIf=!movieForm.controls.category_name.valid>This field 
               is required</p>
        <input type="text" formControlName="category_name"><br/>
        <label>release_year</label>
        <p class='error' 
              *ngIf=!movieForm.controls.release_year.valid>This field 
               is required</p>
        <input type="text" formControlName="release_year"><br/>
        <label>running_time</label>
        <p class='error' 
              *ngIf=!movieForm.controls.running_time.valid>This field  
               is required</p>
        <input type="text" formControlName="running_time"><br/>
        <label>rating_name</label>
        <p class='error' 
               *ngIf=!movieForm.controls.rating_name.valid>This field 
                is required</p>
        <input type="text" formControlName="rating_name"><br/>
        <label>disc_format_name</label>
        <p class='error' 
              *ngIf=!movieForm.controls.disc_format_name.valid>This 
               field is required</p>
        <input type="text" formControlName="disc_format_name"><br/>
        <label>number_discs</label>
        <p class='error' 
              *ngIf=!movieForm.controls.number_discs.valid>This field 
              is required</p>
        <input type="text" formControlName="number_discs"><br/>
        <label>viewing_format_name</label>
        <p class='error' 
            *ngIf=!movieForm.controls.viewing_format_name.valid>This 
             field is required</p>
        <input type="text" formControlName="viewing_format_name"><br/>
        <label>aspect_ratio_name</label>
        <p class='error' 
            *ngIf=!movieForm.controls.aspect_ratio_name.valid>This         
             field is required</p>
        <input type="text" formControlName="aspect_ratio_name"><br/>
        <label>status</label>
        <p class='error' *ngIf=!movieForm.controls.status.valid>This 
              field is required</p>
        <input type="text" formControlName="status"><br/>
        <label>release_date</label>
        <p class='error' 
             *ngIf=!movieForm.controls.release_date.valid>This field is 
              required</p>
        <input type="text" formControlName="release_date"><br/>
        <label>budget</label>
        <p class='error' *ngIf=!movieForm.controls.budget.valid>This 
            field is required</p>
        <input type="text" formControlName="budget"><br/>
        <label>gross</label>
        <p class='error' *ngIf=!movieForm.controls.gross.valid>This 
             field is required</p>
        <input type="text" formControlName="gross"><br/>
        <label>time_stamp</label>
        <p class='error' 
           *ngIf=!movieForm.controls.time_stamp.valid>This field is 
            required</p>
        <input type="text" formControlName="time_stamp"><br/>

        <button (click)="submit()" type="submit">SUBMIT</button>
</form> 

这产生了以下结果:

显示带有每个字段有效性反馈的 HTML 表单。

正如您所看到的,除了movid_id之外的每个表单都显示了“此字段为必填项”错误,因为它们为空。*ngIf结构指令监听与关联变量的任何更改,并在字段变得无效/有效时显示/隐藏段落。表单的另一个有用属性是 pristine。它定义了给定字段是否已被用户修改。在我们的情况下,即使没有进行编辑,它也可以用来避免显示错误。

关于validators的另一个方便的事情是,它们可以使用Validators类的 compose 方法进行组合。在以下示例中,我们将从四个不同的验证器:Validators.requiredValidators.minLengthValidators.maxLengthValidators.pattern,组合一个movie_id字段的验证器。

this.movieForm =  this.formBuilder.group({
    movie_id: ['',  
       Validators.compose(
       [
          Validators.required,
          Validators.minLength(1), 
          Validators.maxLength(4), 
          Validators.pattern('[0-9]+')
       ]
      )
    ],
    title: ['', Validators.required],
    phase: ['', Validators.required],
    category_name: ['', Validators.required],
    release_year: ['', Validators.required],
    running_time: ['', Validators.required],
    rating_name: ['', Validators.required],
    disc_format_name: ['', Validators.required],
    number_discs: ['', Validators.required],
    viewing_format_name: ['', Validators.required],
    aspect_ratio_name: ['', Validators.required],
    status: ['', Validators.required],
    release_date: ['', Validators.required],
    budget: ['', Validators.required],
    gross: ['', Validators.required],
    time_stamp: ['', Validators.required]
}); 

因此,生成的复合验证器将确保movie_id是一个介于14位数字之间的数字。以下屏幕截图显示了带有 movide_id 字段反馈的 HTML 表单。该字段有效,因为它由四个数字组成:

自定义验证

在前一节中,我们看到了如何使用验证器并将验证器组合在一起以创建更复杂的验证。Validators.requiredValidators.minLengthValidators.maxLengthValidators.pattern的组合可以涵盖在开发 Angular 应用程序过程中可能出现的许多验证情况。如果有时候您无法使用内置验证器处理验证需求,那么您可以构建自己的验证器。

在本节中,我们将看到如何验证movie_id字段包含有效的条目(即一个介于一到四位数之间的数字),并且另一个电影尚未使用该 ID。为此,我们可以创建以下类:


import { FormControl } from '@angular/forms';

interface ValidationResult {
 [key:string]:boolean;
}

export class MovieIDValidator{
    static idNotTaken(control: FormControl): ValidationResult { 

        let movies = require('./marvel-cinematic-
                universe.json').movies;
        let found:boolean = false;

        for (var i = 0; i < movies.length; ++i) {

            if(control.value == movies[i].movie_id){
                 return { "idNotTaken": true };
            }
        }

       return null;
    }
} 

在这里,我们可以看到验证结果实际上是一个简单的[key:string]:boolean结构。如果布尔值为 true,则意味着验证器失败(即字段无效)。接下来是MovieIDValidator类本身,我们有一个静态方法返回ValidationResult,并接受FormControl作为参数。在这个方法中,我们从包含漫威电影宇宙的 JSON 文件中提取所有电影。然后,我们遍历所有电影,并检查movie_id字段的当前值是否与现有 ID 匹配。如果是,我们返回{ "idNotTaken": true },这意味着idNotTaken验证器存在问题。将这个新的自定义验证器与其他四个(即Validators.requiredValidators.minLengthValidators.maxLengthValidators.pattern)结合起来非常容易:

import { MovieIDValidator } from './movie-id.validator'

// [...]

this.movieForm =  this.formBuilder.group({
        movie_id: ['',  
          Validators.compose(
            [
             Validators.required,
             Validators.minLength(1), 
             Validators.maxLength(4), 
             Validators.pattern('[0-9]+'),
             MovieIDValidator.idNotTaken
            ]
          )
        ],
        title: ['', Validators.required],
        phase: ['', Validators.required],
        category_name: ['', Validators.required],
        release_year: ['', Validators.required],
        running_time: ['', Validators.required],
        rating_name: ['', Validators.required],
        disc_format_name: ['', Validators.required],
        number_discs: ['', Validators.required],
        viewing_format_name: ['', Validators.required],
        aspect_ratio_name: ['', Validators.required],
        status: ['', Validators.required],
        release_date: ['', Validators.required],
        budget: ['', Validators.required],
        gross: ['', Validators.required],
        time_stamp: ['', Validators.required]
      }); 

我们还可以添加一个异步表单验证器,它返回一个 Promise(例如Promise<ValidationResult>而不是ValidationResult)。当您必须使用远程 API 进行验证时,这非常方便。

import { FormControl } from '@angular/forms';

interface ValidationResult {
 [key:string]:boolean;
}

export class MovieIDValidator{
    static idNotTaken(control: FormControl): ValidationResult { 

        let movies = require('./marvel-cinematic-
           universe.json').movies;
        let found:boolean = false;

        for (var i = 0; i < movies.length; ++i) {

            if(control.value == movies[i].movie_id){
                 return { "idNotTaken": true };
            }
        }

       return null;
    }

    static idTakenAsync(control: FormControl): 
     Promise<ValidationResult> { 

        let p = new Promise((resolve, reject) => {
         setTimeout(() => {

            let movies = require('./marvel-cinematic-
                universe.json').movies;
            let found:boolean = false;

            for (var i = 0; i < movies.length; ++i) {

                if(control.value == movies[i].movie_id){
                     resolve({ "idNotTaken": true });
                }
            }

            resolve(null);

         }, 1000)
       });

       return p;

    }
} 

在这里,我们构建了一个模拟远程 API 调用的 Promise,超时为 1 秒。Promise 的作用与idNotTaken相同,我们检查电影的 ID 是否已经被使用。创建 Promise 后,我们将其返回,以便在相关组件中使用。

使用 ngModel 进行双向数据绑定

在通过表单创建或更新 Angular 应用程序的模型时,使用ngModel进行双向数据绑定非常方便。在前一个应用程序中,我们有以下submit()方法:

private submit(){
  console.log(
    "Form Values",
    this.movieForm.value.movie_id,
    this.movieForm.value.title,
    this.movieForm.value.phase,
    this.movieForm.value.category_name,
    this.movieForm.value.release_year,
    this.movieForm.value.running_time,
    this.movieForm.value.rating_name,
    this.movieForm.value.disc_format_name,
    this.movieForm.value.number_discs,
    this.movieForm.value.viewing_format_name,
    this.movieForm.value.aspect_ratio_name,
    this.movieForm.value.status,
    this.movieForm.value.release_date,
    this.movieForm.value.budget,
    this.movieForm.value.gross,
    this.movieForm.value.time_stamp
  );

  if(this.movieForm.valid){
    let movie:Movie = new Movie(
      this.movieForm.value.movie_id,
      this.movieForm.value.title,
      this.movieForm.value.phase,
      this.movieForm.value.category_name,
      this.movieForm.value.release_year,
      this.movieForm.value.running_time,
      this.movieForm.value.rating_name,
      this.movieForm.value.disc_format_name,
      this.movieForm.value.number_discs,
      this.movieForm.value.viewing_format_name,
      this.movieForm.value.aspect_ratio_name,
      this.movieForm.value.status,
      this.movieForm.value.release_date,
      this.movieForm.value.budget,
      this.movieForm.value.gross,
      this.movieForm.value.time_stamp
    );

    console.log(movie);
   }
  else{
      console.error("Form not valid");
    }
  } 

对于经验丰富的人来说,这看起来很笨拙。事实上,我们知道我们会要求用户输入一个新电影。因此,所有字段都将被显示,并且它们的值将用于创建上述电影。使用双向数据绑定,您可以指定每个 HTML 输入与模型属性之间的绑定。在我们的情况下,这是Movie对象的一个属性。

<form [formGroup]="movieForm">

        <p class='error' *ngIf=!movieForm.valid>Error</p>
        <label>movie_id</label>
        <p class='error' *ngIf=!movieForm.controls.movie_id.valid>This 
              field is required</p>
        <input type="text" formControlName="movie_id" 
             [(ngModel)]="movie.movie_id" name="movie_id" ><br/>
        <label>title</label>
        <p class='error' *ngIf=!movieForm.controls.title.valid>This 
             field is required</p>
        <input type="text" formControlName="title" 
            [(ngModel)]="movie.title" name="movie_title"><br/>
        <label>phase</label>
        <p class='error' *ngIf=!movieForm.controls.phase.valid>This 
            field is required</p>
        <input type="text" formControlName="phase" 
            [(ngModel)]="movie.phase" name="movie_phase"><br/>
        <label>category_name</label>
        <p class='error' *ngIf=!movieForm.controls.
            category_name.valid>This field is required</p>
        <input type="text" formControlName="category_name" 
             [(ngModel)]="movie.category_name"  name="movie_cat"><br/>
        <label>release_year</label>
        <p class='error' *ngIf=!movieForm.controls.release_year
              .valid>This field is required</p>
        <input type="text" formControlName="release_year"  
            [(ngModel)]="movie.release_year" name="movie_year"><br/>
        <label>running_time</label>
        <p class='error'*ngIf=!movieForm.controls.
             running_time.valid>This field is required</p>
        <input type="text" formControlName="running_time" 
              [(ngModel)]="movie.running_time" name="movie_time"><br/>
        <label>rating_name</label>
        <p class='error' *ngIf=!movieForm.controls.rating_name.
        valid>This field is required</p>
        <input type="text" formControlName="rating_name" 
             [(ngModel)]="movie.rating_name" name="movie_rating"><br/>
        <label>disc_format_name</label>
        <p class='error' *ngIf=!movieForm.controls.
            disc_format_name.valid>This field is required</p>
        <input type="text" formControlName="disc_format_name" 
           [(ngModel)]="movie.disc_format_name" name="movie_disc"><br/>
        <label>number_discs</label>
        <p class='error' *ngIf=!movieForm.controls.number_discs.valid>
              This field is required</p>
        <input type="text" formControlName="number_discs" 
           [(ngModel)]="movie.number_discs" name="movie_discs_nb"><br/>
        <label>viewing_format_name</label>
        <p class='error' *ngIf=!movieForm.controls.viewing_format_name.
             valid>This field is required</p>
        <input type="text" formControlName="viewing_format_name" 
             [(ngModel)]="movie.viewing_format_name"
             name="movie_format"><br/>
        <label>aspect_ratio_name</label>
        <p class='error' *ngIf=!movieForm.controls.aspect_ratio_name.
                valid>This field is required</p>
        <input type="text" formControlName="aspect_ratio_name"  
           [(ngModel)]="movie.aspect_ratio_name" 
             name="movie_ratio"><br/>
        <label>status</label>
        <p class='error' *ngIf=!movieForm.
           controls.status.valid>This field is required</p>
        <input type="text" formControlName="status" 
            [(ngModel)]="movie.status" name="movie_status"><br/>
        <label>release_date</label>
        <p class='error' *ngIf=!movieForm.controls.release_date.
              valid>This field is required</p>
        <input type="text" formControlName="release_date" 
            [(ngModel)]="movie.release_date" name="movie_release"><br/>
        <label>budget</label>
        <p class='error' *ngIf=!movieForm.controls.budget.valid>This 
               field is required</p>
        <input type="text" formControlName="budget" 
            [(ngModel)]="movie.budget" name="movie_budget"><br/>
        <label>gross</label>
        <p class='error' *ngIf=!movieForm.controls.gross.valid>This 
              field is required</p>
        <input type="text" formControlName="gross" 
              [(ngModel)]="movie.gross" name="movie_gross"><br/>
        <label>time_stamp</label>
        <p class='error' *ngIf=!movieForm.controls.time_stamp.
               valid>This field is required</p>
        <input type="text" formControlName="time_stamp" 
          [(ngModel)]="movie.time_stamp" name="movie_timestamp"><br/>

        <button (click)="submit()" type="submit">SUBMIT</button>
</form> 

看一下[(ngModel)]指令。在这里,我们使用[]单向绑定,使用()另一种方式。一种方式是表单的模型,另一种方式是从表单到模型。这意味着对表单所做的任何修改都会影响模型,对模型所做的任何修改都会反映在表单上。

现在,我们的提交方法可以简化为以下内容:

private submit(){ if(this.movieForm.valid){ 
  console.log(this.movie);

  //persist
}else{
  console.error("Form not valid");
} 
} 

要牢记的一点是,即使验证器无效,表单的值也会传递到模型。例如,如果您在movie_id字段中输入ABC,则validators将无效,但console.log(this.movie.movie_id)将显示ABC

保持整洁(额外学分)

我一直发现表单是干净、整洁、有组织的 HTML 模板的大敌。即使是小型表单,也经过良好的缩进和注释分隔,但在我看来,它们看起来也很凌乱。为了以 Angular 的方式解决这个问题,我们可以创建指令来保持表单输入的有序。以下是我在为Toolwatch.io创建表单时使用的示例:

<toolwatch-input 
      [id]             = "'email'"
      [control]        = "loginForm.controls.email" 
      [errorLabel]     = "'email-required'"
      [submitAttempt]  = "submitAttempt"
      [autoCapitalize] = false
      [autoCorrect]    = false
      [spellCheck]     = false
> 

正如您所看到的,该指令接受一个不同的@Input参数,控制输入的外观和行为。

以下是相关的组件:

import { Component, Input, EventEmitter, Output  } from '@angular/core';
import {   
  FormControl
} from '@angular/forms';

@Component({
    templateUrl: './toowatch-input.html',
    pipes: [TranslatePipe],
    selector: 'toolwatch-input',
})
export class ToolwatchInput {

    @Input()
     id             : string;
    @Input()
     control        : FormControl;
    @Input()
     model          : any = null;
    @Input()
     type           : string = "text";
    @Input()
     label          : string;
    @Input()
     errorLabel     : string;
    @Input()
     formControlName: string;
    @Input()
     submitAttempt  : boolean = true;
    @Input()
     autoCapitalize : boolean = true;
    @Input()
     autoCorrect    : boolean = true;
    @Input()
     autoComplete   : boolean = true;
    @Input()
     spellCheck     : boolean = true;

    @Output()
     update         = new EventEmitter();

    constructor() {

    }

    ngAfterViewInit() {

        if(this.control == null || this.id == null){
            throw "[Control] and [id] must be set";
        }

        //initialize other variables to the value of id 
        //if they are null
        let variablesToInitialize = [
            "label", 
            "errorLabel", 
            "formControlName"
        ];

        for (var i = variablesToInitialize.length - 1; i >= 0; i--) {
            if(this[variablesToInitialize[i]] == null){
                this[variablesToInitialize[i]] = this.id;
            }
        }
    }

} 

该组件接受以下属性作为输入:

  • id:输入的id

  • control:控制此输入的FormControl

  • model:绑定的模型字段

  • type:输入类型

  • label:要显示的标签

  • errorLabel:要显示的错误标签

  • formControlName:表单控件的名称

  • submitAttempt:如果表单已经提交过一次

  • autoCapitalizeautoCapitalize的 HTML 属性开/关

  • autoCorrectautoCorrect的 HTML 属性开/关

  • autoCompleteautoComplete的 HTML 属性开/关

  • spellCheckspellCheck的 HTML 属性开/关

它还使用id的值初始化了labelerrorLabelformControlName的值,如果它们没有提供。最后,该组件还有一个名为update@Output属性,在value更改时会触发事件,因此您可以注册它。

在 HTML 端,我们有类似以下的内容:

<div  class="group"
  [ngClass]="{ 'has-error' : !control.valid && submitAttempt }"

    >
    <em *ngIf="!control.valid && submitAttempt">
      {{ errorLabel | translate:{value: param} }}
    </em>

    <input #input_field
      [attr.autocapitalize] = "autoCapitalize ? 'on' : 'off'"
      [attr.autocorrect]    = "autoCorrect ? 'on' : 'off'"
      [attr.autocomplete]   = "autoComplete ? 'on' : 'off'"
      [attr.spellcheck]     = "spellCheck ? 'on' : 'off'"
      class                 = "form-control" 
      id                    = "{{id}}" 
      type                  = "{{type}}" 
      [formControl]         = "control" 
      (keyup) = "update.emit(input_field.value)"
    >
    <span class="highlight"></span>
    <span class="bar"></span>
    <label htmlFor="{{id}}">
      {{ label | translate:{value: param} }}
    </label>
</div> 

主要优势在于 HTML 和 CSS 类管理被封装起来,我不必每次想要输入时都复制粘贴它们。

总结

在本章中,我们学习了如何利用响应式表单的优势。响应式表单可以手动创建,也可以使用FormBuilder进行程序化创建。此外,我们强调了响应式表单的响应式部分,强调了 HTML 模型和ngModel之间的关系,因此给定表单上的每个更改都会传播到模型上。我们还看到了如何自定义验证并将我们新获得的关于表单的知识嵌入到清晰、可重用的指令中。

在下一章中,我们将学习如何将 Material Design 与 Angular 集成,以创建出色且响应灵敏的应用程序。

第九章:Angular 中的 Material Design

Material Design 是新的、备受炒作的设计风格。它取代了扁平设计成为新的必须使用的设计。Material Design 是由 Google 在 2014 年推出的,它扩展了 Google Now 的卡片图案。以下是 Google Now 卡片的图片:

Google Now 卡片。

Material Design 背后的整个理念是建立在基于网格的系统、动画和过渡的响应性基础上,同时增加设计的深度。Material Design 的首席设计师 Matias Duarte 这样说:

“与真实的纸张不同,我们的数字材料可以智能地扩展和重塑。材料具有物理表面和边缘。接缝和阴影提供了关于您可以触摸的内容的含义。”

Material Design 是一套非常精确和完整的规范,可以在这里找到:material.google.com/

任何对 CSS3 和 HTML5 有扎实知识的人都可以阅读文档并实现每个组件。然而,这将需要大量的时间和精力。幸运的是,我们不必等那么久。事实上,一组才华横溢的开发人员组成并为 Angular 创建了一个 Material Design 组件。在撰写本文时,这仍处于测试阶段,这意味着一些组件尚未实现或未完全实现。然而,我很少发现自己因为某个组件不存在或不起作用而被困住,以至于不得不改变整个设计。

在本章中,我们将学习如何安装 Material Design 的 Angular 组件,然后使用一些最受欢迎的组件。我们还将看一下材料图标。更详细地说,我们将看到:

  • 如何为 Angular 安装 Material Design

  • 响应式布局的处理方式

  • 材料图标

  • 按钮

  • 菜单

  • 工具栏

  • 对话框

  • 创建自己的主题

安装包

首先,我们需要安装 Angular Material Design 包。使用 Angular CLI 相对简单:

ng new chap10
cd chap10
npm install --save @angular/material 
npm install --save @angular/animations
npm install --save hammerjs 

我们在这里安装了两个包,@angular/materialhammerjs包。第一个包包括了我们的应用程序中将在下一节中使用的 Material Design 模块。然而,第二个包是触摸移动的 JavaScript 实现。一些 Material Design 组件,如slider,依赖于hammerjs

然后,根据NgModule规范,我们可以导入MaterialModule如下:

//src/app/app.module.ts

import { MaterialModule } from '@angular/material';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';

@NgModule({
 declarations: [
   AppComponent
 ],
 imports: [
   BrowserModule,
   FormsModule,
   HttpModule,
   ReactiveFormsModule,
   NgbModule.forRoot(),
   MaterialModule.forRoot()
 ],
 providers: [],
 bootstrap: [AppComponent]
})
export class AppModule { } 

接下来,我们需要选择一个主题。主题是将应用于 Angular Material 组件的一组颜色。在一个主题中,您有以下颜色:

  • 主要调色板包括在所有屏幕和组件上最广泛使用的颜色

  • 强调调色板包括用于浮动操作按钮和交互元素的颜色。

  • 警告调色板包括用于传达错误状态的颜色

  • 前景调色板包括文本和图标的颜色

  • 背景调色板包括用于元素背景的颜色

幸运的是,有默认主题(谷歌在大多数服务中使用的主题),我们可以直接使用。为此,请将以下行添加到您的/src/styles.css文件中:

@import '~@angular/material/core/theming/prebuilt/deeppurple-
     amber.css'; 

在这里,我们使用深紫色主题,这是可用的默认主题之一。您可以在这里看到所有默认主题:node_modules/@angular/material/core/theming/prebuilt

此外,就是这样!您可以运行ng serve来重新编译您的项目,并确认一切都按计划进行。不出所料,目前没有太多要展示的。这是在运行ng serve后拍摄的屏幕截图:

应用程序运行正常!

响应式布局

Material Designs 的一个重要部分是响应式布局,可以适应任何可能的屏幕尺寸。为了实现这一点,我们使用断点宽度:480、600、840、960、1280、1440 和 1600 dp,如以下表格所定义:material.io/guidelines/layout/responsive-ui.html#responsive-ui-breakpoints

断点(dp) 手机/平板竖屏 手机/平板横屏 窗口 间距
0 小手机 超小 4 16
360 中等手机 超小 4 16
400 大手机 超小 4 16
480 大手机 小手机 超小 4 16
600 小平板 中等手机 8 16/24
720 大平板 大手机 8 16/24
840 大平板 大手机 12 16/24
960 小平板 12 24
1024 大平板 中等 12 24
1280 大平板 中等 12 24
1440 12 24
1600 12 24
1920 超大 12 24

请注意,本章中我们将使用的所有 Material Design 指令已经实现了这些断点。然而,如果您开始主题化(请参阅本章的最后一节)或实现自定义指令,您必须牢记它们。CSS 断点相当容易定义,但可能是繁琐的工作:

@media (min-width: 600dp) {
 .class {
   content: 'Whoa.';
 }
} 

现在,前表的前四列相当不言自明,我们有 dp 中的断点,手持设备/平板电脑纵向,手持设备/平板电脑横向和窗口。然而,最后两个需要一些解释。列栏指示每个 dp 大小均等分屏幕的列数。

间距是每个列之间的空间。这是一个 12 列网格布局:

列(粉色)和间距(蓝色)。

要使用网格系统,您可以将md-columns附加到任何给定标签的类中。例如,<button class="md-2">创建一个宽度为两列的按钮。

要查看您的网站在不同尺寸下的效果,您可以使用 Google Chrome 开发者工具(F12然后CTRL + Shift + M)或material.io/resizer/。请注意,如果您尝试分析的网站将X-Frame-Options设置为DENYmaterial.io将会静默失败。

材料图标

让我们从材料图标开始我们的 Material Design 之旅。材料图标是图标字体,已经创建为在任何分辨率和设备(Web、Android 和 iOS 都得到了官方支持)上工作。

图标传达特殊含义,开发人员倾向于使用相同的图标来传达相同的事物。因此,用户更容易在您的应用程序中找到他们的方式。

有数百个图标可供您使用,每天都会添加新的图标。

以下是一些示例:

折叠图标。

您可以在material.io/icons/上看到所有图标。

由于材料图标是 Material Design 的可选部分(也就是说,您可以使用 Material Design 设计应用程序,例如,使用字体 awesome 图标甚至自定义图标),因此还有另一行代码需要添加到您的代码中。在您的src/index.html文件中,在head部分中添加以下内容:

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" 
    rel="stylesheet"> 

最终的src/index.html将如下所示:

<!doctype html>
<html>
<head>
 <meta charset="utf-8">
 <title>Chap10</title>
 <base href="/">

 <meta name="viewport" content="width=device-width, initial-scale=1">
 <link rel="icon" type="image/x-icon" href="favicon.ico">
 <link href="https://fonts.googleapis.com/icon?family=Material+Icons" 
    rel="stylesheet">
</head>
<body>
 <app-root>Loading...</app-root>
</body>
</html> 

现在,为了查看导入是否成功,我们将在自动生成的应用组件中添加一个图标。在 src/app/app.component.html 中,添加以下内容 <i class="material-icons">cast_connected</i>,使其看起来像这样:

<h1>
 {{title}}

 <i class="material-icons">cast_connected</i>
</h1> 

您的浏览器应该刷新 http://localhost:4200/ 页面并显示 cast_connected 图标:

Cast connected 图标。

正如您所看到的,使用 Material 图标非常简单。第一步是在material.io/icons/上识别您想要使用的一个图标的名称,然后创建一个带有 class="material-icons" 属性的 <i></i> 标签,最后包含您想要的图标名称。以下是一些例子:

  • <i class="material-icons">cast_connected</i>

  • <i class="material-icons">gamepad</i>

  • <i class="material-icons">dock</i>

  • <i class="material-icons">mouse</i>

按钮

除了图标之外,与 Material Design 一起使用的最简单的指令之一是按钮指令。我们可以有一个扁平的、凸起的、圆形的按钮,并且有三种不同的预设颜色:primary、accent 和 warn。以下是一个包含模板的组件,尝试一些可能的组合:


 @Component({
  selector: 'buttons',
  template: `
    <button md-button>FLAT</button>
    <button md-raised-button>RAISED</button> 
    <button md-fab>
        <md-icon>add</md-icon>
    </button>
    <button md-mini-fab>
        <md-icon>add</md-icon>
    </button>
    <button md-raised-button color="primary">PRIMARY</button>
    <button md-raised-button color="accent">ACCENT</button>
    <button md-raised-button color="warn">WARN</button>
  `
 })
 export class ButtonsComponent {
  constructor() { }
 }

结果如下所示:

接下来是:

Primary、Accent 和 Warn 颜色要么在您的 style.scss 中定义为 SCCS 变量,要么在默认的 Material Design 主题中定义,如果您没有覆盖它们。

菜单

在这一部分,我们将对“菜单”指令感兴趣。以下组件创建了一个包含四个元素的菜单。第四个元素被禁用(也就是说,我们无法点击它):

@Component({
 selector: 'menu',
 template: `
 <md-menu>
     <button md-menu-item> Refresh </button>
     <button md-menu-item> Settings </button>
     <button md-menu-item> Help </button>
     <button md-menu-item disabled> Sign Out </button>
 </md-menu>
 `
})
export class MenuComponent {
 constructor() { }
} 

当菜单关闭时,它看起来是这样的:

菜单关闭。

并且在用户点击后打开的版本显示在以下截图中:

菜单已打开。

工具栏

Angular Material Design 的工具栏组件应该按以下方式使用:

<md-toolbar>
 One good looking toolbar
</md-toolbar> 

这将产生以下结果:

基本工具栏。

此外,您可以使用 Angular 的 [color]="primary" | "accent" | "warn" 属性。此外,工具栏可以通过使用 <md-toolbar-row> 标记包含行。

<md-toolbar [color]="accent">
  One good looking toolbar
</md-toolbar>
<md-toolbar [color]="warn">
  <span>First Row</span>

  <md-toolbar-row>
    <span>Second Row</span>
  </md-toolbar-row>

  <md-toolbar-row>
    <span>Third Row</span>
  </md-toolbar-row>
</md-toolbar>
<md-toolbar [color]="primary">
  Another good looking toolbar
</md-toolbar> 

以下将产生三个不同的工具栏,相互叠放。第二个工具栏将由三行组成。

对话框

根据谷歌的定义:对话框通知用户特定任务的信息,可能包含关键信息,需要决策,或涉及多个任务。在 Angular 中使用对话框时,有以下方法:

  • open(component: ComponentType<T>, config: MdDialogConfig): MdDialogRef<T>,创建并打开一个新的对话框,供用户进行交互

  • closeAll(): 用于关闭对话框的 void

然后,对话框本身可以使用四个不同的指令:

  • md-dialog-title将包含对话框的标题,如下所示:<md-dialog-title>我的对话框标题</md-dialog-title>

  • md-dialog-content包含对话框的内容。

例如:<md-dialog-content>我的对话框内容</md-dialog-title>

  • md-dialog-close要添加到按钮中(<button md-dialog-close>关闭</button>)。它使按钮关闭对话框本身。

  • md-dialog-actions用于设置对话框的不同操作,即关闭、放弃、同意等。

在下面的示例中,我们首先有一个草稿组件。草稿组件有一个简单的模板,只包含一个按钮。按钮的click事件调用openDialog方法。对于组件本身的定义,我们有一个接收名为dialogMdDialog的构造函数。openDialog方法有两个回调--一个用于实际打开对话框,另一个用于在对话框关闭时打印包含在result:字符串中的result变量:

@Component({
 selector: 'draft-component',
 template: `
 <button type="button" (click)="openDialog()">Open dialog</button>
 `
})
export class DraftComponent {

 dialogRef: MdDialogRef<DraftDialog>;

 constructor(public dialog: MdDialog) { }

 openDialog() {
   this.dialogRef = this.dialog.open(DraftDialog, {
     disableClose: false
   });

   this.dialogRef.afterClosed().subscribe(result => {
     console.log('result: ' + result);
     this.dialogRef = null;
   });
 }
} 

正如您所看到的,DraftComponent组件的dialogRef属性是通用的。更具体地说,它是DraftDialog类的通用实例。让我们来定义它:

@Component({
 selector: 'draft-dialog',
 template: `
 <md-dialog-content>
   Discard Draft?
 </md-dialog-content>
 <md-dialog-actions>
   <button (click)="dialogRef.close('can
cel')">Cancel</button>
   <button md-dialog-close>Discard</button>
 </md-dialog-actions>
 `
})
export class DraftDialog {
 constructor(public dialogRef: MdDialogRef<DraftDialog>) { }
} 

再次强调,这是一个简单的类。在这里,我们可以看到模板包含了四个可能的指令中的三个。的确,我使用了<md-dialog-content>来定义要显示的对话框内容,<md-dialog-actions>来为对话框的操作按钮提供专用空间,最后,使用md-dialog-close来使“放弃”按钮关闭我的对话框。组件本身只有一个构造函数,定义了public属性:MdDialogRef<DraftDialog>

使用此对话框的最后一步是在我们的NgModule中引用它,就像这样:

@NgModule({
 declarations: [
   ...,
   DraftDialog
 ],
 entryComponents: [
   ...,
   DraftDialog
 ],
 ...
})
export class AppModule { } 

当我们按下按钮时,这是对话框的图像:

草稿对话框。

侧边导航抽屉

侧边导航抽屉在移动设备上非常受欢迎。然而,它们开始出现在完整版本的网站中;因此它们在本章中有所涉及。

侧边导航抽屉可以是这样的:

侧边导航抽屉。

在左侧的浅灰色中,我们有导航抽屉,在调用时会弹出我们的内容。在较深的灰色中,我们有页面的内容。

使用以下组件,我们可以重现本节开头显示的侧边导航:

@Component({
 selector: 'sidenav',
 template: `
   <md-sidenav-container>
   <md-sidenav #side (open)="closeButton.focus()">
      Side Navigation.
     <br>
     <button md-button #closeButton      
         (click)="side.close()">Close</button>
   </md-sidenav>

   My regular content. This will be moved into the proper DOM at 
       runtime.
   <button md-button (click)="side.open()">Open side sidenav</button>

 </md-sidenav-container>
 `
})
export class SideNavComponent {
 constructor() { }
} 

这里唯一有趣的是模板。让我们来分解一下。首先,我们有封闭的<md-sidenav-container>标签,它允许我们为内容定义两个单独的区域。这两个区域分别是md-sidenav和我们页面的实际内容。虽然md-sidenav标签清楚地定义了内容的sidenav部分,但我们页面的其余内容(即实际页面)没有被包含在任何特殊的标签中。页面内容只需在md-sidenav定义之外。我们使用#side属性引用md-sidenav块。作为提醒,向任何 Angular 指令添加#myName会给你一个引用,以便在模板的其余部分中访问它。md-sidenav有一个打开方法,将焦点放在其内部定义的#closeButton上。这个按钮有一个click方法,调用#sideclose方法。最后,在页面内容中,我们有一个按钮,当点击时调用#side.open。除了这两个方法(openclose),md-sidenav指令还有一个toggle方法,用于切换sidenav(即opened = !opened)。

主题化

现在,我们可以描述 Angular Material Design 中每个可用组件。然而,它们有很多,它们的用途都不复杂。在我撰写本章时,以下是支持的指令列表:

  • 按钮

  • 卡片

  • 复选框

  • 单选按钮

  • 输入

  • 侧边栏

  • 工具栏

  • 列表

  • 网格

  • 图标

  • 进度

  • 选项卡

  • 滑动

  • 滑块

  • 菜单

  • 工具提示

  • 涟漪

  • 对话框

  • 消息框

在接下来的几个月里,将会添加更多的指令。你可以在这里找到它们:github.com/angular/material2

不用说,我们在指令方面已经覆盖了。尽管有如此广泛的可能性,我们可以通过创建自定义主题进一步定制 Angular 的 Material Design。在 Angular Material 中,主题是通过组合多个调色板创建的。特别是,主题包括:

  • 主要调色板由在所有屏幕和组件上广泛使用的颜色组成

  • 强调调色板由用于浮动操作按钮和交互元素的颜色组成

  • 警告调色板由用于传达错误状态的颜色组成

  • 前景调色板由用于文本和图标的颜色组成

  • 背景调色板由用于元素背景的颜色组成

以下是一个自定义主题的示例:

//src/styles.scss

@import '~https://fonts.googleapis.com/icon?family=Material+Icons';
@import '~@angular/material/core/theming/all-theme';
// Plus imports for other components in your app.

// Include the base styles for Angular Material core. We include this here so that you only
// have to load a single css file for Angular Material in your app.
@include md-core();

// Define the palettes for your theme using the Material Design 
   palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a 
  default, lighter, and darker
// hue.
  $candy-app-primary: md-palette($md-indigo);
  $candy-app-accent:  md-palette($md-pink, A200, A100, A400);

// The warn palette is optional (defaults to red).
   $candy-app-warn:    md-palette($md-red);

// Create the theme object (a Sass map containing all of the palettes).
  $candy-app-theme: md-light-theme($candy-app-primary, $candy-app-  
   accent, $candy-app-warn);

// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each 
   component
// that you are using.
@include angular-material-theme($candy-app-theme); 

因此,我们已经学会了为 Material Design 创建自定义主题。

总结

在本章中,我们通过使用 Angular/Material2 模块了解了 Material Design 和响应式设计。我们看到了一些最常用的指令,如buttonsiconsdialogssidenav。此外,我们还利用了 Angular/Material2 的主题能力来定制 Material Design。

在第十五章中,将 Bootstrap 与 Angular 应用程序集成,我们将看到如何通过使用 Bootstrap(由 Twitter 提供)而不是 Material Design(由 Google 提供)来驱动我们的 Angular2 应用程序的设计。

第十章:实现 Angular 管道

在本章中,您将学习关于 Angular 管道。将 Angular 管道视为过滤器的现代化版本,包括帮助我们在模板中格式化值的函数。Angular 中的管道基本上是 Angular v1 中过滤器的扩展。我们可以在模板中轻松使用许多有用的内置管道。您将学习内置管道,我们还将创建自定义用户定义的管道。

在本章结束时,您将学习并实现以下内容:

  • 介绍 Angular 管道

  • 定义和实现管道

  • 了解各种内置管道

  • DatePipe

  • DecimalPipe

  • CurrencyPipe

  • LowerCasePipe 和 UpperCasePipe

  • JSON 管道

  • SlicePipe

  • async 管道

  • 学习实现自定义用户定义的管道

  • 为管道参数化

  • 链接管道

  • 了解纯管道和不纯管道

Angular 管道-概述

管道允许我们在模板视图中显示值之前格式化值。例如,在大多数现代应用程序中,我们希望显示诸如今天、明天等术语,而不是系统日期格式,例如 2017 年 4 月 13 日 08:00。让我们看看更多现实世界的场景。

您希望应用程序中的提示文本始终为小写吗?没问题;定义并使用LowercasePipe。在天气应用程序中,如果您希望显示月份名称为 MAR 或 APR 而不是其全名,请使用DatePipe

很酷,对吧?你明白了。管道帮助您添加业务规则,因此您可以在模板中实际显示数据之前转换数据。

与 Angular 1.x 过滤器建立联系的一个好方法是通过 Angular 管道,但管道不仅仅是过滤。

我们已经使用了 Angular 路由器来定义路由路径,因此我们在一个页面中拥有所有管道的功能;您可以在相同或不同的应用程序中创建它。随意发挥您的创造力。

在 Angular 1.x 中,我们有过滤器--管道是过滤器的替代品。

在下一节中,您将学习如何定义和使用 Angular 管道。

定义管道

管道运算符用管道符号(|)定义,后跟管道的名称:

{{ appvalue  | pipename }}

以下是一个简单的lowercase管道的示例:

{{"Sridhar Rao"  |  lowercase}} 

在上述代码中,我们使用lowercase管道将文本转换为小写。

现在,让我们编写一个使用lowercase管道示例的示例Component

@Component({
 selector: 'demo-pipe',
 template: `
 Author name is {{authorName | lowercase}}
 `
})
export class DemoPipeComponent {
 authorName = 'Sridhar Rao';
}

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

  • 我们定义了一个DemoPipeComponent组件类

  • 我们创建了一个字符串变量authorName,并赋予了值'Sridhar Rao'

  • 在模板视图中,我们显示了authorName;然而,在将其打印到 UI 之前,我们使用了lowercase管道进行转换

运行上述代码,您应该看到以下输出:

干得好!在前面的例子中,我们使用了内置管道。在接下来的部分,您将学习更多关于内置管道,并创建一些自定义管道。

请注意,管道运算符仅在模板中起作用,而不在控制器内部。

内置管道

Angular 管道是 Angular 1.x 过滤器的现代化版本。Angular 带有许多预定义的内置管道。我们可以直接在视图中使用它们,并在运行时转换数据。

以下是 Angular 内置支持的所有管道的列表:

  • DatePipe

  • DecimalPipe

  • CurrencyPipe

  • LowercasePipe 和 UppercasePipe

  • JSON 管道

  • SlicePipe

  • 异步管道

在接下来的部分,让我们实现并学习更多关于各种管道,并看到它们的实际应用。

DatePipe

DatePipe,顾名思义,允许我们格式化或转换与日期相关的值。DatePipe 也可以根据运行时传递的参数以不同格式转换值。

一般语法如下代码片段所示:

{{today | date}} // prints today's date and time
{{ today | date:'MM-dd-yyyy' }} //prints only Month days and year
{{ today | date:'medium' }} 
{{ today | date:'shortTime' }} // prints short format

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

  • 如前一节所述,一般语法是变量后跟着一个(|)管道运算符,然后是管道运算符的名称

  • 我们使用 DatePipe 来转换today变量

  • 此外,在前面的例子中,您会注意到我们向管道运算符传递了一些参数;我们将在下一节中介绍向管道传递参数

现在,让我们创建一个完整的DatePipe组件示例;以下是实现DatePipe组件的代码片段:

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

@Component({
 template: `
 <h5>Built-In Pipes</h5>
 <ol>
 <li>
 <strong class="packtHeading">DatePipe example 1</strong>
 <p>Today is {{today | date}}
 </li>
 <li>
 <strong class="packtHeading">DatePipe example 2</strong>
 <p>{{ today | date:'MM-dd-yyyy' }} 
 <p>{{ today | date:'medium' }}
 <p>{{ today | date:'shortTime' }} 
 </li>
 </ol>
 `,
})
export class PipeComponent {
 today = new Date();
}

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

  1. 我们创建了一个PipeComponent组件类。

  2. 我们定义了一个today变量。

  3. 在视图中,我们根据不同的参数将变量的值转换为各种表达式。

现在运行应用程序,我们应该看到以下输出:

您在本节中学习了DatePipe。在接下来的部分,您将继续学习和实现其他内置管道,并创建一些自定义用户定义的管道。

DecimalPipe

在本节中,您将了解另一个内置管道--DecimalPipe。

DecimalPipe 允许我们根据区域规则格式化数字。 DecimalPipe 也可以用于以不同格式转换数字。

一般的语法如下:

appExpression | number [:digitInfo]

在上述代码片段中,我们使用了数字管道,可以选择性地传递参数。

让我们看看如何创建一个实现小数点的DatePipe,以下是相同的示例代码:

import { Component } from '@angular/core';
@Component({
 template: `
  <h5>Built-In Pipes</h5>
 <ol>
<li>
<strong class="packtHeading">DecimalPipe example</strong>
 <p>state_tax (.5-5): {{state_tax | number:'.5-5'}}</p>
 <p>state_tax (2.10-10): {{state_tax | number:'2.3-3'}}</p>
 </li>
 </ol>
 `,
})
export class PipeComponent {
 state_tax: number = 5.1445;
}

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

  1. 我们定义了一个组件类,即PipeComponent

  2. 我们定义了一个state_tax变量。

  3. 然后我们在视图中转换了state_tax

  4. 第一个管道操作符告诉表达式将小数打印到小数点后五位。

  5. 第二个管道操作符告诉表达式将值打印到小数点后三位。

上述管道组件示例的输出如下:

毫无疑问,数字管道是各种应用程序中最有用和常用的管道之一。我们可以转换数字值,特别是处理小数和浮点数。

CurrencyPipe

对于希望迎合多国地理位置的应用程序,我们需要显示特定国家的代码及其相应的货币值--这就是CurrencyPipe派上用场的地方。

CurrencyPipe操作符用于在数字值前附加国家代码或货币符号。

看一下实现CurrencyPipe操作符的代码片段:

{{ value | currency:'USD' }}

Expenses in INR: {{ expenses | currency:'INR' }}

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

  1. 第一行代码显示了编写CurrencyPipe的一般语法。

  2. 第二行显示了货币的语法,我们用它来转换expenses的值,并在其后附加了印度货币符号。

现在我们知道如何使用CurrencyPipe操作符,让我们组合一个示例来显示多种货币国家格式;以下是实现CurrencyPipe操作符的完整组件类:

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

@Component({
 selector: 'currency-pipe',
 template: `
 <h5>CurrencyPipe Example</h5>
 <ol>
 <li>
 <p>Salary in USD: {{ salary | currency:'USD':true }}</p>
 <p>Expenses in INR: {{ expenses | currency:'INR':false }}</p>
 </li>
 </ol>
 `
})
export class CurrencyPipeComponent {
 salary: number = 2500;
 expenses: number = 1500;
}

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

  1. 我们创建了一个组件类CurrencyPipeComponent,并声明了几个变量,即salaryexpenses

  2. 在组件模板中,我们通过添加国家货币详情来转换变量的显示。

  3. 在第一个管道操作符中,我们使用了'currency: USD',这将在变量之前附加($)美元符号。

  4. 在第二个管道操作符中,我们使用了'currency : 'INR':false',这将添加货币代码,false将告诉它不要打印符号。

现在,启动应用程序,我们应该看到以下输出:

在本节中,我们讨论并实现了CurrencyPipe。在接下来的几节中,我们将继续探索和学习其他内置管道以及更多内容。

LowerCasePipe 和 UpperCasePipe

LowerCasePipe 和 UpperCasePipe,顾名思义,分别用于将文本转换为小写和大写。

看一下以下代码片段:

Author is Lowercase {{authorName | lowercase }}
Author in Uppercase is {{authorName | uppercase }}

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

  1. 第一行代码使用lowercase管道将authorName的值转换为小写。

  2. 第二行代码使用uppercase管道将authorName的值转换为大写。

现在我们已经看到如何定义小写和大写管道,是时候创建一个完整的组件示例了,该示例实现了管道以显示作者姓名的小写和大写形式。

看一下以下代码片段:

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

@Component({
 selector: 'textcase-pipe',
 template: `
 <h5>Built-In LowercasPipe and UppercasePipe</h5>
 <ol>
 <li>
 <strong>LowercasePipe example</strong>
 <p>Author in lowercase is {{authorName | lowercase}}
 </li>
 <li>
 <strong>UpperCasePipe example</strong>
 <p>Author in uppercase is {{authorName | uppercase}}
 </li>
 </ol>
 `
})
export class TextCasePipeComponent {
 authorName = "Sridhar Rao";
}

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

  1. 我们创建了一个组件类,TextCasePipeComponent,并定义了一个authorName变量。

  2. 在组件视图中,我们使用了lowercaseuppercase管道。

  3. 第一个管道将变量的值转换为小写文本。

  4. 第二个管道将变量的值转换为大写文本。

运行应用程序,我们应该看到以下输出:

在本节中,您学会了如何使用lowercaseuppercase管道来转换值。

JSON Pipe

类似于 Angular 1.x 中的 JSON 过滤器,我们有 JSON 管道,它可以帮助我们将字符串转换为 JSON 格式的字符串。

在小写或大写管道中,我们转换了字符串;使用 JSON 管道,我们可以将字符串转换并显示为 JSON 格式的字符串。

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

<pre>{{ myObj | json }}</pre>

现在,让我们使用前面的语法并创建一个完整的Component示例,其中使用了 JSON Pipe:

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

@Component({ 
 template: `
 <h5>Author Page</h5>
 <pre>{{ authorObj | json }}</pre>
 `
})
export class JSONPipeComponent {
 authorObj: any; 
 constructor() {
 this.authorObj = {
 name: 'Sridhar Rao',
 website: 'http://packtpub.com',
 Books: 'Mastering Angular2'
 };
 }
}

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

  1. 我们创建了一个组件类,JSONPipeComponentauthorObj,并将 JSON 字符串赋给了这个变量。

  2. 在组件模板视图中,我们转换并显示了 JSON 字符串。

运行应用程序,我们应该看到以下输出:

JSON 很快就成为了 Web 应用程序之间集成服务和客户端技术的事实标准。因此,每当我们需要将值转换为视图中的 JSON 结构时,JSON 管道都非常方便。

SlicePipe

SlicePipe 与数组切片 JavaScript 函数非常相似。Slice 管道从字符串中提取两个指定索引之间的字符,并返回新的子字符串。

定义 SlicePipe 的一般语法如下:

{{email_id | slice:0:4 }}

在前面的代码片段中,我们正在对电子邮件地址进行切片,以仅显示变量值email_id的前四个字符。

既然我们知道如何使用 SlicePipe,让我们在组件中将其放在一起。

以下是实现 SlicePipe 的完整代码片段:

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

@Component({
 selector: 'slice-pipe',
 template: `
 <h5>Built-In Slice Pipe</h5>
 <ol>
 <li>
 <strong>Original string</strong>
 <p> Email Id is {{ emailAddress }}
 </li>
 <li>
 <strong>SlicePipe example</strong>
 <p>Sliced Email Id is {{emailAddress | slice : 0: 4}}
 </li>
 </ol>
 `
})
export class SlicePipeComponent {
 emailAddress = "test@packtpub.com";
}

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

  1. 我们创建了一个SlicePipeComponent类。

  2. 我们定义了一个字符串变量emailAddress并为其赋值test@packtpub.com

  3. 然后,我们将 SlicePipe 应用于{{emailAddress | slice : 0: 4}}变量。

  4. 我们从0位置开始获取子字符串,并从变量值emailAddress中获取4个字符。

运行应用程序,我们应该看到以下输出:

SlicePipe 肯定是一个非常有用的内置管道,特别是处理字符串或子字符串。

异步管道

异步管道允许我们直接将 promise 或 observable 映射到我们的模板视图中。为了更好地理解异步管道,让我先介绍一下 observable。

Observables 是 Angular 可注入的服务,可用于将数据流式传输到应用程序中的多个部分。在下面的代码片段中,我们使用async管道作为一个 promise 来解析返回的作者列表:

<ul id="author-list">
 <li *ngFor="let author of authors | async" >
 <!-- loop the object here -->
 </li>
</ul>

async管道现在订阅Observable(作者)并检索最后一个值。

让我们看一下如何使用async管道作为PromiseObservable的示例。

在我们的app.component.ts文件中添加以下代码行:

 getAuthorDetails(): Observable<Author[]> {
  return this.http.get(this.url).map((res: Response) => res.json());
 }

 getAuthorList(): Promise<Author[]> {
  return this.http.get(this.url).toPromise().then((res: Response) => 
   res.json());
 }

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

  1. 我们创建了一个getAuthorDetails方法,并附加了一个相同的 observable。该方法将返回来自url的响应,这是一个 JSON 输出。

  2. getAuthorList方法中,我们绑定了一个需要在通过http请求调用的url返回的输出中解析或拒绝的 promise。

在本节中,我们已经看到了async管道的工作原理。您会发现它与处理服务非常相似。我们可以映射一个 promise 或一个 observable,并将结果映射到模板上。

参数化管道

管道也可以带参数。我们可以在管道后面传递参数。参数在管道后用冒号符号(:)分隔:

{{appValue  |  Pipe1: parameter1: parameter2 }}

让我们快速构建一个简单的管道示例,看看它的运行情况。以下是带有MM-dd-yyyy参数的DatePipe的示例:

{{today | date:'MM-dd-yyyy' }} 

另一个带参数的管道示例如下:

{{salary | currency:'USD':true}}

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

  1. 我们向CurrencyPipe传递了 USD 作为参数,这将告诉管道显示货币代码,例如美元的USD和欧元的EUR

  2. true参数表示显示货币符号($)。默认情况下,它设置为 false。

让我们通过组件的完整代码来看它们的运行情况:

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

@Component({
 template: `
 <h5>Parametrizing pipes</h5>

 <p>Date with parameters {{ today | date:'MM-dd-yyyy' }} 
 <p>Salary in USD: {{salary | currency:'USD':true}}</p>
 `,
})
export class ParamPipeComponent {
 today = new Date();
 salary: number = 1200;
}

在前面的代码片段中,我们创建了一个ParamPipeComponent类,并定义了todaysalary变量。

Component模板视图中,我们为DatePipe传递了date:'MM-dd-yyyy'参数,为CurrencyPipe传递了currency:'USD' :true参数。

以下是前面代码的输出:

在前面的示例中,我们传递了自定义参数,如currencydate格式,给管道,并相应地查看了输出。

在大多数应用用例中,我们需要向管道传递参数,以根据业务逻辑转换值。在本节中,我们重点介绍了通过传递值来对管道进行参数化。

到目前为止,我们一直在使用内置管道并向管道传递参数。

在接下来的章节中,您将学习如何链接管道、创建自定义管道,以及向自定义用户定义的管道传递参数。

链式管道

我们可以将多个管道链接在一起。这在我们需要关联多个需要应用的管道,并且最终输出将被所有应用的管道转换的情况下特别有帮助。

工作流或链将被触发,并依次应用管道。链管道语法的示例如下:

{{today | date | uppercase | slice:0:4}}

我们在前面的代码中应用了两个链式管道。首先,DatePipe应用于today变量,然后立即应用uppercase管道。以下是ChainPipeComponent的整个代码片段:

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

@Component({
 template: `
 <h5>Chain Pipes</h5>
 <p>Month is {{today | date | uppercase | slice:0:4}}
 `,
})
export class ChainPipeComponent {
 today = new Date();
}

我们使用了 slice 来仅显示月份的前四个字符。以下截图显示了前面组件的输出:

应用链式管道时需要记住的一些关键事项如下:

  • 执行顺序是从左到右

  • 管道是依次应用的。

在本节中,您了解了如何在我们的应用程序中将多个管道链接在一起。在下一节中,您将详细了解如何创建自己的自定义管道。

创建自定义管道

到目前为止,一切都很好。管道确实给我们留下了深刻的印象,但等等,我们还可以用管道做更棒的事情。内置管道,正如您所见,非常有限且少。我们当然需要创建自己的自定义管道,以满足我们应用程序的功能。

在本节中,您将学习如何为我们的应用程序创建自定义管道。

在这个例子中,我们将构建一个管道,它将是一个税收计算器。我们传递产品的价格,并使用管道功能自动计算并显示销售税。神奇,对吧?

要创建自定义管道,我们需要执行以下步骤:

  1. 创建一个模板来应用到管道上(在我们的例子中,它是updateTaxPipe)。

  2. 创建一个管道文件,即update-tax.pipe.ts

  3. 每个管道文件都必须从 Angular 核心中导入管道。

  4. 定义管道元数据。

  5. 创建Component类。它应该有transform函数,其中包含管道应该执行的业务逻辑。

在下面的代码片段中,我们正在定义一个名为UpdateTaxPipe的自定义管道,它将接受一个percentage参数,并进行销售税计算并在我们的模板中显示:

{{ productPrice | UpdateTaxPipe: percentage }}

让我们创建我们的update-tax.pipe.ts文件:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
 name : "UpdateTaxPipe"
})

export class UpdateTaxPipe implements PipeTransform{
 transform(value:number, taxVal: number):number{
 return (value*taxVal)/100;
 }
}

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

  1. 为了告诉 Angular 这是一个管道,我们应用了@Pipe装饰器,它是从核心 Angular 库中导入的。

  2. 我们创建了一个自定义管道,名为UpdateTaxPipe,使用了name管道元数据。

  3. 我们创建了一个transform方法,这对于管道是必需的,并在方法内定义了我们的业务逻辑和规则。

  4. 我们向transform方法传递了两个参数,它返回了更新后的值。

无论我们是否包括接口 PipeTransform,Angular 都会寻找并执行transform方法。

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

在本节中,您学习了如何创建自定义管道。创建用户定义的管道非常简单和容易。自定义管道确实帮助我们轻松地集成应用程序的业务逻辑。

尝试创建自定义管道,可以适应一次编写,多次使用逻辑,也可以在许多组件视图中使用;例如,验证电话号码、地址等。

纯管道和非纯管道

管道还接受一个名为 Pure 的元数据。管道有两种状态:

  • 纯管道

  • 非纯管道

纯管道

纯管道只有在输入参数的值发生变化时才会执行。它不会记住或追踪任何先前的值或状态。Angular 内置管道都是pure管道。

到目前为止我们看到的所有管道示例都是纯管道。

非纯管道

无论值或参数是否改变,非纯管道都会在每次变更检测周期中调用。为了使用非纯管道,我们应该将管道修饰符pure设置为false

默认情况下,所有管道修饰符的pure都设置为true

将管道修饰符的值设置为pure将检查管道的输出,无论其值是否改变,都会保持更新管道提供的值。

定义非纯管道与创建任何自定义用户定义管道相同,唯一的区别在于在@Pipe修饰符中,我们将通过将值设置为false来明确指定管道为非纯的。

以下是通过将管道的值设置为 false 来定义非纯管道的语法:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'authorName'
  pure: false
})

在本节中,您了解了不同类型的 Angular 管道,即纯管道和非纯管道。只有在输入组件的值发生变化时才会调用纯管道。无论值是否改变,非纯管道都会在每次变更检测时调用。

摘要

在本章中,您了解了关于 Angular 管道的一切。Angular 管道在转换视图模板中的数据方面非常有用。Angular 管道是 Angular 1.x 中可用的过滤器的现代化版本。

我们可以在模板中轻松使用许多有用的内置管道操作符。在本章中,您了解了内置管道以及创建自定义用户定义管道。

在处理数字时,我们可以使用DatePipeDecimalPipeCurrencyPipe。在专门处理字符串时,我们可以始终使用SlicePipeLowercasePipeUppercasePipe

当我们主要处理服务器端响应或进行异步调用并处理响应时,我们可以使用JSONPipeasyncPipe。我们还涵盖了向管道传递参数,并根据应用程序的需要进行定制。

我们探讨了如何创建和实现自定义用户定义的管道,这些管道还可以接受参数,以根据我们应用程序的需求更好地定制它们。

所以继续,用管道转换你的视图。

在下一章中,您将学习如何实现 Angular 服务。您将学习有关服务和工厂的知识,创建 Angular 服务,使用服务从组件中访问数据以及创建异步服务。

第十一章:实现 Angular 服务

服务在任何 Angular 应用程序中都扮演着重要的角色。我们可以通过充分利用 Angular 中的许多内置服务来设计我们自己的 Angular 服务。在本章中,我们将讨论如何做到这一点,以便您了解如何创建和管理 Angular 服务。

在本章中,我们将涵盖以下主题:

  • 为什么要使用服务或工厂?

  • 创建服务

  • 使用服务从组件中访问数据

  • 创建异步服务

为什么要使用服务或工厂?

我们已经讨论了单向数据绑定、双向数据绑定以及组件之间的数据共享。我们可能已经定义了非常清晰的视图并实现了整洁的组件,但是业务逻辑和数据获取/存储逻辑必须存在于某个地方。构建出色的 Angular 应用程序来自于充分利用内置服务。Angular 框架包括帮助您进行网络、缓存、日志记录、承诺等方面的服务。

编写我们自己的服务或工厂有助于实现代码的可重用性,并使我们能够在应用程序块(如组件、指令等)之间共享特定于应用程序的逻辑。将特定于应用程序的逻辑组织到服务或工厂中会导致更清晰、更明确定义的组件,并帮助您以更易维护的代码组织项目。

在 AngularJS 中,我们为此目的实现服务或工厂。服务是在运行时使用 new 关键字调用的,比如构造函数。以下代码片段显示了服务实现的 AngularJS 代码:

function MovieService($http) {   
  this.getMovieList = function   getMovieList() {   
    return $http.get('/api/movies');   
  };   
}   
angular.module('moviedb').service('MovieService',   MovieService);   

MovieService函数可以注入到任何需要从 API 获取电影列表的控制器中。

在 Angular 中,可以使用工厂来实现相同的功能,并具有额外的功能。工厂是处理创建对象的一种设计模式。我们可以从工厂返回新的类、函数或闭包。与服务类似,工厂也可以注入到控制器中。以下代码片段显示了工厂实现的 AngularJS 代码:

function MovieService($http) {   
  return {   
    getMovieList: function() {   
         return $http.get('/api/movies');   
    }   
  };   
}   
angular.module('moviedb').factory('MovieService',   MovieService);   

服务和工厂都可以注入到控制器中,并且可以调用getMovieList函数,如下所示:

function MovieController(MovieService   service) {   
  service.getMovieList().then(function   (response) {   
      // manage response   
    });   
}   
angular.module('moviedb').controller('MovieController',   
        MovieController);   

虽然工厂是灵活的,但服务是使迁移到 ES6 更容易的最佳选择。使用服务时,ES5 中的构造函数可以在迁移到 ES6 的过程中顺利替换为 ES6 类。我们可以将MovieService服务重写为 ES6 如下:

class MovieService {
 getMovieList() {
  return $http.get('/api/movies');
 }
}
app.service('MovieService', MovieService);

服务是用户定义的类,用于解决特定目的,并可以注入到组件中。Angular 建议在组件中只包含与视图相关的代码,以丰富 Angular 应用程序中的 UI/UX。组件是服务的消费者,它们作为应用程序数据的来源和业务逻辑的库。保持组件清晰并注入服务使我们能够针对模拟服务测试组件:

创建一个服务

应用程序特定或业务逻辑函数,如持久化应用程序数据、记录错误、文件存储等,应该委托给服务,组件应该消费相应的服务来处理适当的业务或应用程序特定逻辑:

让我们创建一个简单的名为BookService的服务,处理获取源中可用的书籍集合。源可以是从 Web API 服务返回的数据或 JSON 文件。

首先,让我们创建一个Book模型来持久化领域对象值。下面显示了Book类的代码片段:

export class Book { 
  id: number; 
  title: string; 
  author: string; 
  publisher: string; 
} 

前面的代码片段显示了一个Book的 TypeScript 类,其中包括idtitleauthorpublisher等属性。现在让我们创建一个名为BookService的服务,处理与Book相关的操作:

import { Injectable } from '@angular/core';
import {Book} from './book';
@Injectable()
export class BookService {
  getBooks() {
  var books: Book[] = [
    { "id": 1, "title": "ASP.NET Web API Security Essentials", author:   
         "Rajesh Gunasundaram", publisher: "Packt Publishing" },
    { "id": 2, "title": "Learning Angular for .Net Developers", author: 
         "Rajesh Gunasundaram", publisher: "Packt Publishing" },
    { "id": 3, "title": "Mastering Angular", author: "Rajesh 
         Gunasundaram", publisher: "Packt Publishing" },
   ];
  return books;
  }
}

在这里,我们首先导入了Book模型类。然后,我们定义了BookService类,其中包含一个getBooks方法,返回书籍的集合。

现在我们需要一个组件来注入BookService并消费。让我们创建一个BookListComponent,通过调用BookServicegetBooks方法来检索书籍列表。以下代码片段显示了BookListComponent类:

import { Component, OnInit } from '@angular/core';
import { Book } from './book';
import { BookService } from './book.service';
@Component({
   selector: 'book-list',
   template: `
   <div *ngFor="let book of books">
   {{book.id}} - {{book.title}}<br/>
   Author: {{book.author}}<br/>
   Publication: {{book.publisher}} 
   </div>
    `,
   providers: [BookService]
  })
 export class BookListComponent implements OnInit {
   books: Array<Book>;
   constructor(private bookService: BookService) { }
   ngOnInit() {   
       this.books = this.bookService.getBooks();
      }
   }

在这里,我们首先从@angular/core中导入ComponentOnInit,然后导入Book模型类和BookService类。然后我们用@Component属性对BookListComponent类进行了注释,以及选择器和模板等元数据信息。BookListComponent类定义了一个Book数组的books变量和一个构造函数,它被注入了BookService。请注意,BookListComponent实现了OnInit生命周期钩子,并且它通过使用注入到构造函数中的BookService实例来调用BookService类的getBooks方法。getBooks返回的书籍列表被赋给了BookListComponent类的books变量。

现在让我们创建一个根组件AppComponent。将BookListComponent作为指令传递,并将BookService作为提供者。以下是AppComponent的代码片段:

import { Component } from '@angular/core';   
import { BookService } from './book.service';   

@Component({   
  selector: 'my-books',   
  template: '   
    <h2>Book Library</h2>   
  <book-list></book-list>   
  '   
})   
export class AppComponent { }   

在这里,我们首先从@angular/core中导入ComponentBookListComponentBookService。然后我们用@Component属性对AppComponent进行了注释,以及选择器和模板等元数据。请注意,模板中有一个特殊的 HTML 标签<book-list/>。在某个地方,我们需要指示 Angular 初始化BooklistComponent并相应地渲染视图。我们还需要告诉 Angular,AppComponent是根组件,通过引导它来实现这一点。我们可以通过为我们的 Angular 应用程序创建一个入口点来实现这一点。

创建一个名为AppModule的类,并用NgModule进行注释(app.module.ts)。这指示 Angular 模块,这个类是应用程序的入口点。这里给出了AppModule的代码片段:

import { NgModule }          from '@angular/core';   
import { BrowserModule }  from '@angular/platform-browser';   
import { AppComponent }   from './app.component';   
import { BookListComponent }  from './book-list.component';   

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

在这里,我们首先从 Angular 核心中导入NgModule。然后我们从 Angular 平台浏览器中导入BrowserModule,因为我们的应用程序在 Web 浏览器上运行。然后我们导入应用程序组件,比如AppComponent,它是一个引导根组件,以及BooklistComponent,导入并添加到声明中。请注意,AppModule被装饰为NgModule,同时具有元数据,如导入、声明和引导。

现在让我们创建一个index.html页面,其中包含以下代码片段:

<!DOCTYPE html>   
<html>   
  <head>   
    <base href="/">   
    <title>Book   Library</title>   
    <meta charset="UTF-8">   
    <meta name="viewport"   content="width=device-width, initial-
          scale=1">   
  </head>   
  <body>   
    <h1>TodoList Angular app for   Packt Publishing...</h1>

    <my-books>Loading...</my-books>   
  </body>   
</html>   

在这里,我们没有引用任何必要的库来自node_modules,因为它们将由 Webpack 加载。Webpack 是一个用于捆绑资源并从服务器提供给浏览器的工具。Webpack 是 systemJS 的推荐替代方案。

使用服务从组件中访问数据

随着 Angular 应用程序的发展,我们不断引入更多的组件,这些组件将处理应用程序的核心数据。因此,我们可能会写重复的代码来访问数据。然而,我们可以通过引入可重用的数据服务来避免编写冗余的代码。需要数据的组件可以注入数据服务,并且可以用来访问数据。通过这种方式,我们可以重用逻辑,编写更少的代码,并在设计组件时有更多的分离。

我们将使用 Angular 的HttpModule,它作为一个npm包进行发布。为了在我们的应用程序中使用HttpModule,我们需要从@Angular/http导入HttpModule,并且 HTTP 服务应该被注入到控制器或应用程序服务的构造函数中。

实施服务

应用程序可以在组件之间共享数据。考虑一个电影数据库应用程序,其中Movies列表或单个Movie对象将在组件之间共享。我们需要一个服务来在任何组件请求时提供Movies列表或单个Movie对象。

首先,让我们使用 Angular CLI 创建一个电影服务。在命令提示符中执行以下命令以生成movie.service的样板代码:

e:\Explore\packt\MovieDB>ng generate   service Movie   
installing service   
  create src\app\movie.service.spec.ts   
  create src\app\movie.service.ts   

e:\Explore\packt\MovieDB>   

在这里,Angular CLI 创建了两个文件,即movie.service.tsmovie.service.spec.ts。生成的movie.service.ts的样板代码如下所示:

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

@Injectable()   
export class MovieService {   

  constructor() { }   

}   

请注意,MovieService类被装饰为@Injectable属性,以便依赖注入来实例化并将此服务注入到任何需要它的组件中。我们通过从 Angular 核心导入它,使这个Injectable函数可用。

接下来,我们需要向生成的MovieService添加getMovies函数。将getMovies()函数引入到MovieService类中如下:

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

@Injectable()   
export class MovieService {   

  constructor() { }   
  getMovies(): void {}   
}   

请注意,我们现在将返回类型设置为 void,但是当我们进行进一步实现时,我们需要进行更改。

我们需要引入一个领域模型,Movie,来表示整个应用程序中的电影。让我们使用 Angular CLI 生成Movie类的样板代码如下:

e:\Explore\packt\MovieDB>ng generate   class Movie   
installing class   
  create src\app\movie.spec.ts   
  create src\app\movie.ts   

e:\Explore\packt\MovieDB>   

在这里,这个命令创建了两个文件,分别是movie.tsmovie.spec.ts。实际上,在领域模式下,我们可能不会编写任何测试方法来断言它,所以你可以安全地删除movie.spec.ts。生成的movie.ts的代码片段如下所示:

export class Movie {   
}   

让我们添加一些属性来使其代表电影的特征。代码如下所示:

export class Movie {   

   public constructor(   
         private _movie_id:number,   
         private _title: string,   
         private _phase: string,   
         private _category_name: string,   
         private _release_year: number,   
         private _running_time: number,   
         private _rating_name: string,   
         private _disc_format_name:   string,   
         private _number_discs: number,   
         private _viewing_format_name:   string,   
         private _aspect_ratio_name:   string,   
         private _status: string,   
         private _release_date: string,   
         private _budget: number,   
         private _gross: number,   
         private _time_stamp:Date){   
   }   

   public toString = () : string => {   

         return `Movie (movie_id:   ${this._movie_id},   
         title: ${this._title},   
         phase: ${this._phase},   
         category_name:   ${this._category_name},   
         release_year:   ${this._release_year},   
         running_time: ${this._running_time},   
         rating_name:   ${this._rating_name},   
         disc_format_name:   ${this._disc_format_name},   
          number_discs:   ${this._number_discs},   
         viewing_format_name:   ${this._viewing_format_name},   
         aspect_ratio_name: ${this._aspect_ratio_name},   
         status: ${this._status},   
         release_date:   ${this._release_date},   
         budget: ${this._budget},   
         gross: ${this._gross},   
         time_stamp:   ${this._time_stamp})`;   

   }   
}   

我们已经准备好领域模型。现在让我们更新MovieServicegetMovies()函数的返回类型如下:

getMovies(): Movie[] {   
    let movies: Movie[] = [   
          {   
               "movie_id" : 1,   
               "title" : "Iron   Man",   
               "phase" : "Phase   One: Avengers Assembled",   
               "category_name"   : "Action",   
               "release_year" :   2015,   
               "running_time" :   126,   
               "rating_name" : "PG-13",   
               "disc_format_name"   : "Blu-ray",   
               "number_discs" :   1,   
               "viewing_format_name"   : "Widescreen",   
               "aspect_ratio_name"   : " 2.35:1",   
               "status" : 1,   
               "release_date" :   "May 2, 2008",   
               "budget" : "140,000,000",   
               "gross" : "318,298,180",   
               "time_stamp" : "2015-05-03"   
         },   
          {   
               "movie_id" : 2,   
               "title" : "Spiderman",   
               "phase" : "Phase   One",   
               "category_name"   : "Action",   
               "release_year" :   2014,   
               "running_time" :   126,   
               "rating_name" : "PG-13",   
               "disc_format_name"   : "Blu-ray",   
               "number_discs" :   1,   
               "viewing_format_name"   : "Widescreen",   
               "aspect_ratio_name"   : " 2.35:1",   
               "status" : 1,   
               "release_date" :   "May 2, 2008",   
               "budget" : "140,000,000",   
               "gross" : "318,298,180",   
               "time_stamp" : "2015-05-03"   
         }   
        ];   
        return movies;   
  }   

MovieService的完整代码片段如下所示:

import { Injectable } from '@angular/core';   
import { Movie} from './movie';   

@Injectable()   
export class MovieService {   
  getMovies(): Movie[] {   
    let movies: Movie[] = [   
          {   
               "movie_id" : 1,   
               "title" : "Iron   Man",   
               "phase" : "Phase   One: Avengers Assembled",   
               "category_name"   : "Action",   
               "release_year" :   2015,   
               "running_time" :   126,   
               "rating_name" : "PG-13",   
               "disc_format_name"   : "Blu-ray",   
               "number_discs" :   1,   
               "viewing_format_name"   : "Widescreen",   
               "aspect_ratio_name"   : " 2.35:1",   
               "status" : 1,   
               "release_date" :   "May 2, 2008",   
               "budget" : "140,000,000",   
               "gross" : "318,298,180",   
               "time_stamp" : "2015-05-03"   
         },   
          {   
               "movie_id" : 2,   
               "title" : "Spiderman",   
               "phase" : "Phase   One",   
               "category_name"   : "Action",   
               "release_year" :   2014,   
               "running_time" :   126,   
               "rating_name" : "PG-13",   
               "disc_format_name"   : "Blu-ray",   
               "number_discs" :   1,   
               "viewing_format_name"   : "Widescreen",   
               "aspect_ratio_name"   : " 2.35:1",   
               "status" : 1,   
               "release_date" :   "May 2, 2008",   
               "budget" : "140,000,000",   
               "gross" : "318,298,180",   
               "time_stamp" : "2015-05-03"   
         }   
        ];   
        return movies;   
  }   
}   

在这里,getMovies()函数返回类型为Movie[]的电影集合。

消费服务

我们已经准备好消费MovieService。让我们在一个组件中消费它。使用 Angular CLI,我们将通过执行以下命令来创建一个组件:

e:\Explore\packt\MovieDB>ng generate   component movie   
installing component   
  create src\app\movie\movie.component.css   
  create   src\app\movie\movie.component.html   
  create   src\app\movie\movie.component.spec.ts   
  create src\app\movie\movie.component.ts   

e:\Explore\packt\MovieDB>   

这将创建四个文件,分别是movie.component.tsmovie.component.htmlmovie.component.cssmovie.component.spec.tsmovie.component.ts文件的代码片段如下所示:

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

@Component({   
  selector: 'app-movie',   
  templateUrl: './movie.component.html',   
  styleUrls: ['./movie.component.css']   
})   
export class MovieComponent implements   OnInit {   

  constructor() { }   

  ngOnInit() {   
  }   

}   

MovieComponent@Component装饰器修饰,以及元数据,比如选择器、templateUrlstyleUrlsMovieService将被挂钩在ngOnInit方法下。让我们继续修改MovieComponent来消费MovieService

首先,我们需要将MovieService导入到我们的组件MovieComponent中。这个导入语句为MovieComponent提供了对MovieService的引用。但是要消费MovieService,我们需要创建MovieService的实例。我们该如何做呢?在标准方式中,我们可以实例化MovieService如下:

let movieService = new MovieService();   

OnInit生命周期钩子方法中导入MovieService并实例化MovieService后的MovieComponent的代码片段如下所示:

import { Component, OnInit } from '@angular/core';   
import { MovieService } from './movie.service';   
import { Movie } from './movie';   

@Component({   
  selector: 'app-movie',   
  templateUrl: './movie.component.html',   
  styleUrls: ['./movie.component.css']   
})   
export class MovieComponent implements   OnInit {   
  movies : Movie[];   

  constructor() { }   

  ngOnInit() {   
    let movieService = new   MovieService();   
    this.movies =   movieService.getMovies();   
  }   

}   

在这里,当OnInit事件被触发时,MovieService被实例化,并且通过调用getMovies()函数来检索电影集合。电影列表将被分配给MovieComponentmovies属性,以便在模板中进一步使用。

创建一个异步服务

我们刚刚创建了一个名为MovieService的服务,它同步调用getMovies()方法来检索电影集合。由于我们正在消费外部来源,比如 Web API,来检索电影集合,我们的应用程序必须等待服务器响应电影列表,因为getMovies函数是同步的。

因此,我们需要实现一种异步机制来检索电影集合。通过这种方式,我们可以避免使我们的应用程序等待 Web API 响应电影集合。我们可以通过使用 Promise 来实现这一点。

什么是 Promise?

Promise是一个真诚的保证,表示将执行某个操作。当服务器响应结果时,它会回调一个函数。我们请求一个异步服务,并使用回调函数执行某些操作,服务会用结果或错误调用我们的回调函数。您可以在第七章中了解更多关于 Promise 的内容,使用可观察对象进行异步编程

在服务中使用 Promise

让我们更新MovieService中的getMovies函数,以返回一个已解决的Promise,如下所示:

getMovies(): Promise<Movie[]> {   
    let movies: Movie[] = [   
          {   
               "movie_id" : 1,   
               "title" : "Iron   Man",   
               "phase" : "Phase   One: Avengers Assembled",   
               "category_name"   : "Action",   
               "release_year" :   2015,   
               "running_time" :   126,   
               "rating_name" : "PG-13",   
               "disc_format_name"   : "Blu-ray",   
               "number_discs" :   1,   
               "viewing_format_name"   : "Widescreen",   
               "aspect_ratio_name"   : " 2.35:1",   
               "status" : 1,   
               "release_date" :   "May 2, 2008",   
               "budget" : "140,000,000",   
               "gross" : "318,298,180",   
               "time_stamp" : "2015-05-03"   
         },   
          {   
               "movie_id" : 2,   
               "title" : "Spiderman",   
               "phase" : "Phase   One",   
               "category_name"   : "Action",   
               "release_year" :   2014,   
               "running_time" :   126,   
               "rating_name" : "PG-13",   
               "disc_format_name"   : "Blu-ray",   
               "number_discs" :   1,   
               "viewing_format_name"   : "Widescreen",   
               "aspect_ratio_name"   : " 2.35:1",   
               "status" : 1,   
               "release_date" :   "May 2, 2008",   
               "budget" : "140,000,000",   
               "gross" : "318,298,180",   
               "time_stamp" : "2015-05-03"   
         }   
        ];   
  return Promise.resolve(movies);   
}   

请注意,我们从getMovies函数中返回电影集合作为已解决的Promise。现在我们需要修改将电影集合分配给MovieComponent中的 movies 属性的代码。

MovieComponent中的现有代码将Promise分配给movies属性,而不是电影集合,因为MovieService中的getMovies现在返回已解决的Promise。因此,让我们修改ngOnInit事件的代码如下:

ngOnInit() {   
    let movieService = new   MovieService();   
    movieService.getMovies().then(movies   => this.movies = movies);   
}   

我们将我们的回调函数提供给Promisethen方法,所以getMovies中的链式函数then有命令将从 Web API 返回的电影集合分配给MovieComponent的属性this.movies

在这里,应用程序不会等待MovieService返回电影集合。movies属性从回调函数中获取分配的电影列表。

摘要

很酷!这就是本章的结束。我们了解了在应用程序中实现服务的重要性和优势。我们还学习了如何在组件中使用服务。

然而,直接实例化MovieService是一个不好的方法。组件不需要知道如何实例化服务;它们的唯一目的是知道如何使用服务。服务还使组件能够与MovieServices的类型和它们的实例化方式紧密耦合。这是不可接受的;组件应尽可能松散耦合。

在下一章中,我们将讨论使用依赖注入将服务注入到组件中,这样我们就可以拥有松散耦合的组件。

第十二章:应用依赖注入

在本章中,您将学习关于 Angular 依赖注入。依赖注入是 Angular 中最引人注目的特性之一;它允许我们创建可注入对象,可以在各种组件之间作为共享资源使用。

在本章中,我们将讨论以下内容:

  • 探索依赖注入

  • 详细了解提供者类

  • 了解分层依赖注入

  • 创建可注入对象

  • 学习将提供者注入到服务中

  • 学习将提供者注入到组件中

  • 学习为提供者类解析依赖项

  • 使用@InjectprovideuseValue装饰器创建示例

没有依赖注入的应用程序

如果没有依赖注入框架,开发人员的生活将非常艰难。看看不使用依赖注入的以下缺点:

  • 每次需要传递构造函数参数时,我们都需要编辑类的构造函数定义

  • 我们需要创建构造函数,并单独注入每个所需的依赖类

让我们看一个没有依赖注入的应用程序,以了解其中的挑战和不足之处:

class products {
 available;
 category;

 constructor() {
  this.available = new warehouse();
  this.category = new category();
 }
}

让我们分析前面的代码片段以更好地理解:

  1. 我们创建了一个名为productsclass

  2. constructor方法中,我们实例化了依赖类warehousecategory

  3. 请注意,如果warehousecategory类的构造函数定义发生更改,我们将需要手动更新所有类的实例。

由于作为开发人员,我们的任务是手动定义所有依赖项,因此前面的代码并不完全可测试和可维护。这就是 Angular 依赖注入的用武之地。

依赖注入 - 介绍

依赖注入DI)是一种编码模式,其中一个类接收依赖项而不是自己创建它们。一些开发人员和技术狂人也将其称为设计模式。

它被广泛使用,通常被称为 DI。我们将在所有章节中将依赖注入系统称为 DI。

以下是我们绝对需要 DI 的原因:

  • DI 是一种软件设计模式,其中一个类接收其依赖项而不是创建对象本身

  • DI 创建并提供动态所需的对象

  • 我们可以将可注入对象视为应用程序的可重用存储库

  • DI 允许远程开发团队独立开发依赖模块。

没有使用 DI,无法完全编写任何 Angular 应用程序。现在,让我们重新审视一下之前没有使用 DI 编写的代码,并使用 Angular DI 编写它:

class products {

constructor(private _warehouse: warehouse, private _category: category) {

  // use _warehouse and _category now as reference
 }
} 

在前面的代码中发生了什么:

  1. 我们创建了一个products类。

  2. constructor中,我们将依赖类--warehousecategory--作为参数传递。

  3. 我们现在可以在整个类中使用实例_warehouse_category

  4. 请注意,我们没有创建依赖类的对象;相反,我们只是通过 DI 系统接收它们。

  5. 我们不必担心warehousecategory所需的依赖关系;这将由 Angular DI 在内部解决。

现在我们知道了什么是 Angular DI,让我们专注于它是如何在我们的 Angular 应用程序中实现和使用的。在学习提供者类和更多内容之前,我们应该了解一些关于 Angular DI 框架的基本知识。

当然,我们将在接下来的几节中详细介绍这些。了解基本概念是很好的:

  1. @Injectable:这个装饰器标记一个类可供注入器实例化。

  2. @Inject:使用@Inject装饰器,我们可以将配置对象注入到任何需要它的构造函数中。

  3. Provider:提供者是我们注册需要注入的依赖项的方式。

现在让我们开始学习提供者类。

理解提供者类

要在我们的应用程序中开始使用 DI,我们需要了解提供者的概念。组件装饰器中的提供者配置告诉 Angular 需要提供哪些类给组件。

在提供者配置中,DI 接受一个类的数组,即我们要提供给组件的注入标记。我们还可以使用useClass指定要为注册的标记实例化的class

快速查看使用提供者配置的语法:

@Component({
 templateUrl: './calculate-tax.component.html',
 styleUrls: ['./calculate-tax.component.css'],
 providers: [MyTax]
})

在前面的代码中,我们告诉 Angular 前面的组件需要由MyTax类提供。

以下是使用提供者类的优点:

  • 提供者是每个注入器维护的

  • 每个provider提供一个 Injectable 的单个实例

  • 提供者类提供了调用方法的返回值

我们还可以明确提到应该从服务中使用的类。

这是一般的语法:

@Component({
 templateUrl: './calculate-tax.component.html',
 styleUrls: ['./calculate-tax.component.css'],
 providers: [
    { provide: MyTax, useClass: MyTax }
  ]
})

在前面的代码片段中,我们明确告诉 Angular 注入MyTax提供者并使用useClass配置使用MyTax类。

让我们更多地了解提供者类如何注册和使用;让我们看一下以下的图表:

让我们详细分析前面的图表,以了解关键要点:

  • 组件共享资源是通过提供者类提供的

  • 提供者类可以注册到多个组件中(一个或多个)

  • 我们还可以将提供者类注册到其他提供者类中

  • 在前面的图表中,组件#1依赖于提供者类#1

  • 在前面的图表中,组件#2依赖于提供者类#1提供者类#2

  • 在前面的图表中,组件#3依赖于提供者类#2提供者类#3

到目前为止,我们了解了 DI 对我们的应用程序有多么关键。DI 确实有助于组织数据,并且是实现独立模块或组件的最合适的设计模式。

这个想法是保持组件独立开发,并在提供者或可注入的地方编写更通用的共享或常用功能。

让我们快速创建一个提供者类的示例,它可以被注入到一个组件中。我们创建一个提供者类--MyTax.ts文件--并添加以下代码片段:

export class MyTax {
 public taxValue: string;
 constructor () {
     }

 getTaxes() {
  this.taxValue=Math.round(Math.random()*100);
  return this.taxValue; 
 }

}

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

  1. 我们创建了一个名为MyTax的提供者类。

  2. 我们将一个taxValue变量定义为数字。

  3. 我们创建了一个getTaxes方法,它将返回一个随机数。

  4. 我们给taxValue变量赋值,并通过getTaxes方法返回值。

现在,我们需要在我们组件的提供者数组配置中注册这个提供者类,并显示taxValue的值。

我们需要创建一个component类--calculate-tax.component.ts,并添加以下代码行:

import { Component } from '@angular/core';
import { MyTax } from './my-tax';

@Component({
 template: `<p>tax option: {{ taxName }}</p>`,
 styleUrls: ['./calculate-tax.component.css'],
 providers: [MyTax]
})
export class CalculateTaxComponent{

 public taxName: string;

 constructor( _myTax : MyTax) {
   this.taxName = _myTax.getTaxes();
 }

}

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

  1. 我们导入了最近创建的提供者类--MyTax

  2. 我们创建并定义了CalculateTax组件。

  3. 我们定义了一个taxName变量,并使用数据绑定在模板中映射了这个变量。

  4. 在构造函数中,我们在应用程序模块的提供者数组中注册了MyTax,Angular DI 将创建提供者类的实例并将其分配给_myTax

  5. 使用提供类的实例,我们调用了getTaxes方法。

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

在本节中,您学习了如何创建提供程序类并在组件中注册它们以供使用。您可以将相同的提供程序类注册到多个组件中;在我们想要共享多个可重用方法的情况下,这无疑是理想的。

在下一节中,您将学习有关分层 DI 的知识--当我们有多个嵌套组件时。

理解分层 DI

在前面的部分中,我们介绍了通过提供程序类进行 DI,还介绍了在各个独立组件之间共享提供程序类。在本节中,您将学习如何在分层组件之间使用带有 DI 的提供程序类。

Angular 在内部创建了一个索引树,跟踪所有组件和正在创建的树结构,并维护其依赖矩阵,该矩阵在实时加载以提供所有必要的模块、服务和组件。

分层组件和各种组件之间的 DI 最好的部分是,我们不必担心这些依赖项是如何创建的,或者它们自身内部需要什么依赖项。

概述-分层组件和 DI

Angular 内部维护组件的树结构是一个公开的秘密。它还维护依赖项的树索引。

在任何真实的 Angular 应用程序中,我们将使用许多组件和服务。这些组件将具有从根组件到子组件和内部子组件等的树结构。

这在内部形成了一个组件树结构。由于我们的组件也将具有依赖项和可注入项,Angular 将在内部形成一个依赖项树矩阵,以跟踪和解析服务或组件所需的所有依赖项。

以下是您需要了解有关分层 DI 的关键事项:

  • Angular 框架在内部为组件创建了一个分层树结构的 DI

  • 提供程序类需要注册到组件中

  • 我们可以将提供程序类注册到其他提供程序类中

在下一节中,您将创建可注入的服务,并在组件中使用它们。

创建可注入项

我们不必创建 Angular 注入器,它是默认注入的。Angular 在引导过程中创建了一个应用程序范围的注入器。

我们使用@Injectable装饰器定义可注入的类,并在类中定义方法。@Injectable使得一个类可以被注入器实例化。

以下是创建@Injectable服务的示例代码:

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

@Injectable()
 export class ListService {
  getList() { 
   console.log("Demo Injectable Service");
  }
}

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

  1. 我们从 Angular 核心模块中导入了Injectable

  2. 我们使用@Injectable装饰器告诉 Angular 以下类可以被注入,并且可以被注入器实例化。

  3. 我们创建了一个名为ListService的类。

  4. 我们实现了一个名为getList的方法,目前只是在console.log中打印一条消息。

注册提供者

注入器使用提供者创建依赖项。提供者需要在消费服务或组件中注册。通过注册它们,提供者类允许我们创建独立的可重用功能,可以由各个团队成员使用。

配置和注册提供者类还可以将功能分解为更小的模块,这样更容易维护和修改。我们可以以不同的方式将提供者类注册到服务和组件中。关于注入器,始终要牢记的重要点如下:

  • 我们必须在我们的NgModule、组件构造函数或指令中创建一个提供者

  • 在组件的构造函数中注册服务

我们在前面的部分创建了一个ListService服务,它有一个方法,现在可以被注册并在多个组件中使用:

让我们详细分析前面的图表,以了解我们正在构建的用例:

  1. 我们将创建一个@Injectable服务类ListService

  2. 我们将创建一个名为TestDiComponent的组件。

  3. 我们需要将ListService注册到TestDiComponent中。

那么,现在让我们立即开始学习如何在ngModule中注册提供者。看一下ngModule文件:

import { ListService } from "./shared/list.service";

@NgModule({
 providers: [
 {
  provide: ListService,
  useClass: ListService
 }
 ]
})

简而言之,上面的代码通常写成如下形式:

import { ListService } from "./shared/list.service";

@NgModule({
 providers: [
   ListService
 ]
})

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

  1. 我们已经将ListService服务类导入到ngModule中。

  2. 请注意,我们在提供者中注册了ListService。Angular 将在运行时内部解析并创建一个注入器。

  3. 在简写表示法中,我们只提到提供者的名称,Angular 将provide属性映射到useClass的值以进行注册和使用。

在前面的部分中,您学会了如何在ngModule的提供者配置数组中注册服务。

在 AppModule 中注册提供者与在组件中注册提供者的主要区别在于服务的可见性。在 AppModule 中注册的服务在整个应用程序中都可用,而在特定组件内注册的服务只在该组件内可用。

在组件内注册提供者

现在,您将学习如何在组件中注册提供者并在组件内使用可注入的服务类。

首先,让我们使用 Angular CLI 的ng命令快速生成一个组件和服务:

ng g component ./test-di

这将生成组件和所需的文件。命令的输出如下所示:

现在,我们必须在同一文件夹中生成一个 Angular 服务。

ng g service ./test-di

上述命令的输出如下:

我们看到 Angular CLI 生成了一个警告消息,指出服务已生成但未提供。

到目前为止,我们已经分别创建了组件和服务,但现在我们需要在组件中注册提供者,以便我们可以使用该服务。

在继续在组件中注册提供者之前,让我们快速查看一下 CLI 工具生成的服务代码。

这是我们的test-di.service.ts文件代码:

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

@Injectable()
 export class TestDiService {
  constructor() { }
}

这是由脚手架 Angular CLI 工具生成的默认代码。让我们添加一个我们想在组件内访问的方法:

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

@Injectable()

 export class TestDiService {
  getAuthors() {
  let Authors =[
   {name :"Sridhar"},
   {name: "Robin"},
   {name: "John"},
   {name: "Aditi"}
  ];
  return Authors;
 }
}

现在让我们在组件test-di.component.ts文件的 providers 数组中注册服务:

import { Component } from '@angular/core';
import { TestDiService } from './test-di.service';

@Component({
 selector: 'app-test-di',
 templateUrl: './test-di.component.html',
 styleUrls: ['./test-di.component.css'],
 providers: [TestDiService]
})

export class TestDiComponent{
 constructor(private _testDiService: TestDiService) {}
 authors = this._testDiService.getAuthors();
}

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

  1. 我们创建了一个名为TestDiComponent的组件。

  2. 我们将新创建的服务TestDiService导入到组件中。

  3. 我们在 providers 中注册了TestDiService,告诉 Angular 动态创建服务的实例。

  4. Angular DI 将创建一个我们在constructor中传递的_testDiService服务类的新private实例。

  5. 我们使用了_testDiService服务的实例,并调用了getAuthors方法来获取作者列表。

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

到目前为止,您已经学会了创建一个Injectable服务,将服务注册到组件装饰器内的提供者数组中,并使用服务的实例来调用方法,这很棒。

在本节中,您学会了如何使用相同的一组共享提供者类创建多个组件。

带有依赖关系的提供者类

在前面的部分中,我们讨论了将服务注册到组件中,但是如果我们的服务本身需要一些依赖怎么办?在本节中,您将学习并实现解决服务所需依赖的方法。

为了更好地理解带有依赖关系的提供者类,让我们了解以下用例。我们有两个服务——CityServiceTestDiService,以及一个组件——TestDiComponent

让我们可视化这些服务和组件的依赖树:

让我们详细分析前面的图表,以了解我们正在构建的用例:

  1. 我们将创建一个Injectable服务——CityService

  2. 我们将创建一个Injectable服务——TestDiService

  3. 我们需要将CityService注册到TestDiService类中。

  4. 我们将创建一个TestDiComponent

  5. 我们需要将TestDiService注册到TestDiComponent中。

在本节中,我们将继续使用之前创建的服务TestDiService和组件TestDiComponent

现在,我们将创建一个名为CityService的额外服务,并将文件保存为city.service.ts

将以下代码片段添加到服务文件中:

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

@Injectable()
export class CityService {

 getCities() {
  let cities =[
  { name :"New York" },
  { name: "Dallas" },
  { name: "New Jersey" },
  { name: "Austin" }
  ];

 return cities;
 }
}

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

  1. 我们创建并导出了一个名为CityService的新服务。

  2. 我们实现了一个getCities方法,该方法返回一个城市列表。

创建服务后,我们导入文件并在app.module.ts文件中将服务注册为提供者:

import { CityService } from "./test-di/city.service";

@NgModule({
 providers: [
   CityService
 ]
})

由于我们在app.module.ts文件的 providers 数组中注册了该服务,它现在可以在整个应用程序中使用。

要在TestDiService中使用该服务,我们必须导入该服务并在构造函数中创建CityService的实例:

import { Injectable } from '@angular/core';
import { CityService } from './city.service';

@Injectable()
export class TestDiService {

  constructor(private _city: CityService) { }
    getAuthors() { 
      let Authors =[
         {name :"Sridhar"},
         {name: "Robin"},
         {name: "John"},
         {name: "Aditi"}
      ];
     return Authors;
  }
  getCityList() {
    let cities = this._city.getCities();
    return cities;
 }
}

在前面的部分提到的示例中,我们使用服务来显示作者列表。

现在,让我们分析前面的代码:

  1. 我们创建了一个名为CityService的服务,并在TestDiService中导入了该类。

  2. 我们在构造方法中创建了CityService类的一个实例——_City

  3. 我们定义了一个方法,即getAuthors

  4. 使用 this 运算符,我们在 getCityList 方法中调用了 CityService 类的 getCities 方法。

  5. getCities 方法返回城市列表。

运行应用程序,您将看到前面代码的输出,如下面的屏幕截图所示:

在本节中,您学习并实现了如何通过使用 @Injectable 装饰器注册其他提供者类来解决提供者类的依赖关系。

使用 @Inject、provide 和 useValue

让我们快速回顾一下学习 DI 的进展。我们讨论了如何编写提供者类和层次组件的依赖注入,以及如何使用 @injectable 装饰器编写可重用的提供者。

在本节中,您将学习如何使用 @InjectprovideuseValue 来在不同组件之间共享数据。

要声明一个服务可以在类中被注入,我们需要一个 @Injectable 装饰器。该装饰器告诉 Angular 将使用 @Injectable 定义的类可用于注入器,以便实例化到其他类、服务或组件中,并且该类应通过 DI 动态解析。我们主要用它们来编写通用服务并创建我们自己的存储库。

正如我们之前提到的,即使服务需要在其中注入依赖项,我们也使用 @Injectable 装饰器。我们还可以将服务注册到另一个服务或任何组件中。

每当我们需要注入构造函数参数的类型时,我们将使用 @inject 装饰器。

看一下 app.module.ts 文件中 ngModule 的以下示例代码:

import { ListService } from "./shared/list.service";

@NgModule({
 providers: [
  {
   provide: ListService,
   useClass: ListService
  }
 ]
})

关于前面的代码,有一些要注意的要点:

  1. 我们导入了之前创建的服务,即 ListService

  2. 现在我们已经导入了服务,我们需要将其添加到 providers 列表中。

  3. 我们明确说明需要注册服务名 ListService

  4. 使用 useClass,我们将指示 Angular 实例化并使用 ListService 类。

如果我们仔细注意,我们主要处理的是服务/提供者类。但是,如果我们需要注入某些变量,以便我们可以在不同的组件和服务之间共享值呢?

太棒了!这就是我们可以轻松使用 @Inject 装饰器并创建一个变量或类名,我们可以在其他组件和服务中重用。

现在看一下ngModule文件;我们已经修改它以适应我们想要在各种服务和组件之间共享的变量:

import { ListService } from "./shared/list.service";

@NgModule({
 providers: [
 {
   provide : 'username',
   useValue: 'Sridhar@gmail.com'
 }
 ]
})

让我们分析前面的代码:

  1. 在提供者中,我们创建了一个新的条目,对于provide,我们应用了一个名称username。请记住,无论您在这里提到的名称是什么,我们都需要在其他服务或组件中始终使用它。

  2. 我们为username变量提供了一个值。

  3. 请注意,这个值不会被更改或更新;把它想象成应用程序中的一个常量值。

现在我们已经创建了一个值常量提供者,让我们看看如何在我们的组件中使用它。

app.component.ts中,添加以下代码片段:

import { Component, Inject } from  '@angular/core';
 @Component({
 selector:  'app-root',
  templateUrl:  './app.component.html',
  styleUrls: ['./app.component.css']
 })  export  class  AppComponent {  title = 'Learning Angular - Packt Way';
  constructor ( @Inject('username') private  username ) {} } 

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

  1. 我们从@angular/core中导入了componentInject模块。

  2. 我们创建了我们的组件,并为组件的 HTML 和样式表定义了相应的 HTML 和 CSS 文件。

  3. AppComponent类中,我们定义了一个title变量并为其赋值。

  4. 我们创建了一个类的构造函数,并传递了一个@inject装饰器来传递我们在app.module.ts文件中定义的username名称。

  5. 现在我们已经在提供者数组配置中注册了username变量,我们可以在组件模板中的任何地方使用该变量的值。

太棒了,现在让我们运行这个应用程序;我们应该看到以下截图中显示的输出:

在以下截图中需要注意的一点是,以绿色标记的变量值'Sridhar@gmail.com'在模板中被打印出来。

在本节中,您学会了使用@Inject装饰器定义和使用常量提供者。

您学会了如何为我们的服务类使用@Injectable;我们可以将其注册为提供者,并在其他服务或组件中使用它。

我们可以定义一些常量变量,也可以注入和在不同组件中使用该值。

现在您应该能够创建多个可重用的服务、提供者类,以及常量变量,这些可以用来创建我们的应用程序存储库。

总结

在本章中,我们讨论了现在我们所知道的 Angular DI。DI 允许我们将提供者类和可注入对象注入到组件中使用提供者。我们学习并实现了提供者类和分层依赖注入。我们还学会了在NgModule中注册提供者,或者直接在组件内部注册提供者。

我们重点关注如何创建和配置注入器,以及如何在组件装饰器中注册服务提供者。

本章解释了提供者类也可以有依赖项,这些依赖项可以在内部再次注入到服务或组件中。在下一章中,您将学习关于 Angular 动画。Angular 动画是一个核心库,通过将动作和过渡应用到应用程序中,提供更好的用户体验。

我们将学习各种过渡和动作,以及如何设计动画;最重要的是,我们将在学习过程中创建一些很酷的东西。

第十三章:处理 Angular 动画

在本章中,我们将学习关于 Angular 动画。动画;这个词听起来很有趣和创造性,所以系好安全带;我们将乐在学习 Angular 动画。Web 应用程序中的动作是关键和重要的设计因素之一,也是良好用户体验的主要驱动因素。特别是过渡,它们非常有趣,因为它们使应用程序的元素从一个状态移动到另一个状态。

本章详细介绍以下主题:

  • 介绍 Angular 动画

  • Angular 2 中内置的类来支持动画

  • 理解和学习如何使用动画模块,transitionstateskeyframes

  • 页面过渡动画

  • 动画切换/折叠手风琴幻灯片

介绍 Angular 动画

Angular 自带了对动画的坚实本地支持,因为运动和过渡是任何应用程序的重要部分。

Angular 具有内置的动画引擎,还支持和扩展了运行在大多数现代浏览器上的 Web 动画 API。

我们必须在项目文件夹中单独安装 Angular 动画。我们将在接下来的部分中创建一些动画示例。

安装 Angular 动画库

正如我们之前讨论的,Angular 动画已经被分离出来作为一个单独的库,需要单独安装。

在这一部分,我们将讨论如何获取最新的 Angular 动画版本并安装它;按照以下步骤进行:

  1. 获取最新的 Angular 动画库。

您可以使用以下npm命令进行安装:

npm install @angular/animations@latest --save

运行上述命令将保存最新版本的 Angular 动画库,并将其添加为package.json文件中的依赖项。

  1. 验证最新安装的 Angular 动画库。

确保我们已经安装了 Angular 动画库,打开package.json文件,应该在依赖列表中有@animations/animations的条目。

一旦 Angular 动画库已经被正确导入和安装,package.json文件应该看起来像以下的截图:

  1. app.module.ts文件中导入 Angular 动画库。

我们需要在app.module.ts文件中导入 Angular 动画库。为了包含该库,我们将使用以下代码片段:

import { BrowserAnimationsModule } from '@angular/platform-
   browser/animations';

  1. ngModule装饰器的导入中包含 Angular 动画库:
@ngModule({
 imports: [
BrowserModule,
BrowserAnimationsModule
 ],
//other imports
})

在前面的代码片段中,我们只是将BrowserAnimationsModule导入到我们的ngModule中,以便在整个应用程序中使用。

太棒了!现在我们的应用程序中有了 Angular 动画库,我们可以继续像往常一样构建我们的组件,添加动画和效果。

在我们开始编写使用动画的组件示例之前,重要的是花一些时间探索 Angular 动画中所有可用的类,以便我们可以利用它们。

Angular 动画 - 特定函数

如前所述,Angular 自带了一个独立的动画库,其中有许多内置类和方法来支持各种动画。

让我们了解本节中提供的各种内置类:

  • trigger

  • transition

  • state

  • style

  • animate

我们将详细学习上述每种方法,但在这之前,让我们快速看一下使用这些方法的一般语法。

编写动画的一般语法示例如下:


animations : [
 trigger('slideInOut', [
  state('in', style({
      transform: 'translate3d(0, 0, 0)'
    })),
  state('out', style({
      transform: 'translate3d(100%, 0, 0)'
    })),
  transition('in => out', animate('400ms ease-in-out')),
  transition('out => in', animate('400ms ease-in-out'))
 ])
]

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

  1. 我们正在定义一个名为slideInOut的触发器。

  2. 我们正在定义两个statesinout

  3. 对于每个状态,我们都分配了一个样式,即每个相应状态的 CSS transform属性。

  4. 我们还添加了transition来提及stateanimation的细节。

看起来很简单,对吧?是的,当然!

现在我们知道了如何编写动画的语法,让我们深入了解 Angular 动画库中提供的每种方法。

触发器

触发器定义了一个将触发动画的名称。触发器名称帮助我们确定基于事件应该触发哪个触发器。

定义触发器的一般语法如下:

trigger('triggerName', [
  we define states and transitions here
])

在前面的代码语法中,我们正在定义以下内容:

  1. 通过传递一个必需的参数来定义触发器,即名称和可选参数,其中可以包括statetransition

  2. 触发器名称;我们定义一个名称来识别触发器。

  3. 我们还可以在触发器定义中将我们的状态和转换定义为参数。

状态

状态是元素在特定时间点的定义动画属性。

状态是我们应用程序的逻辑状态,例如活动和非活动。我们为状态定义状态名称和相应的样式属性。

定义状态的语法的一般语法如下:

state('in', style({
 backgroundColor: '#ffffcc'
}))

在前面的代码语法中,我们正在定义以下内容:

  1. 我们正在定义一个名为'in'state,这是我们应用程序中的一个逻辑状态。

  2. 在样式中,我们定义了需要应用于元素的状态的CSS属性。常规的CSS样式属性在这里被定义。

过渡

过渡允许元素在不同状态之间平滑移动。在过渡中,我们定义了各种状态(一个或多个)的动画。

状态是过渡的一部分。

编写transition的一般语法如下:

//Duration Example - seconds or milliseconds
transition('in => out', animate('100')) 

// Easing Example: refer http://easings.net
transition('in => out', animate('300ms ease-in'))

// Using Delay in Animation
transition('in => out', animate('10s 50ms'))

在前面的代码语法中,我们正在定义以下内容

  1. 我们正在定义我们的过渡状态,即从起始状态到结束状态。在我们的语法中,它是从 in 状态到 out 状态。

  2. 动画选项如下:

  3. 缓动:动画进行的平滑程度

  4. 持续时间:动画从开始到结束运行的时间

  5. 延迟:延迟控制动画触发和过渡开始之间的时间长度。

通过对如何编写 Angular 动画的概念和语法有着深刻的理解,让我们继续使用前面的所有函数来创建示例。

页面过渡动画

在前面的部分中,我们为动画创建了一些状态。在本节中,我们将学习如何使用状态创建过渡。

transition是 Angular 动画库中最重要的方法,因为它负责所有效果和状态变化。

让我们创建一个完整页面过渡的示例。我们将创建组件类learn-animation.component.ts

import { Component } from '@angular/core';
import { state, style, animate, trigger, transition, keyframes} from '@angular/core';

@Component({
 templateUrl: './learn-animation.component.html',
 styleUrls: ['./learn-animation.component.css'],
 animations : [
 trigger('customHover', [
  state('inactive', style({
   transform: 'scale(1)',
    backgroundColor: '#ffffcc'
  })),
  state('active', style({
   transform: 'scale(1.1)',
   backgroundColor: '#c5cae8'
  })),

 transition('inactive => active', animate('100ms ease-in')),
 transition('active => inactive', animate('100ms ease-out'))
 ]),
 ]
})
export class AppComponent {
 title = 'Animation works!';
 constructor() {}

 state: string = 'inactive';
 toggleBackground() {
  this.state = (this.state === 'inactive' ? 'active' : 'inactive');
 }
}

让我们详细分析前面的代码,以了解 Angular 动画:

  1. 我们正在定义一个名为customHover的触发器。

  2. 我们正在定义两个statesinactiveactive

  3. 对于每个状态,我们都分配了一个样式,即 CSS;对于各自的状态,我们分配了transformbackgroundColor属性。

  4. 我们还添加了过渡来提及状态和动画细节:

  5. transition影响状态从inactiveactive的移动。

  6. transition影响状态从activeinactive的移动。

  7. 我们正在定义一个toggleBackground方法,当调用时,将从inactive状态切换到active状态,反之亦然。

现在我们已经创建了组件类,在我们的learn-animation.component.html模板中调用了toggleBackground方法:

<div>
 <div id="content" [@customHover]='state' 
       (mouseover)="toggleBackground()"  
       (mouseout)="toggleBackground()">Watch this fade</div>
</div>

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

  1. learn-animation.component.html中,我们正在定义一个div元素。

  2. 我们正在将mouseovermouseout事件与toggleBackground方法进行绑定。

  3. 由于我们将触发器定义为@customHover,我们将使用它进行属性绑定。在我们放置[@customHover]的任何元素上,将应用所定义的动画。

  4. 由于我们应用了属性绑定,属性@customHover的值将在activeinactive之间切换。

  5. 当我们将鼠标悬停在元素上时,将调用toggleBackground方法,并且我们将看到背景颜色随着transform属性的变化而改变。

  6. 在鼠标移出事件上,再次调用toggleBackground方法,并且样式将重置回原始状态。

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

在本节中,我们讨论了如何使用基本的 Angular 动画。在下一节中,我们将探索更多动画示例。

另一个示例 - Angular 动画

在前一节中,我们学习了动画的基础知识;在本节中,我们将使用 Angular 动画创建另一个示例。

在这个例子中,我们将创建一个按钮和一个div元素。当点击按钮时,div元素将滑入页面。很酷,对吧?

让我们开始吧。将以下代码添加到我们在前一节中创建的组件文件learn-animation.component.ts中:

 trigger('animationToggle', [
  transition('show => hide', [
   style({transform: 'translateX(-100%)'}),
   animate(350) ]),
   transition('hide => show', animate('3000ms'))
 ])

在前面的代码中,需要注意以下重要事项:

  1. 我们正在创建一个带有animationToggle的触发器。

  2. 我们正在定义两个过渡,即从show => hidehide => show

  3. 我们正在向show => hide过渡添加样式属性。

  4. 我们没有向hide => show过渡添加样式属性。

定义过渡样式并不是强制性的,但往往我们需要为具有动画效果的元素定义自定义样式。

运行应用程序,您应该在截图后看到以下应用程序和动画:

在我们的应用程序中,当您点击显示按钮时,DIV元素将从右侧滑入页面到左侧。再次点击按钮,它将切换隐藏。

很酷,对吧?是的。Angular 动画使我们能够为元素创建美丽的动画和过渡效果,这将增加用户体验。

我们将构建许多很酷的示例来实现动画。

使用关键帧 - 样式序列

到目前为止,我们已经使用各种方法实现了 Angular 动画的示例。

当我们设计/决定元素的运动和转换时,我们需要遍历各种样式以实现平滑的过渡。

使用keyframes,我们可以在过渡时定义不同样式的迭代。keyframes本质上是为元素定义的一系列样式。

为了更好地理解这一点,让我们看一下以下代码片段:

transition('frameTest1 => frameTest2', [
 animate(300, keyframes([
 style({opacity: 1, transform: 'rotate(180deg)', offset: 0.3}),
 style({opacity: 1, transform: 'rotate(-90deg)', offset: 0.7}),
 style({opacity: 0, transform: 'rotate(-180deg)', offset: 1.0})
 ]))

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

  1. 我们正在定义从frameTest1 => frameTest2transition

  2. 我们用300毫秒定义了animate属性。

  3. 我们正在定义keyframes,在其中我们定义了三种不同的样式;元素将逐步经历每个transition帧。

现在,让我们用下面的代码扩展前面部分创建的示例。

更新后的learn-animation.component.ts文件将具有以下代码:

import { Component } from '@angular/core';
import { state, style, animate, trigger, transition, keyframes} from '@angular/animations';

@Component({
 selector: 'app-learn-animation',
 templateUrl: './learn-animation.component.html',
 styleUrls: ['./learn-animation.component.css'],
 animations: [
 trigger('animationState', [
   state('frameTest1', style({ transform: 'translate3d(0, 0, 0)'  })),
   state('frameTest2', style({ transform:
                 'translate3d(300px, 0, 0)'  })),
   transition('frameTest1 => frameTest2', 
                  animate('300ms ease-in-out')),

   transition('frameTest2 => frameTest1', [
     animate(1000, keyframes([
       style({opacity: 1, transform: 'rotate(180deg)', offset: 0.3}),
       style({opacity: 1, transform: 'rotate(-90deg)', offset: 0.7}),
       style({opacity: 0, transform: 'rotate(-180deg)', offset: 1.0})
     ]))
   ])
  ])
 ]
})
export class LearnAnimationComponent{
 constructor() {}

 public left : string = 'frameTest1';
 public onClick () : void
 {
  this.left = this.left === 'frameTest1' ? 'frameTest2' : 'frameTest1';
 }
}

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

  1. 我们从 Angular 动画库中导入所需的模块:statestyleanimatekeyframestransition。这些模块帮助我们在应用程序中创建动画。

  2. 我们创建了一个LearnAnimationComponent组件。

  3. 我们为组件指定了animations

  4. 我们定义了一个名为animationState的触发器。

  5. 对于创建的触发器,我们定义了两个状态--frameTest1frameTest2

  6. 我们定义了两个转换:'frameTest2 => frameTest1''frameTest2 => frameTest1'

  7. 对于定义的每个转换,我们已经实现了keyframes,也就是与animate方法一起使用的一系列样式,以实现平滑的过渡和时间延迟。

  8. 在组件类中,我们定义了一个left变量。

  9. 我们正在定义一个onClick方法,切换从frameTest1frameTest2的值。

到目前为止,一切顺利。我们已经实现了组件。

现在是时候更新我们的learn-animation.component.html并将以下代码片段添加到文件中:

<h4>Keyframe Effects</h4>

<div class="animateElement" [@animationState]="left"    
  (click)="onClick()">
     Click to slide right/ Toggle to move div
</div>

好了,一切准备就绪。现在运行应用程序,您应该看到如屏幕截图所示的输出和下面提到的动画:

当您运行应用程序时,您应该看到以下动画

  1. 当您点击DIV元素时--它应该向右滑动

  2. 再次点击DIV元素,元素应该向右移动,DIV元素变换--给人一种 DIV 在旋转的感觉。

在本节中,您将学习如何使用keyframes并为元素创建一系列样式,以实现更平滑的过渡。

动画折叠菜单

在本节中,我们将为我们的应用程序创建一个非常重要的部分,即应用程序的侧边栏菜单。

根据我们迄今为止学到的关于 Angular 动画的知识,在本节中我们将创建一个折叠侧边栏的示例。

让我们更新组件模板learn-animation.component.html,并使用以下代码片段更新文件:

<h4>Collapse Menu</h4>

<button (click)="toggleMenu()" class="menuIcon">Toggle Menu</button>
 <div class="menu" [@toggleMenu]="menuState">
 <ul>
   <li>Home</li>
   <li>Angular</li>
   <li>Material Design</li>
   <li>Sridhar Rao</li>
   <li>Packt Publications</li>
 </ul>
</div>

对前面的代码进行分析如下:

  1. 我们正在添加一个<h4>标题,一个Collapse菜单。

  2. 我们正在定义一个按钮,并将click事件与toggleMenu方法关联起来。

  3. 我们正在创建一个带有示例列表项<li>的无序列表<ul>

现在,我们将向learn-animation.component.css文件添加一些基本的 CSS 样式:

.animateElement{
   background:red;
   height:100px;
   width:100px;
}
.menu {
   background: #FFB300;
   color: #fff;
   position: fixed;
   left: auto;
   top: 0;
   right: 0;
   bottom: 0;
   width: 20%;
   min-width: 250px;
   z-index: 9999;
   font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
 }

 ul {
   font-size: 18px;
   line-height: 3;
   font-weight: 400;
   padding-top: 50px;
   list-style: none;
 }
 .menuIcon:hover {
   cursor: pointer;
 }

到目前为止,我们已经创建了我们的应用程序组件模板learn-animation.component.html并为菜单组件learn-animation.component.css设置了样式。

现在,我们将创建菜单组件类。

将以下代码添加到learn-animation.component.ts文件中:

import { Component } from '@angular/core';
import { state, style, animate, trigger, transition, keyframes} from '@angular/core';

@Component({
 selector: 'app-learn-animation',
 templateUrl: './learn-animation.component.html',
 styleUrls: ['./learn-animation.component.css'],
 animations: [

  trigger('toggleMenu', [
   state('opened', style({
    transform: 'translate3d(0, 0, 0)'
   })),
   state('closed', style({
    transform: 'translate3d(100%, 0, 0)'
   })),
   transition('opened => closed', animate('400ms ease-in-out')),
   transition('closed => opened', animate('400ms ease-in-out'))
  ])
 ])
 ]
})
export class LearnAnimationComponent{

constructor() {}
 menuState : string = 'opened';
 toggleMenu()
 {
  this.menuState = this.menuState === 'closed' ? 'opened' : 'closed';
 }
}

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

  1. 我们正在导入所需的 Angular 动画库模块,例如statestyleanimatetriggertransitionkeyframes

  2. 在动画中,我们定义了一个触发器:toggleMenu

  3. 我们正在创建两种状态:openedclosed

  4. 对于每个状态,我们正在定义一些带有transform的样式属性。

  5. 我们现在定义了转换opened => closedclosed => open,并带有一些动画细节延迟。

  6. 我们已经定义了一个menuState变量。

  7. 在组件类中,我们定义了toggleMenu

  8. toggleMenu方法中,我们正在切换menuState变量值为openedclosed,反之亦然。

现在是演示时间。运行应用程序,您应该看到以下输出:

再次点击 Toggle 菜单按钮,我们应该看到菜单向右滑动,如下截图所示:

在本节中,我们使用 Angular 动画创建了应用程序的侧边栏菜单。

总结

在本章中,我们介绍了 Angular 动画。动画对于设计和构建具有平滑过渡和元素效果的美观用户体验至关重要。

我们介绍了如何安装和导入 Angular 动画库,并在库中使用各种模块。

我们讨论了重要的模块,比如statestyleanimatetriggertransitionkeyframes

我们创建并实现了一些使用 Angular 动画的示例。

最后,我们创建了一个带有一些动画效果的网页应用侧边栏菜单。现在,轮到你了!

在下一章中,您将学习如何将 Bootstrap 与 Angular 应用程序集成。Bootstrap 可以说是目前最流行的前端框架,在本章中,您将了解拥有一个 Angular x Bootstrap 应用程序意味着什么。

第十四章:将 Bootstrap 与 Angular 应用程序集成

Bootstrap 可以说是目前最受欢迎的前端框架。你可能会问,Angular 本身不就是一个前端框架吗?是的。那么为什么我需要在同一个应用程序中使用两个前端框架呢?答案是,你不需要。Bootstrap 是由 Twitter 创建和使用的,非常受欢迎。它允许您管理许多事情,比如使用一个名为网格的系统在页面上布置 HTML 组件。我将在接下来的页面中详细解释这个系统,它允许您在不明确使用 CSS 的情况下将网页空间划分为区域。此外,一切都将立即响应。此外,Bootstrap 提供了动态元素,如轮播、进度条、对用户输入的表单反应等。简而言之,Angular 允许您创建应用程序结构并管理数据呈现,而 Bootstrap 处理图形的呈现。

Bootstrap 围绕三个元素展开:

  • bootstrap.css

  • bootstrap.js

  • glyphicons

在这里,bootstrap.css包含了允许响应式空间划分的框架,而bootstrap.js是一个使您的页面动态化的 JavaScript 框架。

需要注意的是,bootstrap.js依赖于 jQuery 库。

最后,glyphicons是一个包含使用 Bootstrap 时可能需要的所有图标的字体。

在第十章, Angular 中的 Material Design中,您将学习如何使用由 Google 官方提供的Material Design包来创建管理动态元素、轮播和其他进度条的应用程序(ng2-material)。Bootstrap(由 Twitter 提供)和 Material Design(由 Google 为 Angular 提供)最终都旨在实现同样的目标:在严格呈现页面给用户时简化您的生活。例如,它们都确保跨浏览器兼容性,防止在项目之间重复编写代码,并在代码库中添加一致性。

在我看来,您应该使用哪一个是个人选择,我可以预见未来几个月将会有关于 C#与 Java 或 PC 与 Mac 之类的激烈争论。一方面,如果您已经精通 Bootstrap 并且在各处都在使用它,那么您也可以在这里使用它。另一方面,如果 Bootstrap 不是您的技能范围,您可以利用这个机会学习并选择您喜欢的。

第三个选项将是完全跳过本章,如果您已经选择了 Material Design(由 Google 为 Angular 提供)的方法。我不介意,我保证。本章涵盖的主题有:

  • 安装 Bootstrap

  • 了解 Bootstrap 的网格系统

  • 使用 Bootstrap 指令

安装 Bootstrap

话不多说,让我们开始并为 Angular 安装 Bootstrap。

在没有像 Angular 这样的前端框架的标准 Web 应用中使用 Bootstrap 时,您需要使用内容传递网络(CDN)来获取组成 Bootstrap 框架的三个部分(bootstrap.cssbootstrap.jsglyphicons)。即使下载了缩小的文件,这些调用仍然需要时间(例如,三个 HTTP 请求,下载,校验和等)才能完成。对于您的客户来说,使用 Angular,我们可以采用相同的方法,并简单地在src/index.html中添加对某些 CDN 的引用,但这将是一个相当大的错误。

首先,如果用户没有缓存资源的副本,那么我们将遭受与标准 Web 应用相同的副作用,因为我们的客户将不得不等待 CDN 提供 Bootstrap 框架,特别是考虑到我们的应用经过 Angular CLI 部署流程进行了缩小并以单个文件提供。其次,我们将无法轻松地在我们的 Angular 组件中控制 Bootstrap 组件。

将 Bootstrap 与我们的 Angular 应用程序集成的更好方法是使用ng-bootstrap包。该包允许我们在我们的组件中使用 Angular 指令来管理 Bootstrap。在撰写本文时,这是最全面、维护良好且与 Angular 集成良好的包,允许我们在 Angular 中使用 Bootstrap。

为了探索 Bootstrap,我们将在第七章,使用可观察对象进行异步编程和第九章,Angular 中的高级表单中使用的 Marvel Cinematic Universe 的 JSON API 基础上构建。

您可以在github.com/MathieuNls/mastering-angular2/tree/master/chap9找到《第九章》,Angular 中的高级表单的代码。

要将此代码克隆到名为angular-bootstrap的新存储库中,请使用以下命令:

$ **git** clone --depth one https://github.com/MathieuNls/mastering-angular    
    angular-bootstrap
$ **cd** angular-bootstrap
$ **git** filter-branch --prune-empty --subdirectory-filter chap9 HEAD

这些命令将 GitHub 存储库的最新版本拉到名为angular-bootstrap的文件夹中。然后,我们进入angular-bootstrap文件夹,并清除不在第九章 Angular 中的高级表单目录中的所有内容。

现在让我们安装ng-bootstrap包:

npm install --save @ng-bootstrap/ng-bootstrap

现在,在src/app/app.module.ts中,导入import {NgbModule} from @ng-bootstrap/ng-bootstrap包,并将NgbModule.forRoot()添加到AppModule类的导入列表中。如果您重用了第九章 Angular 中的高级表单中的代码,它应该是这样的:

 import { BrowserModule } from '@angular/platform-browser';
 import { NgModule } from '@angular/core';
 import { FormsModule, ReactiveFormsModule  } from '@angular/forms';
 import { HttpModule } from '@angular/http';
 import { NgbModule } from '@ng-bootstrap/ng-bootstrap

 import { AppComponent } from './app.component';

 @NgModule({
   declarations: [
     AppComponent
   ],
   imports: [
     BrowserModule,
     FormsModule,
     HttpModule,
     ReactiveFormsModule,
     NgbModule.forRoot()
   ],
   providers: [],
   bootstrap: [AppComponent]
 })
 export class AppModule { }

这个包允许我们摆脱 jQuery 和bootstrap.js的依赖,但不幸的是,它不包括bootstrap.css。它包含了我们即将使用的网格系统和组件所需的样式。

前往getbootstrap.com/,并在src/index.html中导入以下显示的链接:

<!doctype html>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Chap15</title>
   <base href="/">
   <link rel="stylesheet" 
        href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-
        alpha.4/css/bootstrap.min.css" integrity="sha384-
        2hfp1SzUoho7/TsGGGDaFdsuuDL0LX2hnUp6VkX3CUQ2K4K+xjboZdsXyp4oUHZj" 
        crossorigin="anonymous">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="icon" type="image/x-icon" href="favicon.ico">
 </head>
 <body>
   <app-root>Loading...</app-root>
 </body>
 </html>

通过这些小改变,我们已经可以看到 Bootstrap 正在接管我们的样式。在下面的图片中,左边是我们在第九章 Angular 中的高级表单结束时表单的样子。

然而,右边是我们现在表单的样子。正如您所看到的,这里和那里有一些小的不同。例如,h1标记,错误字段和输入的样式不同:

Bootstrap 之前和之后。

如果我们使用 Google Chrome 的检查功能,我们可以清楚地看到我们的h1标记的应用样式来自 http://maxcdn.bootstrapcdn.com,如下面的屏幕截图所示:

Chrome 检查样式。

就是这样:我们完成了 Bootstrap 的初始化。让我们学习如何使用 Angular 指令来使用 Bootstrap。

理解网格系统

在本章中,我们更关心学习如何使用不同的 Angular Bootstrap 指令,而不是学习 Sass 混合和其他演示技巧。换句话说,网格系统的高级功能超出了本章的范围。然而,在本节中,我将快速介绍网格系统是什么,以及如何使用它的概述。

如果你以前使用过 Bootstrap,尤其是使用过网格系统,你可以跳过这一部分,直接进入下一部分,在那里我们学习如何使用手风琴指令。

因此,网格系统将我们的演示分成了十二列。列的大小可以是额外小、小、中、大和额外大。列的大小可以通过 CSS 类前缀(分别是col-xscol-smcol-mdcol-lgcol-xl)手动设置,并对应不同的屏幕宽度(小于 540 像素、540 像素、720 像素、960 像素和 1140 像素)。

为了了解如何利用网格系统来分隔我们的演示,让我们在src/app/app.component.html中的<h1>{{title}}</h1>标记后面添加以下内容:

<div class="container">
   <div class="row">
     <div class="col-md-1">col-md-1</div>
     <div class="col-md-1">col-md-1</div>
     <div class="col-md-1">col-md-1</div>
     <div class="col-md-1">col-md-1</div>
     <div class="col-md-1">col-md-1</div>
     <div class="col-md-1">col-md-1</div>
     <div class="col-md-1">col-md-1</div>
     <div class="col-md-1">col-md-1</div>
     <div class="col-md-1">col-md-1</div>
     <div class="col-md-1">col-md-1</div>
     <div class="col-md-1">col-md-1</div>
     <div class="col-md-1">col-md-1</div>
   </div>
   <div class="row">
     <div class="col-md-8">col-md-8</div>
     <div class="col-md-4">col-md-4</div>
   </div>
   <div class="row">
     <div class="col-md-4">col-md-4</div>
     <div class="col-md-4">col-md-4</div>
     <div class="col-md-4">col-md-4</div>
   </div>
   <div class="row">
     <div class="col-md-6">col-md-6</div>
     <div class="col-md-6">col-md-6</div>
   </div>
 </div>

正如你所看到的,这里有几个 CSS 类在起作用。首先,让我们看看容器。这是必需的,它定义了 Bootstrap 网格系统将应用的空间。然后,我们有包含col-的行。每行占据屏幕的整个宽度,并被分成列。列的实际宽度取决于你在列类声明的末尾使用的数字(4、8、6 等)。知道行被分成 12 列,我们使用了col-md类前缀,我们可以推断出一行的最大尺寸是 720 像素。因此,每列宽 60 像素。在第一行中,我们在我们的声明中使用了-1后缀;因此,我们有 60 像素宽的列(即屏幕宽度除以 12)。然而,在第二行,我们使用了-8-4后缀。

这意味着我们将有一列的宽度是a-1列的 8 倍(480 像素),另一列的宽度是a-1列的 4 倍(240 像素)。在第三行,我们使用了三个四列,最后,在第四行,我们有两个六列。

要查看发生了什么,请在app/app.component.css中添加以下内容:

.row > [class^="col-"]{
   padding-top: .75rem;
     padding-bottom: .75rem;
     background-color: rgba(86, 61, 124, 0.15);
     border: 1px solid rgba(86, 61, 124, 0.2);
 }

这段 CSS 将为任何col类添加背景和边框,无论它们可能具有的前缀或后缀是什么:

网格系统的运行。

正如你在上图中所看到的,空间被很好地按计划划分。现在,这并不是网格系统的真正优势。主要优势在于,如果屏幕宽度变小于 720 像素,列会自动堆叠在彼此上面。

例如,在 iPhone 6 上,其屏幕宽度为 375px,所有列将堆叠在一起,如下截图所示:

iPhone 6 上的网格系统。

这是官方文档中的另一个例子,可以在v4-alpha.getbootstrap.com/layout/grid/找到:

<!-- Stack the columns on mobile by making one full-width and the other half-width -->
 <div class="row">
   <div class="col-xs-12 col-md-8">.col-xs-12 .col-md-8</div>
   <div class="col-xs-6 col-md-4">.col-xs-6 .col-md-4</div>
 </div>

 <!-- Columns start at 50% wide on mobile and bump up to 33.3% wide on desktop -->
 <div class="row">
   <div class="col-xs-6 col-md-4">.col-xs-6 .col-md-4</div>
   <div class="col-xs-6 col-md-4">.col-xs-6 .col-md-4</div>
   <div class="col-xs-6 col-md-4">.col-xs-6 .col-md-4</div>
 </div>

 <!-- Columns are always 50% wide, on mobile and desktop -->
 <div class="row">
   <div class="col-xs-6">.col-xs-6</div>
   <div class="col-xs-6">.col-xs-6</div>
 </div>

我不会详细介绍网格系统,但知道你可以在 Packt Library 找到很多关于这个主题的精彩书籍。只需查找以下内容:

  • 精通 Bootstrap 4

  • Bootstrap 4 蓝图

使用 Bootstrap 指令

在本节中,我们将学习如何使用一些最常用的 Bootstrap 指令来构建您的应用程序。

手风琴

我们将首先概述手风琴指令。手风琴允许您创建一个可以通过单击其各自的标题独立显示的不同内容面板。

我们将使用我们在第九章中制作的表单,Angular 中的高级表单,允许用户在漫威电影宇宙中添加电影,以实验手风琴。这里的目标是为表单设置一个面板,为电影的枚举设置另一个面板。

让我们从研究创建 Bootstrap 手风琴所需的最小 HTML 开始,如下所示:

<ngb-accordion>
   <ngb-panel>
     <template ngbPanelTitle>
       <span>Mastering angular X Bootstrap</span>
     </template>
     <template ngbPanelContent>
       Some deep insights
     </template>
   </ngb-panel>
   <ngb-panel>
     <template ngbPanelTitle>
       <span>Some Title</span>
     </template>
     <template ngbPanelContent>
       Some text
     </template>
   </ngb-panel>
 </ngb-accordion>

前面的 HTML 模板将产生以下结果:

一个简单的手风琴。

分析前面的代码片段,我们可以看到以下特点:

  • ngb-accordion:这是主要的手风琴指令。它定义了一个包含ngb-panel的手风琴。

  • ngb-panel: 这代表手风琴的一个面板。可以通过单击面板标题来切换其可见性。ngb-panel包含一个可以用于标题或内容的模板。

  • <template ngbPanelContent>:这包含给定面板的标题或内容。

  • <template ngbPanelTitle>: 这包含标题。

到目前为止,一切都相当简单。现在,它变得强大的地方是当您从您的 TypeScript 组件中管理它时。首先,ngb-accordion指令有三个不同的@Input属性,我们利用了它们。第一个是activeIds,它是string[]类型,包含您希望打开的面板的 ID。面板 ID 是从ngb-panel-0自动生成的。面板 ID 的格式为ngb-panel-x。第二个@Input是一个布尔值:closeOthers。这允许您指定是否一次只能打开一个面板。最后,使用string类型来指定手风琴的类型。在 Bootstrap 中,有四种类型被识别:successinfowarningdanger

除了这三个@Inputs之外,ngb-accordion指令还提供了一个名为panelChange@Output。这个@Output会在每次面板的可见性即将被切换时触发。

让我们通过将app/app.component.html转换为以下内容来尝试这些@Input@Output属性:

<div class="container">

     <!-- First Row -->
     <div class="row">
         <h1 class="col-md-12">
           {{title}}
         </h1>
     </div>

     <!-- Second Row -->
     <div class="row">

         <!-- Start of the accordion -->
         <ngb-accordion class="col-md-12" 
         <!-- Bind to a variable called activeIds -->
         [activeIds]="activeIds" 
         <!-- Simply use the string 'success' -->
         type="success" 
         <!-- Simply use true -->
         closeOthers="true"
         <!-- Bind to the output -->
         (panelChange)=pannelChanged($event)
         >
           <!-- Firt pannel -->
           <ngb-panel>
             <template ngbPanelTitle>
               <span>Add a Movie</span>
             </template>
             <!-- Form content is here -->
             <template ngbPanelContent>
               <form [formGroup]="movieForm">
                 <!-- Form content omitted for clarity -->
               </form>
             </template>
           </ngb-panel>
           <!-- Second pannel -->
           <ngb-panel>
             <template ngbPanelTitle>
               <span>Movies</span>
             </template>
             <!-- Movie enumeration is here -->
             <template ngbPanelContent>

                 <ul>
                     <li *ngFor="let movie of movies">{{movie}}</li> 
                 </ul>

             </template>
           </ngb-panel>
         </ngb-accordion>

     </div>
 </div>

在这里,我们使用了[activeIds]="activeIds"type="success"closeOthers="true"(panelChange)=pannelChanged($event)来绑定到我们组件中的一个名为activeIds的变量,将表单类型设置为success,并将closeOthers设置为 true。然后,我们将一个名为pannelChanged的方法绑定到panelChange输出。在app.component.ts中,我们需要添加activeIds变量和pannelChanged方法如下:

  private activeIds = ["ngb-panel-1"];

   private pannelChanged(event:{panelId:string, nextState:boolean}){
     console.log(event.nextState, event.panelId);
   }

在这里,private activeIds = ["ngb-panel-1"];允许我们定义panel-1(第二个)应该默认打开,并且pannelChanged方法应该接收一个由panelId:stringnextState:boolean组成的事件负载。我们记录了这两个负载属性。

应用程序现在看起来像下面截图中显示的那样:

一个由 TypeScript 管理的手风琴。

当您切换面板时,控制台会记录以下内容:

**true** "ngb-panel-0"
**false** "ngb-panel-0"  

警报

本章中我们将探讨的下一个指令是ng-alert。在 Bootstrap 词汇中,警报是以有色div形式显示给用户的重要信息。有四种类型的警报:successinfowarningdanger

要创建一个 Bootstrap 警报,最小可行的 HTML 模板如下:

  <ngb-alert> 
    Something important 
  </ngb-alert> 

这段代码的结果如下截图所示:

一个基本的警报。

与手风琴类似,警报指令提供了一些@Input@Output。我们可以使用@Input作为dismissible:boolean,它管理警报的可解除性,以及type:string,它接受successinfowarningdanger

为了使我们的表单更具 Bootstrap 风格,我们可以用警报替换我们的错误消息。目前,在表单中,错误消息看起来像这样:

<p class='error' *ngIf=!movieForm.controls.movie_id.valid>This field is required</p>

现在的目标是有以下内容:

  <ngb-alert 
   [dismissible]="false" 
   *ngIf=!movieForm.controls.movie_id.valid
   type="danger"
   >
     This field is required
   </ngb-alert>

在上述片段中的每个字段,上述代码将产生以下结果:

危险警报作为表单错误。

日期选择器

本章中的下一个指令是日期选择器。无论您使用什么技术,日期总是有些棘手,因为每个供应商都提出了许多格式。此外,日期国际化使事情变得更加困难。

幸运的是,Bootstrap 带有一个足够简单的日期选择器,允许用户在弹出的日历中选择日期。其代码如下所示:

<div class="input-group">
   <input class="form-control" placeholder="yyyy-mm-dd" 
      ngbDatepicker #dp="ngbDatepicker">
   <div class="input-group-addon" (click)="dp.toggle()" >
     <img src="https://ng-bootstrap.github.io/img/calendar-icon.svg"
         style="width: 1.2rem; height: 
             1rem; cursor: pointer;"/>
    </div>
</div>

这里发生了很多事情。首先,我们有一个formControl输入,其占位符设置为yyyy-mm-dd。您定义的占位符很重要,因为它将作为用户选择的数据的强制格式化程序。对于格式化程序的语法,您可以使用日期的每个经典符号(例如,d、D、j、l、N、S、w、z 等)。换句话说,我们输入的日期将自动匹配此模式。然后,我们有ngbDatepicker #d="ngbDatepicker"ngbDatepicker定义了我们的输入是一个ngbDatepicker#dp="ngbDatepicker"允许我们创建对我们的输入的本地引用。这个名为dp的本地引用在以下div(click)事件上使用:(click)="dp.toggle()"。这个div包含了日历的图像。点击它,一个动态的日历将弹出,我们将能够选择一个日期。

这个 HTML 将给我们以下内容:

日期选择器。

然后,一旦触发了click事件,将显示如下内容:

日期选择器被点击。

为了改善我们对漫威电影宇宙的管理,我们可以将release_date字段更改为日期选择器。目前,release_date字段看起来像这样:

<label>release_date</label>
 <ngb-alert [dismissible]="false" type="danger" 
       *ngIf=!movieForm.controls.release_date.valid>This field is required</ngb-alert>
 <input type="text" formControlName="release_date" [(ngModel)]="movie.release_date"><br/>

如果字段无效,我们会有输入和 Bootstrap 警报。Bootstrap 警报默认是活动的(即当字段为空时)。让我们将我们的输入转换为以下内容:

  <label>release_date</label>
   <ngb-alert [dismissible]="false" type="danger" 
      *ngIf=!movieForm.controls.release_date.valid>This 
      field is required</ngb-alert>
   <div class="input-group">
     <input 
     formControlName="release_date" 
     placeholder="yyyy-mm-dd"  
     ngbDatepicker #dp="ngbDatepicker"
     [(ngModel)]="movie.release_date">
     <div class="input-group-addon" (click)="dp.toggle()" >
       <img src="https://ng-bootstrap.github.io/img/calendar-icon.svg" 
           style="width: 1.2rem; 
           height: 1rem; cursor: pointer;"/>
     </div>
   </div>

这里的不同之处在于我们将输入链接到了我们的formControl。实际上,在第九章 Angular 中的高级表单中,我们定义了表单如下:

this.movieForm =  this.formBuilder.group({
         movie_id: ['',  
           Validators.compose(
             [
              Validators.required,
              Validators.minLength(1), 
              Validators.maxLength(4), 
              Validators.pattern('[0-9]+'),
              MovieIDValidator.idNotTaken
             ]
           )
         ],
         title: ['', Validators.required],
         phase: ['', Validators.required],
         category_name: ['', Validators.required],
         release_year: ['', Validators.required],
         running_time: ['', Validators.required],
         rating_name: ['', Validators.required],
         disc_format_name: ['', Validators.required],
         number_discs: ['', Validators.required],
         viewing_format_name: ['', Validators.required],
         aspect_ratio_name: ['', Validators.required],
         status: ['', Validators.required],
         release_date: ['', Validators.required],
         budget: ['', Valida tors.required],
         gross: ['', Validators.required],
         time_stamp: ['', Validators.required]
});

所以,我们有一个必填的release_date字段。HTML 输入定义了与release_date字段的双向数据绑定,带有[(ngModel)]="movie.release_date",此外,我们还需要在输入框内添加formControlName="release_date"属性。实施后,屏幕上将显示以下内容:

MCU 的日期选择器。

工具提示

接下来,我们有 tooltip 指令,它允许我们在给定一组元素的左侧、右侧、顶部或底部显示信息性文本。

tooltip 指令是最简单的之一。实际上,你只需要为你希望增强的元素添加两个属性:placement 和ngbTooltip。placement 的值可以是 top、bottom、left 或 right,而ngbTooltip的值是你希望显示的文本。

让我们修改movie_id字段的标签:

<ngb-alert [dismissible]="false" type="danger" 
   *ngIf=!movieForm.valid>danger</ngb-alert>
<label >movie_id</label>
<ngb-alert [dismissible]="false" type="danger" 
  *ngIf=!movieForm.controls.movie_id.valid>This field 
    is required</ngb-alert>
  <input type="text" formControlName="movie_id" 
     [(ngModel)]="movie.movie_id" name="movie_id" >
   <br/> to 
    <ngb-alert [dismissible]="false" type="danger" 
       *ngIf=!movieForm.valid>danger</ngb-alert>
    <label placement="top" ngbTooltip="Title of
      your movie"> movie_id</label>
    <ngb-alert [dismissible]="false" type="danger" 
       *ngIf=!movieForm.controls.movie_id.valid>This 
    field is required</ngb-alert>
 <input type="text" formControlName="movie_id" 
    [(ngModel)]="movie.movie_id" name="movie_id" ><br/>

在这里,我们保持了警报和输入不变。但是,我们在标签中添加了 placement 和ngbTooltip属性。结果,当我们悬停在movie_id标签上时,电影标题将显示在顶部。如下截图所示:

movie_id 上的工具提示。

进度条

还有一些其他的 Bootstrap 组件可以用来增强我们的表单;然而,太多的组件很快就会成为可用性过度的情况。例如,将进度条集成到我们的表单中将会很棘手。然而,我们可以为我们想要测试的每个新的 Bootstrap 指令添加一个手风琴面板。

让我们为进度条添加一个面板:

<ngb-panel>
     <template ngbPanelTitle>
         <span>Progress Bar</span>
     </template>

     <template ngbPanelContent>

       <ngb-progressbar type="success" [value]="25"></ngb-progressbar>

    </template>
</ngb-panel>

progressbar指令是另一个简单的指令。它有两个@Input属性:type 和 value。和往常一样,type 可以是successdangerwarninginfo。value 属性可以绑定到一个 TypeScript 变量,而不是像我做的那样硬编码为 25。

这是结果:

movie_id 上的进度条。

评分

评分指令也是非常出名的。它允许用户对某物进行评分,或者显示给定的评分。

正如预期的那样,这个指令很容易理解。它有一个评分输入,您可以硬编码(例如,"rate"=25),绑定([rate]="someVariable"),或者应用双向数据绑定([(rate)]="someVariable")。除了评分输入,您还可以使用[readonly]="read-only"来使您的评分条不可修改。

默认情况下,评分条由 10 颗星组成。评分值可以从 0 到 10,包括小数。

以下是一个新面板内默认评分条的示例:

<ngb-panel>
        <template ngbPanelTitle>
           <span>Rating bar</span>
         </template>
         <template ngbPanelContent>

            <ngb-rating rate="5"></ngb-rating>

          </template>
  </ngb-panel>

这将产生以下结果:

评分条。

摘要

在本章中,我们看到了一些最受欢迎的 Bootstrap 组件。我们学会了如何使用 ng2-Bootstrap 包提供的原生 Angular 指令来使用它们。然而,我们并没有探索每一个 Bootstrap 组件。您可以查看托管在ng-bootstrap.github.io/的官方文档。

在下一章中,您将学习如何使用单元测试来测试您的 Angular 应用程序。

第十五章:使用 Jasmine 和 Protractor 框架测试 Angular 应用程序

测试是现代应用程序开发过程中最重要的方面之一。我们甚至有专门的软件开发方法论,主要是基于测试优先的方法。

除了 Angular 提供的测试工具之外,还有一些推荐的框架,如 Jasmine、Karma 和 Protractor,使用这些框架可以轻松创建、维护和编写测试脚本。使用 Jasmine 和 Protractor 编写的测试脚本可以节省时间和精力,并且最重要的是在开发过程中更早地发现缺陷。

在本章中,您将学习如何使用 Jasmine 和 Protractor 测试 Angular 应用程序。在本章中,我们将讨论以下内容:

  • 了解测试中的重要概念

  • 了解 Angular CLI 用于单元测试特定环境

  • 介绍 Jasmine 框架

  • 使用 Jasmine 编写测试脚本

  • 编写测试脚本来测试 Angular 组件

  • 测试 Angular 组件:一个高级示例

  • 使用 Jasmine 测试脚本测试 Angular 服务

  • 学习 Protractor

  • 使用 Protractor 编写 E2E 测试脚本

测试中的概念

在我们开始测试我们的 Angular 应用程序之前,重要的是我们快速复习并了解一些在测试中常用的术语:

  • 单元测试:一个单元测试可以被视为应用程序中最小的可测试部分。

  • 测试用例:这是一组测试输入、执行条件和期望结果,以实现一个目标。在 Jasmine 框架中,这些被称为规范。

  • TestBed:TestBed 是一种通过传递所有必需的数据和对象来以隔离的方式测试特定模块的方法。

  • 测试套件:这是一组旨在用于端到端测试模块的测试用例集合。

  • 系统测试:对完整和集成的系统进行的测试,以评估系统功能。

  • 端到端测试:这是一种测试方法,用于确定应用程序的行为是否符合要求。我们传递数据、必需对象和依赖项,并在模拟实时用例和场景的情况下从头到尾执行。

既然我们知道了前面的术语,让我们学习如何测试 Angular 应用程序。

了解并设置 Angular CLI 进行测试

到目前为止,我们已经使用 Angular CLI 来设置我们的项目,创建新组件、服务等。我们现在将讨论如何使用命令行工具来设置和执行测试套件,以测试我们的 Angular 应用程序。

首先,快速回顾如何使用 Angular CLI 快速创建项目:

npm install -g angular-cli

使用上述代码片段,我们安装了 Angular 命令行工具。现在,让我们创建一个名为test-app的新目录并进入项目目录:

ng new test-app
cd test-app

现在是时候快速创建一个名为test-app的新组件了:

ng g component ./test-app

现在,我们将看到以下输出:

我们应该看到新目录和相应的文件在目录中创建。命令行工具已经创建了与组件相关的四个文件,包括test-app.component.spec.ts测试脚本占位符文件。

现在,让我们启动我们的应用程序:

ng serve

此时,我们的应用程序已经启动。现在是时候开始测试我们的 Angular 应用程序了。

Jasmine 框架介绍

Jasmine 是一个用于测试 JavaScript 代码的行为驱动开发框架。这是官方网站如何解释 Jasmine 的方式:

Jasmine 是一个用于测试 JavaScript 代码的行为驱动开发框架。它不依赖于任何其他 JavaScript 框架。它不需要 DOM。它有一个清晰明了的语法,让您可以轻松编写测试。

Jasmine 测试套件的一般语法如下所示:

describe("Sample Test Suite", function() {
 it("This is a spec that defines test", function() {
   expect statement // asserts the logic etc
 });
});

让我们分析上述代码片段,以了解测试套件语法。已经按照以下步骤进行了操作:

  1. 每个 Jasmine 测试套件都将有一个describe语句,我们可以给出一个名称。

  2. 在测试套件内,我们使用it语句创建较小的测试用例;每个测试用例将有两个参数,一个名称和一个函数,其中包含需要测试的应用程序逻辑。

  3. 我们使用expect语句来验证数据,以确保我们的应用程序和数据按预期工作。

在下一节中,您将详细了解 Jasmine 框架和可用的方法和函数,我们可以在测试脚本中使用。

Jasmine 框架 - 我们可以使用的全局方法

Jasmine 框架支持并为我们提供了许多预定义的方法来使用和编写我们的测试套件。 Jasmine 对测试环境、对元素进行间谍操作等提供了广泛的支持。请参阅官方网站以获取有关可用方法的完整帮助和文档。

为了编写测试脚本,我们需要对 Jasmine 框架中最常用和频繁使用的一些方法有基本的理解和知识。

Jasmine 中常用的方法

以下是编写测试套件可用的最常用的 Jasmine 全局方法列表:

全局方法 描述
describe describe 函数是实现测试套件的代码块
it 通过调用全局 Jasmine 函数it来定义规范,如所述,它接受一个字符串和一个函数
beforeEach 此方法在调用它的描述中的每个规范之前调用一次
afterEach 此方法在每个规范后调用一次
beforeAll 此方法在描述中的所有规范之前调用一次
afterAll 此方法仅在所有规范调用后调用一次
xdescribe 这会暂时禁用您不想执行的测试
pending 未运行的待定规范将被添加到待定结果列表中
xit 任何使用 xit 声明的规范都会被标记为待定
spyOn 间谍可以替换任何函数并跟踪对它的调用和所有参数;这在描述或 it 语句内部使用
spyOnProperty 对间谍的每次调用都会被跟踪并暴露在 calls 属性上

有关更多详细信息和完整文档,请参阅 GitHub 上的 Jasmine 框架文档。

Angular CLI 和 Jasmine 框架-第一个测试

安装 Angular CLI 时,Jasmine 框架会自动与工具一起提供。

在前面的部分中,我们看到了在 Jasmine 中编写测试的一般语法。现在,让我们使用 Jasmine 框架编写一个快速的测试脚本:

describe('JavaScript addition operator', function () {  it('adds two numbers together', function () {  expect(1 + 2).toEqual(3); }); });

以下是关于前面的测试脚本的重要事项:

  1. 我们编写一个describe语句来描述测试脚本。

  2. 然后我们使用it语句和相应的方法定义一个测试脚本。

  3. expect语句中,我们断言两个数字,并使用toEqual测试两个数字的相加是否等于3

使用 Jasmine 测试 Angular 组件

现在是时候使用 Jasmine 框架创建我们的测试套件了。在第一部分“理解和设置用于测试的 Angular CLI”中,我们使用ng命令创建了TestAppComponent组件和test-app.component.ts文件。我们将在本节中继续使用相同的内容。

要开始,请添加以下代码文件的所有内容:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { TestAppComponent } from './test-app.component';

describe('Testing App Component', () => {
   it('Test learning component', () => {
    let component = new TestAppComponent();
    expect(component).toBeTruthy();
   });
});

让我们逐步分析前面的测试套件步骤。在代码块中遵循的步骤如下:

  1. 在第一步中,我们从@angular/core/testing导入了所有所需的测试模块。

  2. 我们导入了新创建的组件TestAppComponent

  3. 我们通过编写一个带有名称的describe语句Testing App Component来创建了一个测试套件。

  4. 我们使用it和相应的方法() =>编写了一个测试脚本。

  5. 我们创建了一个TestAppComponent类的component对象。

  6. 然后我们断言返回的值是否为 true。如果将该值强制转换为boolean后得到 true,则该值为toBeTruthy

所有编写的测试套件都将以.spec.ts扩展名结尾,例如test-app.component.spec.ts

我们目前做得很好!太棒了,现在我们将运行我们的测试套件并查看其输出。

我们仍在使用 Angular CLI 工具;让我们在项目目录中使用ng命令运行测试,并在终端中运行以下命令:

ng test

命令行工具将构建整个应用程序,打开一个新的 Chrome 窗口,使用 Karma 测试运行器运行测试,并运行 Jasmine 测试套件。

Karma 测试运行器会生成一个在浏览器中执行所有测试并监视karma.conf.js中指定的所有配置的 Web 服务器。我们可以使用测试运行器来运行各种框架,包括 Jasmine 和 Mocha。Web 服务器会收集所有捕获浏览器的结果并显示给开发人员。

我们应该看到如下截图所示的输出:

如果你看到了前面的截图,恭喜你。你已成功执行了测试套件,并注意测试脚本已通过。

恭喜!现在让我们深入研究并为测试组件和服务创建更复杂的测试脚本。

使用 Jasmine 测试 Angular 组件

在我们之前的示例中,我们已经看到了编写测试脚本和测试 Angular 组件的基本示例。

在本节中,我们将探讨编写测试 Angular 组件的最佳实践。我们将使用在前一节中创建的相同组件--TestAppComponent--并通过添加变量和方法来扩展测试套件。

test-app.component.ts文件中,让我们创建一些变量并将它们映射到视图中:

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

@Component({
 selector: 'app-test-app',
 templateUrl: './test-app.component.html',
 styleUrls: ['./test-app.component.css']
})
export class TestAppComponent implements OnInit {
  public authorName = 'Sridhar';
}

让我们分析在我们的test-app.component.ts文件中编写的前面的代码:

  1. 我们创建了一个组件--TestAppComponent

  2. 我们在templateUrlstyleUrls中映射了相应的 HTML 和 CSS 文件。

  3. 我们声明了一个名为authorName的公共变量,并赋予了值'Sridhar'

现在,让我们转到test-app.component.spec.ts。我们将编写我们的测试套件,并定义一个测试用例来验证authorName是否与传递的字符串匹配:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TestAppComponent } from './test-app.component';

 describe('TestAppComponent', () => {
  it('Testing App component', () => {
   let component = new TestAppComponent();
   expect(component.authorName).toMatch('Sridhar');
  });
});

让我们分析在test-app.component.spec.ts文件中前面的代码片段。已遵循以下步骤来编写代码块:

  1. 我们导入了所有必需的模块asynccomponentFixtureTestBed来运行测试。

  2. 我们通过编写describe语句并分配Testing App Component名称来创建了一个测试套件。

  3. 我们创建了一个测试用例,并创建了TestAppComponent类的新实例。

  4. expect语句中,我们断言authorName变量是否与字符串匹配。结果将返回 true 或 false。

很好!到目前为止,一切顺利。现在,继续阅读。

是时候将其提升到下一个级别了。我们将向component类添加新方法,并在specs文件中对它们进行测试。

test-app.component.ts文件中,让我们添加一个变量和一个方法:

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

@Component({
 selector: 'app-test-app',
 templateUrl: './test-app.component.html',
 styleUrls: ['./test-app.component.css']
})
export class TestAppComponent {
 public authorName = 'Sridhar';
 public publisherName = 'Packt'

 public hiPackt() {
 return 'Hello '+ this.publisherName;
 }
}

让我们创建test-app.component.spec.ts文件,并测试在component类中定义的变量和方法。

test-app.component.spec.ts文件中,添加以下代码行:

it('Testing Component Method', () => {
 let component = new TestAppComponent();
 expect(component.hiPackt()).toBe("Hello Packt");
});

让我们详细分析前面的代码片段。已遵守以下步骤:

  1. 我们创建了一个测试用例,并创建了TestAppComponent类的component实例。

  2. expect语句中,我们断言并验证传递的字符串是否与hiPackt方法的返回值匹配。

在运行前面的测试脚本之前,让我们也快速看一下另一个测试用例:

describe('TestAppComponent', () => {  beforeEach(function() {
  this.app = new TestAppComponent();
 });  it('Component should have matching publisher name', function() {
  expect(this.app.publisherName).toBe('Packt');
 }); });

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

  1. 我们实现了beforeEach Jasmine 方法。我们在每个测试脚本之前创建一个AppComponent的实例。

  2. 我们编写了一个测试脚本,并使用了组件的实例,也就是this.app,我们获取了publisherName变量的值,并断言publisherName变量的值是否与toBe('Packt')匹配。

现在,测试应该自动构建,否则调用ng test来运行测试。

我们应该看到以下截图:

太棒了!您学会了编写测试脚本来测试我们的 Angular 组件,包括变量和方法。

您学会了使用 Jasmine 框架的一些内置方法,比如beforeEachexpecttoBeTruthytoBe

在下一节中,我们将继续学习高级技术,并编写更多的测试脚本,以更详细地测试 Angular 组件。

测试 Angular 组件-高级

在本节中,我们将更深入地探讨并学习测试 Angular 组件的一些更重要和高级的方面。

如果你注意到,在前面部分的示例中可以注意到以下内容:

  1. 我们在每个测试用例中单独创建了对象的实例。

  2. 我们必须为每个测试用例单独注入所有的提供者。

相反,如果我们可以在每个测试脚本之前定义组件的实例,那将是很好的。我们可以通过使用TestBed来实现这一点--这是 Angular 提供的用于测试的最重要的实用程序之一。

TestBed

TestBed是 Angular 提供的最重要的测试实用程序。它创建了一个 Angular 测试模块--一个@NgModule类,我们可以用于测试目的。

由于它创建了一个@NgModule,我们可以定义提供者、导入和导出--类似于我们常规的@NgModule配置。

我们可以在asyncsync模式下配置TestBed

  • 为了异步配置TestBed,我们将使用configureTestingModule来定义对象的元数据。

  • 为了同步配置TestBed,我们将根据前面部分的讨论定义组件的对象实例。

现在,让我们看一下以下代码片段:

beforeEach(() => {  fixture = TestBed.createComponent(AppComponent);
  comp = fixture.componentInstance;
  de = fixture.debugElement.query(By.css('h1'));
 });

在前面的代码片段中需要注意的重要事项:

  1. 我们定义了beforeEach,这意味着这段代码将在每个测试用例运行之前运行。

  2. 我们使用TestBed创建了一个组件实例。

  3. 使用TestBed同步方式,我们定义了一个fixture变量,它创建了组件AppComponent

  4. 使用componentInstance,我们创建了一个comp变量,它是AppComponent的一个测试实例。

  5. 使用debugElement函数,我们可以在视图中定义和定位特定的元素。

  6. 使用debugElement,我们可以通过 CSS 元素选择器来定位单个元素。

现在,使用前面的beforeEach方法,该方法具有组件实例,我们将创建用于测试 Angular 组件的测试脚本。

示例 - 使用变化检测编写测试脚本

在本节中,我们将继续编写一些带有变化的测试脚本单元测试。我们还将实现变化检测和元素跟踪。

让我们开始创建一个简单的app.component.ts组件:

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

@Component({
 selector: 'test-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.css']
})

export class AppComponent {
 title = 'Packt Testing works';
}

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

  1. 我们创建了一个AppComponent组件类。

  2. 我们声明了一个具有值的title变量。

  3. 我们将组件的模板和样式文件映射到它们各自的templateUrlstyleUrls

app.component.html中,添加以下代码:

<h1> {{ title }} </h1>

在上述代码中,我们正在添加一个<h1>标签并映射title变量。

现在,是时候创建我们的测试脚本,其中包含多个断言。但在编写测试脚本之前,让我们了解用例:

  1. 我们将编写脚本来检查是否创建了ChangeDetectTestComponent

  2. 我们将编写断言来检查title是否等于Packt Testing works

  3. 最后,我们将检查变化检测并验证h1标记是否应呈现并包含值Packt Testing works

  4. 我们还将利用querySelector来定位特定的元素并匹配值。

现在,让我们来看看前面用例的测试脚本:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectTestComponent } from './change-detect-test.component';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

describe('ChangeDetectTestComponent', () => {

 let comp:ChangeDetectTestComponent;
   let fixture: ComponentFixture<ChangeDetectTestComponent>;
   let de:DebugElement;
   let el:HTMLElement;

 beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ ChangeDetectTestComponent ]
    });
    fixture = TestBed.createComponent(ChangeDetectTestComponent);
    comp = fixture.componentInstance;
    de = fixture.debugElement.query(By.css('h1'));
    el = de.nativeElement;
  });

it('should have as title 'Packt Testing works!'', async(() => {
   const fixture = TestBed.createComponent(ChangeDetectTestComponent);
   const app = fixture.debugElement.componentInstance;
   expect(app.title).toEqual('Packt Testing works');
 }));

it('should render title in a h1 tag', async(() => {
  const fixture = TestBed.createComponent(ChangeDetectTestComponent);
  fixture.detectChanges();
  const compiled = fixture.debugElement.nativeElement;
  expect(compiled.querySelector('h1').textContent).toContain('Packt   
    Testing works');
 }));
});

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

  1. 我们从angular/core/testing中导入所需的模块,即TestBedComponentFixtureasync

  2. 我们定义beforeEach并初始化变量fixturecompde

  3. 在第一个测试脚本中,我们为组件编写了一个简单的期望语句,即tobeTruthy

  4. 在第二个测试脚本中,我们通过TestBed.createComponent创建了组件的实例。

  5. 使用debugElement,我们创建了已创建组件的实例,即app

  6. 使用app组件的实例,我们能够获取组件的title并断言toEqual

  7. 在最后一个测试脚本中,我们使用async方法。我们利用debugElementnativeElement方法并定位一个元素--在我们的情况下是<h1>,并检查标题是否包含Packt Testing Works

  8. 第二个和第三个测试脚本之间的区别在于我们使用了async方法,并等待变化被检测--detectChanges--在第三个测试脚本中。

运行测试,我们应该看到如下截图所示的输出:

在本节中,您学会了如何使用beforeEach为所有测试脚本创建一个组件实例,以及如何使用nativeElement来定位任何元素。

我们使用detectChanges方法来识别元素中发生的变化。

在接下来的部分,我们将继续学习有关 Jasmine 框架测试 Angular 服务的更多知识。

测试 Angular 服务

在本节中,我们将学习有关测试 Angular 服务的知识。

在大多数 Angular 应用程序中,编写服务是一个重要且核心的方面,因为它执行与后端服务的交互;创建和共享组件之间的数据,并且在长期内易于维护。因此,确保我们彻底测试我们的 Angular 服务同样重要。

让我们学习如何编写测试脚本来测试我们的服务。为了测试一个服务,让我们首先使用ng命令创建一个服务。

在您的终端中运行以下命令:

ng g service ./test-app/test-app

上述命令将在test-app文件夹中生成test-app.service.tstest-app.service.spec.ts文件。

服务是可注入的,这意味着我们必须将它们导入到它们各自的组件中,将它们添加到提供者列表中,并在组件构造函数中创建服务的实例。

我们修改test-app.service.ts并向其中添加以下代码:

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

@Injectable()
export class TestAppService {

  getAuthorCount() {
    let Authors =[
      {name :"Sridhar"},
      {name: "Robin"},
      {name: "John"},
      {name: "Aditi"}
   ];
  return Object.keys(Authors).length;
 };
}

从上述代码片段中注意以下重要事项:

  1. 我们从 Angular 核心中导入了injectable

  2. 我们定义了@injectable元数据,并为我们的服务创建了一个类--TestAppService

  3. 我们定义了getAuthorCount方法来返回作者的数量。

我们需要将服务类导入并注入到组件中。为了测试上述服务,我们将在test-app.service.specs.ts文件中编写我们的测试脚本。

我们编写测试服务的方式与编写测试组件的方式类似。

现在,让我们通过在test-app.service.spec.ts文件中添加以下代码来创建测试套件以测试一个服务:

import { TestBed, inject } from '@angular/core/testing';
import { TestAppService } from './test-app.service';

describe('TestAppService', () => {
 beforeEach(() => {
 TestBed.configureTestingModule({
 providers: [TestAppService]
 });
 });

 it('Service should return 4 values', inject([TestAppService], 
  (service: TestAppService) => {
     let countAuthor = service.getAuthorCount;
     expect(countAuthor).toBe(4);
 }));

});

上述代码的分析如下:

  1. 我们将所需的模块TestBedinject导入到spec文件中。

  2. 我们将TestAppService服务导入spec文件。

  3. 使用依赖注入DI),我们创建了TestAppServiceservice实例。

  4. 我们创建一个测试用例;我们需要注入服务,调用getAuthorCount方法,并断言该值是否等于4

当我们运行测试时,以下截图显示了输出:

在本节中,您学习了使用 Jasmine 测试脚本对 Angular 组件和服务进行单元测试。

我们必须在每个测试用例中使用 DI 来注入服务。

测试 Angular 服务-模拟后端服务

在前面的部分,您学习了如何编写测试脚本来测试我们的 Angular 服务。在本节中,我们将编写一个测试脚本,并学习如何在实时项目中模拟后端服务。

以下是我们将为其编写测试脚本的用例:

  1. 编写一个测试脚本来测试服务中的方法。

  2. 编写一个测试脚本来检查方法的返回值是否包含特定值。

  3. 编写一个测试脚本来模拟后端连接使用mockBackend,并检查目标 URL 是否正确。

  4. 编写一个测试脚本来为请求 URL 设置mockResponse

  5. 最后,调用service中编写的方法并映射响应,这应该等于mockResponse

让我们创建我们的服务test.service.ts文件,并将以下代码添加到其中:

import { Injectable } from  '@angular/core'; import { Http } from  '@angular/http'; import { Observable } from  'rxjs'; import  'rxjs/add/operator/map'; @Injectable() export  class  TestService {
 constructor (private  http: Http) {}

 getpublications() {
    return ['Packt', 'Packt PDF', 'Packt Video'];
  }

  getproducts() {
    return  this.http.get('someurl1').map((response) =>  response);
  }

 search(term: string): Observable<any> {
   return  this.http.get(
      'someurl'
    ).map((response) =>  response.json());
  }
}

在前面的代码片段中需要注意的重要事项如下:

  1. 我们将所需的模块导入spec文件,即从Angular/core导入injectable

  2. 我们将所需的模块导入spec文件,即从Angular/http导入Http

  3. 我们将所需的模块导入spec文件,即从Angular/rxjs导入Observable

  4. 我们正在为TestService创建组件类。

  5. 我们正在使用@injectable装饰器,这将允许服务被注入到任何组件或服务中。

  6. 在构造函数中,我们注入HTTP服务并创建一个 HTTP 实例。

  7. 我们正在创建三个方法:getPublicationsgetProductssearch

  8. getProducts中,我们正在进行 HTTP 调用,当然,我们使用它来模拟服务器 URL。

  9. 我们正在将 HTTP 请求的响应映射到response变量。

现在我们的服务准备就绪,我们可以开始编写我们的测试规范文件来测试变量和方法。

spec文件中编写测试脚本之前,让我们创建一个beforeEach方法,其中将包含所有的初始化,并在每个测试脚本之前注册提供者:

  beforeEach(() => {  TestBed.configureTestingModule({
  imports: [ HttpModule ],  providers: [ {  provide:  XHRBackend,
  useClass:  XHRBackend
 }, TestService ]
 }); });

就像我们为测试 Angular 组件定义了beforeEach方法一样,我们也为服务定义了beforeEach方法。在提供者数组配置中,我们正在注册XHRBackend类。

由于服务依赖于其他模块并需要提供者,我们需要使用configureTestingModule来定义和注册所需的服务。

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

  1. 我们正在定义一个beforeEach方法,它将在每个测试脚本之前执行。

  2. 使用TestBed,我们正在使用configuringTestingModule配置测试模块。

  3. 由于configureTestingModule中传递的参数类似于传递给@NgModule装饰器的元数据,我们可以指定提供者和导入项。

  4. imports中,我们导入HttpModule

  5. 我们在提供者列表中配置所需的依赖项--XHRBackendTestService

  6. 我们正在注册一个提供者,使用一个注入令牌XHRBackend并将提供者设置为XHRBackend,这样当我们请求提供者时,DI 系统会返回一个XHRBackend实例。

现在我们可以创建spec文件test.service.spec.ts,并将以下代码添加到文件中:

import {TestService} from  './test.service'; import { TestBed, inject } from  '@angular/core/testing'; import { MockBackend, MockConnection} from  '@angular/http/testing'; import { HttpModule,XHRBackend, ResponseOptions,Response, RequestMethod } from  '@angular/http'; const  mockResponse = { 'isbn':  "123456",
  'book': {  "id":  10,
  "title":  "Packt Angular"
 } }; const  mockResponseText = 'Hello Packt'; describe('service: TestService', () => {  beforeEach(() => {  TestBed.configureTestingModule({
  imports: [ HttpModule ],  providers: [ {  provide:  XHRBackend,
  useClass: XHRBackend  }, TestService]
 }); });  it('Service should return 4 publication values',    
    inject([TestService, XHRBackend], (service: TestService, 
      XHRBackend: XHRBackend) => {  let  names = service.getpublications();
  expect(names).toContain('Packt');
  expect(names).toContain('Packt PDF');
  expect(names).toContain('Packt Video');
  expect(names.length).toEqual(3);
 }));  it('Mocking Services with Json', inject([TestService, XHRBackend], 
     (service: TestService, XHRBackend: XHRBackend) => {  const  expectedUrl = 'someurl';
 XHRBackend.connections.subscribe(
 (connection: MockConnection) => {  expect(connection.request.method).toBe(RequestMethod.Get);
  expect(connection.request.url).toBe(expectedUrl);
  connection.mockRespond(new  Response(
  new  ResponseOptions({ body:  mockResponse }) )); });  service.getbooks().subscribe(res  => {  expect(res).toEqual(mockResponse);
 }); })); });

这是一个很长的代码片段,让我们分解进行分析:

  1. 我们将TestService服务文件导入到spec文件中。

  2. 我们从@angular/core/testing中导入所需的模块TestBedinject

  3. 我们从@angular/http/testing中导入模块MockBackendMockConnection

  4. 我们从@angular/http中导入模块HttpModuleXHRBackendResponseOptionsResponseRequestMethod

  5. 我们定义了一个mockResponse变量,其中包含一个临时的json对象。

  6. 我们还定义了一个mockResponseText变量并为其赋值。

  7. 我们将使用之前定义的beforeEach方法,通过它我们将注册所有的提供者和依赖项。

  8. 在第一个测试脚本中,我们将TestService实例注册为service,将XHRBackend实例注册为XHRBackend

  9. 我们调用service.getpublications()方法,它将返回数组。

  10. 在结果名称中,我们断言值应包含作为测试数据传递的字符串。

  11. 在第二个测试脚本中,我们使用mockBackend创建连接,并使用subscribe传递请求的methodurl

  12. 使用mockRespond连接,我们将响应值设置为mockResponse

  13. 我们还调用getbooks方法,映射响应,并断言toEqual值为mockResponse

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

如果你看到了前面的截图,那太棒了。

到目前为止,在本节中,你已经学习并探索了 Jasmine 框架及其用于测试 Angular 组件和服务的内置方法。

我们讨论了测试 Angular 组件:测试变量和方法。我们还讨论了如何编写beforeEach方法,在每个测试脚本之前执行,并如何创建组件的实例并访问其属性。我们还介绍了如何使用 Jasmine 框架测试 Angular 服务以及测试 Angular 服务及其属性:变量和方法。

对于测试 Angular 服务,你学会了如何创建一个beforeEach方法,在每个测试脚本之前执行,并且在每个测试脚本之前创建提供者和依赖项。

你学会了通过模拟服务来测试后端服务。当你独立开发 Angular 服务和组件时,这非常有用。

在下一节中,你将学习如何使用 Protractor 框架进行端到端测试。

Protractor 框架简介

在前面的部分中,你学习了使用 Jasmine 进行单元测试。在本节中,你将学习如何使用 Protractor 框架进行 Angular 应用程序的端到端测试。

这就是官方网站如何解释 Protractor 的。

Protractor 是一个用于 Angular 和 AngularJS 应用程序的端到端测试框架。Protractor 在真实浏览器中运行测试,与用户交互。

Protractor 框架打包在 Angular CLI 工具中,我们可以在主项目目录中找到创建的e2e文件夹:

你将学习为你的 Angular 应用程序编写端到端测试,并将它们保存在e2e文件夹下。

记住,最好的做法是为每个功能或页面创建单独的 E2E 脚本。

Protractor - 快速概述

Protractor 是 Selenium WebDriver 的封装,提供了许多内置的类和方法,我们可以用来编写端到端测试。

Protractor API 主要公开了各种类和方法,主要围绕BrowserElementLocatorsExpectedConditions

Protractor 支持 Chrome、Firefox、Safari 和 IE 的最新两个主要版本,这意味着我们可以编写测试脚本并在任何/所有可用的主流浏览器上运行它们。

为了编写端到端测试,我们需要定位页面中的元素,读取它们的属性,更新属性,并调用附加到元素的方法,或者发送和验证数据。

我们将讨论 Protractor 框架中提供的各种类和方法,通过这些方法,我们可以编写端到端测试来自动化应用程序功能。

让我们了解一下可用的方法和类,我们可以使用 Protractor 框架。

Protractor 和 DOM

在本节中,您将学习如何使用 Protractor 与页面中的 DOM 元素进行交互。

Protractor API 支持并公开了用于定位页面中元素的类和方法。我们需要明确说明我们是需要定位特定元素,还是期望返回一组元素。

element函数用于在网页上查找 HTML 元素。它返回一个ElementFinder对象,可用于与元素交互或获取有关其属性和附加方法的信息。

我们需要动态地在页面中查找、编辑、删除和添加元素及其属性。但是,要实现这些用例,我们需要首先定义并找到目标元素。

我们可以使用以下方法定义目标元素:

  • element:此方法将返回单个/特定元素:
element( by.css ( 'firstName' ) );

  • element.all:此方法返回一个元素集合:
element.all(by.css('.parent'))

使用上述方法,我们可以定位页面中的任何元素。在下一节中,您将学习可以与elementelement.all方法一起使用的可用方法。

一些可用于选择元素的方法

在前面的部分中,我们看到了一系列最常用的方法,用于选择或定位页面中的元素或多个元素。

要使用前面讨论的方法,您需要明确说明您是需要定位特定元素,还是期望返回一组元素。

在本节中,让我们了解一下在测试脚本中定位/选择元素的可用方法和方式。我们可以一次定位一个或多个元素。

我们可以使用几乎所有的属性、属性和自定义指令来定位特定的元素。

让我们看一下在测试脚本中定位元素的一些方法:

  • by.css:我们可以传递 CSS 选择器来选择一个或多个元素:
element( by.css('.firstName' ) );

CSS选择器是定位和选择元素最常用的方法。

  • by.model:我们使用这个来选择或定位使用绑定到元素的ng-model名称的元素:
element( by.model ( 'firstName' ) );

请注意,官方文档仍建议使用 CSS 选择器而不是模型。

  • by.repeater:我们使用这个方法来选择使用ng-repeat指令显示的元素:
element( by.repeater('user in users').row(0).column('name') );

  • by.id:我们使用这个方法来使用它的 ID 选择一个元素:
element( by.id( 'firstName' ) );

  • by.binding:使用这个来选择与单向或双向 Angular 绑定相关的元素:
element( by.binding( 'firstName' ) );

  • by.xpath:使用这个来通过xpath遍历选择元素:
element(by.css('h1')).element(by.xpath('following-
  sibling::div'));

  • first()last()或特定元素:我们使用这些方法来获取特定位置或索引处的元素:
 element.all(by.css('.items li')).first();

我们了解了一些方法,可以使用它们的属性和信息来定位元素。有关可用方法的完整列表,请参阅 GitHub 上 Protractor 的官方文档。

在下一节中,您将了解可以使用的各种内置方法,以编写测试脚本来自动化应用程序逻辑。

探索 Protractor API

在本节中,您将了解 Protractor API 中各种内置类和方法,我们可以用来编写我们的测试脚本。

Protractor API 具有许多预定义的内置属性和方法,用于支持BrowserElementLocatorsExpectedConditions

它提供了许多内置方法,从点击事件到设置输入表单的数据,从获取文本到获取 URL 详细信息等等,以模拟应用程序页面中的操作和事件。

让我们快速看一下一些可用的内置方法来模拟用户交互:

  • click:使用这个方法,我们可以安排一个命令来点击这个元素。该方法用于模拟页面中的任何点击事件:
element.all( by.id('sendMail') ).click();

  • getTagName:这会获取元素的标签/节点名称:
element(by.css('.firstName')).getTagName()

  • sendKeys:使用这个方法,我们可以安排一个命令在 DOM 元素上输入一个序列:
element(by.css('#firstName')).sendKeys("sridhar");

  • isDisplayed:使用此方法,我们可以安排一个命令来测试此元素当前是否显示在页面中:
element(by.css('#firstPara')).isDisplayed();

  • Wait:使用此方法,我们可以执行一个命令来等待条件保持或承诺被解决:
browser.wait(function() {
  return true;
}).then(function () {
  // do some operation
});

  • getWebElement:使用此方法,我们可以找到由此ElementFinder表示的网页元素:
element(by.id('firstName')).getWebElement();

  • getCurrentUrl:使用此方法,我们可以检索当前应用程序页面的 URL。此方法与browser模块一起使用:
var curUrl = browser.getCurrentUrl();

有关属性和方法的完整列表,请参考 GitHub 上 Protractor 的官方文档。

在本节中,您了解了一些可用于编写测试脚本和在页面中自动化应用程序工作流程的方法。

我们将通过示例学习在以下部分中使用一些内置方法。在下一节中,我们将开始使用 Protractor 编写测试脚本。

Protractor - 初步

在本节中,让我们开始使用 Protractor 编写测试脚本。我们将利用本章前面看到的方法和元素定位来编写我们的测试脚本。

Protractor 框架测试套件的一般语法如下:

describe("Sample Test Suite", function() {
 it("This is a spec that defines test", function() {
     // expect statement to assert the logic etc
 });
});

分析上述代码片段,您会意识到它与我们为 Jasmine 测试脚本创建的非常相似。太棒了!

为 Jasmine 和 Protractor 编写的测试套件看起来很相似。主要区别在于我们通过elementbrowser模块来定位页面中的任何特定 DOM 元素。

现在,在app.e2e-specs.ts文件中,我们编写我们的第一个端到端测试脚本;将以下代码片段添加到文件中:

import {element, by, browser} from 'protractor';

  describe('dashboard App', () => {
   it('should display message saying app works', () => {
    browser.get('/');
    let title = element(by.tagName('h1')).getText();
    expect(title).toEqual('Testing E2E');
   });
});

让我们详细分析上述代码片段。已遵循以下步骤:

  1. 我们正在从protractor库中导入所需的模块elementbybrowser到我们的测试脚本中。

  2. 使用describe语句,我们为我们的端到端测试规范分配一个名称,并为其编写specDefinitions

  3. 我们使用it语句定义一个测试脚本,并在函数中使用browser导航到主页并检查<H1>标签和值是否等于Testing E2E

我们已经定义了我们的e2e测试脚本;现在让我们使用ng命令运行测试,如下所示:

ng e2e

上述命令将运行,调用浏览器,执行e2e测试脚本,然后关闭浏览器。

您应该在终端中看到以下结果:

如果您看到所有测试脚本都通过了,那么我们所有的 E2E 测试都通过了。恭喜!

该命令需要在项目目录的父目录中运行。

使用 Protractor 编写 E2E 测试

在前面的部分中,您学会了如何使用 Protractor 编写您的第一个测试脚本。在本节中,我们将扩展我们的示例,并为其添加更多内容。

让我们来看看我们在示例中将自动化的用例:

  1. 我们将检查我们的主页是否具有标题Testing E2E

  2. 我们将检查页面上是否显示了具有firstPara ID 的元素。

  3. 我们将断言具有firstPara ID 的元素的class属性是否等于'custom-style'

  4. 最后,我们读取页面的当前 URL,并检查它是否等于我们在断言中传递的值。

现在让我们为此编写我们的 E2E 规范。在app.e2e.spec.ts文件中,添加以下代码行:

import { browser, by, element } from 'protractor';

describe('Form automation Example', function() {
 it('Check paragraphs inner text', function() {
    browser.get('/first-test');
    var s = element(by.css('#firstPara')).getText();
    expect(s).toEqual('Testing E2E');
  });

 it('Should check for getAttribute - class', function() {
    browser.get('/first-test');
    var frstPa = element(by.id('firstPara'));
    expect(frstPa.getAttribute('class')).toEqual('custom-style');
  });

 it('Should check element for isDisplayed method', function() {
    browser.get('/first-test');
    var ele = element(by.css('#firstPara')).isDisplayed();
    expect(ele).toBeTruthy();
  });

 it('Check the applications current URL', function() {
    var curUrl = browser.getCurrentUrl();
    expect(curUrl).toBe('http://localhost:49152/first-test');
  });

});

前面代码的分解和分析如下:

  1. 我们从protractor导入了所需的模块elementbybrowser

  2. 我们编写了一个describe语句,创建了一个名为“表单自动化示例”的测试套件。

  3. 对于第一个测试脚本,我们告诉protractor使用browser通过get方法导航到/first-test URL。

  4. 我们获得了具有idfirstPara的元素及其文本,并检查其值是否等于Testing E2E

  5. 在第二个测试脚本中,我们使用get方法导航到 URL/first-test,并获得具有idfirstPara的相同元素。

  6. 现在使用getAttribute方法,我们获取元素的class属性,并检查其值是否与'custom-style'匹配。

  7. 在第三个测试脚本中,我们告诉protractor使用browser通过get方法导航到/first-test URL。

  8. 使用isDisplayed方法,我们检查元素是否在页面上显示。

  9. 在第四个测试脚本中,我们告诉protractor使用browser方法getCurrentUrl来获取页面的currentUrl

  10. 我们检查currentUrl是否与测试脚本中传递的值匹配。

为了运行端到端测试,我们将使用ng命令。在项目目录中,运行以下命令:

ng e2e

以下截图显示了一旦所有测试通过后我们将看到的输出:

创建和运行测试是多么简单和容易,对吧?

这是一个很好的开始,我们将继续学习使用高级技术编写更多的测试脚本。

继续前进,编写自动化测试脚本来插入你的逻辑和应用程序。

使用 Protractor 编写 E2E 测试-高级

到目前为止,在之前的章节中,我们已经涵盖了使用 Protractor 框架安装、使用和编写测试脚本。我们已经学习并实现了 Protractor API 公开的内置方法和类。

在本节中,我们将介绍编写高级测试脚本,这些脚本将在页面中进行交互,并对元素进行彻底测试。

让我们看一下我们将涵盖的用例:

  1. 我们将测试我们的数组数值。

  2. 我们将使用class属性来定位我们的元素。

  3. 我们将检查页面的标题。

  4. 我们将模拟附加在按钮上的click事件,然后验证另一个元素的文本更改。

让我们开始编写我们的测试脚本。

我们需要首先创建我们的test-app.component.html文件。创建文件,并将以下代码添加到文件中:

<h3 class="packtHeading">Using protractor - E2E Tests</h3>

<input id="sendEmailCopy" type="checkbox"> Send email copy

<!-- paragraph to load the result -->
<p class="afterClick">{{afterClick}}</p>

<!-- button to click -->
<button (click)="sendMail()">Send mail!</button>

上述代码片段的分析如下:

  1. 我们定义了一个h3标题标签,并分配了一个class属性,值为packtHeading

  2. 我们创建了一个 ID 为sendEmailCopyinput类型checkbox元素。

  3. 我们定义了一个带有class属性为afterClick的段落p标签,并绑定了{{ }}中的值。

  4. 我们定义了一个button并附加了一个click事件来调用sendMail方法。

  5. sendMail方法的目的是改变paragraph标签内的文本。

现在我们已经定义了模板文件,是时候创建我们的组件文件了。

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

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

@Component({ 
 selector: 'app-test-app',
 templateUrl: './test-app.component.html',
 styleUrls: ['./test-app.component.css']
})

export class TestAppComponent { 
  constructor() {} 

  public myModel = "Testing E2e";
  public authorName = 'Sridhar';
  public publisherName = 'Packt';
  public afterClick = 'Element is not clicked';

  public hiPackt() {
    return 'Hello ' + this.publisherName;  
  }
  public sendMail() {
   this.afterClick = 'Element is clicked';
  }
}

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

  1. 我们从@angular/core导入了ComponentOninit模块。

  2. 我们还从@angular/forms导入了FormsModule

  3. 我们创建了Component并将 HTML 和 CSS 文件分别关联到templateUrlstylesUrl

  4. 我们定义了myModelauthorNamepublisherNameafterClick变量。

  5. 我们为定义的变量赋值。

  6. 我们定义了一个hiPackt方法,它将显示Hello Packt

  7. 我们定义了一个sendMail方法,当调用时将更新afterClick变量的值。

到目前为止,一切顺利。跟着我继续;我们很快就要编写出漂亮的测试脚本了。

现在,我们已经定义了模板文件并实现了组件文件;我们非常了解组件的功能。现在是时候开始测试部分了。

让我们创建测试规范app.e2e.spec.ts文件,并将以下代码片段添加到其中:

import {element, by, browser} from 'protractor';

describe('dashboard App', () => {
 beforeEach(function () {
   browser.get('/test-app');
 });

 it('should display message saying app works', () => {
  const title = element(by.tagName('h1')).getText();
  expect(title).toEqual('Learning Angular - Packt Way');
 });

 it('should display message saying app works', () => {
  element(by.tagName('button')).click();
  const title = element(by.css('.afterClick')).getText();
  expect(title).toEqual('Element is not clicked');
 });

 it('Should check is radio button is selected or deselected',  
  function() {
    var mailCopy = element(by.id('sendEmailCopy'));
    expect(mailCopy.isSelected()).toBe(false);
    mailCopy.click();
    expect(mailCopy.isSelected()).toBe(true);
 });

 it('Check the applications current URL', function() {
   var curUrl = browser.getCurrentUrl();
   expect(curUrl).toBe('http://localhost:49152/test-app');
 });

});

让我们详细看看我们的测试规范中发生了什么:

  1. 我们定义了一个beforeEach方法,它将在测试脚本之前执行,并打开浏览器 URL。

  2. 现在,我们编写一个测试脚本来测试h1标签的title值,使用断言toEqual

  3. 在第二个测试脚本中,我们使用tagName获取button元素,并调用click方法。

  4. 由于方法是clicked,段落的值已经更新。

  5. 我们将使用by.css检索段落元素,并获取其中的段落文本value

  6. 我们断言新更新的value是否等于Element is clicked

  7. 在第三个测试脚本中,我们使用isSelected方法检查input元素类型checkbox是否被选中。

  8. 使用click方法,我们现在切换checkbox并再次检查值。这个测试脚本是为了向您展示如何操作表单元素。

  9. 最后,在最后一个测试脚本中,我们使用getCurrentUrl获取当前页面的 URL,并检查它是否匹配/test-app

就这样,全部完成了。现在,我们已经有了模板文件,创建了组件,也有了测试规范文件。

现在是展示时间。让我们运行应用程序,我们应该看到以下截图中显示的输出:

在本节中,您学会了使用 Protractor 框架编写测试脚本。我们探索了框架中所有内置的可用方法,供我们在编写脚本时使用。

我们注意到编写的测试脚本与 Jasmine 测试脚本类似。我们还看到了如何使用各种方法(如by.cssby.bindingby.id)来定位特定元素或元素集合。

我们讨论了使用 Protractor 框架进行事件处理和绑定。

总结

测试是应用程序开发中最关键和重要的方面之一。在本章中,您学习了如何使用 Angular CLI、Jasmine 和 Protractor 框架。使用 Jasmine 和 Protractor 进行自动化测试可以帮助您节省时间和精力。

您学习了为 Angular 组件和服务编写单元测试脚本,以及如何为工作流自动化测试编写 E2E 测试用例。您详细了解了 Jasmine 框架和 Protractor 框架中内置到函数中的方法和变量。

我们深入研究了针对特定元素的定位,以及一起检索元素集合以读取、更新和编辑属性和数值。继续使用这些出色的测试框架来自动化您的应用程序。

在下一章中,您将学习 Angular 中的设计模式。Typescript 是一种面向对象的编程语言,因此我们可以利用几十年关于面向对象架构的知识。您还将探索一些最有用的面向对象设计模式,并学习如何在 Angular 中应用它们。

第十六章:Angular 中的设计模式

TypeScript 是一种面向对象的编程语言,因此我们可以利用几十年的面向对象架构知识。在这一章中,我们将探讨一些最有用的面向对象设计模式,并学习如何在 Angular 中应用它们。

Angular 本身是一个面向对象的框架,它强制你以某种方式进行大部分开发。例如,你需要有组件、服务、管道等。强制你使用这些构建块有助于构建良好的架构。这很像 Zend 框架为 PHP 或 Ruby on Rails 为 Ruby 所做的事情。框架的存在是为了让你的生活更轻松,加快开发时间。

虽然 Angular 的设计方式远远超出平均水平,但我们总是可以做得更好。我并不是说我在这一章中提出的设计是最终的,你可以用它来解决从面包店的一页纸到火星一号任务的仪表板的任何问题--这样的设计并不存在--但它确实会提高你的工具箱。

在这一章中,我们将学习使用以下模式:

  • 模型-视图-控制器(MVC)

  • 单例

  • 依赖注入

  • 原型

  • 可重用池

  • 工厂

  • 备忘录

模型-视图-控制器(MVC)

哦,MVC,好老的 MVC。多年来你为我们服务得很好。现在,人们希望你退休,最好不要有麻烦。此外,即使我也能看到,更年轻的单向用户界面架构可以比你更聪明,让你看起来像是过去的遗物。

在本节中,我们将首先描述 MVC 是什么,不管用什么编程语言来实现它,然后我们将看到将 MVC 应用于前端编程的缺点。最后,我将介绍一种在 Angular 中实现有意义的 MVC 的方法,这种方法考虑了实现的便利性、维护性和性能。

MVC 的大局

MVC 设计模式的整体原则非常简单。事实上,如下图所示,它由三个部分组成:模型、视图和控制器。更具体地说,MVC 的意图是定义对象之间的一对多依赖关系,以便当一个对象改变状态时,所有依赖它的对象都会被通知并自动更新:

MVC 概述

让我们逐块分析前面的图像:

  • 模型根据控制器发送的命令存储应用程序所需的数据。

  • 控制器接收用户的操作(即点击按钮)并相应地指导模型更新。它还可以设置在任何给定时刻使用哪个视图

  • 视图模型更改时生成和更新。

就是这样。

让我们看看纯 TypeScript 中一个简单的 MVC 实现会是什么样子。

首先,让我们像我们在第十章中所做的那样定义一个Movie类,在 Angular 中的 Material Design。在这个版本的Movie类中,我们只有两个属性,titlerelease_year,它们是使用 TypeScript 构造函数定义的:

class Movie{

     constructor(private title:string, private release_year:number){}

     public getTitle():string{
         return this.title;
     }
     public getReleaseYear():number{
         return this.release_year;
     }
 }

然后,我们定义一个Model类,导入包含Movie类的movie.ts文件,使用引用关键字。这个Model类将负责更新视图,它有一个电影数组和两个方法。第一个方法addMovie(title:string, year:number)是公共的,它在 movies 属性的末尾添加一个新的电影。它还调用类的第二个方法:appendView(movie:Movie),这个方法是私有的。这个第二个方法根据 MVC 定义操纵视图。视图操纵相当简单;我们在视图的movie元素中添加一个新的li标签。新创建的li标签的内容是电影标题和发行年份的连接:

class Model{

     private movies:Movie[] = [];

     constructor(){
     }

     public addMovie(title:string, year:number){
         let movie:Movie = new Movie(title, year);
         this.movies.push(movie);
         this.appendView(movie);
     }

     private appendView(movie:Movie){
         var node = document.createElement("LI"); 
         var textnode = document.createTextNode(movie.getTitle() +
              "-" + movie.getReleaseYear()); 
         node.appendChild(textnode);
         document.getElementById("movies").appendChild(node);
     }

 }

我们现在可以为我们的纯 TypeScript MVC 定义一个控制器。控制器有一个私有的model:Model属性,在构造函数中初始化。此外,定义了一个click方法。这个方法接受一个字符串和一个数字作为参数,分别用于标题和发行年份。正如你所看到的,click方法将标题和发行年份转发给模型的addMovie方法。然后,控制器的工作就完成了。它不会操纵视图。你还会注意到controller.ts文件的最后一行:let controller = new Controller();。这行允许我们创建一个Controller类的实例,视图可以绑定到它:


 class Controller{

     private model:Model;

     constructor(){

         this.model = new Model();
     }

     click(title:string, year:number){

         console.log(title, year);
         this.model.addMovie(title, year);

     }

 }
 let controller = new Controller();

我们 MVC 实现的最后一部分是视图。我们有一个简单的 HTML 表单,提交时会调用以下内容:controller.click(this.title.value, this.year.value); return false;. 控制器在controller.ts文件中已经定义为let controller = new Controller();。然后,对于参数,我们发送this.title.valuethis.year.value,其中 this 指的是<form>

标题和年份分别指的是电影的标题和发行年份字段。我们还必须添加return false以防止页面重新加载。实际上,HTML 表单的默认行为是在提交时导航到操作 URL:

<html>
     <head>
         <script src="mvc.js"></script>
     </head>
     <body>
         <h1>Movies</h1>
         <div id="movies">
         </div>

         <form action="#" onsubmit="controller.click(this.title.value,
              this.year.value); return false;">
             Title: <input name="title" type="text" id="title">
             Year: <input name="year" type="text" id="year">
            <input type="submit">
         </form>

     </body>
 </html>

在页眉中,我们添加了通过以下命令生成的mvc.js脚本:tsc--out mvc.jscontroller.ts model.ts movie.ts。生成的 JavaScript 如下所示:

var Movie = (function () {
     function Movie(title, release_year) {
         this.title = title;
         this.release_year = release_year;
     }
     Movie.prototype.getTitle = function () {
         return this.title;
     };
     Movie.prototype.getReleaseYear = function () {
         return this.release_year;
     };
     return Movie;
 }());
 /// <reference path="./movie.ts"/>
 var Model = (function () {
     function Model() {
         this.movies = [];
     }
     Model.prototype.addMovie = function (title, year) {
         var movie = new Movie(title, year);
         this.movies.push(movie);
         this.appendView(movie);
     };
     Model.prototype.appendView = function (movie) {
         var node = document.createElement("LI");
         var textnode = document.createTextNode(movie.getTitle() +
                 "-" + movie.getReleaseYear());
         node.appendChild(textnode);
         document.getElementById("movies").appendChild(node);
     };
     return Model;
 }());
 /// <reference path="./model.ts"/>
 var Controller = (function () {
     function Controller() {
         this.model = new Model();
     }
     Controller.prototype.add = function (title, year) {
         console.log(title, year);
         this.model.addMovie(title, year);
     };
     return Controller;
 }());
 var controller = new Controller();

在执行方面,在加载时,HTML 页面将如下截图所示:

加载时的 MVC。

然后,如果您使用表单并添加电影,它将自动影响视图,并显示新的电影,如下图所示:

在使用表单后的 MVC。

前端的 MVC 限制

那么,为什么在前端编程中使用 MVC 模式不那么常见,尤其是在像 Angular 这样的框架支持下?首先,如果您正在为提供服务的应用程序使用 Angular,您很可能会有一个与之交换一些信息的后端。然后,如果您的后端也使用 MVC 设计模式,您将得到以下层次结构:

前端和后端的 MVC。

在这个层次结构中,我们在另一个 MVC 实现的顶部有一个 MVC 实现。这两种实现通过一个 API 服务进行通信,该服务向后端控制器发送请求并解析生成的视图。具体示例是,如果用户必须登录您的应用程序,他们将在由用户模型和登录控制器提供支持的前端上看到登录视图。一旦输入了所有信息(电子邮件、密码),用户就会点击登录按钮。

这个点击触发了模型更新,然后模型使用 API 服务触发 API 调用。API 服务向您的 API 的user/signin端点发出请求。在后端,请求被用户控制器接收并转发到用户模型。后端用户模型将查询您的数据库,以查看是否有提供的用户和密码匹配的用户。最后,如果登录成功,将输出一个包含用户信息的视图。回到前端,API 服务将解析生成的视图并将相关信息返回给前端用户模型。依次,前端用户模型将更新前端视图。

对于一些开发人员来说,这么多层以及架构在前端和后端上的重复似乎不太对,尽管它通过明确定义的关注点分离带来了可维护性。

双重 MVC 并不是唯一的问题。另一个问题是,前端模型不会是纯模型,因为它们必须考虑 UI 本身的变量,比如可见标签、表单有效性等等。因此,你的前端模型往往会变成一团丑陋的代码,其中 UI 变量与用户的实际表示相互交织。

现在,像往常一样,你可以避免这些陷阱,利用 MVC 模式的优势。让我们在下一节中看看如何做到这一点。

Angular 是 MVC

在本节中,我提出了一个在 Angular 中证明有效的 MVC 架构。我在toolwatch.io(Web、Android 和 iOS)过去的八个月中使用了这个架构。显然,我们在 Web 版本或移动应用上提出的功能是相同的,工作方式也相同;不同的是视图和导航方案。

下图展示了整体架构:

Angular 的 MVC。

从上到下,我们有后端、可重用的前端部分和专门的前端(即移动端或 Web 端)。正如你所看到的,在后端,没有任何变化。我们保留了传统的 MVC。请注意,前端部分也可以与非 MVC 后端一起工作。

我们的模型将使用该服务通过假想的 JSON API 从远程数据库获取、放置和删除一个简单的 TypeScript 对象。

我们的用户 TypeScript 对象如下所示:

export class User {

     public constructor(private _email:string, private _password:string){}

     get email():string{
         return this._password;
     }

     get password():string{
         return this._email;
     }

     set email (email:string){
         this._password = email;
     }

     set password (password:string){
         this._email = password;
     }

 }

这里没有太多花哨的东西。只是一个简单的 TypeScript 对象,包含两个属性:email:_stringpassword:_string。这两个属性在构造函数中使用 TypeScript 内联声明样式进行初始化。我们还利用了 TypeScript 的 getter/setter 来访问password:string_email:string属性。你可能已经注意到,TypeScript 的 getter/setter 看起来像 C#属性。嗯,微软是 TypeScript 的主要工业调查者之一,所以这是有道理的。

我确实喜欢写作的简洁性,特别是在构造函数中与内联属性声明结合在一起时。然而,我不喜欢的是必须使用下划线变量名。问题在于,再次,这个 TypeScript 将被转译成 JavaScript,在 JavaScript 中,变量和函数比如说 Java 或 C#更加抽象。

实际上,在我们当前的示例中,我们可以调用user类的 getter 如下:

user:User = new User('mathieu.nayrolles@gmail.com', 'password');

console.log(user.email); // will print mathieu.nayrolles@gmail.com

正如你所看到的,TypeScript 并不关心它调用的目标的类型。它可以是一个名为 email 的变量,也可以是一个名为email()的函数。无论哪种方式,它都可以工作,产生不同的结果,但它可以工作。这种奇怪行为背后的基本原理是,在面向对象的程序中,在 JavaScript 中,这是可以接受的:

var email = function(){
     return "mathieu.nayrolles@gmail.com";
 }
 console.log(email);

因此,我们需要用不同的名称区分函数的实际变量。因此有了下划线。

现在我们有一个完全可靠的用户对象来操作,让我们回到我们的 MVC 实现。现在我们可以有一个UserModel来操作用户普通旧 TypeScript 对象POTO)和图形界面所需的变量:

export class UserModel{

     private user:User;
     private _loading:boolean = false;

     public constructor(private api:APIService){}

     public signin(email:string, password:string){

         this._loading = true;

         this.api.getUser(new User(email, password)).then(

             user => {
                 this.user = user;
                 this._loading = false;
             }
         );
     }

     public signup(email:string, password:string){

         this._loading = true;
         this.api.postUser(new User(email, password)).then(
             user => {
                 this.user = user;
                 this._loading = false;
             }   
         );
     }

     get loading():boolean{
         return this._loading;
     }
 }

我们的模型,名为UserModel,接收了一个APIService的注入。APIService的实现留给读者作为练习。然而,它将非常类似于我们在第九章中看到的Angular 2 中的高级表单。除了APIServiceUserModel拥有user:Userloading:bool属性。user:User代表了实际的用户及其密码和电子邮件。然而,loading:bool将用于确定视图中是否应该显示加载旋转器。正如你所看到的,UserModel定义了signinsignup方法。在这些方法中,我们调用了假设的APIServicegetUserpostUser方法,它们都接受一个 User 作为参数,并返回一个包含所述用户通过 JSON API 同步的 Promise。收到 Promise 后,我们关闭loading:bool旋转器。

然后,让我们来看看控制器,它也将是 Angular 环境中的一个组件,因为 Angular 组件控制显示的视图等等。

@Component({
     templateUrl: 'user.html'
 })
 export class UserComponent{

     private model:UserModel;

     public constructor(api:APIService){

         this.model = new UserModel(api);
     }

     public signinClick(email:string, password:string){
         this.model.signin(email, password);
     }

     public signupClick(email:string, password:string){
         this.model.signup(email, password);
     }

 }

正如你所看到的,控制器(组件)很简单。我们只有一个对模型的引用,并且我们接收一个注入的APIService以传递给模型。然后,我们有signinClicksignupClick方法,从视图接收用户输入并将其传递给model。最后一部分,视图,看起来像这样:


 <h1>Signin</h1>

 <form action="#" onsubmit="signinClick(this.email.value, this.password.value); return false;">

     email: <input name="email" type="text" id="email">
     password: <input name="password" type="password" id="password">
    <input [hidden]="model.loading" type="submit">
    <i [hidden]="!model.loading" class="fa fa-spinner" 
         aria-hidden="true">loading</i>
 </form>

 <h1>Signup</h1>

 <form action="#" onsubmit="signupClick(this.email.value,
      this.password.value); return false;">

     email: <input name="email" type="text" id="email">
     password: <input name="password" type="password" id="password">
     <input [hidden]="model.loading" type="submit">
     <i [hidden]="!model.loading" class="fa fa-spinner" 
       aria-hidden="true">loading</i>
 </form>

在这里,我们有两个表单,一个用于登录,一个用于注册。这两个表单除了它们使用的onsubmit方法不同之外,都是相似的。登录表单使用我们控制器的signinClick方法,注册表单使用signupClick方法。除了这两个表单,我们还在每个表单上有一个 Font Awesome 旋转器,只有在用户模型正在加载时才可见。我们通过使用[hidden] Angular 指令来实现这一点:[hidden]="!model.loading"。同样,当模型正在加载时,提交按钮也是隐藏的。

所以,这就是一个应用于 Angular 的功能性 MVC。

正如我在本节开头所说的,对我来说,MVC 模式在 Angular 中的实际用处来自于它的可扩展性。事实上,利用 TypeScript 的面向对象的特性(以及随之而来的内容)允许我们为不同的 Angular 应用程序专门定制控制器和模型。例如,如果你有一个 Angular 网站和一个 Angular 移动应用程序,就像我在toolwatch.io中所做的那样,那么你可以在两边重用业务逻辑。当我们本可以只有一个时,如果随着时间的推移,我们需要编写和维护两个登录、两个注册和两个所有内容,那将是一件遗憾的事情!

例如,在toolwatch.io,Web 应用程序使用标准的 Angular,我们使用 Ionic2 和 Angular 构建移动应用程序。显然,我们在移动应用程序(Android 和 iOS)和网站之间共享了许多前端逻辑。最终,它们倾向于实现相同的目的和功能。唯一的区别是用于使用这些功能的媒介。

在下图中,我粗略地表示了一种利用 MVC 模式实现重用和可扩展性的完整方式:

可重用的 Angular MVC。

后端保持不变。我们在那里有相同的 MVC 模式。作为提醒,后端上的 MVC 模式完全取决于您,例如,您可以利用前端 MVC 模式与功能性的 Go 后端。在此处公开的 MVC 的先前版本不同的是引入了可重用的前端部分。在这部分中,我们仍然有一个负责消费我们的 JSON API 的 API 服务。然后,我们有一个实现IModel接口的模型:

export interface IModel{

     protected get(POTO):POTO;
     protected put(POTO):POTO;
     protected post(POTO):POTO;
     protected delete(POTO):boolean;
     protected patch(POTO):POTO;

 }

该接口定义了必须在随后的模型中实现的putpostdeletepatch方法。这些方法接受和返回的POTO类型是您程序中任何领域模型的母类。领域模型代表您的业务逻辑中可同步的实体,例如我们之前使用的用户。领域模型和 MVC 的模型部分不应混淆。它们根本不是同一回事。在这种架构中,用户将扩展POTO

这次的模型(MVC 模式)也包含一个POTO来实现IModel接口。此外,它包含您需要更新视图的变量和方法。模型本身的实现如我在本节前面所示,相当简单。然而,我们可以通过利用 TypeScript 的通用方面并设想以下内容来提高一些东西:

export class AbstractModel<T extends POTO> implements IModel{
     protected T domainModel;

     public constructor(protected api:APIService){}

     protected get(POTO):T{
         //this.api.get ...
     };
     protected put(T):T{
         //this.api.put...
     };
     protected post(T):T{
         //this.api.post...
     };
     protected delete(T):boolean{
         //this.api.delete...
     };
     protected patch(T):T{
         //this.api.patch...
     };
}

export class UserModel extends AbstractModel<User>{

     public constructor(api:APIService){
         super(api);
     }

     public signin(email:string, password:string){

         this._loading = true;

         this.get(new User(email, password)).then(

             user => {
                 this.user = user;
                 this._loading = false;
             }
         );
     }

     public signup(email:string, password:string){

         this._loading = true;
         this.post(new User(email, password)).then(
             user => {
                 this.user = user;
                 this._loading = false;
             }   
         );
     }
     //Only the code specialized for the UI! 
 }

在这里,我们有一个通用的AbstractModel,它受到POTO的约束。这意味着AbstractModel通用类的实际实例(在诸如 C++之类的语言中称为模板)受到类专门化的POTO的约束。换句话说,只有诸如User之类的领域模型才能被使用。到目前为止,关注点的分离以及可重用性都非常出色。可重用部分的最后一部分是控制器。在我们的注册/登录示例中,它看起来会非常像这样:

export class UserController{

     public UserComponent(protected model:UserModel){
     }

     public signin(email:string, password:string){
         this.model.signin(email, password);
     }

     public signup(email:string, password:string){
         this.model.signup(email, password);
     }

 }

现在,为什么我们在这里需要一个额外的构建块,不能像我们在 Angular MVC 的简化版本中那样使用简单的 Angular 组件呢?嗯,问题在于,取决于您在 Angular 核心之上使用的内容(Ionic、Meteor 等),组件并不一定是主要的构建块。例如,在 Ionic2 世界中,您使用的是页面,这是经典组件的自定义版本。

因此,例如,移动部分会是这样的:

export class LoginPage extends UserController{

     public LoginPage(api:APIService){
         super(new UserModel(api));
     }

     //Only what's different on mobile!

 }

如果需要的话,你也可以扩展UserModel并添加一些专业化,就像在 Angular 的可重用 MVC 图中所示的那样。在浏览器端,添加这段代码:

@Component({
     templateUrl: 'login.html'
 })
 export class LoginComponent extends UserController{

     public UserComponent(api:APIService){

         super(new UserModel(api));
     }

     //Only what's different on the browser !

 }

再一次,你也可以扩展UserModel并添加一些专业化。唯一剩下的要涵盖的部分是视图。令我绝望的是,没有办法扩展或添加样式文件。因此,除非 HTML 文件在移动应用和浏览器应用之间是相同的,否则我们注定要在客户端之间有 HTML 文件的重复。根据经验,这种情况并不经常发生。

整个可重用的前端可以作为 Git 子模块、独立库或NgModule进行发布。我个人喜欢使用 Git 子模块的方法,因为它允许我在进行对共享前端进行修改时,享受客户端自动刷新的同时拥有两个独立的存储库。

请注意,如果你有几个前端同时访问同一个后端,而不是几种类型的前端,这个 MVC 也是有效的。例如,在电子商务设置中,你可能希望拥有不同品牌的网站来销售在同一个后端中管理的不同产品,就像 Magento 的视图所能实现的那样。

单例模式和依赖注入

前端应用程序中使用的另一个方便的模式是单例模式。单例模式确保程序中只存在一个给定对象的实例。此外,它提供了对对象的全局访问点。

实际上看起来是这样的:

export class MySingleton{

     private static instance:MySingleton = null;

   //This constructor is private in order to prevent new creation  
   //of MySingleton objects    private constructor(){

     }

     public static getInstance():MySingleton{
         if(MySingleton.instance == null){
             MySingleton.instance = new MySingleton();
         }         return MySingleton.instance;    }

 }

 let singleton:MySingleton = MySingleton.getInstance();

我们有一个类,它有一个private static instance:MySingleton属性。然后,我们有一个private构造函数,使以下操作失败:

let singleton:MySingleton = new MySingleton(); 

请注意,它失败是因为你的 TypeScript 转译器抱怨可见性。然而,如果你将MySingleton类转译为 JavaScript 并在另一个 TypeScript 项目中导入它,你将能够使用新的操作符,因为转译后的 JavaScript 根本没有可见性。

这种单例模式的相当简单的实现的问题是并发。确实,如果两个进程同时调用getInstance():MySingleton,那么程序中将会有两个MySingleton的实例。为了确保这种情况不会发生,我们可以使用一种称为早期实例化的技术:

export class MySingleton{

     private static instance:MySingleton = new MySingleton();

     private constructor(){

     }
 }

 singleton:MySingleton = MySingleton.getInstance();

虽然你可以在 TypeScript 中实现你的单例,但你也可以利用 Angular 创建单例的方式:服务!确实,在 Angular 中,服务只被实例化一次,并注入到需要它们的任何组件中。这里有一个通过NgModule进行服务和注入的例子,我们在本书中之前已经看过:

// ./service/api.service.ts
 import { Injectable } from '@angular/core';

 @Injectable()
 export class APIService {

     private increment:number = 0;

     public constructor(){
         this.increment++;
     }

     public toString:string{
         return "Current instance: " + this.increment;
     }
 }

 // ./app.component.ts

 @Component({
   selector: 'app-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.css'],
 })
 export class AppComponent {

     public constructor(api:APIService){
         console.log(api);
     }

 }

 // ./other.component.ts

 @Component({
   selector: 'other-root',
   templateUrl: './other.component.html',
   styleUrls: ['./other.component.css'],
 })
 export class OtherComponent {

     public constructor(api:APIService){
         console.log(api);
     }
 }

 //app.module.ts

 import { BrowserModule } from '@angular/platform-browser';
 import { NgModule } from '@angular/core';
 import { FormsModule, ReactiveFormsModule  } from '@angular/forms';
 import { HttpModule } from '@angular/http';
 import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
 import { APIService } from './services/api.service'

 import { AppComponent } from './app.component';
 import { OtherComponent } from './other.component';

 @NgModule({
   declarations: [
     AppComponent,
     OtherComponent
   ],
   imports: [
     BrowserModule,
     FormsModule,
     HttpModule,
     ReactiveFormsModule,
     NgbModule.forRoot()
   ],
   providers: [APIService],
   bootstrap: [AppComponent]
 })
 export class AppModule { }

在上述代码中,我们有:

  • APIService显示了@Injectable()注解,使其可以被注入。另外,APIService有一个increment:number属性,每次创建新实例时都会增加。increment:number是静态的,它将告诉我们程序中有多少个实例。最后,APIService有一个toString:string方法,返回当前实例编号。

  • AppComponent是一个经典组件,它接收了APIService的注入。

  • OtherComponent是另一个经典组件,它接收了APIService的注入。

  • /app.module.ts包含我们的NgModule。在NgModule中,这里显示的大部分声明已经在本书中讨论过。新颖之处来自于providers: [APIService]部分。在这里,我们声明了APIService本身的提供者。由于APIService并没有做什么太疯狂的事情,它可以通过引用类来提供。更复杂的服务,例如需要注入的服务,需要定制的提供者。

现在,如果我们导航到这两个组件,结果是:

Current instance: 1
Current instance: 1

这证明只创建了一个实例,并且相同的实例已被注入到两个组件中。因此,我们有一个单例。然而,这个单例虽然方便,但并不安全。你为什么这样问。嗯,APIService也可以在组件级别提供,就像这样:

// ./app.component.ts

 @Component({
   selector: 'app-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.css'],
   providers: [APIService],
 })
 export class AppComponent {

     public constructor AppComponent(APIService api){
         console.log(api);
     }

 }

 // ./other.component.ts

 @Component({
   selector: 'other-root',
   templateUrl: './other.component.html',
   styleUrls: ['./other.component.css'],
   providers: [APIService],
 })
 export class OtherComponent {

     public constructor OtherComponent(APIService api){
         console.log(api);
     }
 }

在这种情况下,将创建两个单独的实例,导致以下输出:

Current instance: 1
Current instance: 2

因此,使用 Angular 服务,你无法强制实施单例模式,与其普通的 TypeScript 对应相反。另外,普通的 TypeScript 会比 Angular 服务快上一个数量级,因为我们完全跳过了注入过程。确切的数字严重依赖于你的机器的 CPU/RAM。

在单例的情况下,唯一剩下的问题是何时使用它。单例强制程序中给定类的唯一实例。因此,它非常适合与后端或任何硬件访问进行通信。例如,在与后端通信的情况下,可能希望只有一个APIService处理 API 密钥、API 限制和跨站请求伪造令牌,而无需确保我们在所有组件、模型等中传递相同的服务实例。在硬件访问的情况下,可能希望确保只有一个连接打开到用户的网络摄像头或麦克风,这样在使用完毕后可以正确释放它们。

原型和可重用池

面向对象的开发人员寻找减少创建对象成本的方法,特别是当这些对象因为需要进行数据库拉取或复杂的数学运算而昂贵时。减少特定对象创建成本的另一个原因是当你创建大量对象时。如今,后端开发人员倾向于忽视优化的这一方面,因为按需的 CPU/内存已经变得便宜且易于调整。在你的后端上每月多花几美元就可以获得额外的核心或 256MB 的 RAM。

这对于桌面应用程序开发人员来说曾经是一个大问题。在客户端桌面上,没有办法按需添加 CPU/RAM,但是相当规律的四核处理器和消费级 PC 上荒谬的大量 RAM 使这个问题变得不那么棘手。

如今,似乎只有游戏和密集型分析解决方案开发人员似乎关心这个问题。那么,毕竟为什么你应该关心对象的创建时间呢?嗯,你正在构建的东西很可能会被旧设备访问(我仍然在厨房或沙发上使用 iPad 1 进行休闲浏览)。虽然桌面应用程序开发人员可以发布最低和推荐配置,并通过拒绝安装来强制执行它们,但作为 Web 开发人员,我们没有这种奢侈。现在,如果你的网站表现不佳,用户不会质疑他们的设备,而是质疑你的技能。最终,即使在一台性能强大的机器上,他们也不会使用你的产品。

让我们看看如何使用Prototype设计模式。Prototype设计模式允许对象创建定制对象,而无需知道它们的类或任何创建它们的详细信息。其目的是通过复制这个原型来创建新对象,而不是实际实例化一个新对象。首先,我们需要一个Prototype接口,如下所示:

export interface Prototype{

     clone(): Prototype;
 }

Prototype接口只定义了一个clone方法,该方法返回一个符合Prototype的对象。您已经猜到了,创建对象的优化方式是在需要时进行克隆!所以假设您有一个Movie对象,由于某种原因,需要花费时间来构建:

export class Movie implements Prototype {

     private title:string;
     private year:number;
     //...

     public constructor(); 
     public constructor(title?: string, year?: number); 
     public constructor(title?: string, year?: number) { 
    {
         if(title == undefined || year == undefined){
             //do the expensive creation
         }else{
             this.title = title;
             this.year = year;
         }
     }

     clone() : Movie {
         return new Movie(this.title, this.year);
     }
 }

 let expensiveMovie:Movie = new Movie();
 cheapMovie = expensiveMovie.clone();

正如您所看到的,TypeScript 中的覆盖函数与大多数语言不同。在这里,构造函数的两个签名叠在一起,并共享相同的实现。

此外,这就是Prototype模式的全部内容。

通常与原型模式一起使用的另一个模式是对象池模式。在使用昂贵的创建对象时,克隆它们确实会有所不同。更大的不同之处在于根本不做任何事情:不创建,不克隆。为了实现这一点,我们可以使用池模式。在这种模式下,我们有一组对象池,可以被任何客户端或组件共享,例如在 Angular 应用程序的情况下。池的实现很简单:

export class MoviePool{

     private static movies:[{movie:Movie, used:boolean}];
     private static nbMaxMovie = 10;
     private static instance:MoviePool;

     private constructor(){}

     public static getMovie(){

         //first hard create
         if(MoviePool.movies.length == 0){

             MoviePool.movies.push({movie:new Movie(), used:true});
             return MoviePool.movies[0].movie;

         }else{

             for(var reusableMovie of MoviePool.movies){
                 if(!reusableMovie.used){
                     reusableMovie.used = true;
                     return reusableMovie.movie;
                 }
             }
         }

         //subsequent clone create
         if(MoviePool.movie.length < MoviePool.nbMaxMovie){

             MoviePool.movies.push({movie:MoviePool.movies[MoviePool.movies.
                length - 1].movie.clone(), used:true});
             return MoviePool.movies[MoviePool.movies.length - 1].movie;
         }

         throw new Error('Out of movies');
     }

     public static releaseMovie(movie:Movie){
         for(var reusableMovie of MoviePool.movies){
             if(reusableMovie.movie === movie){
                 reusableMovie.used = false;
             }
             return;
         }
     }
 }

首先,池也是一个单例。事实上,如果任何人都可以随意创建池,那么将不会有太多意义。因此,我们有static instance:MoviePool和私有构造函数,以确保只能创建一个池。然后,我们有以下属性:

private static movies:[{movie:Movie, used:boolean}];  

movies属性存储了一系列电影和一个布尔值,用于确定当前是否有人在使用任何给定的电影。由于电影对象在理论上很费力创建或在内存中维护,因此有必要对我们的池中可以拥有多少这样的对象进行硬性限制。这个限制由私有的static nbMaxMovie = 10属性管理。要获取电影,组件必须调用getMovie():Movie方法。这个方法在第一部电影上进行了硬性创建,然后利用原型模式来创建任何后续的电影。

每当从池中检出一部电影时,getMovie方法会将使用的布尔值更改为 true。请注意,在池已满且我们没有任何空闲电影可供赠送的情况下,将抛出错误。

最后,组件需要一种方法来将它们的电影归还到池中,以便其他组件可以使用它们。这是通过releaseMovie方法实现的。该方法接收一个已检出的电影,并遍历池中的电影,将相应的布尔值设置为 false。因此,电影可以供其他组件使用。

工厂模式

假设我们有一个User类,其中有两个私有变量:lastName:stringfirstName:string。此外,这个简单的类提供了一个打印“Hi I am”,this.firstName,this.lastName 的方法 hello:

class User{
     constructor(private lastName:string, private firstName:string){
     }
     hello(){
         console.log("Hi I am", this.firstName, this.lastName);
     }
 }

现在,考虑到我们通过 JSON API 接收用户。很可能会是这样的:

[{"lastName":"Nayrolles","firstName":"Mathieu"}...].  

通过以下代码片段,我们可以创建一个User

let userFromJSONAPI: User = JSON.parse('[{"lastName":"Nayrolles","firstName":"Mathieu"}]')[0]; 

到目前为止,TypeScript 编译器没有抱怨,并且执行顺利。这是因为解析方法返回any(即 Java 对象的 TypeScript 等价物)。当然,我们可以将any转换为User。然而,userFromJSONAPI.hello()将产生:

json.ts:19
 userFromJSONAPI.hello();
                  ^
 TypeError: userFromUJSONAPI.hello is not a function
     at Object.<anonymous> (json.ts:19:18)
     at Module._compile (module.js:541:32)
     at Object.loader (/usr/lib/node_modules/ts-node/src/ts-node.ts:225:14)
     at Module.load (module.js:458:32)
     at tryModuleLoad (module.js:417:12)
     at Function.Module._load (module.js:409:3)
     at Function.Module.runMain (module.js:575:10)
     at Object.<anonymous> (/usr/lib/node_modules/ts-node/
        src/bin/ts-node.ts:110:12)
     at Module._compile (module.js:541:32)
     at Object.Module._extensions..js (module.js:550:10)

为什么?嗯,=语句的左侧被定义为User,但当我们将其转译为 JavaScript 时,它将被擦除。

使用类型安全的 TypeScript 方法来做这件事将是:

let validUser = JSON.parse('[{"lastName":"Nayrolles","firstName":"Mathieu"}]')
 .map((json: any):User => {
     return new User(json.lastName, json.firstName);
 })[0];

有趣的是,typeof函数也无法帮助你。在这两种情况下,它都会显示Object而不是User,因为 JavaScript 中并不存在User的概念。

虽然直接的类型安全方法可以工作,但它并不是非常可扩展或可重用的。实际上,无论何时接收到一个 JSON 用户,都必须在每个地方重复使用 map callback方法。最方便的方法是使用Factory模式来做到这一点。Factory用于创建对象,而不会将实例化逻辑暴露给客户端。

如果我们要有一个工厂来创建一个用户;它会是这样的:

export class POTOFactory{

     /**
      * Builds an User from json response
      * @param  {any}  jsonUser
      * @return {User}         
      */
     static buildUser(jsonUser: any): User {

         return new User(
             jsonUser.firstName,
             jsonUser.lastName
         );
     }
 }

在这里,我们有一个名为buildUser的静态方法,它接收一个 JSON 对象,并从 JSON 对象中获取所有必需的值,以调用一个假设的User构造函数。该方法是静态的,就像工厂的所有方法一样。实际上,我们不需要在工厂中保存任何状态或实例绑定的变量;我们只需要封装用户的创建过程。请注意,您的工厂可能会与您的 POTO 的其余部分共享。

备忘录模式

在 Angular 的上下文中,备忘录模式是一个非常有用的模式。在由 Angular 驱动的应用程序中,我们过度使用双向数据绑定,例如UserMovie等领域模型。

让我们考虑两个组件:一个名为Dashboard,另一个名为EditMovie。在 Dashboard 组件上,您有一个电影列表显示在我们类似 IMDB 的应用程序的上下文中。这样的仪表板视图可能如下所示:

<div *ngFor="let movie of model.movies"> 
   <p>{{movie.title}}</p> 
   <p>{{movie.year}}</p> 
</div> 

这个简单的视图拥有一个ngFor指令,它遍历模型中包含的电影列表。然后,对于每部电影,它显示两个包含标题和发行年份的 p 元素。

现在,EditMovie组件访问model.movies数组中的一部电影,并允许用户对其进行编辑:

<form> 
   <input id="title" name="title" type="text" [(ngModel)]="movie.title" /> 
   <input id="year" name="year" type="text" [(ngModel)]="movie.year" /> 
</form> 
<a href="/back">Cancel</a> 

由于这里使用了双向数据绑定,对电影标题和年份的修改将直接影响仪表板。正如您所注意到的,我们这里有一个取消按钮。虽然用户可能期望修改是实时同步的,但他/她也期望取消按钮/链接取消对电影所做的修改。

这就是备忘录模式发挥作用的地方。这种模式允许您对对象执行撤消操作。它可以以许多种方式实现,但最简单的方式是使用克隆。使用克隆,我们可以在给定时刻存储对象的一个版本,并在需要时取回它。让我们根据Prototype模式来增强我们的Movie对象:

export class Movie implements Prototype { 
   private title:string; 
   private year:number; 
   //... 
       public constructor(); 
         public constructor(title?: string, year?: number); 

         public constructor(title?: string, year?: number) { 

         if(title == undefined || year == undefined){ 
         //do the expensive creation 
         }else{ 
         this.title = title; 
         this.year = year; 
         } 
   } 

   clone() : Movie { 
         return new Movie(this.title, this.year); 
   } 
   restore(movie:Movie){ 
         this.title = movie.title; 
         this.year = movie.year; 
   } 
} 

在这个新版本中,我们添加了restore(movie:Movie)方法,它接受一个Movie作为参数,并将本地属性设置为接收到的电影的值。

然后,在实践中,我们的EditMovie组件的构造函数可能如下所示:

import { Component } from '@angular/core'; 
import { Movie } from './movie'; 
@Component({ 
  selector: 'app-root', 
  templateUrl: './app.component.html', 
  styleUrls: ['./app.component.css'] 
}) 
export class AppComponent { 
  title = 'app works!'; 

  private memento: Movie; 

  constructor(){ 
    this.memento = new Movie("Title", 2015); 
    let movieTmp = this.memento.clone(); 
    this.memento.setTitle("Another Title"); 
    //Prints Another title 
    console.log(this.memento.getTitle()); 
    this.memento.restore(movieTmp); 
    //Prints Title 
    console.log(this.memento.getTitle()); 
  } 
} 

有趣的是,您不限于一次性保存状态;您可以拥有尽可能多的状态。

摘要

在本章中,我们学习了如何使用一些经典的面向对象模式,这些模式适用于可重用和易于维护/扩展的现实世界应用程序。MVC 被调整为 Angular,并扩展以在不同应用程序之间实现高度可重用的业务逻辑。然后,我们看到如何使用单例模式以及依赖注入和原型模式与池相结合来控制对象的创建,以限制系统中昂贵对象的数量。最后,我们学习了如何使用工厂模式来避免在 JSON 到 TypeScript 自动(和部分)对象转换中的陷阱,并看到如何使用备忘录模式执行撤消操作。

如果你想学习更多关于模式来提高你的性能、操作成本和可维护性,你可以查看即将推出的 Packt Publishing 出版的《Angular 设计模式与最佳实践》一书。这本书深入探讨了模式及其实施,以找到最适合你的应用程序。

posted @ 2024-05-18 12:03  绝不原创的飞龙  阅读(5)  评论(0编辑  收藏  举报