填写这份《一分钟调查》,帮我们(开发组)做得更好!去填写Home

依赖注入实战

Dependency injection in action

本章涉及到 Angular 依赖注入(DI)的很多特性。

This guide explores many of the features of dependency injection (DI) in Angular.

要查看包含本章代码片段的可工作范例,参阅现场演练 / 下载范例

See the现场演练 / 下载范例for a working example containing the code snippets in this guide.

多个服务实例(沙箱式隔离)

Multiple service instances (sandboxing)

在组件树的同一个级别上,有时需要一个服务的多个实例。

Sometimes you want multiple instances of a service at the same level of the component hierarchy.

一个用来保存其伴生组件的实例状态的服务就是个好例子。 每个组件都需要该服务的单独实例。 每个服务有自己的工作状态,与其它组件的服务和状态隔离。这叫做沙箱化,因为每个服务和组件实例都在自己的沙箱里运行。

A good example is a service that holds state for its companion component instance. You need a separate instance of the service for each component. Each service has its own work-state, isolated from the service-and-state of a different component. This is called sandboxing because each service and component instance has its own sandbox to play in.

在这个例子中,HeroBiosComponent 渲染了 HeroBioComponent 的三个实例。

In this example, HeroBiosComponent presents three instances of HeroBioComponent.

ap/hero-bios.component.ts
      
      @Component({
  selector: 'app-hero-bios',
  template: `
    <app-hero-bio [heroId]="1"></app-hero-bio>
    <app-hero-bio [heroId]="2"></app-hero-bio>
    <app-hero-bio [heroId]="3"></app-hero-bio>`,
  providers: [HeroService]
})
export class HeroBiosComponent {
}
    

每个 HeroBioComponent 都能编辑一个英雄的生平。HeroBioComponent 依赖 HeroCacheService 服务来对该英雄进行读取、缓存和执行其它持久化操作。

Each HeroBioComponent can edit a single hero's biography. HeroBioComponent relies on HeroCacheService to fetch, cache, and perform other persistence operations on that hero.

src/app/hero-cache.service.ts
      
      @Injectable()
export class HeroCacheService {
  hero: Hero;
  constructor(private heroService: HeroService) {}

  fetchCachedHero(id: number) {
    if (!this.hero) {
      this.hero = this.heroService.getHeroById(id);
    }
    return this.hero;
  }
}
    

这三个 HeroBioComponent 实例不能共享同一个 HeroCacheService 实例。否则它们会相互冲突,争相把自己的英雄放在缓存里面。

Three instances of HeroBioComponent can't share the same instance of HeroCacheService, as they'd be competing with each other to determine which hero to cache.

它们应该通过在自己的元数据(metadata)providers 数组里面列出 HeroCacheService, 这样每个 HeroBioComponent 就能拥有自己独立的 HeroCacheService 实例了。

Instead, each HeroBioComponent gets its own HeroCacheService instance by listing HeroCacheService in its metadata providers array.

src/app/hero-bio.component.ts
      
      @Component({
  selector: 'app-hero-bio',
  template: `
    <h4>{{hero.name}}</h4>
    <ng-content></ng-content>
    <textarea cols="25" [(ngModel)]="hero.description"></textarea>`,
  providers: [HeroCacheService]
})

export class HeroBioComponent implements OnInit  {
  @Input() heroId: number;

  constructor(private heroCache: HeroCacheService) { }

  ngOnInit() { this.heroCache.fetchCachedHero(this.heroId); }

  get hero() { return this.heroCache.hero; }
}
    

父组件 HeroBiosComponent 把一个值绑定到 heroIdngOnInit 把该 id 传递到服务,然后服务获取和缓存英雄。hero 属性的 getter 从服务里面获取缓存的英雄,并在模板里显示它绑定到属性值。

The parent HeroBiosComponent binds a value to heroId. ngOnInit passes that ID to the service, which fetches and caches the hero. The getter for the hero property pulls the cached hero from the service. The template displays this data-bound property.

现场演练现场演练 / 下载范例中找到这个例子,确认三个 HeroBioComponent 实例拥有自己独立的英雄数据缓存。

Find this example inlive codelive code / 下载范例and confirm that the three HeroBioComponent instances have their own cached hero data.

使用参数装饰器来限定依赖查找方式

Qualify dependency lookup with parameter decorators

当类需要某个依赖项时,该依赖项就会作为参数添加到类的构造函数中。 当 Angular 需要实例化该类时,就会调用 DI 框架来提供该依赖。 默认情况下,DI 框架会在注入器树中查找一个提供者,从该组件的局部注入器开始,如果需要,则沿着注入器树向上冒泡,直到根注入器。

When a class requires a dependency, that dependency is added to the constructor as a parameter. When Angular needs to instantiate the class, it calls upon the DI framework to supply the dependency. By default, the DI framework searches for a provider in the injector hierarchy, starting at the component's local injector of the component, and if necessary bubbling up through the injector tree until it reaches the root injector.

  • 第一个配置过该提供者的注入器就会把依赖(服务实例或值)提供给这个构造函数。

    The first injector configured with a provider supplies the dependency (a service instance or value) to the constructor.

  • 如果在根注入器中也没有找到提供者,则 DI 框架将会抛出一个错误。

    If no provider is found in the root injector, the DI framework throws an error.

通过在类的构造函数中对服务参数使用参数装饰器,可以提供一些选项来修改默认的搜索行为。

