Angular2-切换指南-二-

29 阅读1小时+

Angular2 切换指南(二)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:Angular 2 中的依赖注入

在本章中,我们将解释如何利用框架的依赖注入DI)机制及其各种特性。

我们将探讨以下主题:

  • 配置和创建注入器。

  • 使用注入器实例化对象。

  • 将依赖项注入到我们的指令和组件中。这样,我们将能够重用服务中定义的业务逻辑,并将其与 UI 逻辑连接起来。

  • 注释我们将编写的 ES5 代码,以便获得与使用 TypeScript 语法时相同的结果。

我为什么需要依赖注入?

假设我们有一个依赖于EngineTransmission类的Car类。我们如何实现这个系统?让我们看一下:

class Engine {…}
class Transmission {…}
class Car {
  engine;
  transmission;
  constructor() {
    this.engine = new Engine();
    this.transmission = new Transmission();
  }
}

在这个例子中,我们在Car类的构造函数中创建了它的依赖关系。虽然看起来很简单,但远非灵活。每次我们创建Car类的实例时,都会创建相同的EngineTransmission类的实例。这可能会有问题,原因如下:

  • Car类变得不太可测试,因为我们无法独立测试它的enginetransmission依赖关系。

  • Car类与用于实例化其依赖关系的逻辑耦合在一起。

Angular 2 中的依赖注入

我们可以采用的另一种方法是利用 DI 模式。我们已经从 AngularJS 1.x 中熟悉了它。让我们演示如何在 Angular 2 的上下文中使用 DI 重构前面的代码:

class Engine {…}
class Transmission {…}

@Injectable()
class Car {
  engine;
  transmission;
  constructor(engine: Engine, transmission: Transmission) {
    this.engine = engine;
    this.transmission = transmission;
  }
}

在前面的片段中,我们所做的只是在Car类的定义顶部添加了@Injectable类装饰器,并为其构造函数的参数提供了类型注解。

Angular 2 中 DI 的好处

还有一步剩下,我们将在下一节中看一下。但让我们看看所述方法的好处是什么:

  • 我们可以轻松地为测试环境传递Car类的不同版本的依赖关系。

  • 我们不再与依赖关系实例化周围的逻辑耦合在一起。

Car类只负责实现自己的领域特定逻辑,而不是与其他功能耦合,比如管理它的依赖关系。我们的代码也变得更加声明性和易于阅读。

现在,在我们意识到 DI 的一些好处之后,让我们看看为使这段代码工作所缺少的部分!

配置注入器

在我们的 Angular 2 应用程序中,通过框架的 DI 机制实例化各个依赖项的基本方法称为注入器。注入器包含一组提供者,封装了与token关联的已注册依赖项实例化的逻辑。我们可以将 token 视为注入器中注册的不同提供者的标识符。

让我们看一下下面的代码片段,它位于ch5/ts/injector-basics/injector.ts

import 'reflect-metadata';
import {
  Injector, Inject, Injectable,
  OpaqueToken, provide
} from 'angular2/core';

const BUFFER_SIZE = new OpaqueToken('buffer-size');

class Buffer {
  constructor(@Inject(BUFFER_SIZE) private size: Number) {
    console.log(this.size);
  }
}

@Injectable()
class Socket {
  constructor(private buffer: Buffer) {}
}

let injector = Injector.resolveAndCreate([
  provide(BUFFER_SIZE, { useValue: 42 }),
  Buffer,
  Socket
]);

injector.get(Socket);

您可以使用以下命令运行该文件:

**cd app**
**ts-node ch5/ts/injector-basics/injector.ts**

如果您还没有安装ts-node,请参阅第三章 TypeScript Crash Course,了解如何继续在计算机上安装并运行它。

我们导入了InjectorInjectableInjectOpaqueTokenprovide

注入器表示用于实例化不同依赖项的容器。使用provide函数声明的规则和 TypeScript 编译器生成的元数据,它知道如何创建它们。

在前面的代码片段中,我们首先定义了BUFFER_SIZE常量,并将其设置为new OpaqueToken('buffer-size')的值。我们可以将BUFFER_SIZE的值视为应用程序中无法复制的唯一值(OpaqueToken是 ES2015 中Symbol类的替代品,因为在撰写本文时,TypeScript 不支持Symbol)。

我们定义了两个类:BufferSocketBuffer类有一个构造函数,只接受一个名为size的依赖项,类型为Number。为了为依赖项解析过程添加额外的元数据,我们使用@Inject参数装饰器。这个装饰器接受一个标识符(也称为token),表示我们要注入的依赖项。通常情况下,它是依赖项的类型(即类的引用),但在某些情况下,它可以是不同类型的值。例如,在我们的例子中,我们使用了OpaqueToken类的实例。

使用生成的元数据进行依赖项解析

现在让我们看一下Socket类。我们用@Injectable装饰它。这个装饰器应该被任何接受依赖项的类使用,这些依赖项应该通过 Angular 2 的依赖注入机制注入。

@Injectable装饰器会强制 TypeScript 编译器为给定类接受的依赖项的类型生成额外的元数据。这意味着如果我们省略@Injectable装饰器,Angular 的 DI 机制将不会意识到与它需要解决的依赖项相关联的标记。

如果在类的顶部没有使用装饰器,TypeScript 不会生成任何元数据,这主要是出于性能方面的考虑。想象一下,如果为每个接受依赖项的类生成了这样的元数据,那么输出将充斥着未使用的额外类型元数据。

使用@Injectable的另一种方法是使用@Inject装饰器显式声明依赖项的类型。看一下下面的例子:

class Socket {
  constructor(@Inject(Buffer) private buffer: Buffer) {}
}

这意味着前面的代码与使用@Injectable的代码具有等效的语义,正如前面提到的。唯一的区别是,Angular 2 将会直接从@Injector装饰器添加的元数据中获取依赖项的类型(即与之关联的标记),而不是使用@Injectable时,它将查看编译器生成的元数据。

实例化注入器

现在,让我们创建一个注入器的实例,以便用它来实例化已注册的标记:

let injector = Injector.resolveAndCreate([
  provide(BUFFER_SIZE, { useValue: 42 }),
  Buffer,
  Socket
]);

我们使用resolveAndCreate的静态方法创建Injector的一个实例。这是一个工厂方法,接受一个提供者数组作为参数,并返回一个新的Injector

resolve意味着提供者将经过解析过程,其中包括一些内部处理(展平多个嵌套数组并将单个提供者转换为数组)。稍后,注入器可以根据提供者封装的规则实例化我们已注册提供者的任何依赖项。

在我们的例子中,我们使用provide方法明确告诉 Angular 2 DI 机制在需要BUFFER_SIZE标记时使用值42。另外两个提供者是隐式的。一旦它们的所有依赖项都得到解决,Angular 2 将通过使用new运算符调用提供的类来实例化它们。

我们在Buffer类的构造函数中请求BUFFER_SIZE的值:

class Buffer {
  constructor(@Inject(BUFFER_SIZE) private size: Number) {
    console.log(this.size);
  }
}

在前面的例子中,我们使用了@Inject参数装饰器。它提示 DI 机制,Buffer类的构造函数的第一个参数应该用与传递给注入器的BUFFER_SIZE标记相关联的提供者实例化。

引入前向引用

Angular 2 引入了前向引用的概念。这是由于以下原因所必需的:

  • ES2015 类不会被提升。

  • 允许解析在声明依赖提供者之后声明的依赖项。

在本节中,我们将解释前向引用解决的问题以及我们可以利用它们的方式。

现在,假设我们已经以相反的顺序定义了BufferSocket类:

// ch5/ts/injector-basics/forward-ref.ts

@Injectable()
class Socket {
  constructor(private buffer: Buffer) {…}
}

// undefined
console.log(Buffer);

class Buffer {
  constructor(@Inject(BUFFER_SIZE) private size: Number) {…}
}

// [Function: Buffer]
console.log(Buffer);

在这里,我们有与前面例子中相同的依赖关系,但在这种情况下,Socket类的定义在Buffer类的定义之前。请注意,直到 JavaScript 虚拟机评估Buffer类的声明之前,Buffer标识符的值将等于undefined。然而,Socket接受的依赖项类型的元数据将在Socket类定义之后生成并放置。这意味着除了解释生成的 JavaScript 之外,Buffer标记的值将等于undefined——也就是说,在 Angular 2 的 DI 机制的上下文中,框架将获得一个无效的值。

运行前面的代码片段将导致以下形式的运行时错误:

**Error: Cannot resolve all parameters for Socket(undefined). Make sure they all have valid type or annotations.**

解决这个问题的最佳方法是通过交换定义的顺序。我们可以继续的另一种方法是利用 Angular 2 提供的解决方案:前向引用:

import {forwardRef} from 'angular2/core';
…
@Injectable()
class Socket {
  constructor(@Inject(forwardRef(() => Buffer))
    private buffer: Buffer) {}
}
class Buffer {…}

前面的代码片段演示了我们如何利用前向引用。我们所需要做的就是使用@Inject参数装饰器,并将forwardRef函数的调用结果传递给它。forwardRef函数是一个高阶函数,接受一个参数——另一个负责返回与需要被注入的依赖项(或更准确地说是与其提供者相关联的)关联的标记的函数。这样,框架提供了一种推迟解析依赖项类型(标记)的过程的方式。

依赖项的标记将在首次需要实例化Socket时解析,而不是默认行为,在给定类的声明时需要标记。

配置提供程序

现在,让我们看一个类似于之前使用的示例,但注入器的配置不同的示例。

let injector = Injector.resolveAndCreate([
  provide(BUFFER_SIZE, { useValue: 42 }),
  provide(Buffer, { useClass: Buffer }),
  provide(Socket, { useClass: Socket })
]);

在这种情况下,在提供程序内部,我们明确声明了我们希望使用Buffer类来构建具有与Buffer类引用相等的标记的依赖项。对于与Socket标记关联的依赖项,我们做了完全相同的事情;但这次,我们提供了Socket类。这就是当我们省略provide函数的调用并只传递一个类的引用时,Angular 2 将如何进行。

明确声明用于实例化相同类的类可能看起来毫无价值,鉴于我们迄今为止看到的例子,这完全正确。然而,在某些情况下,我们可能希望为与给定类标记关联的依赖项的实例化提供不同的类。

例如,假设我们有一个名为Http的服务,它在一个名为UserService的服务中使用:

class Http {…}

@Injectable()
class UserService {
  constructor(private http: Http) {}
}

let injector = Injector.resolveAndCreate([
  UserService,
  Http
]);

UserService服务使用Http与 RESTful 服务进行通信。我们可以使用injector.get(UserService)来实例化UserService。这样,由注入器的get方法调用的UserService构造函数将接受Http服务的实例作为参数。然而,如果我们想要测试UserService,我们实际上并不需要对 RESTful 服务进行 HTTP 调用。在单元测试的情况下,我们可以提供一个虚拟实现,只会伪造这些 HTTP 调用。为了向UserService服务注入一个不同类的实例,我们可以将注入器的配置更改为以下内容:

class DummyHttp {…}

// ...

let injector = Injector.resolveAndCreate([
  UserService,
  provide(Http, { useClass: DummyHttp })
]);

现在,当我们实例化UserService时,它的构造函数将接收一个DummyHttp服务实例的引用。这段代码位于ch5/ts/configuring-providers/dummy-http.ts中。

使用现有的提供程序

另一种方法是使用提供程序配置对象的useExisting属性:

// ch5/ts/configuring-providers/existing.ts
let injector = Injector.resolveAndCreate([
  DummyService,
  provide(Http, { useExisting: DummyService }),
  UserService
]);

在前面的片段中,我们注册了三个令牌:DummyServiceUserServiceHttp。我们声明要将Http令牌绑定到现有令牌DummyService。这意味着当请求Http服务时,注入器将找到用作useExisting属性值的令牌的提供者并实例化它或获取与之关联的值。我们可以将useExisting视为创建给定令牌的别名:

let dummyHttp = {
  get() {},
  post() {}
};
let injector = Injector.resolveAndCreate([
  provide(DummyService, { useValue: dummyHttp }),
  provide(Http, { useExisting: DummyService }),
  UserService
]);
console.assert(injector.get(UserService).http === dummyHttp);

前面的片段将创建一个Http令牌到DummyHttp令牌的别名。这意味着一旦请求Http令牌,调用将转发到与DummyHttp令牌关联的提供者,该提供者将解析为值dummyHttp

定义实例化服务的工厂

现在,假设我们想创建一个复杂的对象,例如代表传输层安全TLS)连接的对象。这样一个对象的一些属性是套接字、一组加密协议和证书。在这个问题的背景下,我们迄今为止看到的 Angular 2 的 DI 机制的特性似乎有点有限。

例如,我们可能需要配置TLSConnection类的一些属性,而不将其实例化过程与所有配置细节耦合在一起(选择适当的加密算法,打开我们将建立安全连接的 TCP 套接字等)。

在这种情况下,我们可以利用提供者配置对象的useFactory属性:

let injector = Injector.resolveAndCreate([
  provide(TLSConnection, {
    useFactory: (socket: Socket, certificate: Certificate, crypto: Crypto) =>  {
      let connection = new TLSConnection();
      connection.certificate = certificate;
      connection.socket = socket;
      connection.crypto = crypto;
      socket.open();
      return connection;
    },
    deps: [Socket, Certificate, Crypto]
  }),
  provide(BUFFER_SIZE, { useValue: 42 }),
  Buffer,
  Socket,
  Certificate,
  Crypto
]);

