Angular 的依赖注入

最后一个例子介绍了一个假设的Injector对象。Angular 2更进一步简化了DI。 使用Angular 2,程序员几乎不必陷入注入细节。

Angular 2的DI系统(大部分)通过@NgModule来控制。 具体来说是providersdeclarations数组。 (declarations是我们放置组件,管道和指令的地方;providers 是我们提供服务的地方)例如:

import { Injectable, NgModule } from '@angular/core';
@Component({...})
class ChatWidget {
  constructor(private authService: AuthService, private authWidget: AuthWidget,
    private chatSocket: ChatSocket) {}
}
@NgModule({
  declarations: [ ChatWidget ]
})
export class AppModule {};

在上面的例子中,AppModule被告知关于ChatWidget类。 另一种说法是,Angular 2已经提供了一个ChatWidget

这看起来很简单,但读者会想知道Angular 2如何知道如何构建ChatWidget 。如果ChatWidget是一个字符串或一个简单的函数怎么办?

Angular 2假设它被赋予一个类。

AuthServiceAuthWidgetChatSocket怎么样? ChatWidget如何获取这些?

这不是,至少还没有。 Angular 2不知道他们。 这可以很容易改变:

import { Injectable, NgModule } from '@angular/core';
@Component({...})
class ChatWidget {
  constructor(private authService: AuthService, private authWidget: AuthWidget,
    private chatSocket: ChatSocket) {}
}
@Component({...})
class AuthWidget {}
@Injectable()
class AuthService {}
@Injectable()
class ChatSocket {}
@NgModule({
  declarations[ ChatWidget, AuthWidget ]
  providers: [ AuthService, ChatSocket ],
})

好吧,这开始看起更完整一些了。虽然还不清楚ChatWidget如何被告知关于它的依赖。也许这与那些奇怪的@Injectable语句有关。

看起来像@SomeName的语句是装饰器。 装饰器 是JavaScript的扩展。 简而言之,装饰器让程序员修改和/或标记方法,类,属性和参数。有很多种装饰器。 在本节中,重点将放在与DI相关的装饰器:@Inject@Injectable。 有关装饰器的更多信息,请参阅EcmaScript 6和TypeScript特性部分

@Inject()是一个手动机制,让Angular 2知道必须注入参数。 它可以这样使用:

import { Component, Inject } from '@angular/core';
import { Hamburger } from '../services/hamburger';
@Component({
  selector: 'app',
  template: `Bun Type: {{ bunType }}`
})
export class App {
  bunType: string;
  constructor(@Inject(Hamburger) h) {
    this.bunType = h.bun.type;
  }
}

在上面示例中,我们要求chatWidget是单例的,Angular通过调用@Inject(ChatWidget)与类符号ChatWidget关联。 需要特别注意的是,我们使用ChatWidget的类型和作为其单例的引用。 我们没有使用ChatWidget来实例化任何东西,Angular在幕后帮我们做好了。

当使用TypeScript时,@Inject只需要注入primitives 。 TypeScript的类型让Angular 2知道在大多数情况下要做什么。 以上示例将在TypeScript中简化为:

import { Component } from '@angular/core';
import { ChatWidget } from '../components/chat-widget';
@Component({
  selector: 'app',
  template: `Encryption: {{ encryption }}`
})
export class App {
  encryption: boolean;
  constructor(chatWidget: ChatWidget) {
    this.encryption = chatWidget.chatSocket.encryption;
  }
}

View Example

@Injectable()让Angular 2知道一个类可以用于依赖注入器。 如果类上有其他Angular 2装饰器或没有任何依赖,@Injectable()不是必须的。

重要的是任何要注入Angular 2的类都被装饰 。 然而,最佳实践是使用@Injectable()来装饰注入,因为它对开发者更强的语义。

这里有一个用@Injectable标记的 ChatWidget示例:

import {Injectable} from '@angular/core';
import {AuthService} from './auth-service';
import {AuthWidget} from './auth-widget';
import {ChatSocket} from './chat-socket';
@Injectable()
export class ChatWidget {
  constructor(public authService: AuthService, public authWidget: AuthWidget, public chatSocket: ChatSocket) {
  }
}

在上面的例子中,Angular 2的注入器通过使用类型信息来确定要注入到ChatWidget的构造函数中。 这是可能的,因为这些特定的依赖关系是类型化的,并且不是原始类型。 在某些情况下,Angular 2的DI需要更多的信息,而不仅仅是类型。

类以外的注入

到目前为止,注入过的唯一类型是类,但Angular 2不限于注入类。 还简要提及了 providers 的概念。

到目前为止, providers 已经在数组中使用Angular 2的@NgModule元数据。 providers 也都是类标识符。 Angular 2让程序员用更详细的“食谱”指定 providers 。 这是通过为提供Angular 2一个对象字面量({})实现的:

import { NgModule } from '@angular/core';
import { App } from './containers/app'; // hypothetical app component
import { ChatWidget } from './components/chat-widget';
@NgModule({
  providers: [ { provide: ChatWidget, useClass: ChatWidget } ],
})
export class DiExample {};