There are a number of options for modifying the default search behavior, using parameter decorators on the service-valued parameters of a class constructor.

@Optional 来让依赖是可选的,以及使用 @Host 来限定搜索方式

Make a dependency @Optional and limit search with @Host

依赖可以注册在组件树的任何层级上。 当组件请求某个依赖时,Angular 会从该组件的注入器找起,沿着注入器树向上,直到找到了第一个满足要求的提供者。如果没找到依赖,Angular 就会抛出一个错误。

Dependencies can be registered at any level in the component hierarchy. When a component requests a dependency, Angular starts with that component's injector and walks up the injector tree until it finds the first suitable provider. Angular throws an error if it can't find the dependency during that walk.

某些情况下,你需要限制搜索,或容忍依赖项的缺失。 你可以使用组件构造函数参数上的 @Host@Optional 这两个限定装饰器来修改 Angular 的搜索行为。

In some cases, you need to limit the search or accommodate a missing dependency. You can modify Angular's search behavior with the @Host and @Optional qualifying decorators on a service-valued parameter of the component's constructor.

  • @Optional 属性装饰器告诉 Angular 当找不到依赖时就返回 null。

    The @Optional property decorator tells Angular to return null when it can't find the dependency.

  • @Host 属性装饰器会禁止在宿主组件以上的搜索。宿主组件通常就是请求该依赖的那个组件。 不过,当该组件投影进某个组件时,那个父组件就会变成宿主。下面的例子中介绍了第二种情况。

    The @Host property decorator stops the upward search at the host component. The host component is typically the component requesting the dependency. However, when this component is projected into a parent component, that parent component becomes the host. The following example covers this second case.

如下例所示,这些装饰器可以独立使用,也可以同时使用。这个 HeroBiosAndContactsComponent 是你以前见过的那个 HeroBiosComponent 的修改版。

These decorators can be used individually or together, as shown in the example. This HeroBiosAndContactsComponent is a revision of HeroBiosComponent which you looked at above.

src/app/hero-bios.component.ts (HeroBiosAndContactsComponent)
      
      @Component({
  selector: 'app-hero-bios-and-contacts',
  template: `
    <app-hero-bio [heroId]="1"> <app-hero-contact></app-hero-contact> </app-hero-bio>
    <app-hero-bio [heroId]="2"> <app-hero-contact></app-hero-contact> </app-hero-bio>
    <app-hero-bio [heroId]="3"> <app-hero-contact></app-hero-contact> </app-hero-bio>`,
  providers: [HeroService]
})
export class HeroBiosAndContactsComponent {
  constructor(logger: LoggerService) {
    logger.logInfo('Creating HeroBiosAndContactsComponent');
  }
}
    

注意看模板:

Focus on the template:

dependency-injection-in-action/src/app/hero-bios.component.ts
      
      template: `
  <app-hero-bio [heroId]="1"> <app-hero-contact></app-hero-contact> </app-hero-bio>
  <app-hero-bio [heroId]="2"> <app-hero-contact></app-hero-contact> </app-hero-bio>
  <app-hero-bio [heroId]="3"> <app-hero-contact></app-hero-contact> </app-hero-bio>`,
    

<hero-bio> 标签中是一个新的 <hero-contact> 元素。Angular 就会把相应的 HeroContactComponent投影(transclude)进 HeroBioComponent 的视图里, 将它放在 HeroBioComponent 模板的 <ng-content> 标签槽里。

Now there's a new <hero-contact> element between the <hero-bio> tags. Angular projects, or transcludes, the corresponding HeroContactComponent into the HeroBioComponent view, placing it in the <ng-content> slot of the HeroBioComponent template.

src/app/hero-bio.component.ts (template)
      
      template: `
  <h4>{{hero.name}}</h4>
  <ng-content></ng-content>
  <textarea cols="25" [(ngModel)]="hero.description"></textarea>`,
    

HeroContactComponent 获得的英雄电话号码,被投影到上面的英雄描述里,结果如下:

The result is shown below, with the hero's telephone number from HeroContactComponent projected above the hero description.

这里的 HeroContactComponent 演示了限定型装饰器。

Here's HeroContactComponent, which demonstrates the qualifying decorators.

src/app/hero-contact.component.ts
      
      @Component({
  selector: 'app-hero-contact',
  template: `
  <div>Phone #: {{phoneNumber}}
  <span *ngIf="hasLogger">!!!</span></div>`
})
export class HeroContactComponent {

  hasLogger = false;

  constructor(
      @Host() // limit to the host component's instance of the HeroCacheService
      private heroCache: HeroCacheService,

      @Host()     // limit search for logger; hides the application-wide logger
      @Optional() // ok if the logger doesn't exist
      private loggerService?: LoggerService
  ) {
    if (loggerService) {
      this.hasLogger = true;
      loggerService.logInfo('HeroContactComponent can log!');
    }
  }

  get phoneNumber() { return this.heroCache.hero.phone; }

}
    

注意构造函数的参数。

Focus on the constructor parameters.

src/app/hero-contact.component.ts
      
      @Host() // limit to the host component's instance of the HeroCacheService
private heroCache: HeroCacheService,

@Host()     // limit search for logger; hides the application-wide logger
@Optional() // ok if the logger doesn't exist
private loggerService?: LoggerService
    