前面的代码一开始似乎有点复杂,但让我们一步一步地来看看它。我们可以从我们已经熟悉的部分开始:

let injector = Injector.resolveAndCreate([
  ...
  provide(BUFFER_SIZE, { useValue: 42 }),
  Buffer,
  Socket,
  Certificate,
  Crypto
]);

最初,我们注册了一些提供者:BufferSocketCertificateCrypto。就像前面的例子一样,我们还注册了BUFFER_SIZE令牌,并将其与值42关联起来。这意味着我们已经可以创建BufferSocketCertificateCrypto类型的对象:

// buffer with size 42
console.log(injector.get(Buffer));
// socket with buffer with size 42
console.log(injector.get(Socket));

我们可以通过以下方式创建和配置TLSConnection对象的实例:

let connection = new TLSConnection();
connection.certificate = certificate;
connection.socket = socket;
connection.crypto = crypto;
socket.open();
return connection;

现在,如果我们注册一个具有TLSConnection标记作为依赖项的提供者,我们将阻止 Angular 的依赖注入机制处理依赖项解析过程。为了解决这个问题,我们可以使用提供者配置对象的useFactory属性。这样,我们可以指定一个函数,在这个函数中我们可以手动创建与提供者标记相关联的对象的实例。我们可以将useFactory属性与deps属性一起使用,以指定要传递给工厂的依赖项:

provide(TLSConnection, {
  useFactory: (socket: Socket, certificate: Certificate, crypto: Crypto) =>  {
    // ...
  },
  deps: [Socket, Certificate, Crypto]
})

在前面的片段中,我们定义了用于实例化TLSConnection的工厂函数。作为依赖项,我们声明了SocketCertificateCrypto。这些依赖项由 Angular 2 的 DI 机制解析并注入到工厂函数中。您可以在ch5/ts/configuring-providers/factory.ts中查看整个实现并进行操作。

子注入器和可见性

在本节中,我们将看看如何构建注入器的层次结构。这是 Angular 2 引入的一个全新概念。每个注入器可以有零个或一个父注入器,每个父注入器可以分别有零个或多个子注入器。与 AngularJS 1.x 相比,在 Angular 2 中,所有注册的提供者都存储在树中,而不是存储在一个扁平的结构中。扁平结构更为有限;例如,它不支持标记的命名空间;也就是说,我们不能为同一个标记声明不同的提供者,这在某些情况下可能是必需的。到目前为止,我们看了一个没有任何子节点或父节点的注入器的示例。现在让我们构建一个注入器的层次结构!

为了更好地理解这种注入器的层次结构,让我们看一下下图:

子注入器和可见性

在这里,我们看到一个树,其中每个节点都是一个注入器,每个注入器都保留对其父级的引用。注入器House有三个子注入器:BathroomKitchenGarage

Garage有两个子节点:CarStorage。我们可以将这些注入器视为内部注册了提供者的容器。

假设我们想要获取与标记Tire相关联的提供程序的值。如果我们使用注射器Car,这意味着 Angular 2 的 DI 机制将尝试在Car及其所有父级GarageHouse中查找与此标记相关联的提供程序,直到找到为止。

构建注射器的层次结构

为了更好地理解上一段,让我们看一个简单的例子:

// ch5/ts/parent-child/simple-example.ts
class Http {}

@Injectable()
class UserService {
  constructor(public http: Http) {}
}

let parentInjector = Injector.resolveAndCreate([
  Http
]);
let childInjector = parentInjector.resolveAndCreateChild([
  UserService
]);

// UserService { http: Http {} }
console.log(childInjector.get(UserService));
// true
console.log(childInjector.get(Http) === parentInjector.get(Http));

由于它们对于解释前面的片段并不重要,所以省略了导入部分。我们有两个服务,HttpUserService,其中UserService依赖于Http服务。

最初,我们使用Injector类的resolveAndCreate静态方法创建了一个注射器。我们向此注射器传递了一个隐式提供程序,稍后将解析为具有Http标记的提供程序。使用resolveAndCreateChild,我们解析了传递的提供程序并实例化了一个注射器,该注射器指向parentInjector(因此我们得到与上图中GarageHouse之间相同的关系)。

现在,使用childInjector.get(UserService),我们能够获取与UserService标记相关联的值。类似地,使用childInjector.get(Http)parentInjector.get(Http),我们得到与Http标记相关联的相同值。这意味着childInjector向其父级请求与请求的标记相关联的值。

然而,如果我们尝试使用parentInjector.get(UserService),我们将无法获取与该标记相关联的值,因为在此注射器中,我们没有注册具有此标记的提供程序。

配置依赖关系

现在我们熟悉了注射器的层次结构,让我们看看如何从其中获取适当注射器的依赖关系。

使用@Self 装饰器

现在假设我们有以下配置:

abstract class Channel {}
class Http extends Channel {}
class WebSocket extends Channel {}

@Injectable()
class UserService {
  constructor(public channel: Channel) {}
}

let parentInjector = Injector.resolveAndCreate([
  provide(Channel, { useClass: Http })
]);
let childInjector = parentInjector.resolveAndCreateChild([
  provide(Channel, { useClass: WebSocket }),
  UserService
]);

我们可以使用以下方法实例化UserService标记:

childInjector.get(UserService);

UserService中,我们可以声明我们要使用@Self装饰器从当前注射器(即childInjector)获取Channel依赖项的值:

@Injectable()
class UserService {
  constructor(@Self() public channel: Channel) {}
}

尽管在实例化UserService期间,这将是默认行为,但使用@Self,我们可以更加明确。假设我们将childInjector的配置更改为以下内容:

let parentInjector = Injector.resolveAndCreate([
  provide(Channel, { useClass: Http })
]);
let childInjector = parentInjector.resolveAndCreateChild([
  UserService
]);

如果我们在UserService构造函数中保留@Self装饰器,并尝试使用childInjector实例化UserService,由于缺少Channel的提供程序,我们将收到运行时错误。

跳过自注入器

在某些情况下,特别是在注入 UI 组件的依赖项时,我们可能希望使用父注入器中注册的提供者,而不是当前注入器中注册的提供者。我们可以通过利用@SkipSelf装饰器来实现这种行为。例如,假设我们有以下类Context的定义:

class Context {
  constructor(public parentContext: Context) {}
}

Context类的每个实例都有一个父级。现在让我们构建一个包含两个注入器的层次结构,这将允许我们创建一个具有父上下文的上下文:

let parentInjector = Injector.resolveAndCreate([
  provide(Context, { useValue: new Context(null) })
]);
let childInjector = parentInjector.resolveAndCreateChild([
  Context
]);

由于根上下文没有父级,我们将设置其提供者的值为new Context(null)

如果我们想要实例化子上下文,我们可以使用:

childInjector.get(Context);

对于子级的实例化,Context将由childInjector中注册的提供者使用。但是,作为一个依赖项,它接受一个Context类的实例对象。这些类存在于同一个注入器中,这意味着 Angular 将尝试实例化它,但它具有Context类型的依赖项。这个过程将导致一个无限循环,从而导致运行时错误。

为了防止这种情况发生,我们可以以以下方式更改Context的定义:

class Context {
  constructor(@SkipSelf() public parentContext: Context) {}
}

我们引入的唯一变化是参数装饰器@SkipSelf的添加。

具有可选依赖项

Angular 2 引入了@Optional装饰器,它允许我们处理没有与之关联的已注册提供者的依赖项。假设一个提供者的依赖项在负责其实例化的任何目标注入器中都不可用。如果我们使用@Optional装饰器,在实例化缺失依赖项的依赖提供者时,缺失依赖项的值将被传递为null

现在让我们看一个例子:

abstract class SortingAlgorithm {
  abstract sort(collection: BaseCollection): BaseCollection;
}

@Injectable()
class Collection extends BaseCollection {
  private sort: SortingAlgorithm;
  constructor(sort: SortingAlgorithm) {
    super();
    this.sort = sort || this.getDefaultSort();
  }
}

let injector = Injector.resolveAndCreate([
  Collection
]);

在这种情况下,我们定义了一个名为SortingAlgorithm的抽象类和一个名为Collection的类,它接受一个扩展SortingAlgorithm的具体类的实例作为依赖项。在Collection构造函数内,我们将sort实例属性设置为传递的SortingAlgorithm类型的依赖项或默认的排序算法实现。

我们没有在我们配置的注入器中为SortingAlgorithm令牌定义任何提供者。因此,如果我们想使用injector.get(Collection)来获取Collection类的实例,我们将会得到一个运行时错误。这意味着,如果我们想使用框架的 DI 机制获取Collection类的实例,我们必须为SortingAlgorithm令牌注册一个提供者,尽管我们可以回退到默认排序算法的实现。

Angular 2 通过@Optional装饰器为这个问题提供了解决方案。

这就是我们可以使用框架提供的@Optional装饰器来解决问题的方式。

// ch5/ts/decorators/optional.ts
@Injectable()
class Collection extends BaseCollection {
  private sort: SortingAlgorithm;
  constructor(@Optional() sort: SortingAlgorithm) {
    super();
    this.sort = sort || this.getDefaultSort();
  }
}

在前面的片段中,我们将sort依赖声明为可选的,这意味着如果 Angular 2 找不到其令牌的任何提供者,它将传递null值。

使用多提供者

多提供者是 Angular 2 DI 机制引入的另一个新概念。它们允许我们将多个提供者与相同的令牌关联起来。如果我们正在开发一个带有不同服务的默认实现的第三方库,但是你想允许用户使用自定义的实现来扩展它,这将非常有用。它们还专门用于在 Angular 2 表单模块中对单个控件进行多个验证。我们将在第六章和第七章中解释这个模块。

另一个适用于多提供者的用例示例是 Angular 2 在其 WebWorkers 实现中用于事件管理的。他们为事件管理插件创建了多提供者。每个提供者返回一个不同的策略,支持不同的事件集(触摸事件、键盘事件等)。一旦发生特定事件,他们可以选择处理它的适当插件。

让我们来看一个例子,说明了多提供者的典型用法:

// ch5/ts/configuring-providers/multi-providers.ts
const VALIDATOR = new OpaqueToken('validator');

interface EmployeeValidator {
  (person: Employee): boolean;
}

class Employee {...}

let injector = Injector.resolveAndCreate([
  provide(VALIDATOR, { multi: true,
    useValue: (person: Employee) => {
      if (!person.name) {
        return 'The name is required';
      }
    }
  }),
  provide(VALIDATOR, { multi: true,
    useValue: (person: Employee) => {
      if (!person.name || person.name.length < 1) {
        return 'The name should be more than 1 symbol long';
      }
    }
  }),
  Employee
]);

在前面的代码片段中,我们声明了一个名为VALIDATOR的常量,其中包含OpaqueToken的新实例。我们还创建了一个注入器,在那里我们注册了三个提供程序——其中两个被用作值函数,根据不同的标准,验证Employee类的实例。这些函数的类型是EmployeeValidator

为了声明我们希望注入器将所有注册的验证器传递给Employee类的构造函数,我们需要使用以下构造函数定义:

class Employee {
  name: string;
  constructor(@Inject(VALIDATOR) private validators: EmployeeValidator[]) {}
  validate() {
    return this.validators
      .map(v => v(this))
      .filter(value => !!value);
  }
}

在前面的示例中,我们声明了一个名为Employee的类,它接受一个依赖项——一个EmployeeValidators数组。在validate方法中,我们对当前类实例应用了各个验证器,并过滤结果,以便只获取返回错误消息的验证器。

请注意构造函数参数validators的类型是EmployeeValidator[]。由于我们不能将类型“对象数组”用作提供程序的标记,因为它不是有效的类型引用,所以我们需要使用@Inject参数装饰器。

在组件和指令中使用 DI

在第四章中,使用 Angular 2 组件和指令入门,当我们开发了我们的第一个 Angular 2 指令时,我们看到了如何利用 DI 机制将服务注入到我们的 UI 相关组件(即指令和组件)中。

让我们从依赖注入的角度快速看一下我们之前做的事情:

// ch4/ts/tooltip/app.ts
// ...
@Directive(...)
export class Tooltip {
  @Input()
  saTooltip:string;

  constructor(private el: ElementRef, private overlay: Overlay) {
    this.overlay.attach(el.nativeElement);
  }
  // ...
}
@Component({
  // ...
  providers: [Overlay],
  directives: [Tooltip]
})
class App {}

由于大部分早期实现的代码与我们当前的重点无直接关系,因此被省略。

请注意Tooltip的构造函数接受两个依赖项:

  • ElementRef类的一个实例。

  • Overlay类的一个实例。

依赖项的类型是与其提供程序关联的标记,来自提供程序的相应值将使用 Angular 2 的 DI 机制进行注入。

尽管Tooltip类的依赖项声明看起来与我们在之前的部分中所做的完全相同,但既没有任何显式配置,也没有任何注入器的实例化。

介绍元素注入器

