依赖注入

依赖注入是重要的程序设计模式。 Angular 有自己的依赖注入框架,离开了它,几乎没法构建 Angular 应用。 它使用得非常广泛,以至于几乎每个人都会把它简称为 DI

本章将学习什么是 DI,它有什么用。 然后,将学习在 Angular 应用中该如何使用它

Contents

运行在线例子 / 可下载的例子.

为什么需要依赖注入?

要理解为什么依赖注入这么重要,不妨先考虑不使用它的一个例子。想象下列代码:

src/app/car/car.ts (without DI)

  1. export class Car {
  2. public engine: Engine;
  3. public tires: Tires;
  4. public description = 'No DI';
  5. constructor() {
  6. this.engine = new Engine();
  7. this.tires = new Tires();
  8. }
  9. // Method using the engine and tires
  10. drive() {
  11. return `${this.description} car with ` +
  12. `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
  13. }
  14. }

Car类会在它的构造函数中创建所需的每样东西。 问题何在?

问题在于,这个Car类过于脆弱、缺乏弹性并且难以测试。

Car类需要一个引擎 (engine) 和一些轮胎 (tire),它没有去请求现成的实例, 而是在构造函数中用具体的EngineTires类实例化出自己的副本。

如果Engine类升级了,它的构造函数要求传入一个参数,这该怎么办? 这个Car类就被破坏了,在把创建引擎的代码重写为this.engine = new Engine(theNewParameter)之前,它都是坏的。 当第一次写Car类时,我们不关心Engine构造函数的参数,现在也不想关心。 但是,当Engine类的定义发生变化时,就不得不在乎了,Car类也不得不跟着改变。 这就会让Car类过于脆弱。

如果想在Car上使用不同品牌的轮胎会怎样?太糟了。 我们被锁定在Tires类创建时使用的那个品牌上。这让Car类缺乏弹性。

现在,每辆车都有它自己的引擎。它不能和其它车辆共享引擎。 虽然这对于汽车来说还算可以理解,但是设想一下那些应该被共享的依赖,比如用来联系厂家服务中心的车载无线电。 我们的车缺乏必要的弹性,无法共享当初给其它消费者创建的车载无线电。

当给Car类写测试的时候,我们就会受制于它背后的那些依赖。 能在测试环境中成功创建新的Engine吗? Engine自己又依赖什么?那些依赖本身又依赖什么? Engine的新实例会发起到服务器的异步调用吗? 我们当然不想在测试期间这么一层层追下去。

如果Car应该在轮胎气压低的时候闪动警示灯该怎么办? 如果没法在测试期间换上一个低气压的轮胎,那该如何确认它能正确的闪警示灯?

我们没法控制这辆车背后隐藏的依赖。 当不能控制依赖时,类就会变得难以测试。

该如何让Car更强壮、有弹性以及可测试?

答案非常简单。把Car的构造函数改造成使用 DI 的版本:

public description = 'DI';

constructor(public engine: Engine, public tires: Tires) { }

发生了什么?我们把依赖的定义移到了构造函数中。 Car类不再创建引擎engine或者轮胎tires。 它仅仅“消费”它们。

这个例子又一次借助 TypeScript 的构造器语法来同时定义参数和属性。

现在,通过往构造函数中传入引擎和轮胎来创建一辆车。

// Simple car with 4 cylinders and Flintstone tires.
let car = new Car(new Engine(), new Tires());

酷!引擎和轮胎这两个依赖的定义与Car类本身解耦了。 只要喜欢,可以传入任何类型的引擎或轮胎,只要它们能满足引擎或轮胎的通用 API 需求。

这样一来,如果有人扩展了Engine类,那就不再是Car类的烦恼了。

Car消费者也有这个问题。消费者必须更新创建这辆车的代码,就像这样:

class Engine2 {
  constructor(public cylinders: number) { }
}
// Super car with 12 cylinders and Flintstone tires.
let bigCylinders = 12;
let car = new Car(new Engine2(bigCylinders), new Tires());

这里的要点是:Car本身不必变化。下面就来解决消费者的问题。

Car类非常容易测试,因为现在我们对它的依赖有了完全的控制权。 在每个测试期间,我们可以往构造函数中传入 mock 对象,做想让它们做的事:

class MockEngine extends Engine { cylinders = 8; }
class MockTires  extends Tires  { make = 'YokoGoodStone'; }

// Test car with 8 cylinders and YokoGoodStone tires.
let car = new Car(new MockEngine(), new MockTires());

刚刚学习了什么是依赖注入

它是一种编程模式,可以让类从外部源中获得它的依赖,而不必亲自创建它们。

酷!但是,可怜的消费者怎么办? 那些希望得到一个Car的人们现在必须创建所有这三部分了:CarEngineTiresCar类把它的快乐建立在了消费者的痛苦之上。 需要某种机制为我们把这三个部分装配好。

可以写一个巨型类来做这件事:

src/app/car/car-factory.ts

  1. import { Engine, Tires, Car } from './car';
  2. // BAD pattern!
  3. export class CarFactory {
  4. createCar() {
  5. let car = new Car(this.createEngine(), this.createTires());
  6. car.description = 'Factory';
  7. return car;
  8. }
  9. createEngine() {
  10. return new Engine();
  11. }
  12. createTires() {
  13. return new Tires();
  14. }
  15. }

现在只需要三个创建方法,这还不算太坏。 但是当应用规模变大之后,维护它将变得惊险重重。 这个工厂类将变成由相互依赖的工厂方法构成的巨型蜘蛛网。

如果能简单的列出想建造的东西,而不用定义该把哪些依赖注入到哪些对象中,那该多好!

到了依赖注入框架一展身手的时候了! 想象框架中有一个叫做注入器 (injector) 的东西。 用这个注入器注册一些类,它会弄明白如何创建它们。

当需要一个Car时,就简单的找注入器取车就可以了。

let car = injector.get(Car);

皆大欢喜。Car不需要知道如何创建EngineTires。 消费者不需要知道如何创建Car。 开发人员不需要维护巨大的工厂类。 Car和消费者只要简单地请求想要什么,注入器就会交付它们。

这就是“依赖注入框架”存在的原因。

现在,我们知道了什么是依赖注入,以及它的优点。再来看看它在 Angular 中是怎么实现的。

Angular 依赖注入

Angular 附带了自己的依赖注入框架。此框架也能被当做独立模块用于其它应用和框架中。

要了解Angular构建组件时注入器做了什么,我们先从英雄指南中构建的HeroesComponent的简化版本开始。

  1. import { Component } from '@angular/core';
  2. @Component({
  3. selector: 'my-heroes',
  4. template: `
  5. <h2>Heroes</h2>
  6. <hero-list></hero-list>
  7. `
  8. })
  9. export class HeroesComponent { }

HeroesComponent英雄特性区域的根组件。它管理区域内所有子组件。 简化后的版本只有一个子组件HeroListComponent,用来显示英雄列表。

现在HeroListComponentHEROES获得英雄数据,是在另一个文件中定义的内存数据集。 它在开发的早期阶段可能还够用,但离完美就差得远了。 一旦开始测试此组件,或者想从远端服务器获得英雄数据,就不得不修改heroes的实现, 还要修改每个用到了HEROES模拟数据的地方。

最好用一个服务把获取英雄数据的代码封装起来。

因为服务是一个分离关注点, 建议你把服务代码放到它自己的文件里。

参阅这一条来了解详情。

HeroService暴露了getHeroes方法,返回跟以前一样的模拟数据,但它的消费者不需要知道这一点。

src/app/heroes/hero.service.ts

  1. import { Injectable } from '@angular/core';
  2. import { HEROES } from './mock-heroes';
  3. @Injectable()
  4. export class HeroService {
  5. getHeroes() { return HEROES; }
  6. }

注意服务类上面这个@Injectable()装饰器。很快会讨论它的用途。

我们甚至没有假装这是一个真实的服务。 如果真的从远端服务器获取数据,这个 API 必须是异步的,可能得返回 ES2015 承诺 (promise)。 需要重新处理组件消费该服务的方式。通常这个很重要,但是目前的故事不需要。

服务只是 Angular 中的一个类。 有 Angular 注入器注册它之前,没有任何特别之处。

配置注入器

不需要创建 Angular 注入器。 Angular 在启动过程中自动为我们创建一个应用级注入器。

src/main.ts (bootstrap)

platformBrowserDynamic().bootstrapModule(AppModule);

我们必须通过注册提供商 (provider) 来配置注入器,这些提供商为应用创建所需服务。 在本章的稍后部分会解释什么是提供商

或者在 NgModule 中注册提供商,或者在应用组件中。

NgModule中注册提供商

AppModuleproviders中注册了两个提供商,UserServiceAPP_CONFIG

src/app/app.module.ts (excerpt)

@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [
    AppComponent,
    CarComponent,
    HeroesComponent,
    /* . . . */
  ],
  providers: [
    UserService,
    { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

由于HeroService会在HeroesComponent及其子组件中使用,所以顶层组件HeroesComponent是一个合理的注册位置。

在组件中注册提供商

下面是更新的HerosComponent,把HeroService注册到了它的providers数组中。

src/app/heroes/heroes.component.ts

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

import { HeroService }        from './hero.service';

@Component({
  selector: 'my-heroes',
  providers: [HeroService],
  template: `
  <h2>Heroes</h2>
  <hero-list></hero-list>
  `
})
export class HeroesComponent { }

该用 NgModule 还是应用组件?

一方面,NgModule 中的提供商是被注册到根注入器。这意味着在 NgModule 中注册的提供商可以被整个应用访问。

另一方面,在应用组件中注册的提供商只在该组件及其子组件中可用。

这里,APP_CONFIG服务需要在应用中到处可用,所以它被注册到了AppModule @NgModuleproviders数组。 但是,由于HeroService只在英雄特性区用到了,其它地方没有用过,因此在HeroesComponent中注册它是有道理的。

参见 NgModule FAQ 一章的 我该把“全应用级”提供商加到根模块AppModule还是根组件AppComponent

为注入准备HeroListComponent

HeroListComponent应该从注入的HeroService获取英雄数据。 遵照依赖注入模式的要求,组件必须在它的构造函数中请求这些服务,就像以前解释过的那样。 只是个小改动:

  1. import { Component } from '@angular/core';
  2. import { Hero } from './hero';
  3. import { HeroService } from './hero.service';
  4. @Component({
  5. selector: 'hero-list',
  6. template: `
  7. <div *ngFor="let hero of heroes">
  8. {{hero.id}} - {{hero.name}}
  9. </div>
  10. `
  11. })
  12. export class HeroListComponent {
  13. heroes: Hero[];
  14. constructor(heroService: HeroService) {
  15. this.heroes = heroService.getHeroes();
  16. }
  17. }

来看看构造函数

往构造函数中添加参数并不是这里所发生的一切。

constructor(heroService: HeroService) {
  this.heroes = heroService.getHeroes();
}

注意,构造函数参数的类型是HeroService,并且HeroListComponent类有一个@Component装饰器 (往上翻可以确认)。另外,记得父级组件 (HeroesComponent) 有HeroServiceproviders信息。

构造函数参数类型、@Component装饰器和父级的providers信息合起来告诉 Angular 的注入器, 任何新建HeroListComponent的时候,注入一个HeroService的实例。

隐式注入器的创建

本章前面的部分我们看到了如何使用注入器来创建一个新Car。 你可以像这样显式创建注入器:

  injector = ReflectiveInjector.resolveAndCreate([Car, Engine, Tires]);
  let car = injector.get(Car);

无论在《英雄指南》还是其它范例中,都没有出现这样的代码。 在必要时,可以使用显式注入器的代码,但却很少这样做。 当 Angular 创建组件时 —— 无论通过像<hero-list></hero-list>这样的 HTML 标签还是通过路由导航到组件 —— 它都会自己管理好注入器的创建和调用。 只要让 Angular 做好它自己的工作,我们就能安心享受“自动依赖注入”带来的好处。

单例服务

在一个注入器的范围内,依赖都是单例的。 在这个例子中,HeroesComponent和它的子组件HeroListComponent共享同一个HeroService实例。

然而,Angular DI 是一个分层的依赖注入系统,这意味着嵌套的注入器可以创建它们自己的服务实例。 要了解更多知识,参见多级依赖注入器一章。

测试组件

前面强调过,设计一个适合依赖注入的类,可以让这个类更容易测试。 要有效的测试应用中的一部分,只需要在构造函数的参数中列出依赖。

例如,新建的HeroListComponent实例使用一个模拟 (mock) 服务,以便可以在测试中操纵它:

let expectedHeroes = [{name: 'A'}, {name: 'B'}]
let mockService = <HeroService> {getHeroes: () => expectedHeroes }

it('should have heroes when HeroListComponent created', () => {
  let hlc = new HeroListComponent(mockService);
  expect(hlc.heroes.length).toEqual(expectedHeroes.length);
});

要学习更多知识,参见测试

当服务需要别的服务时

这个HeroService非常简单。它本身不需要任何依赖。

如果它也有依赖,该怎么办呢?例如,它需要通过日志服务来汇报自己的活动。 我们同样用构造函数注入模式,来添加一个带有Logger参数的构造函数。

下面是在原来的基础上所做的修改:

  1. import { Injectable } from '@angular/core';
  2. import { HEROES } from './mock-heroes';
  3. import { Logger } from '../logger.service';
  4. @Injectable()
  5. export class HeroService {
  6. constructor(private logger: Logger) { }
  7. getHeroes() {
  8. this.logger.log('Getting heroes ...');
  9. return HEROES;
  10. }
  11. }

现在,这个构造函数要求注入一个Logger类的实例,并把它存到名为logger的私有属性中。 当别人请求英雄数据时,在getHeroes()方法中调用这个属性的方法。

为什么要用 @Injectable()?

@Injectable() 标识一个类可以被注入器实例化。 通常,在试图实例化没有被标识为@Injectable()的类时,注入器会报错。

碰巧,第一版的HeroService省略了@Injectable(),那因为它没有注入的参数。 但是现在必须要有它,因为服务有了一个注入的依赖。 我们需要它,因为 Angular 需要构造函数参数的元数据来注入一个Logger

建议:为每个服务类都添加 @Injectable()

建议为每个服务类都添加@Injectable(),包括那些没有依赖严格来说并不需要它的。因为:

注入器同时负责实例化像HerosComponent这样的组件。为什么不标记HerosComponent@Injectable()呢?

我们可以添加它。但是没有必要,因为HerosComponent已经有@Component装饰器了, @Component(和随后将会学到的@Directive@Pipe一样)是 Injectable 的子类型。 实际上,正是这些@Injectable()装饰器是把一个类标识为注入器实例化的目标。

在运行时,注入器可以从编译后的 JavaScript 代码中读取类的元数据, 并使用构造函数的参数类型信息来决定注入什么。

不是每一个 JavaScript 类都有元数据。 TypeScript 编译器默认忽略元数据。 如果emitDecoratorMetadata编译器选项为true(在tsconfig.json中它应该为true), 编译器就会在生成的 JavaScript 中,为每一个至少拥有一个装饰器的类添加元数据。

当然,任何装饰器都会触发这个效果,用 @Injectable() 来标识服务 只是为了让这一意图更明显。

别忘了带括号

总是使用@Injectable()的形式,不能只用@Injectable。 如果忘了括号,应用就会神不知鬼不觉的失败!

创建和注册日志服务

要把日志服务注入到HeroService中需要两步:

  1. 创建日志服务。

  2. 把它注册到应用中。

这个日志服务很简单:

src/app/logger.service.ts

  1. import { Injectable } from '@angular/core';
  2. @Injectable()
  3. export class Logger {
  4. logs: string[] = []; // capture logs for testing
  5. log(message: string) {
  6. this.logs.push(message);
  7. console.log(message);
  8. }
  9. }

应用的每个角落都可能需要日志服务,所以把它放到项目的app目录, 并在应用模块AppModule的元数据providers数组里注册它。

src/app/app.module.ts (excerpt)

providers: [Logger]

如果忘了注册这个日志服务,Angular 会在首次查找这个日志服务时,抛出一个异常。

EXCEPTION: No provider for Logger! (HeroListComponent -> HeroService -> Logger)
(异常:Logger类没有提供商!(HeroListComponent -> HeroService -> Logger))

Angular 告诉我们,依赖注入器找不到日志服务的提供商。 在创建HeroListComponent的新实例时需要创建并注入HeroService, 而HeroService需要创建并注入一个Logger实例, Angular 需要这个Logger实例的提供商来。

这个“创建链”始于Logger的提供商。这个提供商就是下一节的主题 。

注入器的提供商们

提供商提供依赖值的一个具体的、运行时的版本。 注入器依靠提供商创建服务的实例,注入器再将服务的实例注入组件或其它服务。

必须为注入器注册一个服务的提供商,否则它不知道该如何创建该服务。

我们在前面通过AppModule元数据中的providers数组注册过Logger服务,就像这样:

providers: [Logger]

有很多方式可以提供一些实现 Logger类的东西。 Logger类本身是一个显而易见而且自然而然的提供商。 但它不是唯一的选项。

可以用其它备选提供商来配置注入器,只要它们能交付一个行为类似于Logger的对象就可以了。 可以提供一个替代类。你可以提供一个类似日志的对象。 可以给它一个提供商,让它调用可以创建日志服务的工厂函数。 所有这些方法,只要用在正确的场合,都可能是一个好的选择。

最重要的是,当注入器需要一个Logger时,它得先有一个提供商。

Provider类和一个提供商的字面量

像下面一样写providers数组:

providers: [Logger]

这其实是用于注册提供商的简写表达式。 使用的是一个带有两个属性的提供商对象字面量:

[{ provide: Logger, useClass: Logger }]

第一个是令牌 (token),它作为键值 (key) 使用,用于定位依赖值和注册提供商。

第二个是一个提供商定义对象。 可以把它看做是指导如何创建依赖值的配方。 有很多方式创建依赖值…… 也有很多方式可以写配方。

备选的类提供商

某些时候,我们会请求一个不同的类来提供服务。 下列代码告诉注入器,当有人请求Logger时,返回BetterLogger

[{ provide: Logger, useClass: BetterLogger }]

带依赖的类提供商

假设EvenBetterLogger可以在日志消息中显示用户名。 这个日志服务从注入的UserService中取得用户, UserService通常也会在应用级注入。

@Injectable()
class EvenBetterLogger extends Logger {
  constructor(private userService: UserService) { super(); }

  log(message: string) {
    let name = this.userService.user.name;
    super.log(`Message to ${name}: ${message}`);
  }
}

就像之前在BetterLogger中那样配置它。

[ UserService,
  { provide: Logger, useClass: EvenBetterLogger }]

别名类提供商

假设某个旧组件依赖一个OldLogger类。 OldLoggerNewLogger具有相同的接口,但是由于某些原因, 我们不能升级这个旧组件并使用它。

组件想使用OldLogger记录消息时,我们希望改用NewLogger的单例对象来记录。

不管组件请求的是新的还是旧的日志服务,依赖注入器注入的都应该是同一个单例对象。 也就是说,OldLogger应该是NewLogger的别名。

我们当然不会希望应用中有两个不同的NewLogger实例。 不幸的是,如果尝试通过useClass来把OldLogger作为NewLogger的别名,就会导致这样的后果。

[ NewLogger,
  // Not aliased! Creates two instances of `NewLogger`
  { provide: OldLogger, useClass: NewLogger}]

解决方案:使用useExisting选项指定别名。

[ NewLogger,
  // Alias OldLogger w/ reference to NewLogger
  { provide: OldLogger, useExisting: NewLogger}]

值提供商

有时,提供一个预先做好的对象会比请求注入器从类中创建它更容易。

// An object in the shape of the logger service
let silentLogger = {
  logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
  log: () => {}
};

于是可以通过useValue选项来注册提供商,它会让这个对象直接扮演 logger 的角色。

[{ provide: Logger, useValue: silentLogger }]

查看更多useValue的例子,见非类依赖InjectionToken部分。

工厂提供商

有时,我们需要动态创建这个依赖值,因为它所需要的信息直到最后一刻才能确定。 也许这个信息会在浏览器的会话中不停地变化。

还假设这个可注入的服务没法通过独立的源访问此信息。

这种情况下,请调用工厂提供商

下面通过添加新的业务需求来说明这一点: HeroService 必须对普通用户隐藏掉秘密英雄。 只有授权用户才能看到秘密英雄。

就像EvenBetterLogger那样,HeroService需要了解此用户的身份。 它需要知道,这个用户是否有权看到隐藏英雄。 这个授权可能在单一的应用会话中被改变,例如,改用另一个用户的身份登录时。

EvenBetterLogger不同,不能把UserService注入到HeroService中。 HeroService无权访问用户信息,来决定谁有授权谁没有授权。

HeroService的构造函数带上一个布尔型的标志,来控制是否显示隐藏的英雄。

src/app/heroes/hero.service.ts (excerpt)

constructor(
  private logger: Logger,
  private isAuthorized: boolean) { }

getHeroes() {
  let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
  this.logger.log(`Getting heroes for ${auth} user.`);
  return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}

我们可以注入Logger,但是不能注入逻辑型的isAuthorized。 我们不得不通过通过工厂提供商创建这个HeroService的新实例。

工厂提供商需要一个工厂方法:

src/app/heroes/hero.service.provider.ts (excerpt)

let heroServiceFactory = (logger: Logger, userService: UserService) => {
  return new HeroService(logger, userService.user.isAuthorized);
};

虽然HeroService不能访问UserService,但是工厂方法可以。

同时把LoggerUserService注入到工厂提供商中,并且让注入器把它们传给工厂方法:

src/app/heroes/hero.service.provider.ts (excerpt)

export let heroServiceProvider =
  { provide: HeroService,
    useFactory: heroServiceFactory,
    deps: [Logger, UserService]
  };

useFactory字段告诉 Angular:这个提供商是一个工厂方法,它的实现是heroServiceFactory

deps属性是提供商令牌数组。 LoggerUserService类作为它们自身类提供商的令牌。 注入器解析这些令牌,把相应的服务注入到工厂函数中相应的参数中去。

注意,我们在一个导出的变量中捕获了这个工厂提供商:heroServiceProvider。 这个额外的步骤让工厂提供商可被复用。 无论哪里需要,都可以使用这个变量注册HeroService

在这个例子中,只在HeroesComponent中需要它, 这里,它代替了元数据providers数组中原来的HeroService注册。 对比一下新的和旧的实现:

  1. import { Component } from '@angular/core';
  2. import { heroServiceProvider } from './hero.service.provider';
  3. @Component({
  4. selector: 'my-heroes',
  5. template: `
  6. <h2>Heroes</h2>
  7. <hero-list></hero-list>
  8. `,
  9. providers: [heroServiceProvider]
  10. })
  11. export class HeroesComponent { }

依赖注入令牌

当向注入器注册提供商时,实际上是把这个提供商和一个 DI 令牌关联起来了。 注入器维护一个内部的令牌-提供商映射表,这个映射表会在请求依赖时被引用到。 令牌就是这个映射表中的键值。

在前面的所有例子中,依赖值都是一个类实例,并且类的类型作为它自己的查找键值。 在下面的代码中,HeroService类型作为令牌,直接从注入器中获取HeroService 实例:

heroService: HeroService;

编写需要基于类的依赖注入的构造函数对我们来说是很幸运的。 只要定义一个HeroService类型的构造函数参数, Angular 就会知道把跟HeroService类令牌关联的服务注入进来:

constructor(heroService: HeroService)

这是一个特殊的规约,因为大多数依赖值都是以类的形式提供的。

非类依赖

如果依赖值不是一个类呢?有时候想要注入的东西是一个字符串,函数或者对象。

应用程序经常为很多很小的因素定义配置对象(例如应用程序的标题或网络API终点的地址)。 但是这些配置对象不总是类的实例,它们可能是对象,如下面这个:

src/app/app-config.ts (excerpt)

export interface AppConfig {
  apiEndpoint: string;
  title: string;
}

export const HERO_DI_CONFIG: AppConfig = {
  apiEndpoint: 'api.heroes.com',
  title: 'Dependency Injection'
};

我们想让这个配置对象在注入时可用,而且知道可以使用值提供商来注册一个对象。

但是,这种情况下用什么作令牌呢? 我们没办法找一个类来当作令牌,因为没有Config类。

TypeScript 接口不是一个有效的令牌

CONFIG常量有一个接口:AppConfig。不幸的是,不能把 TypeScript 接口用作令牌:

// FAIL! Can't use interface as provider token
[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]
// FAIL! Can't inject using the interface as the parameter type
constructor(private config: AppConfig){ }

对于习惯于在强类型的语言中使用依赖注入的开发人员,这会看起来很奇怪, 因为在强类型语言中,接口是首选的用于查找依赖的主键。

这不是 Angular 的错。接口只是 TypeScript 设计时 (design-time) 的概念。JavaScript 没有接口。 TypeScript 接口不会出现在生成的 JavaScript 代码中。 在运行期,没有接口类型信息可供 Angular 查找。

InjectionToken

解决方案是为非类依赖定义和使用InjectionToken作为提供商令牌。 定义方式是这样的:

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

export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');

类型参数,虽然是可选的,但可以向开发者和开发工具传达类型信息。 而且这个令牌的描述信息也可以为开发者提供帮助。

使用这个InjectionToken对象注册依赖的提供商:

providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]

现在,在@Inject装饰器的帮助下,这个配置对象可以注入到任何需要它的构造函数中:

constructor(@Inject(APP_CONFIG) config: AppConfig) {
  this.title = config.title;
}

虽然AppConfig接口在依赖注入过程中没有任何作用,但它为该类中的配置对象提供了强类型信息。

或者在 ngModule 中提供并注入这个配置对象,如AppModule

src/app/app.module.ts (ngmodule-providers)

providers: [
  UserService,
  { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],

可选依赖

HeroService需要一个Logger,但是如果想不提供 Logger 也能得到它,该怎么办呢? 可以把构造函数的参数标记为@Optional(),告诉 Angular 该依赖是可选的:

import { Optional } from '@angular/core';
constructor(@Optional() private logger: Logger) {
  if (this.logger) {
    this.logger.log(some_message);
  }
}

当使用@Optional()时,代码必须准备好如何处理空值。 如果其它的代码没有注册一个 logger,注入器会设置该logger的值为空 null。

总结

本章,我们学习了 Angular 依赖注入的基础知识。 我们可以注册很多种类的提供商,知道如何通过添加构造函数的参数来请求一个注入对象(例如一个服务)。

Angular 依赖注入比前面描述的更能干。 学习更多高级特性,如对嵌套注入器的支持,见多级依赖注入一章。

附录:直接使用注入器

这里的InjectorComponent直接使用了注入器, 但我们很少直接使用它。

src/app/injector.component.ts

  1. @Component({
  2. selector: 'my-injectors',
  3. template: `
  4. <h2>Other Injections</h2>
  5. <div id="car">{{car.drive()}}</div>
  6. <div id="hero">{{hero.name}}</div>
  7. <div id="rodent">{{rodent}}</div>
  8. `,
  9. providers: [Car, Engine, Tires, heroServiceProvider, Logger]
  10. })
  11. export class InjectorComponent implements OnInit {
  12. car: Car;
  13. heroService: HeroService;
  14. hero: Hero;
  15. constructor(private injector: Injector) { }
  16. ngOnInit() {
  17. this.car = this.injector.get(Car);
  18. this.heroService = this.injector.get(HeroService);
  19. this.hero = this.heroService.getHeroes()[0];
  20. }
  21. get rodent() {
  22. let rousDontExist = `R.O.U.S.'s? I don't think they exist!`;
  23. return this.injector.get(ROUS, rousDontExist);
  24. }
  25. }

Injector本身是可注入的服务。

在这个例子中,Angular 把组件自身的Injector注入到了组件的构造函数中。 然后,组件在ngOnInit()中向注入的注入器请求它所需的服务。

注意,这些服务本身没有注入到组件,它们是通过调用injector.get()获得的。

get()方法如果不能解析所请求的服务,会抛出异常。 调用get()时,还可以使用第二个参数,一旦获取的服务没有在当前或任何祖先注入器中注册过, 就把它作为返回值。

刚描述的这项技术是服务定位器模式的一个范例。

避免使用此技术,除非确实需要它。 它会鼓励鲁莽的方式,就像在这里看到的。 它难以解释、理解和测试。 仅通过阅读构造函数,没法知道这个类需要什么或者它将做什么。 它可以从任何祖先组件中获得服务,而不仅仅是它自己。 会迫使我们深入它的实现,才可能明白它都做了啥。

框架开发人员必须采用通用的或者动态的方式获取服务时,可能采用这个方法。

附录:为什么建议每个文件只放一个类

在同一个文件中有多个类容易造成混淆,最好避免。 开发人员期望每个文件只放一个类。这会让它们开心点。

如果我们蔑视这个建议,并且 —— 比如说 —— 把HeroServiceHeroesComponent组合在同一个文件里, 就得把组件定义放在最后面! 如果把组件定义在了服务的前面, 在运行时抛出空指针错误。

forwardRef()方法的帮助下,实际上也可以先定义组件, 具体说明见这篇博客。 但是为什么要先给自己找麻烦呢? 还是通过在独立的文件中定义组件和服务,完全避免此问题吧。

下一步

模板语法