@Host() 函数是构造函数属性 heroCache 的装饰器,确保从其父组件 HeroBioComponent 得到一个缓存服务。如果该父组件中没有该服务,Angular 就会抛出错误,即使组件树里的再上级有某个组件拥有这个服务,还是会抛出错误。

The @Host() function decorating the heroCache constructor property ensures that you get a reference to the cache service from the parent HeroBioComponent. Angular throws an error if the parent lacks that service, even if a component higher in the component tree includes it.

另一个 @Host() 函数是构造函数属性 loggerService 的装饰器。 在本应用程序中只有一个在 AppComponent 级提供的 LoggerService 实例。 该宿主 HeroBioComponent 没有自己的 LoggerService 提供者。

A second @Host() function decorates the loggerService constructor property. The only LoggerService instance in the app is provided at the AppComponent level. The host HeroBioComponent doesn't have its own LoggerService provider.

如果没有同时使用 @Optional() 装饰器的话,Angular 就会抛出错误。当该属性带有 @Optional() 标记时,Angular 就会把 loggerService 设置为 null,并继续执行组件而不会抛出错误。

Angular throws an error if you haven't also decorated the property with @Optional(). When the property is marked as optional, Angular sets loggerService to null and the rest of the component adapts.

下面是 HeroBiosAndContactsComponent 的执行结果:

Here's HeroBiosAndContactsComponent in action.

如果注释掉 @Host() 装饰器,Angular 就会沿着注入器树往上走,直到在 AppComponent 中找到该日志服务。日志服务的逻辑加了进来,所显示的英雄信息增加了 "!!!" 标记,这表明确实找到了日志服务。

If you comment out the @Host() decorator, Angular walks up the injector ancestor tree until it finds the logger at the AppComponent level. The logger logic kicks in and the hero display updates with the "!!!" marker to indicate that the logger was found.

如果你恢复了 @Host() 装饰器,并且注释掉 @Optional 装饰器,应用就会抛出一个错误,因为它在宿主组件这一层找不到所需的 LoggerEXCEPTION: No provider for LoggerService! (HeroContactComponent -> LoggerService)

If you restore the @Host() decorator and comment out @Optional, the app throws an exception when it cannot find the required logger at the host component level. EXCEPTION: No provider for LoggerService! (HeroContactComponent -> LoggerService)

使用 @Inject 指定自定义提供者

Supply a custom provider with @Inject

自定义提供者让你可以为隐式依赖提供一个具体的实现,比如内置浏览器 API。下面的例子使用 InjectionToken 来提供 localStorage,将其作为 BrowserStorageService 的依赖项。

Using a custom provider allows you to provide a concrete implementation for implicit dependencies, such as built-in browser APIs. The following example uses an InjectionToken to provide the localStorage browser API as a dependency in the BrowserStorageService.

src/app/storage.service.ts
      
      import { Inject, Injectable, InjectionToken } from '@angular/core';

export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
  providedIn: 'root',
  factory: () => localStorage
});

@Injectable({
  providedIn: 'root'
})
export class BrowserStorageService {
  constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}

  get(key: string) {
    return this.storage.getItem(key);
  }

  set(key: string, value: string) {
    this.storage.setItem(key, value);
  }

  remove(key: string) {
    this.storage.removeItem(key);
  }

  clear() {
    this.storage.clear();
  }
}
    

factory 函数返回 window 对象上的 localStorage 属性。Inject 装饰器修饰一个构造函数参数,用于为某个依赖提供自定义提供者。现在,就可以在测试期间使用 localStorage 的 Mock API 来覆盖这个提供者了,而不必与真实的浏览器 API 进行交互。

The factory function returns the localStorage property that is attached to the browser window object. The Inject decorator is a constructor parameter used to specify a custom provider of a dependency. This custom provider can now be overridden during testing with a mock API of localStorage instead of interacting with real browser APIs.

使用 @Self@SkipSelf 来修改提供者的搜索方式

Modify the provider search with @Self and @SkipSelf

注入器也可以通过构造函数的参数装饰器来指定范围。下面的例子就在 Component 类的 providers 中使用浏览器的 sessionStorage API 覆盖了 BROWSER_STORAGE 令牌。同一个 BrowserStorageService 在构造函数中使用 @Self@SkipSelf 装饰器注入了两次,来分别指定由哪个注入器来提供依赖。

Providers can also be scoped by injector through constructor parameter decorators. The following example overrides the BROWSER_STORAGE token in the Component class providers with the sessionStorage browser API. The same BrowserStorageService is injected twice in the constructor, decorated with @Self and @SkipSelf to define which injector handles the provider dependency.

src/app/storage.component.ts
      
      import { Component, OnInit, Self, SkipSelf } from '@angular/core';
import { BROWSER_STORAGE, BrowserStorageService } from './storage.service';

@Component({
  selector: 'app-storage',
  template: `
    Open the inspector to see the local/session storage keys:

    <h3>Session Storage</h3>
    <button (click)="setSession()">Set Session Storage</button>

    <h3>Local Storage</h3>
    <button (click)="setLocal()">Set Local Storage</button>
  `,
  providers: [
    BrowserStorageService,
    { provide: BROWSER_STORAGE, useFactory: () => sessionStorage }
  ]
})
export class StorageComponent implements OnInit {

  constructor(
    @Self() private sessionStorageService: BrowserStorageService,
    @SkipSelf() private localStorageService: BrowserStorageService,
  ) { }

  ngOnInit() {
  }

