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

依赖注入实战

Dependency Injection in Action

本节将会涉及 Angular 依赖注入(DI)的很多特性。

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

要获取本文的代码,参见在线例子 / 下载范例

See the在线例子 / 下载范例of the code in this cookbook.

嵌套的服务依赖

Nested service dependencies

这些被注入服务的消费者不需要知道如何创建这个服务。新建和缓存这个服务是依赖注入器的工作。消费者只要让依赖注入框架知道它需要哪些依赖项就可以了。

The consumer of an injected service doesn't need to know how to create that service. It's the job of the DI framework to create and cache dependencies. The consumer just needs to let the DI framework know which dependencies it needs.

有时候一个服务依赖其它服务...而其它服务可能依赖另外的更多服务。 依赖注入框架会负责正确的顺序解析这些嵌套的依赖项。 在每一步,依赖的使用者只要在它的构造函数里简单声明它需要什么,框架就会完成所有剩下的事情。

Sometimes a service depends on other services, which may depend on yet other services. The dependency injection framework resolves these nested dependencies in the correct order. At each step, the consumer of dependencies declares what it requires in its constructor, and lets the framework provide them.

下面的例子往 AppComponent 里声明它依赖 LoggerServiceUserContext

The following example shows that AppComponent declares its dependence on LoggerService and UserContext.

constructor(logger: LoggerService, public userContext: UserContextService) { userContext.loadUser(this.userId); logger.logInfo('AppComponent initialized'); }
src/app/app.component.ts
      
      constructor(logger: LoggerService, public userContext: UserContextService) {
  userContext.loadUser(this.userId);
  logger.logInfo('AppComponent initialized');
}
    

UserContext 转而依赖 LoggerServiceUserService(这个服务用来收集特定用户信息)。

UserContext in turn depends on both LoggerService and UserService, another service that gathers information about a particular user.

@Injectable({ providedIn: 'root' }) export class UserContextService { constructor(private userService: UserService, private loggerService: LoggerService) { } }
user-context.service.ts (injection)
      
      @Injectable({
  providedIn: 'root'
})
export class UserContextService {
  constructor(private userService: UserService, private loggerService: LoggerService) {
  }
}
    

当 Angular 新建 AppComponent 时,依赖注入框架会先创建一个 LoggerService 的实例,然后创建 UserContextService 实例。 UserContextService 也需要框架刚刚创建的这个 LoggerService 实例,这样框架才能为它提供同一个实例。UserContextService 还需要框架创建过的 UserServiceUserService 没有其它依赖,所以依赖注入框架可以直接 new 出该类的一个实例,并把它提供给 UserContextService 的构造函数。

When Angular creates AppComponent, the DI framework creates an instance of LoggerService and starts to create UserContextService. UserContextService also needs LoggerService, which the framework already has, so the framework can provide the same instance. UserContextService also needs UserService, which the framework has yet to create. UserService has no further dependencies, so the framework can simply use new to instantiate the class and provide the instance to the UserContextService constructor.

父组件 AppComponent 不需要了解这些依赖的依赖。 只要在构造函数中声明自己需要的依赖即可(这里是 LoggerServiceUserContextService),框架会帮你解析这些嵌套的依赖。

The parent AppComponent doesn't need to know about the dependencies of dependencies. Declare what's needed in the constructor (in this case LoggerService and UserContextService) and the framework resolves the nested dependencies.

当所有的依赖都就位之后,AppComponent 就会显示该用户的信息。

When all dependencies are in place, AppComponent displays the user information.

Logged In User

把服务的范围限制到某个组件的子树下

Limit service scope to a component subtree

Angular 应用程序有多个依赖注入器,组织成一个与组件树平行的树状结构。 每个注入器都会创建依赖的一个单例。在所有该注入器负责提供服务的地方,所提供的都是同一个实例。 可以在注入器树的任何层级提供和建立特定的服务。这意味着,如果在多个注入器中提供该服务,那么该服务也就会有多个实例。

An Angular application has multiple injectors, arranged in a tree hierarchy that parallels the component tree. Each injector creates a singleton instance of a dependency. That same instance is injected wherever that injector provides that service. A particular service can be provided and created at any level of the injector hierarchy, which means that there can be multiple instances of a service if it is provided by multiple injectors.