在幕后,Angular 将为所有指令和组件创建注入器,并向其添加一组默认提供者。这就是所谓的元素注入器,是框架自己处理的事情。与组件关联的注入器称为宿主注入器。每个指令和组件注入器中的一个提供者与ElementRef令牌相关联;它将返回指令的宿主元素的引用。但是Overlay类的提供者在哪里声明?让我们看一下顶层组件的实现:

@Component({
  // ...
  providers: [Overlay],
  directives: [Tooltip]
})

class App {}

我们通过在@Component装饰器内声明providers属性来为App组件配置元素注入器。在这一点上,注册的提供者将被相应元素注入器关联的指令或组件以及组件的整个子树所看到,除非在层次结构的某个地方被覆盖。

声明元素注入器的提供者

将所有提供者的声明放在同一个地方可能会非常不方便。例如,想象一下,我们正在开发一个大型应用程序,其中有数百个组件依赖于成千上万的服务。在这种情况下,在根组件中配置所有提供者并不是一个实际的解决方案。当两个或更多提供者与相同的令牌相关联时,将会出现名称冲突。配置将会很庞大,很难追踪不同的令牌需要被注入的地方。

正如我们提到的,Angular 2 的@Directive(以及相应的@Component)装饰器允许我们使用providers属性引入特定于指令的提供者。以下是我们可以采用的方法:

@Directive({
  selector: '[saTooltip]',
  providers: [OverlayMock]
})
export class Tooltip {
  @Input()
  saTooltip: string;

  constructor(private el: ElementRef, private overlay: Overlay) {
    this.overlay.attach(el.nativeElement);
  }
  // ...
}

// ...

bootstrap(App);

前面的示例覆盖了Tooltip指令声明中Overlay令牌的提供者。这样,Angular 在实例化工具提示时将注入OverlayMock的实例,而不是Overlay

覆盖提供者的更好方法是使用bootstrap函数。我们可以这样做:

bootstrap(AppMock, [provide(Overlay, {
  useClass: OverlayMock
})]);

在前面的bootstrap调用中,我们为Overlay服务提供了一个不同的顶层组件和提供者,它将返回OverlayMock类的实例。这样,我们可以测试Tooltip指令,忽略Overlay的实现。

探索组件的依赖注入

由于组件通常是带有视图的指令,到目前为止我们所看到的关于 DI 机制如何与指令一起工作的一切对组件也是有效的。然而,由于组件提供的额外功能,我们可以对它们的提供程序有更多的控制。

正如我们所说,与每个组件关联的注入器将被标记为宿主注入器。有一个称为@Host的参数装饰器,它允许我们从任何注入器中检索给定的依赖项,直到达到最近的宿主注入器。这意味着通过在指令中使用@Host装饰器,我们可以声明我们要从当前注入器或任何父注入器中检索给定的依赖项,直到达到最近父组件的注入器。

添加到@Component装饰器的viewProviders属性负责实现更多的控制。

viewProviders 与 providers

让我们来看一个名为MarkdownPanel的组件的示例。这个组件将以以下方式使用:

<markdown-panel>
  <panel-title># Title</pane-title>
  <panel-content>
# Content of the panel
* First point
* Second point
  </panel-content>
</markdown-panel>

面板每个部分的内容将从 markdown 翻译成 HTML。我们可以将这个功能委托给一个名为Markdown的服务:

import * as markdown from 'markdown';
class Markdown {
  toHTML(md) {
    return markdown.toHTML(md);
  }
}

Markdown服务将 markdown 模块包装起来,以便通过 DI 机制进行注入。

现在让我们实现MarkdownPanel

在下面的代码片段中,我们可以找到组件实现的所有重要细节:

// ch5/ts/directives/app.ts
@Component({
  selector: 'markdown-panel',
  viewProviders: [Markdown],
  styles: [...],
  template: `
    <div class="panel">
      <div class="panel-title">
        <ng-content select="panel-title"></ng-content>
      </div>
      <div class="panel-content">
        <ng-content select="panel-content"></ng-content>
      </div>
    </div>`
})
class MarkdownPanel {
  constructor(private el: ElementRef, private md: Markdown) {}
  ngAfterContentInit() {
    let el = this.el.nativeElement;
    let title = el.querySelector('panel-title');
    let content = el.querySelector('panel-content');
    title.innerHTML = this.md.toHTML(title.innerHTML);
    content.innerHTML = this.md.toHTML(content.innerHTML);
  }
}

我们使用了markdown-panel选择器并设置了viewProviders属性。在这种情况下,只有一个单一的视图提供程序:Markdown服务的提供程序。通过设置这个属性,我们声明了所有在其中声明的提供程序将可以从组件本身和所有的视图子级中访问。

现在,假设我们有一个名为MarkdownButton的组件,并且我们希望以以下方式将其添加到我们的模板中:

    <markdown-panel>
      <panel-title>### Small title</panel-title>
      <panel-content>
      Some code
      </panel-content>
      <markdown-button>*Click to toggle*</markdown-button>
   </markdown-panel>

Markdown服务将无法被下面使用panel-content元素的MarkdownButton访问;但是,如果我们在组件的模板中使用按钮,它将是可访问的:

@Component({
  selector: 'markdown-panel',
  viewProviders: [Markdown],
  directives: [MarkdownButton],
  styles: […],
  template: `
    <div class="panel">
      <markdown-button>*Click to toggle*</markdown-button>
      <div class="panel-title">
        <ng-content select="panel-title"></ng-content>
      </div>
      <div class="panel-content">
        <ng-content select="panel-content"></ng-content>
      </div>
    </div>`
})

如果我们需要提供程序在所有内容和视图子级中可见,我们只需要将viewProviders属性的属性名更改为providers

你可以在ch5/ts/directives/app.ts目录下的文件中找到这个示例。

使用 ES5 的 Angular DI

我们已经熟练使用 TypeScript 进行 Angular 2 的依赖注入!正如我们所知,我们在开发 Angular 2 应用程序时并不局限于 TypeScript;我们也可以使用 ES5、ES2015 和 ES2016(以及 Dart,但这超出了本书的范围)。

到目前为止,我们在构造函数中使用标准的 TypeScript 类型注释声明了不同类的依赖关系。所有这些类都应该用@Injectable装饰器进行修饰。不幸的是,Angular 2 支持的其他一些语言缺少了一些这些特性。在下表中,我们可以看到 ES5 不支持类型注释、类和装饰器:

 ES5ES2015ES2016
装饰器是(没有参数装饰器)
类型注释

在这种情况下,我们如何利用这些语言中的 DI 机制?Angular 2 提供了一个内部 JavaScript领域特定语言DSL),允许我们利用 ES5 的整个框架功能。

现在,让我们将我们在上一节中看到的MarkdownPanel示例从 TypeScript 翻译成 ES5。首先,让我们从Markdown服务开始:

// ch5/es5/simple-example/app.js
var Markdown = ng.core.Class({
  constructor: function () {},
  toHTML: function (md) {
    return markdown.toHTML(md);
  }
});

我们定义了一个名为Markdown的变量,并将其值设置为从调用ng.core.Class返回的结果。这种构造允许我们使用 ES5 模拟 ES2015 类。ng.core.Class方法的参数是一个对象字面量,必须包含constructor函数的定义。因此,ng.core.Class将返回一个 JavaScript 构造函数,其中包含来自对象字面量的constructor的主体。传递参数边界内定义的所有其他方法将被添加到函数的原型中。

一个问题已经解决:我们现在可以在 ES5 中模拟类;还有两个问题没有解决!

现在,让我们看看如何定义MarkdownPanel组件:

// ch5/es5/simple-example/app.js

var MarkdownPanel = ng.core.Component({
  selector: 'markdown-panel',
  viewProviders: [Markdown],
  styles: [...],
  template: '...'
})
.Class({
  constructor: [Markdown, ng.core.ElementRef, function (md, el) {
    this.md = md;
    this.el = el;
  }],
  ngAfterContentInit: function () {
    …
  }
});

从第四章, 使用 Angular 2 组件和指令入门,我们已经熟悉了用于定义组件的 ES5 语法。现在,让我们看一下MarkdownPanel的构造函数,以确保我们如何声明我们组件甚至一般类的依赖关系。

从前面的片段中,我们可以注意到构造函数的值这次不是一个函数,而是一个数组。这可能让你觉得很熟悉,就像在 AngularJS 1.x 中一样,我们可以通过列出它们的名称来声明给定服务的依赖项:

Module.service('UserMapper',
  ['User', '$http', function (User, $http) {
    // …
  }]);

尽管 Angular 2 中的语法类似,但它带来了许多改进。例如,我们不再局限于使用字符串来表示依赖项的标记。

现在,假设我们想将Markdown服务作为可选依赖项。在这种情况下,我们可以通过传递装饰器数组来实现:

…
.Class({
  constructor: [[ng.core.Optional(), Markdown],
    ng.core.ElementRef, function (md, el) {
      this.md = md;
      this.el = el;
    }],
  ngAfterContentInit: function () {
    …
  }
});
…

通过嵌套数组,我们可以应用一系列装饰器:[[ng.core.Optional(), ng.core.Self(), Markdown], ...]。在这个例子中,@Optional@Self装饰器将按指定的顺序向类添加关联的元数据。

尽管使用 ES5 使我们的构建更简单,并允许我们跳过转译的中间步骤,这可能很诱人,但谷歌的建议是利用 TypeScript 的静态类型优势。这样,我们就有了更清晰的语法,更少的输入,更好的语义,并提供了强大的工具。

总结

在本章中,我们介绍了 Angular 2 的 DI 机制。我们简要讨论了在项目中使用依赖注入的优点,通过在框架的上下文中引入它。我们旅程的第二步是如何实例化和配置注入器;我们还解释了注入器的层次结构和已注册提供者的可见性。为了更好地分离关注点,我们提到了如何在指令和组件中注入承载应用程序业务逻辑的服务。我们最后关注的一点是如何使用 ES5 语法与 DI 机制配合使用。

在下一章中,我们将介绍框架的新路由机制。我们将解释如何配置基于组件的路由器,并向我们的应用程序添加多个视图。我们将要涵盖的另一个重要主题是新的表单模块。通过构建一个简单的应用程序,我们将演示如何创建和管理表单。

第六章:使用 Angular 2 路由器和表单

到目前为止,我们已经熟悉了框架的核心。我们知道如何定义组件和指令来开发我们应用程序的视图。我们还知道如何将与业务相关的逻辑封装到服务中,并使用 Angular 2 的依赖注入机制将所有内容连接起来。

在本章中,我们将解释一些概念,这些概念将帮助我们构建真实的 Angular 2 应用程序。它们如下:

  • 框架的基于组件的路由器。

  • 使用 Angular 2 表单。

  • 开发基于模板的表单。

  • 开发自定义表单验证器。

让我们开始吧!

开发“Coders repository”应用程序

在解释前面提到的概念的过程中,我们将开发一个包含开发人员存储库的示例应用程序。在我们开始编码之前,让我们解释一下应用程序的结构。

“Coders repository”将允许其用户通过填写有关他们的详细信息的表单或提供开发人员的 GitHub 句柄并从 GitHub 导入其个人资料来添加开发人员。

注意

为了本章的目的,我们将在内存中存储开发人员的信息,这意味着在刷新页面后,我们将丢失会话期间存储的所有数据。

应用程序将具有以下视图:

  • 所有开发人员的列表。

  • 一个添加或导入新开发人员的视图。

  • 显示给定开发人员详细信息的视图。此视图有两个子视图:

  • 基本详情:显示开发人员的姓名及其 GitHub 头像(如果有)。

  • 高级资料:显示开发人员已知的所有详细信息。

应用程序主页的最终结果将如下所示:

开发“Coders repository”应用程序

图 1

注意

在本章中,我们将只构建列出的视图中的一些。应用程序的其余部分将在第七章中解释,解释管道和与 RESTful 服务通信

每个开发人员将是以下类的实例:

// ch6/ts/multi-page-template-driven/developer.ts
export class Developer {
  public id: number;
  public githubHandle: string;
  public avatarUrl: string;
  public realName: string;
  public email: string;
  public technology: string;
  public popular: boolean;
}

所有开发人员将驻留在DeveloperCollection类中:

// ch6/ts/multi-page-template-driven/developer_collection.ts
class DeveloperCollection {
  private developers: Developer[] = [];
  getUserByGitHubHandle(username: string) {
    return this.developers
            .filter(u => u.githubHandle === username)
            .pop();
  }
  getUserById(id: number) {
    return this.developers
             .filter(u => u.id === id)
             .pop();
  }
  addDeveloper(dev: Developer) {
    this.developers.push(dev);
  }
  getAll() {
    return this.developers;
  }
}

这里提到的类封装了非常简单的逻辑,并没有任何特定于 Angular 2 的内容,因此我们不会深入讨论任何细节。

现在,让我们继续实现,通过探索新的路由器。

探索 Angular 2 路由器

正如我们已经知道的那样,为了引导任何 Angular 2 应用程序,我们需要开发一个根组件。 "Coders repository"应用程序并没有什么不同;在这种特定情况下唯一的额外之处是我们将有多个页面需要使用 Angular 2 路由连接在一起。

让我们从路由器配置所需的导入开始,并在此之后定义根组件:

// ch6/ts/step-0/app.ts
import {
  ROUTER_DIRECTIVES,
  ROUTER_PROVIDERS,
  Route,
  Redirect,
  RouteConfig,
  LocationStrategy,
  HashLocationStrategy
} from 'angular2/router';