  setSession() {
    this.sessionStorageService.set('hero', 'Dr Nice - Session');
  }

  setLocal() {
    this.localStorageService.set('hero', 'Dr Nice - Local');
  }
}
    

使用 @Self 装饰器时,注入器只在该组件的注入器中查找提供者。@SkipSelf 装饰器可以让你跳过局部注入器,并在注入器树中向上查找,以发现哪个提供者满足该依赖。 sessionStorageService 实例使用浏览器的 sessionStorage 来跟 BrowserStorageService 打交道,而 localStorageService 跳过了局部注入器,使用根注入器提供的 BrowserStorageService,它使用浏览器的 localStorage API。

Using the @Self decorator, the injector only looks at the component's injector for its providers. The @SkipSelf decorator allows you to skip the local injector and look up in the hierarchy to find a provider that satisfies this dependency. The sessionStorageService instance interacts with the BrowserStorageService using the sessionStorage browser API, while the localStorageService skips the local injector and uses the root BrowserStorageService that uses the localStorage browser API.

注入组件的 DOM 元素

Inject the component's DOM element

即便开发者极力避免,仍然会有很多视觉效果和第三方工具 (比如 jQuery) 需要访问 DOM。这会让你不得不访问组件所在的 DOM 元素。

Although developers strive to avoid it, many visual effects and third-party tools, such as jQuery, require DOM access. As a result, you might need to access a component's DOM element.

为了说明这一点,请看属性型指令中那个 HighlightDirective 的简化版。

To illustrate, here's a minimal version of HighlightDirective from the Attribute Directives page.

src/app/highlight.directive.ts
      
      import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {

  @Input('appHighlight') highlightColor: string;

  private el: HTMLElement;

  constructor(el: ElementRef) {
    this.el = el.nativeElement;
  }

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.highlightColor || 'cyan');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string) {
    this.el.style.backgroundColor = color;
  }
}
    

当用户把鼠标移到 DOM 元素上时,指令将指令所在的元素的背景设置为一个高亮颜色。

The directive sets the background to a highlight color when the user mouses over the DOM element to which the directive is applied.

Angular 把构造函数参数 el 设置为注入的 ElementRef,该 ElementRef 代表了宿主的 DOM 元素,它的 nativeElement 属性把该 DOM 元素暴露给了指令。

Angular sets the constructor's el parameter to the injected ElementRef. (An ElementRef is a wrapper around a DOM element, whose nativeElement property exposes the DOM element for the directive to manipulate.)

下面的代码把指令的 appHighlight 属性(Attribute)填加到两个 <div> 标签里,一个没有赋值,一个赋值了颜色。

The sample code applies the directive's appHighlight attribute to two <div> tags, first without a value (yielding the default color) and then with an assigned color value.

src/app/app.component.html (highlight)
      
      <div id="highlight"  class="di-component"  appHighlight>
  <h3>Hero Bios and Contacts</h3>
  <div appHighlight="yellow">
    <app-hero-bios-and-contacts></app-hero-bios-and-contacts>
  </div>
</div>
    

下图显示了鼠标移到 <hero-bios-and-contacts> 标签上的效果:

The following image shows the effect of mousing over the <hero-bios-and-contacts> tag.

定义提供者

Defining providers

用于实例化类的默认方法不一定总适合用来创建依赖。你可以到依赖提供者部分查看其它方法。 HeroOfTheMonthComponent 例子示范了一些替代方案,展示了为什么需要它们。 它看起来很简单:一些属性和一些由 logger 生成的日志。

A dependency can't always be created by the default method of instantiating a class. You learned about some other methods in Dependency Providers. The following HeroOfTheMonthComponent example demonstrates many of the alternatives and why you need them. It's visually simple: a few properties and the logs produced by a logger.

它背后的代码定制了 DI 框架提供依赖项的方法和位置。 这个例子阐明了通过提供对象字面量来把对象的定义和 DI 令牌关联起来的另一种方式。

The code behind it customizes how and where the DI framework provides dependencies. The use cases illustrate different ways to use the provide object literal to associate a definition object with a DI token.

hero-of-the-month.component.ts
      
      import { Component, Inject } from '@angular/core';

import { DateLoggerService } from './date-logger.service';
import { Hero } from './hero';
import { HeroService } from './hero.service';
import { LoggerService } from './logger.service';
import { MinimalLogger } from './minimal-logger.service';
import { RUNNERS_UP,
         runnersUpFactory } from './runners-up';

@Component({
  selector: 'app-hero-of-the-month',
  templateUrl: './hero-of-the-month.component.html',
  providers: [
    { provide: Hero,          useValue:    someHero },
    { provide: TITLE,         useValue:   'Hero of the Month' },
    { provide: HeroService,   useClass:    HeroService },
    { provide: LoggerService, useClass:    DateLoggerService },
    { provide: MinimalLogger, useExisting: LoggerService },
    { provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }
  ]
})
export class HeroOfTheMonthComponent {
  logs: string[] = [];

  constructor(
      logger: MinimalLogger,
      public heroOfTheMonth: Hero,
      @Inject(RUNNERS_UP) public runnersUp: string,
      @Inject(TITLE) public title: string)
  {
    this.logs = logger.logs;
    logger.logInfo('starting up');
  }
}
    

providers 数组展示了你可以如何使用其它的键来定义提供者:useValueuseClassuseExistinguseFactory