由根注入器提供的依赖可以注入到应用中任何地方的任何组件中。 但有时候你可能希望把服务的有效性限制到应用程序的一个特定区域。 比如,你可能希望用户明确选择一个服务,而不是让根注入器自动提供它。

Dependencies provided by the root injector can be injected into any component anywhere in the application. In some cases, you might want to restrict service availability to a particular region of the application. For instance, you might want to let users explicitly opt in to use a service, rather than letting the root injector provide it automatically.

通过在组件树的子级根组件中提供服务,可以把一个被注入服务的作用域局限在应用程序结构中的某个分支中。 这个例子中展示了如何通过把服务添加到子组件 @Component() 装饰器的 providers 数组中,来为 HeroesBaseComponent 提供另一个 HeroService 实例:

You can limit the scope of an injected service to a branch of the application hierarchy by providing that service at the sub-root component for that branch. This example shows how to make a different instance of HeroService available to HeroesBaseComponent by adding it to the providers array of the @Component() decorator of the sub-component.

@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) { } }
src/app/sorted-heroes.component.ts (HeroesBaseComponent excerpt)
      
      @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) { }
}
    

当 Angular 新建 HeroBaseComponent 的时候,它会同时新建一个 HeroService 实例,该实例只在该组件及其子组件(如果有)中可见。

When Angular creates HeroesBaseComponent, it also creates a new instance of HeroService that is visible only to that component and its children, if any.

也可以在应用程序别处的另一个组件里提供 HeroService。这样就会导致在另一个注入器中存在该服务的另一个实例。

You could also provide HeroService to a different component elsewhere in the application. That would result in a different instance of the service, living in a different injector.

这个例子中,局部化的 HeroService 单例,遍布整份范例代码,包括 HeroBiosComponentHeroOfTheMonthComponentHeroBaseComponent。 这些组件每个都有自己的 HeroService 实例,用来管理独立的英雄库。

Examples of such scoped HeroService singletons appear throughout the accompanying sample code, including HeroBiosComponent, HeroOfTheMonthComponent, and HeroesBaseComponent. Each of these components has its own HeroService instance managing its own independent collection of heroes.

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

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.

@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 { }
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.

@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; } }
src/app/hero-cache.service.ts
      
      
  1. @Injectable()
  2. export class HeroCacheService {
  3. hero: Hero;
  4. constructor(private heroService: HeroService) {}
  5.  
  6. fetchCachedHero(id: number) {
  7. if (!this.hero) {
  8. this.hero = this.heroService.getHeroById(id);
  9. }
  10. return this.hero;
  11. }
  12. }

这三个 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.

@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; } }
src/app/hero-bio.component.ts
      
      
  1. @Component({
  2. selector: 'app-hero-bio',
  3. template: `
  4. <h4>{{hero.name}}</h4>
  5. <ng-content></ng-content>
  6. <textarea cols="25" [(ngModel)]="hero.description"></textarea>`,
  7. providers: [HeroCacheService]
  8. })
  9.  
  10. export class HeroBioComponent implements OnInit {
  11. @Input() heroId: number;
  12.  
  13. constructor(private heroCache: HeroCacheService) { }
  14.  
  15. ngOnInit() { this.heroCache.fetchCachedHero(this.heroId); }
  16.  
  17. get hero() { return this.heroCache.hero; }
  18. }

父组件 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.

Bios

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

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 框架将会给构造函数返回一个 null。

    If no provider is found in the root injector, the DI framework returns null to the constructor.

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

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.

@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'); } }
src/app/hero-bios.component.ts (HeroBiosAndContactsComponent)
      
      
  1. @Component({
  2. selector: 'app-hero-bios-and-contacts',
  3. template: `
  4. <app-hero-bio [heroId]="1"> <app-hero-contact></app-hero-contact> </app-hero-bio>
  5. <app-hero-bio [heroId]="2"> <app-hero-contact></app-hero-contact> </app-hero-bio>
  6. <app-hero-bio [heroId]="3"> <app-hero-contact></app-hero-contact> </app-hero-bio>`,
  7. providers: [HeroService]
  8. })
  9. export class HeroBiosAndContactsComponent {
  10. constructor(logger: LoggerService) {
  11. logger.logInfo('Creating HeroBiosAndContactsComponent');
  12. }
  13. }

注意看模板:

Focus on the template:

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>`,
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.

template: ` <h4>{{hero.name}}</h4> <ng-content></ng-content> <textarea cols="25" [(ngModel)]="hero.description"></textarea>`,
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.

bio and contact

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

Here's HeroContactComponent, which demonstrates the qualifying decorators.

@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; } }
src/app/hero-contact.component.ts
      
      
  1. @Component({
  2. selector: 'app-hero-contact',
  3. template: `
  4. <div>Phone #: {{phoneNumber}}
  5. <span *ngIf="hasLogger">!!!</span></div>`
  6. })
  7. export class HeroContactComponent {
  8.  
  9. hasLogger = false;
  10.  
  11. constructor(
  12. @Host() // limit to the host component's instance of the HeroCacheService
  13. private heroCache: HeroCacheService,
  14.  
  15. @Host() // limit search for logger; hides the application-wide logger
  16. @Optional() // ok if the logger doesn't exist
  17. private loggerService: LoggerService
  18. ) {
  19. if (loggerService) {
  20. this.hasLogger = true;
  21. loggerService.logInfo('HeroContactComponent can log!');
  22. }
  23. }
  24.  
  25. get phoneNumber() { return this.heroCache.hero.phone; }
  26.  
  27. }

注意构造函数的参数。

Focus on the constructor parameters.

@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
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.

Bios with contact into

如果注释掉 @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.

Without @Host

如果你恢复了 @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.

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) { 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(); } }
src/app/storage.service.ts
      
      
  1. import { Inject, Injectable, InjectionToken } from '@angular/core';
  2.  
  3. export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
  4. providedIn: 'root',
  5. factory: () => localStorage
  6. });
  7.  
  8. @Injectable({
  9. providedIn: 'root'
  10. })
  11. export class BrowserStorageService {
  12. constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}
  13.  
  14. get(key: string) {
  15. this.storage.getItem(key);
  16. }
  17.  
  18. set(key: string, value: string) {
  19. this.storage.setItem(key, value);
  20. }
  21.  
  22. remove(key: string) {
  23. this.storage.removeItem(key);
  24. }
  25.  
  26. clear() {
  27. this.storage.clear();
  28. }
  29. }

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 interactive 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.

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', 'Mr. Nice - Session'); } setLocal() { this.localStorageService.set('hero', 'Mr. Nice - Local'); } }
src/app/storage.component.ts
      
      
  1. import { Component, OnInit, Self, SkipSelf } from '@angular/core';
  2. import { BROWSER_STORAGE, BrowserStorageService } from './storage.service';
  3.  
  4. @Component({
  5. selector: 'app-storage',
  6. template: `
  7. Open the inspector to see the local/session storage keys:
  8.  
  9. <h3>Session Storage</h3>
  10. <button (click)="setSession()">Set Session Storage</button>
  11.  
  12. <h3>Local Storage</h3>
  13. <button (click)="setLocal()">Set Local Storage</button>
  14. `,
  15. providers: [
  16. BrowserStorageService,
  17. { provide: BROWSER_STORAGE, useFactory: () => sessionStorage }
  18. ]
  19. })
  20. export class StorageComponent implements OnInit {
  21.  
  22. constructor(
  23. @Self() private sessionStorageService: BrowserStorageService,
  24. @SkipSelf() private localStorageService: BrowserStorageService,
  25. ) { }
  26.  
  27. ngOnInit() {
  28. }
  29.  
  30. setSession() {
  31. this.sessionStorageService.set('hero', 'Mr. Nice - Session');
  32. }
  33.  
  34. setLocal() {
  35. this.localStorageService.set('hero', 'Mr. Nice - Local');
  36. }
  37. }

使用 @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 browswer 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 simplified version of HighlightDirective from the Attribute Directives page.

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; } }
src/app/highlight.directive.ts
      
      
  1. import { Directive, ElementRef, HostListener, Input } from '@angular/core';
  2.  
  3. @Directive({
  4. selector: '[appHighlight]'
  5. })
  6. export class HighlightDirective {
  7.  
  8. @Input('appHighlight') highlightColor: string;
  9.  
  10. private el: HTMLElement;
  11.  
  12. constructor(el: ElementRef) {
  13. this.el = el.nativeElement;
  14. }
  15.  
  16. @HostListener('mouseenter') onMouseEnter() {
  17. this.highlight(this.highlightColor || 'cyan');
  18. }
  19.  
  20. @HostListener('mouseleave') onMouseLeave() {
  21. this.highlight(null);
  22. }
  23.  
  24. private highlight(color: string) {
  25. this.el.style.backgroundColor = color;
  26. }
  27. }

当用户把鼠标移到 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.)

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

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

<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>
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.

Highlighted bios

使用提供商来定义依赖

Define dependencies with providers

本节会示范如何编写提供商来交付被依赖的服务。

This section demonstrates how to write providers that deliver dependent services.

为了从依赖注入器中获取服务,你必须传给它一个令牌。 Angular 通常会通过指定构造函数参数以及参数的类型来处理它。 参数的类型可以用作注入器的查阅令牌。 Angular 会把该令牌传给注入器,并把它的结果赋给相应的参数。

In order to get a service from a dependency injector, you have to give it a token. Angular usually handles this transaction by specifying a constructor parameter and its type. The parameter type serves as the injector lookup token. Angular passes this token to the injector and assigns the result to the parameter.

下面是一个典型的例子。

The following is a typical example.

constructor(logger: LoggerService) { logger.logInfo('Creating HeroBiosComponent'); }
src/app/hero-bios.component.ts (component constructor injection)
      
      constructor(logger: LoggerService) {
  logger.logInfo('Creating HeroBiosComponent');
}
    

Angular 会要求注入器提供与 LoggerService 相关的服务,并把返回的值赋给 logger 参数。

Angular asks the injector for the service associated with LoggerService and assigns the returned value to the logger parameter.

如果注入器已经缓存了与该令牌相关的服务实例,那么它就会直接提供此实例。 如果它没有,它就要使用与该令牌相关的提供商来创建一个。

If the injector has already cached an instance of the service associated with the token, it provides that instance. If it doesn't, it needs to make one using the provider associated with the token.

如果注入器无法根据令牌在自己内部找到对应的提供商,它便将请求移交给它的父级注入器,这个过程不断重复,直到没有更多注入器为止。 如果没找到,注入器就抛出一个错误...除非这个请求是可选的

If the injector doesn't have a provider for a requested token, it delegates the request to its parent injector, where the process repeats until there are no more injectors. If the search fails, the injector throws an error—unless the request was optional.

新的注入器没有提供商。 Angular 会使用一组首选提供商来初始化它本身的注入器。 你必须为自己应用程序特有的依赖项来配置提供商。

A new injector has no providers. Angular initializes the injectors it creates with a set of preferred providers. You have to configure providers for your own app-specific dependencies.

定义提供商

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.

Hero of the month

它背后的代码定制了 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.

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'); } }
hero-of-the-month.component.ts
      
      
  1. import { Component, Inject } from '@angular/core';
  2.  
  3. import { DateLoggerService } from './date-logger.service';
  4. import { Hero } from './hero';
  5. import { HeroService } from './hero.service';
  6. import { LoggerService } from './logger.service';
  7. import { MinimalLogger } from './minimal-logger.service';
  8. import { RUNNERS_UP,
  9. runnersUpFactory } from './runners-up';
  10.  
  11. @Component({
  12. selector: 'app-hero-of-the-month',
  13. templateUrl: './hero-of-the-month.component.html',
  14. providers: [
  15. { provide: Hero, useValue: someHero },
  16. { provide: TITLE, useValue: 'Hero of the Month' },
  17. { provide: HeroService, useClass: HeroService },
  18. { provide: LoggerService, useClass: DateLoggerService },
  19. { provide: MinimalLogger, useExisting: LoggerService },
  20. { provide: RUNNERS_UP, useFactory: runnersUpFactory(2), deps: [Hero, HeroService] }
  21. ]
  22. })
  23. export class HeroOfTheMonthComponent {
  24. logs: string[] = [];
  25.  
  26. constructor(
  27. logger: MinimalLogger,
  28. public heroOfTheMonth: Hero,
  29. @Inject(RUNNERS_UP) public runnersUp: string,
  30. @Inject(TITLE) public title: string)
  31. {
  32. this.logs = logger.logs;
  33. logger.logInfo('starting up');
  34. }
  35. }

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 令牌关联一个固定的值。 使用该技巧来进行