这个例子是另一个provide一个类的例子,但它使用Angular 2的更长格式。

这个长格式很方便。 如果程序员想要关闭ChatWidget实现,例如允许一个MockChatWidget,他们可以轻松地做到:

import { NgModule } from '@angular/core';
import { App } from './containers/app'; // hypothetical app component
import { ChatWidget } from './components/chat-widget';
import { MockChatWidget } from './components/mock-chat-widget';
@NgModule({
  providers: [ { provide: ChatWidget, useClass: MockChatWidget } ],
})
export class DiExample {};

这个实现交换的最好的部分是注入系统知道如何构建MockChatWidget,并将排序所有provider

注射器可以使用多个类。 useValueuseFactory是Angular 2可以使用的 provider “recipes”的另外两个示例。 例如:

import { NgModule } from '@angular/core';
import { App } from './containers/app'; // hypothetical app component
const randomFactory = () => { return Math.random(); };
@NgModule({
  providers: [ { provide: 'Random', useFactory: randomFactory } ],
})
export class DiExample {};

在假设的app组件中,“Random”可以注入:

import { Component, Inject, provide } from '@angular/core';
@Component({
  selector: 'app',
  template: `Random: {{ value }}`
})
export class App {
  value: number;
  constructor(@Inject('Random') r) {
    this.value = r;
  }
}

View Example

一个重要的注意事项是,provide 函数和消费者中的 ‘Random’ 都在引号中。 这是因为作为一个工厂,我们没有在任何地方访问Random标识符。

上面的例子使用Angular 2的useFactory。 当Angular 2被告知使用useFactoryprovide 东西时,Angular 2期望提供的值是一个函数。 有时函数和类甚至比需要的更多。 Angular 2有一个名为useValue的“食谱”,这些情况几乎完全相同:

import { NgModule } from '@angular/core';
import { App } from './containers/app'; // hypothetical app component
@NgModule({
  providers: [ { provide: 'Random', useValue: Math.random() } ],
})
export class DiExample {};

View Example

在这种情况下,Math.random的乘积被分配给传递给 provideruseValue属性。

避免注入冲突:OpaqueToken

由于Angular允许使用令牌作为其依赖注入系统的标识符,潜在问题之一是使用相同的令牌来表示不同的实体。 例如,如果字符串'token'用于注入一个实体,可能完全不相关的东西也使用'token'注入一个不同的实体。 当Angular解析这些实体之一时,它可能正在解决错误的实体。 这种行为可能很少发生,或者当它在一个小团队中发生时很容易解决 - 但是当涉及到在同一代码库上单独工作的多个团队或来自不同来源的第三方模块时,这些冲突成为一个更大的问题。

考虑这个例子,其中主应用程序是两个模块的消费者:一个提供电子邮件服务,另一个提供日志服务。

app/email/email.service.ts

export const apiConfig = 'api-config';
@Injectable()
export class EmailService {
  constructor(@Inject(apiConfig) public apiConfig) { }
}

app/email/email.module.ts

@NgModule({
  providers: [ EmailService ],
})
export class EmailModule { }

电子邮件服务API需要一些由字符串api-config标识的配置设置,由DI系统提供。 此模块应足够灵活,以便其可由不同模块在不同应用程序中使用。 这意味着这些设置应该由应用程序特性决定,因此由导入EmailModuleAppModule提供。

app/logger/logger.service.ts

export const apiConfig = 'api-config';
@Injectable()
export class LoggerService {
  constructor(@Inject(apiConfig) public apiConfig) { }
}

app/logger/logger.module.ts

@NgModule({
  providers: [ LoggerService ],
})
export class LoggerModule { }

另一个服务LoggerModule由与创建EmailModule的团队不同的团队创建,并且还需要一个配置对象。 不出所料,他们决定对它们的配置对象使用相同的令牌,即字符串api-config。 为了避免两个具有相同名称的令牌之间的冲突,我们可以尝试重命名导入,如下所示。

app/app.module.ts

import { apiConfig as emailApiConfig } from './email/index';
import { apiConfig as loggerApiConfig } from './logger/index';
@NgModule({
  ...
  providers: [
    { provide: emailApiConfig, useValue: { apiKey: 'email-key', context: 'registration' } },
    { provide: loggerApiConfig, useValue: { apiKey: 'logger-key' } },
  ],
  ...
})
export class AppModule { }

View Example

当应用程序运行时,它会遇到一个冲突问题,导致两个模块获得相同的配置值,在本例中为{apiKey:'logger-key'}。 当主应用程序指定这些设置时,Angular将使用loggerApiConfig值覆盖第一个emailApiConfig值,因为它是最后提供的。 在这种情况下,模块实现细节泄露到父模块。 不仅如此,这些细节通过模块导出被混淆,这可能导致有问题的调试。现在该轮到OpaqueToken上场了。

OpaqueToken

OpaqueTokens是独特的和不可变的值,允许开发人员避免注入令牌id冲突。