The providers array shows how you might use the different provider-definition keys; useValue, useClass, useExisting, or useFactory.

值提供者:useValue

Value providers: useValue

useValue 键让你可以为 DI 令牌关联一个固定的值。 使用该技巧来进行运行期常量设置,比如网站的基础地址和功能标志等。 你也可以在单元测试中使用值提供者,来用一个 Mock 数据来代替一个生产环境下的数据服务。

The useValue key lets you associate a fixed value with a DI token. Use this technique to provide runtime configuration constants such as website base addresses and feature flags. You can also use a value provider in a unit test to provide mock data in place of a production data service.

HeroOfTheMonthComponent 例子中有两个值-提供者

The HeroOfTheMonthComponent example has two value providers.

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      { provide: Hero,          useValue:    someHero },
{ provide: TITLE,         useValue:   'Hero of the Month' },
    
  • 第一处提供了用于 Hero 令牌的 Hero 类的现有实例,而不是要求注入器使用 new 来创建一个新实例或使用它自己的缓存实例。这里令牌就是这个类本身。

    The first provides an existing instance of the Hero class to use for the Hero token, rather than requiring the injector to create a new instance with new or use its own cached instance. Here, the token is the class itself.

  • 第二处为 TITLE 令牌指定了一个字符串字面量资源。 TITLE 提供者的令牌不是一个类,而是一个特别的提供者查询键,名叫InjectionToken,表示一个 InjectionToken 实例。

    The second specifies a literal string resource to use for the TITLE token. The TITLE provider token is not a class, but is instead a special kind of provider lookup key called an injection token, represented by an InjectionToken instance.

你可以把 InjectionToken 用作任何类型的提供者的令牌,但是当依赖是简单类型(比如字符串、数字、函数)时,它会特别有用。

You can use an injection token for any kind of provider but it's particularly helpful when the dependency is a simple value like a string, a number, or a function.

一个值-提供者的值必须在指定之前定义。 比如标题字符串就是立即可用的。 该例中的 someHero 变量是以前在如下的文件中定义的。 你不能使用那些要等以后才能定义其值的变量。

The value of a value provider must be defined before you specify it here. The title string literal is immediately available. The someHero variable in this example was set earlier in the file as shown below. You can't use a variable whose value will be defined later.

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      const someHero = new Hero(42, 'Magma', 'Had a great month!', '555-555-5555');
    

其它类型的提供者都会惰性创建它们的值,也就是说只在需要注入它们的时候才创建。

Other types of providers can create their values lazily; that is, when they're needed for injection.

类提供者:useClass

Class providers: useClass

useClass 提供的键让你可以创建并返回指定类的新实例。

The useClass provider key lets you create and return a new instance of the specified class.

你可以使用这类提供者来为公共类或默认类换上一个替代实现。比如,这个替代实现可以实现一种不同的策略来扩展默认类,或在测试环境中模拟真实类的行为。

You can use this type of provider to substitute an alternative implementation for a common or default class. The alternative implementation could, for example, implement a different strategy, extend the default class, or emulate the behavior of the real class in a test case.

请看下面 HeroOfTheMonthComponent 里的两个例子:

The following code shows two examples in HeroOfTheMonthComponent.

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      { provide: HeroService,   useClass:    HeroService },
{ provide: LoggerService, useClass:    DateLoggerService },
    

第一个提供者是展开了语法糖的,是一个典型情况的展开。一般来说,被新建的类(HeroService)同时也是该提供者的注入令牌。 通常都选用缩写形式,完整形式可以让细节更明确。

The first provider is the de-sugared, expanded form of the most typical case in which the class to be created (HeroService) is also the provider's dependency injection token. The short form is generally preferred; this long form makes the details explicit.

第二个提供者使用 DateLoggerService 来满足 LoggerService。该 LoggerServiceAppComponent 级别已经被注册。当这个组件要求 LoggerService 的时候,它得到的却是 DateLoggerService 服务的实例。

The second provider substitutes DateLoggerService for LoggerService. LoggerService is already registered at the AppComponent level. When this child component requests LoggerService, it receives a DateLoggerService instance instead.

这个组件及其子组件会得到 DateLoggerService 实例。这个组件树之外的组件得到的仍是 LoggerService 实例。

This component and its tree of child components receive DateLoggerService instance. Components outside the tree continue to receive the original LoggerService instance.

DateLoggerServiceLoggerService 继承;它把当前的日期/时间附加到每条信息上。

DateLoggerService inherits from LoggerService; it appends the current date/time to each message:

src/app/date-logger.service.ts
      
      @Injectable({
  providedIn: 'root'
})
export class DateLoggerService extends LoggerService
{
  logInfo(msg: any)  { super.logInfo(stamp(msg)); }
  logDebug(msg: any) { super.logInfo(stamp(msg)); }
  logError(msg: any) { super.logError(stamp(msg)); }
}

function stamp(msg: any) { return msg + ' at ' + new Date(); }
    

别名提供者:useExisting

Alias providers: useExisting

useExisting 提供了一个键,让你可以把一个令牌映射成另一个令牌。实际上,第一个令牌就是第二个令牌所关联的服务的别名,这样就创建了访问同一个服务对象的两种途径。

The useExisting provider key lets you map one token to another. In effect, the first token is an alias for the service associated with the second token, creating two ways to access the same service object.

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      { provide: MinimalLogger, useExisting: LoggerService },
    