在前面的片段中,我们直接从 Angular 2 路由器模块中导入了一些东西,这些东西是在框架的核心之外外部化的。

使用ROUTER_DIRECTIVES,路由器提供了一组常用的指令,我们可以将其添加到根组件使用的指令列表中。这样,我们将能够在模板中使用它们。

导入ROUTE_PROVIDERS包含一组与路由器相关的提供者,例如用于将RouteParams令牌注入组件构造函数的提供者。

RouteParams令牌提供了从路由 URL 中访问参数的能力,以便对给定页面关联的逻辑进行参数化。我们稍后将演示此提供程序的典型用例。

导入LocationStrategy类是一个抽象类,定义了HashLocationStrategy(用于基于哈希的路由)和PathLocationStrategy(利用历史 API 用于基于 HTML5 的路由)之间的公共逻辑。

注意

HashLocationStrategy不支持服务器端渲染。这是因为页面的哈希值不会发送到服务器,因此服务器无法找到与给定页面关联的组件。除了 IE9 之外,所有现代浏览器都支持 HTML5 历史 API。您可以在书的最后一章中找到有关服务器端渲染的更多信息。

我们没有看到的最后导入是RouteConfig,它是一个装饰器,允许我们定义与给定组件关联的路由;以及RouteRedirect,分别允许我们定义单个路由和重定向。使用RouteConfig,我们可以定义一组路由的层次结构,这意味着 Angular 2 的路由器支持嵌套路由,这与其前身 AngularJS 1.x 不同。

定义根组件并引导应用程序

现在,让我们定义一个根组件并配置应用程序的初始引导:

// ch6/ts/step-0/app.ts
@Component({
  selector: 'app',
  template: `…`,
  providers: [DeveloperCollection],
  directives: [ROUTER_DIRECTIVES]
})
@RouteConfig([…])
class App {}

bootstrap(…);

在前面的片段中,您可以注意到一个我们已经熟悉的语法,来自第四章,“开始使用 Angular 2 组件和指令”和第五章,“Angular 2 中的依赖注入”。我们定义了一个带有app选择器的组件,稍后我们将看一下template,以及提供者和指令的集合。

App组件使用了一个名为DeveloperCollection的单个提供者。这是一个包含应用程序存储的所有开发人员的类。您可以注意到我们添加了ROUTER_DIRECTIVES;它包含了 Angular 路由中定义的所有指令的数组。在这个数组中的一些指令允许我们链接到@RouteConfig装饰器中定义的其他路由(routerLink指令),并声明与不同路由相关联的组件应该呈现的位置(router-outlet)。我们将在本节后面解释如何使用它们。

现在让我们来看一下bootstrap函数的调用:

bootstrap(App, [
  ROUTER_PROVIDERS,
  provide(LocationStrategy, { useClass: HashLocationStrategy })
)]);

作为bootstrap的第一个参数,我们像往常一样传递应用程序的根组件。第二个参数是整个应用程序都可以访问的提供者列表。在提供者集中,我们添加了ROUTER_PROVIDERS,并且还配置了LocationStrategy令牌的提供者。Angular 2 使用的默认LocationStrategy令牌是PathLocationStrategy(即基于 HTML5 的令牌)。然而,在这种情况下,我们将使用基于哈希的令牌。