import { OpaqueToken } from '@angular/core';
const name = 'token';
const token1 = new OpaqueToken(name);
const token2 = new OpaqueToken(name);
console.log(token1 === token2); // false

这里,不管是否将相同的值传递给令牌的构造器,它将不会导致相同的符号。

app/email/email.module.ts

export const apiConfig = new OpaqueToken('api-config');
@Injectable()
export class EmailService {
  constructor(@Inject(apiConfig) public apiConfig: EmailConfig) { }
}
export const apiConfig = new OpaqueToken('api-config');
@Injectable()
export class LoggerService {
  constructor(@Inject(apiConfig) public apiConfig: LoggerConfig) { }
}

View Example

在将标识令牌转换为OpaqueTokens而不改变任何其他内容之后,避免了冲突。 每个服务从根模块获取正确的配置对象,Angular现在能够区分使用相同字符串的两个令牌。

注入树

Angular 2注入器(一般)返回单例。 也就是说,在前面的示例中,应用程序中的所有组件都将接收相同的随机数。 在Angular1.x中只有一个注入器,并且所有服务都是单例。 Angular 2通过使用注入器树来克服这个限制。

Angular 2中,每个应用程序不只有一个注入器,每个应用程序至少有一个注入器。 注入器被组织在与Angular 2的组件树平行的树中。

考虑下面的树,它是一个包含两个打开的聊天窗口和登录/注销小部件的聊天应用程序的模型。

Image of a Component Tree, and a DI TreeFigure: Image of a Component Tree, and a DI Tree

在上图中,有一个根注入器,它通过@NgModuleproviders数组建立。有一个LoginService注册到根注入器。

  • 根注入器下面是根@Component。这个特定的组件没有 providers 数组,并将使用根注入器的所有依赖项。
  • 还有两个子注入器,每个ChatWindow组件一个。每个组件都有自己的ChatService实例。
  • 第三个子组件是Logout/Login,但它没有注入器。
  • 有几个没有注射器的孙子组件。每个ChatWindowChatFeedChatInput组件。还有LoginWidgetLogoutWidget组件,其中Logout/Login作为它们的父组件。
  • 注入器树不会为每个组件创建新的注入器,但会为每个在其装饰器中具有providers数组的组件创建一个新的注入器。
  • 没有providers 数组的组件查看其注册器的父组件。如果父级没有注入器,它将查找,直到它到达根注入器。

警告: 请小心使用providers 数组。如果子组件使用父组件的providers数组中依赖进行装饰,则子组件将影响父组件的依赖关系。这可能带来各种意想不到的后果。

考虑下面的例子:app/module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { App } from './app.component';
import { ChildInheritor, ChildOwnInjector } from './components/index';
import { Unique } from './services/unique';
const randomFactory = () => { return Math.random(); };
@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [
    App,
    ChildInheritor,
    ChildOwnInjector,
  ],
  /** Provide dependencies here */
  providers: [
    Unique,
  ],
  bootstrap: [ App ],
})
export class AppModule {}

在上面的示例中,Unique被引导到根注入器。

app/services/unique.ts

import { Injectable } from '@angular/core';
@Injectable()
export class Unique {
  value: string;
  constructor() {
    this.value = (+Date.now()).toString(16) + '.' +
      Math.floor(Math.random() * 500);
  }
}

Unique服务在实例化时生成其实例唯一的值。

app/components/child-inheritor.component.ts

import { Component, Inject } from '@angular/core';
import { Unique } from '../services/unique';
@Component({
  selector: 'child-inheritor',
  template: `<span>{{ value }}</span>`
})
export class ChildInheritor {
  value: number;
  constructor(u: Unique) {
    this.value = u.value;
  }
}

子继承器没有注入器。它将向上遍历组件树,寻找注入器。

app/components/child-own-injector.component.ts

import { Component, Inject } from '@angular/core';
import { Unique } from '../services/unique';
@Component({
  selector: 'child-own-injector',
  template: `<span>{{ value }}</span>`,
  providers: [Unique]
})
export class ChildOwnInjector {
  value: number;
  constructor(u: Unique) {
    this.value = u.value;
  }
}

子组件自己的注入组件有一个注入器,它填充了自己的Unique实例。 此组件不会与根注入器的Unique实例共享相同的值。

app/containers/app.ts

import { Component, Inject } from '@angular/core';
import { Unique } from '../services/unique';
@Component({
  selector: 'app',
  template: `
     <p>
     App's Unique dependency has a value of {{ value }}
     </p>
     <p>
     which should match
     </p>
     <p>
     ChildInheritor's value: <child-inheritor></child-inheritor>
     </p>
     <p>
     However,
     </p>
     <p>
     ChildOwnInjector should have its own value: <child-own-injector></child-own-injector>
     <p>
     ChildOwnInjector's other instance should also have its own value <child-own-injector></child-own-injector>
     </p>
       `,
})
export class App {
  value: number;
  constructor(u: Unique) {
    this.value = u.value;
  }
}

View Example