你可以使用别名接口来窄化 API。下面的例子中使用别名就是为了这个目的。

You can use this technique to narrow an API through an aliasing interface. The following example shows an alias introduced for that purpose.

想象 LoggerService 有个很大的 API 接口,远超过现有的三个方法和一个属性。你可能希望把 API 接口收窄到只有两个你确实需要的成员。在这个例子中,MinimalLogger类-接口,就这个 API 成功缩小到了只有两个成员:

Imagine that LoggerService had a large API, much larger than the actual three methods and a property. You might want to shrink that API surface to just the members you actually need. In this example, the MinimalLogger class-interface reduces the API to two members:

src/app/minimal-logger.service.ts
      
      // Class used as a "narrowing" interface that exposes a minimal logger
// Other members of the actual implementation are invisible
export abstract class MinimalLogger {
  logs: string[];
  logInfo: (msg: string) => void;
}
    

下面的例子在一个简化版的 HeroOfTheMonthComponent 中使用 MinimalLogger

The following example puts MinimalLogger to use in a simplified version of HeroOfTheMonthComponent.

src/app/hero-of-the-month.component.ts (minimal version)
      
      @Component({
  selector: 'app-hero-of-the-month',
  templateUrl: './hero-of-the-month.component.html',
  // TODO: move this aliasing, `useExisting` provider to the AppModule
  providers: [{ provide: MinimalLogger, useExisting: LoggerService }]
})
export class HeroOfTheMonthComponent {
  logs: string[] = [];
  constructor(logger: MinimalLogger) {
    logger.logInfo('starting up');
  }
}
    

HeroOfTheMonthComponent 构造函数的 logger 参数是一个 MinimalLogger 类型,在支持 TypeScript 感知的编辑器里,只能看到它的两个成员 logslogInfo

The HeroOfTheMonthComponent constructor's logger parameter is typed as MinimalLogger, so only the logs and logInfo members are visible in a TypeScript-aware editor.

实际上,Angular 把 logger 参数设置为注入器里 LoggerService 令牌下注册的完整服务,该令牌恰好是以前提供的那个 DateLoggerService 实例。

Behind the scenes, Angular sets the logger parameter to the full service registered under the LoggingService token, which happens to be the DateLoggerService instance that was provided above.

在下面的图片中,显示了日志日期,可以确认这一点:

This is illustrated in the following image, which displays the logging date.

工厂提供者:useFactory

Factory providers: useFactory

useFactory 提供了一个键,让你可以通过调用一个工厂函数来创建依赖实例,如下面的例子所示。

The useFactory provider key lets you create a dependency object by calling a factory function, as in the following example.

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      { provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }
    

注入器通过调用你用 useFactory 键指定的工厂函数来提供该依赖的值。 注意,提供者的这种形态还有第三个键 deps,它指定了供 useFactory 函数使用的那些依赖。

The injector provides the dependency value by invoking a factory function, that you provide as the value of the useFactory key. Notice that this form of provider has a third key, deps, which specifies dependencies for the useFactory function.

使用这项技术,可以用包含了一些依赖服务和本地状态输入的工厂函数来建立一个依赖对象

Use this technique to create a dependency object with a factory function whose inputs are a combination of injected services and local state.

这个依赖对象(由工厂函数返回的)通常是一个类实例,不过也可以是任何其它东西。 在这个例子中,依赖对象是一个表示 "月度英雄" 参赛者名称的字符串。

The dependency object (returned by the factory function) is typically a class instance, but can be other things as well. In this example, the dependency object is a string of the names of the runners up to the "Hero of the Month" contest.

在这个例子中,局部状态是数字 2,也就是组件应该显示的参赛者数量。 该状态的值传给了 runnersUpFactory() 作为参数。 runnersUpFactory() 返回了提供者的工厂函数,它可以使用传入的状态值和注入的服务 HeroHeroService

In the example, the local state is the number 2, the number of runners up that the component should show. The state value is passed as an argument to runnersUpFactory(). The runnersUpFactory() returns the provider factory function, which can use both the passed-in state value and the injected services Hero and HeroService.

runners-up.ts (excerpt)
      
      export function runnersUpFactory(take: number) {
  return (winner: Hero, heroService: HeroService): string => {
    /* ... */
  };
}
    

runnersUpFactory() 返回的提供者的工厂函数返回了实际的依赖对象,也就是表示名字的字符串。

The provider factory function (returned by runnersUpFactory()) returns the actual dependency object, the string of names.

  • 这个返回的函数需要一个 Hero 和一个 HeroService 参数。

    The function takes a winning Hero and a HeroService as arguments.

Angular 根据 deps 数组中指定的两个令牌来提供这些注入参数。

Angular supplies these arguments from injected values identified by the two tokens in the deps array.

  • 该函数返回名字的字符串,Angular 可以把它们注入到 HeroOfTheMonthComponentrunnersUp 参数中。

    The function returns the string of names, which Angular than injects into the runnersUp parameter of HeroOfTheMonthComponent.

该函数从 HeroService 中接受候选的英雄,从中取 2 个参加竞赛,并把他们的名字串接起来返回。 参阅现场演练 / 下载范例查看完整源码。

The function retrieves candidate heroes from the HeroService, takes 2 of them to be the runners-up, and returns their concatenated names. Look at the现场演练 / 下载范例for the full source code.

提供替代令牌:类接口与 'InjectionToken'