默认位置策略的两个最大优势是它得到了 Angular 2 的服务器渲染模块的支持,并且应用程序的 URL 对最终用户看起来更自然(没有使用#)。另一方面,如果我们使用PathLocationStrategy,我们可能需要配置我们的应用程序服务器,以便正确处理路由。

使用 PathLocationStrategy

如果我们想使用PathLocationStrategy,我们可能需要提供APP_BASE_HREF。例如,在我们的情况下,bootstrap配置应该如下所示:

import {APP_BASE_HREF} from 'angular2/router';
//...
bootstrap(App, [
  ROUTER_PROVIDERS,
  // The following line is optional, since it's
  // the default value for the LocationStrategy token
  provide(LocationStrategy, { useClass: PathLocationStrategy }),
  provide(APP_BASE_HREF, {
    useValue: '/dist/dev/ch6/ts/multi-page-template-driven/'
  }
)]);

默认情况下,与APP_BASE_HREF令牌关联的值是/;它表示应用程序内的基本路径名称。例如,在我们的情况下,“Coders repository”将位于/ch6/ts/multi-page-template-driven/目录下(即http://localhost:5555/dist/dev/ch6/ts/multi-page-template-driven/)。

使用@RouteConfig 配置路由

作为下一步,让我们来看看放置在@RouteConfig装饰器中的路由声明。

// ch6/ts/step-0/app.ts
@Component(…)
@RouteConfig([
  new Route({ component: Home, name: 'Home', path: '/' }),
  new Route({
    component: AddDeveloper,
    name: 'AddDeveloper',
    path: '/dev-add'
  }),
  //…
  new Redirect({
    path: '/add-dev',
    redirectTo: ['/dev-add']
  })
]) 
class App {}

正如前面的片段所示,@RouteConfig装饰器接受一个路由数组作为参数。在这个例子中,我们定义了两种类型的路由:使用RouteRedirect类。它们分别用于定义应用程序中的路由和重定向。

每个路由必须定义以下属性:

  • component:与给定路由相关联的组件。

  • name:用于在模板中引用的路由名称。

  • path:用于路由的路径。它将显示在浏览器的位置栏中。

注意

Route类还支持一个数据属性,其值可以通过使用RouteData令牌注入到其关联组件的构造函数中。数据属性的一个示例用例可能是,如果我们想要根据包含@RouteConfig声明的父组件的类型来注入不同的配置对象。

另一方面,重定向只包含两个属性:

  • path:用于重定向的路径。

  • redirectTo:用户被重定向到的路径。

在前面的例子中,我们声明希望用户打开路径/add-dev的页面被重定向到['/dev-add']

现在,为了使一切正常运行,我们需要定义AddDeveloperHome组件,这些组件在@RouteConfig中被引用。最初,我们将提供一个基本的实现,随着章节的进行逐步扩展。在ch6/ts/step-0中,创建一个名为home.ts的文件,并输入以下内容:

import {Component} from 'angular2/core';
@Component({
  selector: 'home',
  template: `Home`
})
export class Home {}

不要忘记在app.ts中导入Home组件。现在,打开名为add_developer.ts的文件,并输入以下内容:

import {Component} from 'angular2/core';

@Component({
  selector: 'dev-add',
  template: `Add developer`
})
export class AddDeveloper {}

使用 routerLink 和 router-outlet

我们已经声明了路由和与各个路由相关联的所有组件。唯一剩下的就是定义根App组件的模板,以便将所有内容链接在一起。

将以下内容添加到ch6/ts/step-0/app.ts@Component装饰器内的template属性中:

@Component({
  //…
  template: `
    <nav class="navbar navbar-default">
      <ul class="nav navbar-nav">
        <li><a [routerLink]="['/Home']">Home</a></li>
        <li><a [routerLink]="['/AddDeveloper']">Add developer</a></li>
      </ul>
    </nav>
    <router-outlet></router-outlet>
  `,
  //…
})

在上面的模板中有两个特定于 Angular 2 的指令:

  • routerLink:这允许我们添加到特定路由的链接。

  • router-outlet:这定义了当前选定路由相关的组件需要被渲染的容器。

让我们来看一下routerLink指令。它接受一个路由名称和参数的数组作为值。在我们的例子中,我们只提供了一个以斜杠为前缀的单个路由名称(因为这个路由在根级别)。注意,routerLink使用的路由名称是在@RouteConfig内部的路由声明的name属性声明的。在本章的后面,我们将看到如何链接到嵌套路由并传递路由参数。

这个指令允许我们独立于我们配置的LocationStrategy来声明链接。例如,假设我们正在使用HashLocationStrategy;这意味着我们需要在模板中的所有路由前加上#。如果我们切换到PathLocationStrategy,我们就需要移除所有的哈希前缀。routerLink的另一个巨大好处是它对我们透明地使用 HTML5 历史推送 API,这样就可以节省我们大量的样板代码。

上一个模板中的下一个对我们新的指令是router-outlet。它的责任类似于 AngularJS 1.x 中的ng-view指令。基本上,它们都有相同的作用:指出target组件应该被渲染的位置。这意味着根据定义,当用户导航到/时,Home组件将在router-outlet指出的位置被渲染,当用户导航到/dev-add时,AddDeveloper组件也是一样。

现在我们有这两条路线已经在运行了!打开http://localhost:5555/dist/dev/ch6/ts/step-0/,你应该会看到以下的截图:

使用 routerLink 和 router-outlet

图 2

如果没有,请看一下ch6/ts/step-1,里面包含了最终结果。

使用 AsyncRoute 进行懒加载

AngularJS 1.x 模块允许我们将应用程序中逻辑相关的单元分组在一起。然而,默认情况下,它们需要在初始应用程序的bootstrap期间可用,并且不允许延迟加载。这要求在初始页面加载期间下载整个应用程序的代码库,对于大型单页应用程序来说,这可能是无法接受的性能损失。

在一个完美的场景中,我们希望只加载与用户当前浏览页面相关的代码,或者根据与用户行为相关的启发式预取捆绑模块,这超出了本书的范围。例如,从我们示例的第一步打开应用程序:http://localhost:5555/dist/dev/ch6/ts/step-1/。一旦用户在/,我们只需要Home组件可用,一旦他或她导航到/dev-add,我们希望加载AddDeveloper组件。

让我们在 Chrome DevTools 中检查实际发生了什么:

使用 AsyncRoute 进行延迟加载

图 3

我们可以注意到在初始页面加载期间,我们下载了与所有路由相关的组件,甚至不需要的AddDeveloper。这是因为在app.ts中,我们明确要求HomeAddDeveloper组件,并在@RouteConfig声明中使用它们。

在这种特定情况下,加载这两个组件可能看起来不像是一个大问题,因为在这一步,它们非常简单,没有任何依赖关系。然而,在现实生活中的应用程序中,它们将导入其他指令、组件、管道、服务,甚至第三方库。一旦需要任何组件,它的整个依赖图将被下载,即使在那一点上并不需要该组件。

Angular 2 的路由器提供了解决这个问题的解决方案。我们只需要从angular2/router模块中导入AsyncRoute类,并在@RouteConfig中使用它,而不是使用Route

// ch6/ts/step-1-async/app.ts

import {AsyncRoute} from 'angular2/router';
@Component(…)
@RouteConfig([
  new AsyncRoute({
    loader: () =>
      System.import('./home')
        .then(m => m.Home),
      name: 'Home',
      path: '/'
    }),
  new AsyncRoute({
    loader: () =>
      System.import('./add_developer')
        .then(m => m.AddDeveloper),
      name: 'AddDeveloper',
      path: '/dev-add'
    }),
    new Redirect({ path: '/add-dev', redirectTo: ['/dev-add'] })
])
class App {}

AsyncRoute类的构造函数接受一个对象作为参数,该对象具有以下属性:

  • loader:返回一个需要用与给定路由相关联的组件解析的 promise 的函数。

  • name:路由的名称,可以在模板中使用它(通常在routerLink指令内部)。

  • path:路由的路径。

一旦用户导航到与@RouteConfig装饰器中的任何异步路由定义匹配的路由,其关联的加载程序将被调用。当加载程序返回的 promise 被解析为目标组件的值时,该组件将被缓存和渲染。下次用户导航到相同的路由时,将使用缓存的组件,因此路由模块不会下载相同的组件两次。

注意

请注意,前面的示例使用了 System,但是 Angular 的AsyncRoute实现并不与任何特定的模块加载器耦合。例如,可以使用 require.js 实现相同的结果。

使用 Angular 2 表单

现在让我们继续实现应用程序。在下一步中,我们将在AddDeveloperHome组件上工作。您可以通过扩展ch6/ts/step-0中当前的内容继续实现,或者如果您还没有达到步骤 1,您可以继续在ch6/ts/step-1中的文件上工作。

Angular 2 提供了两种开发带有验证的表单的方式:

  • 基于模板驱动的方法:提供了一个声明性的 API,我们可以在组件的模板中声明验证。

  • 基于模型驱动的方法:使用FormBuilder提供了一个命令式的 API。

在下一章中,我们将探讨两种方法。让我们从模板驱动的方法开始。

开发模板驱动的表单

对于每个CRUD创建检索更新和删除)应用程序,表单都是必不可少的。在我们的情况下,我们想要为输入我们想要存储的开发者的详细信息构建一个表单。

在本节结束时,我们将拥有一个表单,允许我们输入给定开发者的真实姓名,添加他或她喜欢的技术,输入电子邮件,并声明他或她是否在社区中受欢迎。最终结果将如下所示:

Developing template-driven forms

图 4

将以下导入添加到add_developer.ts

import {
  FORM_DIRECTIVES,
  FORM_PROVIDERS
} from 'angular2/common;

我们需要做的下一件事是将FORM_DIRECTIVES添加到AddDeveloper组件使用的指令列表中。FORM_DIRECTIVES指令包含一组预定义指令,用于管理 Angular 2 表单,例如formngModel指令。

FORM_PROVIDERS是一个包含一组预定义提供程序的数组,我们可以在应用程序的类中使用它们的令牌来注入与其关联的值。

现在将AddDeveloper的实现更新为以下内容:

@Component({
  selector: 'dev-add',
  templateUrl: './add_developer.html',
  styles: […],
  directives: [FORM_DIRECTIVES],
  providers: [FORM_PROVIDERS]
})
export class AddDeveloper {
  developer = new Developer();
  errorMessage: string;
  successMessage: string;
  submitted = false;
  technologies: string[] = [
    'JavaScript',
    'C',
    'C#',
    'Clojure'
  ];
  constructor(private developers: DeveloperCollection) {}
  addDeveloper() {}
}

developer属性包含与当前要添加到表单中的开发者相关的信息。最后两个属性,errorMessagesuccessMessage,分别用于在成功将开发者成功添加到开发者集合中或发生错误时显示当前表单的错误或成功消息。

深入研究模板驱动表单的标记

作为下一步,让我们创建AddDeveloper组件的模板(step-1/add_developer.html)。将以下内容添加到文件中:

<span *ngIf="errorMessage"
       class="alert alert-danger">{{errorMessage}}</span>
<span *ngIf="successMessage"
       class="alert alert-success">{{successMessage}}</span>

这两个元素旨在在添加新开发人员时显示错误和成功消息。当errorMessagesuccessMessage分别具有非假值时(即,与空字符串、falseundefined0NaNnull不同的值),它们将可见。

现在让我们开发实际的表单:

<form #f="ngForm" (ngSubmit)="addDeveloper()"
      class="form col-md-4" [hidden]="submitted">
  <div class="form-group">
    <label class="control-label"
           for="realNameInput">Real name</label>
    <div>
      <input id="realNameInput" class="form-control"
             type="text" ngControl="realName" required
             [(ngModel)]="developer.realName">
    </div>
  </div>
  <button class="btn btn-default"
          type="submit" [disabled]="!f.form.valid">Add</button>
  <!-- MORE CODE TO BE ADDED -->
</form> 

我们使用 HTML 的form标签声明一个新的表单。一旦 Angular 2 在父组件的模板中找到带有包含表单指令的这样的标签,它将自动增强其功能,以便用作 Angular 表单。一旦表单被 Angular 处理,我们可以应用表单验证和数据绑定。之后,使用#f="ngForm",我们将为模板定义一个名为f的局部变量,这允许我们引用当前的表单。表单元素中剩下的最后一件事是提交事件处理程序。我们使用一个我们已经熟悉的语法(ngSubmit)="expr",在这种情况下,表达式的值是附加到组件控制器的addDeveloper方法的调用。

现在,让我们来看一下类名为control-groupdiv元素。

注意

请注意,这不是一个特定于 Angular 的类;这是 Bootstrap 定义的一个CSS类,我们使用它来提供表单更好的外观和感觉。

在其中,我们可以找到一个没有任何 Angular 特定标记的label元素和一个允许我们设置当前开发人员的真实姓名的输入元素。我们将控件设置为文本类型,并声明其标识符等于realNameInputrequired属性由 HTML5 规范定义,并用于验证。通过在元素上使用它,我们声明这个元素需要有一个值。虽然这个属性不是特定于 Angular 的,但使用ngControl属性,Angular 将通过包含验证行为来扩展required属性的语义。这种行为包括在控件状态改变时设置特定的CSS类,并管理框架内部保持的状态。

ngControl指令是NgControlName指令的选择器。它通过在值更改时对它们运行验证并在控件生命周期期间应用特定类来增强表单控件的行为。您可能熟悉这一点,因为在 AngularJS 1.x 中,表单控件在其生命周期的特定阶段装饰有ng-pristineng-invalidng-valid类等。

以下表总结了框架在表单控件生命周期中添加的CSS类:

描述
ng-untouched控件尚未被访问
ng-touched控件已被访问
ng-pristine控件的值尚未更改
ng-dirty控件的值已更改
ng-valid控件附加的所有验证器都返回true
ng-invalid控件附加的任何验证器具有false

根据这个表,我们可以定义我们希望所有具有无效值的输入控件以以下方式具有红色边框:

input.ng-dirty.ng-invalid {
  border: 1px solid red;
}

在 Angular 2 的上下文中,前面的CSS的确切语义是对所有已更改且根据附加到它们的验证器无效的输入元素使用红色边框。

现在,让我们探讨如何将不同的验证行为附加到我们的控件上。

使用内置表单验证器

我们已经看到,我们可以使用required属性来改变任何控件的验证行为。Angular 2 提供了另外两个内置验证器,如下所示:

  • minlength:允许我们指定给定控件应具有的值的最小长度。

  • maxlength:允许我们指定给定控件应具有的值的最大长度。

这些验证器是用 Angular 2 指令定义的,可以以以下方式使用:

<input id="realNameInput" class="form-control"
       type="text" ngControl="realName"
       minlength="2"
       maxlength="30">

通过这种方式,我们指定希望输入的值在230个字符之间。

定义自定义控件验证器

Developer类中定义的另一个数据属性是email字段。让我们为这个属性添加一个输入字段。在前面表单的按钮上方,添加以下标记:

<div class="form-group">
  <label class="control-label" for="emailInput">Email</label>
  <div>
    <input id="emailInput"
           class="form-control"
           type="text" ngControl="email"
     [(ngModel)]="developer.email"/>
  </div>
</div>

我们可以将[(ngModel)]属性视为 AngularJS 1.x 中ng-model指令的替代方法。我们将在使用 Angular 2 进行双向数据绑定部分详细解释它。

尽管 Angular 2 提供了一组预定义的验证器,但它们并不足以满足我们的数据可能存在的各种格式。有时,我们需要为特定于应用程序的数据定义自定义验证逻辑。例如,在这种情况下,我们想要定义一个电子邮件验证器。一个典型的正则表达式,在一般情况下有效(但并不涵盖定义电子邮件地址格式的整个规范),如下所示:/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/

ch6/ts/step-1/add_developer.ts中,定义一个函数,该函数接受 Angular 2 控件的实例作为参数,并在控件的值为空或与前面提到的正则表达式匹配时返回null,否则返回{ 'invalidEmail': true }

function validateEmail(emailControl) {
  if (!emailControl.value ||
    /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(emailControl.value)) {
    return null;
  } else {
    return { 'invalidEmail': true };
  }
}

现在,从模块angular2/commonangular2/core导入NG_VALIDATORSDirective,并将此验证函数包装在以下指令中:

@Directive({
  selector: '[email-input]',
  providers: [provide(NG_VALIDATORS, {
    useValue: validateEmail, multi: true
  })]
})
class EmailValidator {}

在上述代码中,我们为令牌NG_VALIDATORS定义了一个多提供者。一旦我们注入与该令牌关联的值,我们将获得一个包含所有附加到给定控件的验证器的数组(有关多提供者的部分,请参阅第五章, Angular 2 中的依赖注入)。

使我们的自定义验证工作的唯一两个步骤是首先将email-input属性添加到电子邮件控件中:

<input id="emailInput"
   class="form-control"
 **email-input**
   type="text" ngControl="email"
   [(ngModel)]="developer.email"/>

接下来,将指令添加到组件AddDeveloper指令使用的列表中:

@Component({
  selector: 'dev-add',
  templateUrl: './add_developer.html',
  styles: [`
    input.ng-touched.ng-invalid {
      border: 1px solid red;
    }
  `],
  directives: [FORM_DIRECTIVES, **EmailValidator**],
  providers: [FORM_PROVIDERS]
})
class AddDeveloper {…}

注意

我们正在使用AddDeveloper控件的外部模板。关于给定模板是否应该被外部化或内联在具有templateUrltemplate的组件中,没有最终答案。最佳实践规定,我们应该内联短模板并外部化较长的模板,但没有具体定义哪些模板被认为是短的,哪些是长的。模板应该内联还是放入外部文件的决定取决于开发人员的个人偏好或组织内的常见惯例。

使用 Angular 与选择输入

作为下一步,我们应该允许应用程序的用户输入开发人员最精通的技术。我们可以定义一个技术列表,并在表单中显示为选择输入。

AddDeveloper类中,添加technologies属性:

class AddDeveloper {
  …
  technologies: string[] = [
    'JavaScript',
    'C',
    'C#',
    'Clojure'
  ];
  …
}

现在在模板中,在submit按钮的上方,添加以下标记:

<div class="form-group">
  <label class="control-label"
         for="technologyInput">Technology</label>
  <div>
    <select class="form-control"
            ngControl="technology" required
            [(ngModel)]="developer.technology">
        <option *ngFor="#t of technologies"
                [value]="t">{{t}}</option>
    </select>
  </div>
</div>

就像我们之前声明的输入元素一样,Angular 2 将根据选择输入的状态添加相同的类。为了在选择元素的值无效时显示红色边框,我们需要修改CSS规则:

@Component({
  …
  styles: [
    `input.ng-touched.ng-invalid,
     select.ng-touched.ng-invalid {
      border: 1px solid red;
    }`
  ],
  …
})
class AddDeveloper {…}

注意

注意,将所有样式内联到组件声明中可能是一种不好的做法,因为这样它们就无法重复使用。我们可以将所有组件中的通用样式提取到单独的文件中。@Component装饰器有一个名为styleUrls的属性,类型为array,我们可以在其中添加对给定组件使用的提取样式的引用。这样,如果需要,我们可以仅内联特定于组件的样式。

在此之后,我们将使用ngControl="technology"声明控件的名称等于"technology"。通过使用required属性,我们将声明应用程序的用户必须指定当前开发人员精通的技术。让我们最后一次跳过[(ngModel)]属性,看看如何定义选择元素的选项。

select元素内部,我们将使用以下方式定义不同的选项:

<option *ngFor="#t of technologies"
        [value]="t">{{t}}</option>

这是我们已经熟悉的语法。我们将简单地遍历AddDeveloper类中定义的所有技术,并对于每种技术,我们将显示一个值为技术名称的选项元素。

使用 NgForm 指令

我们已经提到,表单指令通过添加一些额外的 Angular 2 特定逻辑来增强 HTML5 表单的行为。现在,让我们退一步,看看包围输入元素的表单:

<form #f="ngForm" (ngSubmit)="addDeveloper()"
      class="form col-md-4" [hidden]="submitted">
  …
</form>

在上面的片段中,我们定义了一个名为f的新标识符,它引用了表单。我们可以将表单视为控件的组合;我们可以通过表单的 controls 属性访问各个控件。此外,表单还具有toucheduntouchedpristinedirtyinvalidvalid属性,这些属性取决于表单中定义的各个控件。例如,如果表单中的控件都没有被触摸过,那么表单本身的状态就是 untouched。然而,如果表单中的任何控件至少被触摸过一次,那么表单的状态也将是 touched。同样,只有当表单中的所有控件都有效时,表单才会有效。

为了说明form元素的用法,让我们定义一个带有选择器control-errors的组件,该组件显示给定控件的当前错误。我们可以这样使用它:

<label class="control-label" for="realNameInput">Real name</label>
<div>
  <input id="realNameInput" class="form-control" type="text"
     ngControl="realName" [(ngModel)]="developer.realName"
         required maxlength="50">
  <control-errors control="realName"
    [errors]="{
      'required': 'Real name is required',
      'maxlength': 'The maximum length of the real name is 50 characters'
      }"
   />
</div>

请注意,我们还向realName控件添加了maxlength验证器。

control-errors元素具有以下属性:

  • control:声明我们想要显示错误的控件的名称。

  • errors:创建控制错误和错误消息之间的映射。

现在在add_developer.ts中添加以下导入:

import {NgControl, NgForm} from 'angular2/common';
import {Host} from 'angular2/core';

在这些导入中,NgControl类是表示单个表单组件的抽象类,NgForm表示 Angular 表单,Host是与依赖注入机制相关的参数装饰器,我们已经在第五章中介绍过,Angular 2 中的依赖注入

以下是组件定义的一部分:

@Component({
  template: '<div>{{currentError}}</div>',
  selector: 'control-errors',
  inputs: ['control', 'errors']
})
class ControlErrors {
  errors: Object;
  control: string;
  constructor(@Host() private formDir: NgForm) {}
  get currentError() {…}
}

ControlErrors组件定义了两个输入:control——使用ngControl指令声明的控件的名称(ngControl属性的值)——和errors——错误和错误消息之间的映射。它们可以分别由control-errors元素的controlerrors属性指定。

例如,如果我们有控件:

<input type="text" ngControl="foobar" required />

我们可以通过以下方式声明其关联的control-errors组件:

<control-errors control="foobar"
      [errors]="{
       'required': 'The value of foobar is required'
      }"></control-errors>

在上面片段中的currentError getter 中,我们需要做以下两件事:

  • 找到使用control属性声明的组件的引用。

  • 返回与使当前控件无效的任何错误相关联的错误消息。

以下是实现此行为的代码片段:

@Component(…)
class ControlErrors {
  …
  get currentError() {
    let control = this.formDir.controls[this.control];
    let errorsMessages = [];
    if (control && control.touched) {
      errorsMessages = Object.keys(this.errors)
        .map(k => control.hasError(k) ? this.errors[k] : null)
        .filter(error => !!error);
    }
    return errorsMessages.pop();
  }
}