Provider token alternatives: class interface and 'InjectionToken'

当使用类作为令牌,同时也把它作为返回依赖对象或服务的类型时,Angular 依赖注入使用起来最容易。

Angular dependency injection is easiest when the provider token is a class that is also the type of the returned dependency object, or service.

但令牌不一定都是类,就算它是一个类,它也不一定都返回类型相同的对象。这是下一节的主题。

However, a token doesn't have to be a class and even when it is a class, it doesn't have to be the same type as the returned object. That's the subject of the next section.

类-接口

Classinterface

前面的月度英雄的例子使用了 MinimalLogger 类作为 LoggerService 提供者的令牌。

The previous Hero of the Month example used the MinimalLogger class as the token for a provider of LoggerService.

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      { provide: MinimalLogger, useExisting: LoggerService },
    

MinimalLogger 是一个抽象类。

MinimalLogger is an abstract class.

dependency-injection-in-action/src/app/minimal-logger.service.ts
      
      // Class used as a "narrowing" interface that exposes a minimal logger
// Other members of the actual implementation are invisible
export abstract class MinimalLogger {
  logs: string[];
  logInfo: (msg: string) => void;
}
    

你通常从一个可扩展的抽象类继承。但这个应用中并没有类会继承 MinimalLogger

An abstract class is usually a base class that you can extend. In this app, however there is no class that inherits from MinimalLogger.

LoggerServiceDateLoggerService本可以MinimalLogger 中继承。 它们也可以实现 MinimalLogger,而不用单独定义接口。 但它们没有。 MinimalLogger 在这里仅仅被用作一个 "依赖注入令牌"。

The LoggerService and the DateLoggerService could have inherited from MinimalLogger, or they could have implemented it instead, in the manner of an interface. But they did neither. MinimalLogger is used only as a dependency injection token.

当你通过这种方式使用类时,它称作类接口

When you use a class this way, it's called a class interface.

就像 DI 提供者中提到的那样,接口不是有效的 DI 令牌,因为它是 TypeScript 自己用的,在运行期间不存在。使用这种抽象类接口不但可以获得像接口一样的强类型,而且可以像普通类一样把它用作提供者令牌。

As mentioned in DI Providers, an interface is not a valid DI token because it is a TypeScript artifact that doesn't exist at run time. Use this abstract class interface to get the strong typing of an interface, and also use it as a provider token in the way you would a normal class.

类接口应该定义允许它的消费者调用的成员。窄的接口有助于解耦该类的具体实现和它的消费者。

A class interface should define only the members that its consumers are allowed to call. Such a narrowing interface helps decouple the concrete class from its consumers.

用类作为接口可以让你获得真实 JavaScript 对象中的接口的特性。 但是,为了最小化内存开销,该类应该是没有实现的。 对于构造函数,MinimalLogger 会转译成未优化过的、预先最小化过的 JavaScript。

Using a class as an interface gives you the characteristics of an interface in a real JavaScript object. To minimize memory cost, however, the class should have no implementation. The MinimalLogger transpiles to this unoptimized, pre-minified JavaScript for a constructor function.

dependency-injection-in-action/src/app/minimal-logger.service.ts
      
      var MinimalLogger = (function () {
  function MinimalLogger() {}
  return MinimalLogger;
}());
exports("MinimalLogger", MinimalLogger);
    

注意,只要不实现它,不管添加多少成员,它都不会增长大小,因为这些成员虽然是有类型的,但却没有实现。

Notice that it doesn't have any members. It never grows no matter how many members you add to the class, as long as those members are typed but not implemented.

你可以再看看 TypeScript 的 MinimalLogger 类,确定一下它是没有实现的。

Look again at the TypeScript MinimalLogger class to confirm that it has no implementation.

'InjectionToken' 对象

'InjectionToken' objects

依赖对象可以是一个简单的值,比如日期,数字和字符串,或者一个无形的对象,比如数组和函数。

Dependency objects can be simple values like dates, numbers and strings, or shapeless objects like arrays and functions.

这样的对象没有应用程序接口,所以不能用一个类来表示。更适合表示它们的是:唯一的和符号性的令牌,一个 JavaScript 对象,拥有一个友好的名字,但不会与其它的同名令牌发生冲突。

Such objects don't have application interfaces and therefore aren't well represented by a class. They're better represented by a token that is both unique and symbolic, a JavaScript object that has a friendly name but won't conflict with another token that happens to have the same name.

InjectionToken 具有这些特征。在Hero of the Month例子中遇见它们两次,一个是 title 的值,一个是 runnersUp 工厂提供者。

InjectionToken has these characteristics. You encountered them twice in the Hero of the Month example, in the title value provider and in the runnersUp factory provider.

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      { provide: TITLE,         useValue:   'Hero of the Month' },
{ provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }
    

这样创建 TITLE 令牌:

You created the TITLE token like this:

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      import { InjectionToken } from '@angular/core';

export const TITLE = new InjectionToken<string>('title');
    

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

The type parameter, while optional, conveys the dependency's type to developers and tooling. The token description is another developer aid.

注入到派生类

Inject into a derived class

当编写一个继承自另一个组件的组件时,要格外小心。如果基础组件有依赖注入,必须要在派生类中重新提供和重新注入它们,并将它们通过构造函数传给基类。

Take care when writing a component that inherits from another component. If the base component has injected dependencies, you must re-provide and re-inject them in the derived class and then pass them down to the base class through the constructor.