currentError的实现的第一行中,我们使用注入表单的controls属性获取目标控件。它的类型是{[key: string]: AbstractControl},其中键是我们用ngControl指令声明的控件的名称。一旦我们获得了目标控件的实例引用,我们可以检查它的状态是否被触摸(即是否已聚焦),如果是,我们可以循环遍历ControlError实例的errors属性中的所有错误。map函数将返回一个包含错误消息或null值的数组。唯一剩下的事情就是过滤掉所有的null值,并且只获取错误消息。一旦我们获得了每个错误的错误消息,我们将通过从errorMessages数组中弹出它来返回最后一个。

最终结果应如下所示:

使用 NgForm 指令

图 5

如果在实现ControlErrors组件的过程中遇到任何问题,您可以查看ch6/ts/multi-page-template-driven/add_developer.ts中的实现。

每个控件的hasError方法接受一个错误消息标识符作为参数,该标识符由验证器定义。例如,在前面定义自定义电子邮件验证器的示例中,当输入控件具有无效值时,我们将返回以下对象字面量:{ 'invalidEmail': true }。如果我们将ControlErrors组件应用于电子邮件控件,则其声明应如下所示:

  <control-errors control="email"
    [errors]="{ 'invalidEmail': 'Invalid email address' }"/>

Angular 2 的双向数据绑定

关于 Angular 2 最著名的传言之一是,双向数据绑定功能被移除,因为强制的单向数据流。这并不完全正确;Angular 2 的表单模块实现了一个带有选择器[(ngModel)]的指令,它允许我们轻松地实现双向数据绑定——从视图到模型,以及从模型到视图。

让我们来看一个简单的组件:

// ch6/ts/simple-two-way-data-binding/app.ts

import {Component} from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
import {NgModel} from 'angular2/common';

@Component({
  selector: 'app',
  directives: [NgModel],
  template: `
    <input type="text" [(ngModel)]="name"/>
    <div>{{name}}</div>
  `,
})
class App {
  name: string;
}

bootstrap(App, []);

在上面的示例中,我们从angular2/common包中导入了指令NgModel。稍后,在模板中,我们将属性[(ngModel)]设置为值name

起初,语法[(ngModel)]可能看起来有点不寻常。从第四章使用 Angular 2 组件和指令入门中,我们知道语法(eventName)用于绑定由给定组件触发的事件(或输出)。另一方面,我们使用语法[propertyName]="foobar"通过将属性(或在 Angular 2 组件术语中的输入)的值设置为表达式foobar的评估结果来实现单向数据绑定。NgModel语法将两者结合起来,以实现双向数据绑定。这就是为什么我们可以将其视为一种语法糖,而不是一个新概念。与 AngularJS 1.x 相比,这种语法的主要优势之一是我们可以通过查看模板来判断哪些绑定是单向的,哪些是双向的。

注意

就像(click)有其规范语法on-click[propertyName]bind-propertyName一样,[(ngModel)]的替代语法是bindon-ngModel

如果你打开http://localhost:5555/dist/dev/ch6/ts/simple-two-way-data-binding/,你会看到以下结果:

使用 Angular 2 进行双向数据绑定

图 6

一旦你改变输入框的值,它将自动更新以下标签。

我们已经在前面的模板中使用了NgModel指令。例如,我们绑定了开发人员的电子邮件:

<input id="emailInput"
       class="form-control" type="text"
       ngControl="email" [(ngModel)]="developer.email"
       email-input/>

这样,一旦我们改变文本输入的值,附加到AddDeveloper组件实例的开发人员对象的电子邮件属性的值就会被更新。

存储表单数据

让我们再次查看AddDeveloper组件控制器的接口:

export class AddDeveloper {
  submitted: false;
  successMessage: string;
  developer = new Developer();
  //…
  constructor(private developers: DeveloperCollection) {}
  addDeveloper(form) {…}
}

它有一个Developer类型的字段,我们使用NgModel指令将表单控件绑定到其属性。该类还有一个名为addDeveloper的方法,该方法在表单提交时被调用。我们通过绑定submit事件来声明这一点:

<!-- ch6/ts/multi-page-template-driven/add_developer.html -->
<form #f="form" (ngSubmit)="addDeveloper()"
      class="form col-md-4" [hidden]="submitted"><button class="btn btn-default"
      type="submit" [disabled]="!f.form.valid">Add</button>
</form>

在上面的片段中,我们可以注意到两件事。我们使用#f="ngForm"引用了表单,并将按钮的 disabled 属性绑定到表达式!f.form.valid。我们已经在前一节中描述了NgForm控件;一旦表单中的所有控件都具有有效值,其 valid 属性将为 true。

现在,假设我们已经为表单中的所有输入控件输入了有效值。这意味着其submit按钮将被启用。一旦我们按下Enter或点击Add按钮,将调用addDeveloper方法。以下是此方法的示例实现:

class AddDeveloper {
  //…
addDeveloper() {
    this.developer.id = this.developers.getAll().length + 1;
    this.developers.addDeveloper(this.developer);
    this.successMessage = `Developer ${this.developer.realName} was successfully added`;
    this.submitted = true;
  }

最初,我们将当前开发人员的id属性设置为DeveloperCollection中开发人员总数加一。稍后,我们将开发人员添加到集合中,并设置successMessage属性的值。就在这之后,我们将提交属性设置为true,这将导致隐藏表单。

列出所有存储的开发人员

现在我们可以向开发人员集合添加新条目了,让我们在“Coders repository”的首页上显示所有开发人员的列表。

打开文件ch6/ts/step-1/home.ts并输入以下内容:

import {Component} from 'angular2/core';
import {DeveloperCollection} from './developer_collection';

@Component({
  selector: 'home',
  templateUrl: './home.html'
})
export class Home {
  constructor(private developers: DeveloperCollection) {}
  getDevelopers() {
    return this.developers.getAll();
  }
}

这对我们来说并不新鲜。我们通过提供外部模板并实现getDevelopers方法来扩展Home组件的功能,该方法将其调用委托给构造函数中注入的DeveloperCollection实例。

模板本身也是我们已经熟悉的东西:

<table class="table" *ngIf="getDevelopers().length > 0">
  <thead>
    <th>Email</th>
    <th>Real name</th>
    <th>Technology</th>
    <th>Popular</th>
  </thead>
  <tr *ngFor="#dev of getDevelopers()">
    <td>{{dev.email}}</td>
    <td>{{dev.realName}}</td>
    <td>{{dev.technology}}</td>
    <td [ngSwitch]="dev.popular">
      <span *ngSwitchWhen="true">Yes</span>
      <span *ngSwitchWhen="false">Not yet</span>
    </td>
  </tr>
</table>
<div *ngIf="getDevelopers().length == 0">
  There are no any developers yet
</div>

我们将所有开发人员列为 HTML 表格中的行。对于每个开发人员,我们检查其 popular 标志的状态。如果其值为true,那么在Popular列中,我们显示一个带有文本Yes的 span,否则我们将文本设置为No

当您在添加开发人员页面输入了一些开发人员,然后导航到主页时,您应该看到类似以下截图的结果:

列出所有存储的开发人员

图 7

注意

您可以在ch6/ts/multi-page-template-driven找到应用程序的完整功能。

摘要

到目前为止,我们已经解释了 Angular 2 中路由的基础知识。我们看了一下如何定义不同的路由,并实现与它们相关的组件,这些组件在路由更改时显示出来。为了链接到不同的路由,我们解释了routerLink,并且我们还使用了router-outlet指令来指出与各个路由相关的组件应该被渲染的位置。

我们还研究了 Angular 2 表单功能,包括内置和自定义验证。之后,我们解释了NgModel指令,它为我们提供了双向数据绑定。

在下一章中,我们将介绍如何开发基于模型的表单和子路由以及参数化路由,使用Http模块进行 RESTful 调用,并使用自定义管道转换数据。

第七章:解释管道和与 RESTful 服务通信

在上一章中,我们介绍了框架的一些非常强大的功能。然而,我们可以更深入地了解 Angular 的表单模块和路由器的功能。在接下来的章节中,我们将解释如何:

  • 开发模型驱动的表单。

  • 定义参数化路由。

  • 定义子路由。

  • 使用Http模块与 RESTful API 进行通信。

  • 使用自定义管道转换数据。

我们将在扩展“Coders repository”应用程序的功能过程中探索所有这些概念。在上一章的开头,我们提到我们将允许从 GitHub 导入开发者。但在我们实现这个功能之前,让我们扩展表单的功能。

在 Angular 2 中开发模型驱动的表单

这些将是完成“Coders repository”最后的步骤。您可以在ch6/ts/step-1/(或ch6/ts/step-2,具体取决于您之前的工作)的基础上构建,以便使用我们将要介绍的新概念扩展应用程序的功能。完整的示例位于ch7/ts/multi-page-model-driven

这是我们在本节结束时要实现的结果:

在 Angular 2 中开发模型驱动的表单

在上面的截图中,有以下两种表单:

  • 一个用于从 GitHub 导入现有用户的表单,其中包含:

  • GitHub 句柄的输入。

  • 一个指出我们是否要从 GitHub 导入开发者或手动输入的复选框。

  • 一个用于手动输入新用户的表单。

第二种形式看起来与我们在上一节中完成的方式完全一样。然而,这一次,它的定义看起来有点不同:

<form class="form col-md-4"
      [ngFormModel]="addDevForm" [hidden]="submitted">
  <!-- TODO -->
</form>

请注意,这一次,我们没有submit处理程序或#f="ngForm"属性。相反,我们使用[ngFormModel]属性来绑定到组件控制器内定义的属性。通过使用这个属性,我们可以绑定到一个叫做ControlGroup的东西。正如其名称所示,ControlGroup类包括一组控件以及与它们关联的验证规则集。

我们需要使用类似的声明来导入开发者表单。然而,这一次,我们将提供不同的[ngFormModel]属性值,因为我们将在组件控制器中定义一个不同的控件组。将以下片段放在我们之前介绍的表单上方:

<form class="form col-md-4"
   [ngFormModel]="importDevForm" [hidden]="submitted">
<!-- TODO -->
</form>

现在,让我们在组件的控制器中声明importDevFormaddDevForm属性:

import {ControlGroup} from 'angular2/common';
@Component(…)
export class AddDeveloper {
  importDevForm: ControlGroup;
  addDevForm: ControlGroup;
  …
  constructor(private developers: DeveloperCollection,
    fb: FormBuilder) {…}
  addDeveloper() {…}
}

最初,我们从angular2模块中导入了ControlGroup类,然后在控制器中声明了所需的属性。让我们还注意到AddDeveloper的构造函数有一个额外的参数叫做fb,类型为FormBuilder

FormBuilder提供了一个可编程的 API,用于定义ControlGroups,在这里我们可以为组中的每个控件附加验证行为。让我们使用FormBulder实例来初始化importDevFormaddDevForm属性:

constructor(private developers: DeveloperCollection,
  fb: FormBuilder) {
  this.importDevForm = fb.group({
    githubHandle: ['', Validators.required],
    fetchFromGitHub: [false]
  });
  this.addDevForm = fb.group({
    realName: ['', Validators.required],
    email: ['', validateEmail],
    technology: ['', Validators.required],
    popular: [false]
  });
}
…

FormBuilder实例有一个名为group的方法,允许我们定义给定表单中各个控件的默认值和验证器等属性。

根据前面的片段,importDevForm有两个我们之前介绍的字段:githubHandlefetchFromGitHub。我们声明githubHandle控件的值是必填的,并将fetchFromGitHub控件的默认值设置为false

在第二个表单addDevForm中,我们声明了四个控件。对于realName控件的默认值,我们将其设置为空字符串,并使用Validators.requred来引入验证行为(这正是我们为githubHandle控件所做的)。作为电子邮件输入的验证器,我们将使用validateEmail函数,并将其初始值设置为空字符串。用于验证的validateEmail函数是我们在上一章中定义的:

function validateEmail(emailControl) {
  if (!emailControl.value ||
     /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(emailControl.value)) {
    return null;
  } else {
    return { 'invalidEmail': true };
  }
}

我们在这里定义的最后两个控件是technology控件,其值是必填的,初始值为空字符串,以及popular控件,其初始值设置为false

使用控件验证器的组合

我们看了一下如何将单个验证器应用于表单控件。然而,在一些应用程序中,领域可能需要更复杂的验证逻辑。例如,如果我们想要将必填和validateEmail验证器都应用于电子邮件控件,我们应该这样做:

this.addDevForm = fb.group({
  …
  email: ['', Validators.compose([
    Validators.required,
    validateEmail]
  )],
  …
});

Validators对象的compose方法接受一个验证器数组作为参数,并返回一个新的验证器。新的验证器的行为将是由作为参数传递的各个验证器中定义的逻辑组成,并且它们将按照它们在数组中被引入的顺序应用。

传递给group方法的对象文字的属性名称应与我们在模板中为输入设置的ngControl属性的值相匹配。

这是importDevForm的完整模板:

<form class="form col-md-4"
   [ngFormModel]="importDevForm" [hidden]="submitted" >
  <div class="form-group">
    <label class="control-label"
           for="githubHandleInput">GitHub handle</label>
    <div>
      <input id="githubHandleInput"
             class="form-control" type="text"
             [disabled]="!fetchFromGitHub" 
             ngControl="githubHandle">
      <control-errors control="githubHandle"
        [errors]="{
          'required': 'The GitHub handle is required'
        }"></control-errors>
    </div>
  </div>
  <div class="form-group">
    <label class="control-label"
           for="fetchFromGitHubCheckbox">
       Fetch from GitHub
    </label>
    <input class="checkbox-inline" id="fetchFromGitHubCheckbox"
           type="checkbox" ngControl="fetchFromGitHub"
           [(ngModel)]="fetchFromGitHub">
  </div>
</form>

在前面的模板中,您可以注意到一旦提交的标志具有值true,表单将对用户隐藏。在第一个输入元素旁边,我们将ngControl属性的值设置为githubHandle

注意

请注意,给定输入元素的ngControl属性的值必须与我们在组件控制器中的ControlGroup定义中用于相应控件声明的名称相匹配。

关于githubHandle控件,我们还将disabled属性设置为等于表达式评估的结果:!fetchFromGitHub。这样,当fetchFromGitHub复选框未被选中时,githubHandle控件将被禁用。类似地,在前几节的示例中,我们使用了先前定义的ControlErrors组件。这次,我们设置了一个带有消息GitHub 句柄是必需的的单个错误。

addDevForm表单的标记看起来非常相似,因此我们不会在这里详细描述它。如果您对如何开发它的方法不是完全确定,可以查看ch7/ts/multi-page-model-driven/add_developer.html中的完整实现。

我们要查看的模板的最后部分是Submit按钮:

<button class="btn btn-default"
        (click)="addDeveloper()"
        [disabled]="(fetchFromGitHub && !importDevForm.valid) ||
                    (!fetchFromGitHub && !addDevForm.valid)">
  Add
</button>

单击按钮将调用组件控制器中定义的addDeveloper方法。在[disabled]属性的值设置为的表达式中,我们最初通过使用与复选框绑定的属性的值来检查选择了哪种表单,也就是说,我们验证用户是否想要添加新开发人员或从 GitHub 导入现有开发人员。如果选择了第一个选项(即,如果复选框未被选中),我们将验证添加新开发人员的ControlGroup是否有效。如果有效,则按钮将启用,否则将禁用。当用户选中复选框以从 GitHub 导入开发人员时,我们也会执行相同的操作。

探索 Angular 的 HTTP 模块

现在,在我们为导入现有开发人员和添加新开发人员开发表单之后,是时候在组件的控制器中实现其背后的逻辑了。

为此,我们需要与 GitHub API 进行通信。虽然我们可以直接从组件的控制器中进行此操作,但通过这种方式,我们可以将其与 GitHub 的 RESTful API 耦合在一起。为了进一步分离关注点,我们可以将与 GitHub 通信的逻辑提取到一个名为GitHubGateway的单独服务中。打开一个名为github_gateway.ts的文件,并输入以下内容:

import {Injectable} from 'angular2/core';
import {Http} from 'angular2/http';

@Injectable()
export class GitHubGateway {
  constructor(private http: Http) {}
  getUser(username: string) {
    return this.http
            .get(`https://api.github.com/users/${username}`);
  }
}

最初,我们从angular2/http模块导入了Http类。所有与 HTTP 相关的功能都是外部化的,并且在 Angular 的核心之外。由于GitHubGateway接受一个依赖项,需要通过框架的 DI 机制进行注入,因此我们将其装饰为@Injectable装饰器。

我们将要使用的 GitHub 的 API 中唯一的功能是用于获取用户的功能,因此我们将定义一个名为getUser的单个方法。作为参数,它接受开发者的 GitHub 句柄。

注意

请注意,如果您每天对 GitHub 的 API 发出超过 60 个请求,您可能会收到错误GitHub API 速率限制已超出。这是由于没有 GitHub API 令牌的请求的速率限制。有关更多信息,请访问github.com/blog/1509-personal-api-tokens

getUser方法中,我们使用了在constructor函数中收到的Http服务的实例。Http服务的 API 尽可能接近 HTML5 fetch API。但是,有一些区别。其中最重要的一个是,在撰写本内容时,Http实例的所有方法都返回Observables而不是Promises

Http服务实例具有以下 API:

  • request(url: string | Request, options: RequestOptionsArgs): 对指定的 URL 进行请求。可以使用RequestOptionsArgs配置请求:
http.request('http://example.com/', {
  method: 'get',
  search: 'foo=bar',
  headers: new Headers({
    'X-Custom-Header': 'Hello'
	})
});
  • get(url: string, options?: RequestOptionsArgs): 对指定的 URL 进行 get 请求。可以使用第二个参数配置请求头和其他选项。

  • post(url: string, options?: RequestOptionsArgs): 对指定的 URL 进行 post 请求。可以使用第二个参数配置请求体、头和其他选项。

  • put(url: string, options?: RequestOptionsArgs): 对指定的 URL 进行 put 请求。可以使用第二个参数配置请求头和其他选项。

  • patch(url: string, options?: RequestOptionsArgs): 发送一个 patch 请求到指定的 URL。请求头和其他选项可以使用第二个参数进行配置。

  • delete(url: string, options?: RequestOptionsArgs): 发送一个 delete 请求到指定的 URL。请求头和其他选项可以使用第二个参数进行配置。

  • head(url: string, options?: RequestOptionsArgs): 发送一个 head 请求到指定的 URL。请求头和其他选项可以使用第二个参数进行配置。

使用 Angular 的 HTTP 模块

现在,让我们实现从 GitHub 导入现有用户的逻辑!打开文件 ch6/ts/step-2/add_developer.ts 并输入以下导入:

import {Response, HTTP_PROVIDERS} from 'angular2/http';
import {GitHubGateway} from './github_gateway';

HTTP_PROVIDERSGitHubGateway 添加到 AddDeveloper 组件的提供者列表中:

@Component({
  …
  providers: [GitHubGateway, FORM_PROVIDERS, HTTP_PROVIDERS]
})
class AddDeveloper {…}

作为下一步,我们必须在类的构造函数中包含以下参数:

constructor(private githubAPI: GitHubGateway,
  private developers: DeveloperCollection,
  fb: FormBuilder) {
  //…
}

这样,AddDeveloper 类的实例将有一个名为 githubAPI 的私有属性。

唯一剩下的就是实现 addDeveloper 方法,并允许用户使用 GitHubGateway 实例导入现有的开发者。

用户按下 添加 按钮后,我们需要检查是否需要导入现有的 GitHub 用户或添加新的开发者。为此,我们可以使用 fetchFromGitHub 控件的值:

if (this.importDevForm.controls['fetchFromGitHub'].value) {
  // Import developer
} else {
  // Add new developer
}

如果它有一个真值,那么我们可以调用 githubAPI 属性的 getUser 方法,并将 githubHandle 控件的值作为参数传递:

this.githubAPI.getUser(model.githubHandle)

getUser 方法中,我们将调用 Http 服务的 get 方法,该方法返回一个可观察对象。为了获取可观察对象即将推送的结果,我们需要向其 subscribe 方法传递一个回调函数:

this.githubAPI.getUser(model.githubHandle)
  .map((r: Response) => r.json())
  .subscribe((res: any) => {
    // "res" contains the response of the GitHub's API 
  });

在上面的代码片段中,我们首先建立了 HTTP get 请求。之后,我们将得到一个可观察对象,通常会发出一系列的值(在这种情况下,只有一个值—请求的响应),并将它们映射到它们的主体的 JSON 表示。如果响应失败或其主体不是有效的 JSON 字符串,那么我们将得到一个错误。

注意

请注意,为了减小 RxJS 的体积,Angular 的核心团队只包含了它的核心部分。为了使用 mapcatch 方法,您需要在 add_developer.ts 中添加以下导入:

**import 'rxjs/add/operator/map';**
**import 'rxjs/add/operator/catch';**

现在让我们实现订阅回调的主体:

let dev = new Developer();
dev.githubHandle = res.login;
dev.email = res.email;
dev.popular = res.followers >= 1000;
dev.realName = res.name;
dev.id = res.id;
dev.avatarUrl = res.avatar_url;
this.developers.addDeveloper(dev);
this.successMessage = `Developer ${dev.githubHandle} successfully imported from GitHub`;

在前面的例子中,我们设置了一个新的Developer实例的属性。在这里,我们建立了从 GitHub 的 API 返回的对象与我们应用程序中开发者表示之间的映射。我们还认为如果开发者拥有超过 1,000 个粉丝,那么他或她就是受欢迎的。

addDeveloper方法的整个实现可以在ch7/ts/multi-page-model-driven/add_developer.ts中找到。

注意

为了处理失败的请求,我们可以使用可观察实例的catch方法:

 **this.githubAPI.getUser(model.githubHandle)**
 **.catch((error, source, caught) => {**
 **console.log(error)**
 **return error;**
 **})**

定义参数化视图

作为下一步,让我们为每个开发者专门创建一个页面。在这个页面内,我们将能够详细查看他或她的个人资料。一旦用户在应用程序的主页上点击任何开发者的名称,他或她应该被重定向到一个包含所选开发者详细资料的页面。最终结果将如下所示:

定义参数化视图

为了做到这一点,我们需要将开发者的标识符传递给显示开发者详细资料的组件。打开app.ts并添加以下导入:

import {DeveloperDetails} from './developer_details';

我们还没有开发DeveloperDetails组件,所以如果运行应用程序,你会得到一个错误。我们将在下一段定义组件,但在此之前,让我们修改App组件的@RouteConfig定义:

@RouteConfig([
  //…
  new Route({
    component: DeveloperDetails,
    name: 'DeveloperDetails',
    path: '/dev-details/:id/...'
  }),
  //…
])
class App {}

我们添加了一个单一路由,与DeveloperDetails组件相关联,并且作为别名,我们使用了字符串"DeveloperDetails"

component属性的值是对组件构造函数的引用,该构造函数应该处理给定的路由。一旦应用程序的源代码在生产中被压缩,组件名称可能会与我们输入的名称不同。这将在使用routerLink指令在模板中引用路由时创建问题。为了防止这种情况发生,核心团队引入了name属性,在这种情况下,它等于控制器的名称。

注意

尽管到目前为止的所有示例中,我们将路由的别名设置为与组件控制器的名称相同,但这并不是必需的。这个约定是为了简单起见,因为引入两个名称可能会令人困惑:一个用于指向路由,另一个用于与给定路由相关联的控制器。

path属性中,我们声明该路由有一个名为id的单个参数,并用"..."提示框架,这个路由将在其中有嵌套路由。

现在,让我们将当前开发人员的id作为参数传递给routerLink指令。在你的工作目录中打开home.html,并用以下内容替换我们显示开发人员的realName属性的表格单元格:

<td>
  <a [routerLink]="['/DeveloperDetails',
      { 'id': dev.id }, 'DeveloperBasicInfo']">
    {{dev.realName}}
  </a>
</td>

routerLink指令的值是一个包含以下三个元素的数组:

  • '/DeveloperDetails':显示根路由的字符串

  • { 'id': dev.id }:声明路由参数的对象文字

  • 'DeveloperBasicInfo':显示在组件别名为DeveloperDetails的嵌套路由中应该呈现的组件的路由名称

定义嵌套路由

现在让我们跳到DeveloperDetails的定义。在你的工作目录中,创建一个名为developer_details.ts的文件,并输入以下内容:

import {Component} from 'angular2/core';
import {
  ROUTER_DIRECTIVES,
  RouteConfig,
  RouteParams
} from 'angular2/router';
import {Developer} from './developer';
import {DeveloperCollection} from './developer_collection';

@Component({
  selector: 'dev-details',
  template: `…`,
})
@RouteConfig(…)
export class DeveloperDetails {
  public dev: Developer;
  constructor(routeParams: RouteParams,
    developers: DeveloperCollection) {
    this.dev = developers.getUserById(
      parseInt(routeParams.params['id'])
    );
  }
}

在上面的代码片段中,我们定义了一个带有控制器的组件DeveloperDetails。您可以注意到,在控制器的构造函数中,通过 Angular 2 的 DI 机制,我们注入了与RouteParams令牌相关联的参数。注入的参数为我们提供了访问当前路由可见参数的权限。我们可以使用注入对象的params属性访问它们,并使用参数的名称作为键来访问目标参数。

由于我们从routeParams.params['id']得到的参数是一个字符串,我们需要将其解析为数字,以便获取与给定路由相关联的开发人员。现在让我们定义与DeveloperDetails相关的路由:

@Component(…)
@RouteConfig([{
    component: DeveloperBasicInfo,
    name: 'DeveloperBasicInfo',
    path: '/'
  },
  {
    component: DeveloperAdvancedInfo,
    name: 'DeveloperAdvancedInfo',
    path: '/dev-details-advanced'
  }])
export class DeveloperDetails {…}

在上面的代码片段中,对我们来说没有什么新的。路由定义遵循我们已经熟悉的完全相同的规则。

现在,让我们在组件的模板中添加与各个嵌套路由相关的链接:

@Component({
  selector: 'dev-details',
  directives: [ROUTER_DIRECTIVES],
  template: `
    <section class="col-md-4">
      <ul class="nav nav-tabs">
        <li>
          <a [routerLink]="['./DeveloperBasicInfo']">
            Basic profile
          </a>
        </li>
        <li>
          <a [routerLink]="['./DeveloperAdvancedInfo']">
            Advanced details
          </a>
        </li>
      </ul>
      <router-outlet/>
    </section>
  `,
})
@RouteConfig(…)
export class DeveloperDetails {…}