在这个刻意生成的例子里,SortedHeroesComponent 继承自 HeroesBaseComponent,显示一个被排序的英雄列表。

In this contrived example, SortedHeroesComponent inherits from HeroesBaseComponent to display a sorted list of heroes.

HeroesBaseComponent 能自己独立运行。它在自己的实例里要求 HeroService,用来得到英雄,并将他们按照数据库返回的顺序显示出来。

The HeroesBaseComponent can stand on its own. It demands its own instance of HeroService to get heroes and displays them in the order they arrive from the database.

src/app/sorted-heroes.component.ts (HeroesBaseComponent)
      
      @Component({
  selector: 'app-unsorted-heroes',
  template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
  providers: [HeroService]
})
export class HeroesBaseComponent implements OnInit {
  constructor(private heroService: HeroService) { }

  heroes: Array<Hero>;

  ngOnInit() {
    this.heroes = this.heroService.getAllHeroes();
    this.afterGetHeroes();
  }

  // Post-process heroes in derived class override.
  protected afterGetHeroes() {}

}
    

让构造函数保持简单

Keep constructors simple

构造函数应该只用来初始化变量。 这条规则让组件在测试环境中可以放心地构造组件,以免在构造它们时,无意中做出一些非常戏剧化的动作(比如与服务器进行会话)。 这就是为什么你要在 ngOnInit 里面调用 HeroService,而不是在构造函数中。

Constructors should do little more than initialize variables. This rule makes the component safe to construct under test without fear that it will do something dramatic like talk to the server. That's why you call the HeroService from within the ngOnInit rather than the constructor.

用户希望看到英雄按字母顺序排序。与其修改原始的组件,不如派生它,新建 SortedHeroesComponent,以便展示英雄之前进行排序。 SortedHeroesComponent 让基类来获取英雄。

Users want to see the heroes in alphabetical order. Rather than modify the original component, sub-class it and create a SortedHeroesComponent that sorts the heroes before presenting them. The SortedHeroesComponent lets the base class fetch the heroes.

可惜,Angular 不能直接在基类里直接注入 HeroService。必须在这个组件里再次提供 HeroService,然后通过构造函数传给基类。

Unfortunately, Angular cannot inject the HeroService directly into the base class. You must provide the HeroService again for this component, then pass it down to the base class inside the constructor.

src/app/sorted-heroes.component.ts (SortedHeroesComponent)
      
      @Component({
  selector: 'app-sorted-heroes',
  template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
  providers: [HeroService]
})
export class SortedHeroesComponent extends HeroesBaseComponent {
  constructor(heroService: HeroService) {
    super(heroService);
  }

  protected afterGetHeroes() {
    this.heroes = this.heroes.sort((h1, h2) => {
      return h1.name < h2.name ? -1 :
            (h1.name > h2.name ? 1 : 0);
    });
  }
}
    

现在,请注意 afterGetHeroes() 方法。 你的第一反应是在 SortedHeroesComponent 组件里面建一个 ngOnInit 方法来做排序。但是 Angular 会先调用派生类的 ngOnInit,后调用基类的 ngOnInit, 所以可能在英雄到达之前就开始排序。这就产生了一个讨厌的错误。

Now take note of the afterGetHeroes() method. Your first instinct might have been to create an ngOnInit method in SortedHeroesComponent and do the sorting there. But Angular calls the derived class's ngOnInit before calling the base class's ngOnInit so you'd be sorting the heroes array before they arrived. That produces a nasty error.

覆盖基类的 afterGetHeroes() 方法可以解决这个问题。

Overriding the base class's afterGetHeroes() method solves the problem.

分析上面的这些复杂性是为了强调避免使用组件继承这一点。

These complications argue for avoiding component inheritance.

使用一个前向引用(forwardRef)来打破循环

Break circularities with a forward class reference (forwardRef)

在 TypeScript 里面,类声明的顺序是很重要的。如果一个类尚未定义,就不能引用它。

The order of class declaration matters in TypeScript. You can't refer directly to a class until it's been defined.

这通常不是一个问题,特别是当你遵循一个文件一个类规则的时候。 但是有时候循环引用可能不能避免。当一个类A 引用类 B,同时'B'引用'A'的时候,你就陷入困境了:它们中间的某一个必须要先定义。

This isn't usually a problem, especially if you adhere to the recommended one class per file rule. But sometimes circular references are unavoidable. You're in a bind when class 'A' refers to class 'B' and 'B' refers to 'A'. One of them has to be defined first.

Angular 的 forwardRef() 函数建立一个间接地引用,Angular 可以随后解析。

The Angular forwardRef() function creates an indirect reference that Angular can resolve later.

这个关于父查找器的例子中全都是没办法打破的循环类引用。

The Parent Finder sample is full of circular class references that are impossible to break.

当一个类需要引用自身的时候,你面临同样的困境,就像在 AlexComponentprovdiers 数组中遇到的困境一样。 该 providers 数组是一个 @Component() 装饰器函数的一个属性,它必须在类定义之前出现。

You face this dilemma when a class makes a reference to itself as does AlexComponent in its providers array. The providers array is a property of the @Component() decorator function which must appear above the class definition.

使用 forwardRef 来打破这种循环:

Break the circularity with forwardRef.

parent-finder.component.ts (AlexComponent providers)
      
      providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],