在模板中,我们声明了两个相对于当前路径的链接。第一个指向DeveloperBaiscInfo,这是在DeveloperDetails组件的@RouteConfig中定义的第一个路由的名称,相应地,第二个指向DeveloperAdvancedInfo

由于这两个组件的实现非常相似,让我们只看一下DeveloperBasicInfo。作为练习,您可以开发第二个,或者查看ch7/ts/multi-page-model-driven/developer_advanced_info.ts中的实现:

import {
  Component,
  Inject,
  forwardRef,
  Host
} from 'angular2/core';
import {DeveloperDetails} from './developer_details';
import {Developer} from './developer';

@Component({
  selector: 'dev-details-basic',
  styles: […],
  template: `
    <h2>{{dev.realName}}</h2>
    <img *ngIf="dev.avatarUrl == null"
      class="avatar" src="./gravatar-60-grey.jpg" width="150">
    <img *ngIf="dev.avatarUrl != null"
      class="avatar" [src]="dev.avatarUrl" width="150">
  `
})
export class DeveloperBasicInfo {
  dev: Developer;
  constructor(@Inject(forwardRef(() => DeveloperDetails))
    @Host() parent: DeveloperDetails) {
    this.dev = parent.dev;
  }
}

在上述代码片段中,我们结合了@Inject参数装饰器和@Host来注入父组件。在@Inject内部,我们使用forwardRef,因为在developer_basic_infodeveloper_details之间存在循环依赖(在developer_basic_info中,我们导入developer_details,而在developer_details中,我们导入developer_basic_info)。

我们需要一个对父组件实例的引用,以便获取与所选路由对应的当前开发者的实例。

使用管道转换数据

现在是 Angular 2 为我们提供的最后一个构建块的时间,这是我们尚未详细介绍的管道。

就像 AngularJS 1.x 中的过滤器一样,管道旨在封装所有数据转换逻辑。让我们来看看我们刚刚开发的应用程序的主页模板:

…
<td [ngSwitch]="dev.popular">
  <span *ngSwitch-when="true">Yes</span>
  <span *ngSwitch-when="false">Not yet</span>
</td>
…

在上述代码片段中,根据popular属性的值,我们使用NgSwitchNgSwitchThen指令显示了不同的数据。虽然这样可以工作,但是有些冗余。

开发无状态管道

让我们开发一个管道,转换popular属性的值并在NgSwitchNgSwitchThen的位置使用它。该管道将接受三个参数:应该被转换的值,当值为真时应该显示的字符串,以及在值为假时应该显示的另一个字符串。

通过使用 Angular 2 自定义管道,我们将能够简化模板为:

<td>{{dev.popular | boolean: 'Yes': 'No'}}</td>

我们甚至可以使用表情符号:

<td>{{dev.popular | boolean: '👍': '👎'}}</td>
```ts

我们将管道应用到值上的方式与在 AngularJS 1.x 中的方式相同。我们传递给管道的参数应该用冒号(`:`)符号分隔。

为了开发一个 Angular 2 管道,我们需要以下导入:

import {Pipe, PipeTransform} from 'angular2/core';


`Pipe`装饰器可用于向实现数据转换逻辑的类添加元数据。`PipeTransform`是一个具有名为 transform 的单个方法的接口:

import {Pipe, PipeTransform} from 'angular2/core';

@Pipe({ name: 'boolean' }) export class BooleanPipe implements PipeTransform { constructor() {} transform(flag: boolean, args: string[]): string { return flag ? args[0] : args[1]; } }


上述代码片段是`BooleanPipe`的整个实现。管道的名称决定了它在模板中的使用方式。

在能够使用管道之前,我们需要做的最后一件事是将`BooleanPipe`类添加到`Home`组件使用的管道列表中(`BooleanPipe`已经通过`@Pipe`装饰器附加了元数据,所以它的名称已经附加到它上面):

@Component({ … pipes: [BooleanPipe], }) export class Home { constructor(private developers: DeveloperCollection) {} getDevelopers() {…} }


## 使用 Angular 内置的管道

Angular 2 提供了以下一组内置管道:

+   `CurrencyPipe`:此管道用于格式化货币数据。作为参数,它接受货币类型的缩写(即`"EUR"``"USD"`等)。可以按以下方式使用:

{{ currencyValue | currency: 'USD' }}


+   `DatePipe`:此管道用于日期转换。可以按以下方式使用:

{{ dateValue | date: 'shortTime' }}


+   `DecimalPipe`:此管道用于转换十进制数。它接受的参数形式为`"{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}"`。可以按以下方式使用:

{{ 42.1618 | number: '3.1-2' }}


+   `JsonPipe`:这将 JavaScript 对象转换为 JSON 字符串。可以按以下方式使用:

{{ { foo: 42 } | json }}


+   `LowerCasePipe`:将字符串转换为小写。可以按以下方式使用:

{{ FOO | lowercase }}


+   `UpperCasePipe`:将字符串转换为大写。可以按以下方式使用:

{{ 'foo' | uppercase }}


+   `PercentPipe`:这将数字转换为百分比。可以按以下方式使用:

{{ 42 | percent: '2.1-2' }}


+   `SlicePipe`:返回数组的一个切片。该管道接受切片的起始和结束索引。可以按以下方式使用:

{{ [1, 2, 3] | slice: 1: 2 }}


+   `AsyncPipe`:这是一个`有状态`管道,接受一个 observable 或一个 promise。我们将在本章末尾看一下它。

## 开发有状态的管道

之前提到的所有管道之间有一个共同点——每次将它们应用于相同的值并传递相同的参数集时,它们都会返回完全相同的结果。具有引用透明属性的这种管道称为纯管道。

`@Pipe`装饰器接受以下类型的对象文字:`{ name: string, pure?: boolean }`,其中`pure`属性的默认值为`true`。这意味着当我们使用`@Pipe`装饰器装饰给定的类时,我们可以声明我们希望管道实现的逻辑是有状态的还是无状态的。纯属性很重要,因为如果管道是无状态的(即,对于相同的值和相同的参数集合应用时返回相同的结果),则可以优化变更检测。

现在让我们构建一个有状态的管道!我们的管道将向 JSON API 发出 HTTP `get`请求。为此,我们将使用`angular2/http`模块。

### 注意

请注意,在管道中具有业务逻辑并不被认为是最佳实践。这种类型的逻辑应该被提取到一个服务中。这里的示例仅用于学习目的。

在这种情况下,管道需要根据请求的状态(即是否挂起或已完成)来保持不同的状态。我们将以以下方式使用管道:

{{ "example.com/user.json" | fetchJson | json }}


这样,我们就可以在 URL 上应用`fetchJson`管道,一旦我们从远程服务获得响应并且请求的承诺已经解决,我们就可以在响应中得到的对象上应用`json`管道。该示例还展示了如何在 Angular 2 中链式应用管道。

同样,在前面的示例中,为了开发一个无状态的管道,我们需要导入`Pipe``PipeTransform`。然而,这次,由于 HTTP 请求功能,我们还需要从`'angular2/http'`模块导入`Http``Response`类:

import {Pipe, PipeTransform} from 'angular2/core'; import {Http, Response} from 'angular2/http'; import 'rxjs/add/operator/toPromise';


每当将`fetchJson`管道应用于与上一次调用中获得的参数不同的参数时,我们需要发起新的 HTTP `get`请求。这意味着作为管道的状态,我们至少需要保留远程服务响应的值和最后的 URL

@Pipe({ name: 'fetchJson', pure: false }) export class FetchJsonPipe implements PipeTransform { private data: any; private prevUrl: string; constructor(private http: Http) {} transform(url: string): any {…} }


剩下的逻辑只有`transform`方法:

… transform(url: string): any { if (this.prevUrl !== url) { this.http.get(url).toPromise(Promise) .then((data: Response) => data.json()) .then(result => this.data = result); this.prevUrl = url; } return this.data || {}; } …


在其中,我们最初将作为参数传递的 URL 与我们当前保留引用的 URL 进行比较。如果它们不同,我们将使用传递给`constructor`函数的`Http`类的本地实例发起新的 HTTP `get`请求。一旦请求完成,我们将将响应解析为 JSON,并将`data`属性设置为结果。

现在,假设管道已经开始了`Http get`请求,在请求完成之前,变更检测机制再次调用了管道。在这种情况下,我们将比较`prevUrl`属性和`url`参数。如果它们相同,我们将不会执行新的`http`请求,并立即返回`data`属性的值。如果`prevUrl`的值与`url`不同,我们将开始一个新的请求。

## 使用有状态的管道

现在让我们使用我们开发的管道!我们将要实现的应用程序为用户提供了一个文本输入和一个按钮。一旦用户在文本输入中输入一个值并按下按钮,文本输入框下方将显示与 GitHub 用户对应的头像,如下面的屏幕截图所示:

![使用有状态的管道](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3835161d8df246349f829d3ccf4fcf74~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771339764&x-signature=SFjIyna3NlAF8SW4XNC7g8GpGbc%3D)

现在,让我们开发一个示例组件,允许我们输入 GitHub 用户的句柄:

// ch7/ts/statful_pipe/app.ts

@Component({ selector: 'app', providers: [HTTP_PROVIDERS], pipes: [FetchJsonPipe, ObjectGetPipe], template: <input type="text" #input> <button (click)=" setUsername(input.value)">Get Avatar</button> }) class App { username: string; setUsername(user: string) { this.username = user; } }


在前面的例子中,我们添加了`FetchJsonPipe`用于`App`组件。唯一剩下的就是显示用户的 GitHub 头像。我们可以通过修改前面组件的模板来轻松实现这一点,使用以下`img`声明:

<img width="160" [src]="(('api.github.com/users/' + username) | fetchJson).avatar_url">


最初,我们将 GitHub 句柄附加到用于从 API 获取用户的基本 URL 上。后来,我们对其应用了`fetchJson`过滤器,并从返回的结果中得到了`avatar_url`属性。

### 注意

虽然前面的例子可以工作,但在管道中放入业务逻辑是不自然的。最好将与 GitHub API 通信的逻辑实现为一个服务,或者至少在组件中调用`Http`类的实例的`get`方法。

## 使用 AngularAsyncPipe

Angular`AsyncPipe`转换方法接受 observable 或 promise 作为参数。一旦参数推送一个值(即 promise 已解析或 observable 的`subscribe`回调被调用并传递了一个值),`AsyncPipe`将返回它作为结果。让我们看看以下例子:

// ch7/ts/async-pipe/app.ts @Component({ selector: 'greeting', template: 'Hello {{ greetingPromise | async }}' }) class Greeting { greetingPromise = new Promise(resolve => this.resolve = resolve); resolve: Function; constructor() { setTimeout(_ => { this.resolve('Foobar!'); }, 3000); } }


在这里,我们定义了一个 Angular 2 组件,它有两个属性:`greetingPromise`的类型为`Promise<string>``resolve`的类型为`Function`。我们用一个新的`Promise<string>`实例初始化了`greetingPromise`属性,并将`resolve`属性的值设置为`promise``resolve`回调函数。

在类的构造函数中,我们启动了一个持续 3,000 毫秒的超时,在其回调函数中,我们解析了 promise。一旦 promise 被解析,表达式`{{ greetingPromise | async }}`的值将被评估为字符串`Foobar!`。用户在屏幕上看到的最终结果是文本**Hello Foobar!**。

当我们将`async`管道与`Http`请求或与推送值序列的 observable 结合使用时,`async`管道非常强大。

### 使用 observables 和 AsyncPipe

我们已经熟悉了前几章中的 observables 的概念。我们可以说,observable 对象允许我们订阅一系列值的发射,例如:

let observer = new Observable(observer => { setInterval(() => { observer.next(new Date().getTime()); }, 1000); }); observer.subscribe(date => console.log(date));


一旦我们订阅了可观察对象,它将开始每秒发出值,这些值将被打印在控制台中。让我们将这段代码与组件的定义结合起来,实现一个简单的计时器:

// ch7/ts/async-pipe/app.ts @Component({ selector: 'timer' }) class Timer { username: string; timer: Observable; constructor() { let counter = 0; this.timer = new Observable(observer => { setInterval(() => { observer.next(new Date().getTime()); }, 1000); }); } }


为了能够使用计时器组件,唯一剩下的事情就是添加它的模板。我们可以通过使用`async`管道直接在我们的模板中订阅可观察对象:

{{ timer | async | date: "medium" }}


这样,每秒我们将得到可观察对象发出的新值,并且`date`管道将把它转换成可读形式。

# 总结

在本章中,我们深入研究了 Angular 2 表单,通过开发一个模型驱动的表单,并将其与`http`模块结合起来,以便能够将开发人员添加到我们的存储库中。我们看了一些新的基于组件的路由的高级特性,并了解了如何使用和开发我们定制的有状态和无状态管道。

下一章将致力于我们如何使我们的 Angular 2 应用程序对 SEO 友好,通过利用模块 universal 提供的服务器端渲染。我们还将看看 angular-cli 和其他工具,这些工具使我们作为开发人员的体验更好。