最后一个例子介绍了一个假设的Injector
对象。Angular 2更进一步简化了DI。 使用Angular 2,程序员几乎不必陷入注入细节。
Angular 2的DI系统(大部分)通过@NgModule
来控制。 具体来说是providers
和declarations
数组。 (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假设它被赋予一个类。
AuthService
,AuthWidget
和ChatSocket
怎么样? 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()
@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;
}
}
@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
。
注射器可以使用多个类。 useValue
和useFactory
是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;
}
}
一个重要的注意事项是,provide
函数和消费者中的 ‘Random’ 都在引号中。 这是因为作为一个工厂,我们没有在任何地方访问Random
标识符。
上面的例子使用Angular 2的useFactory
。 当Angular 2被告知使用useFactory
来provide
东西时,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 {};
在这种情况下,Math.random
的乘积被分配给传递给 provider
的useValue
属性。
避免注入冲突: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系统提供。 此模块应足够灵活,以便其可由不同模块在不同应用程序中使用。 这意味着这些设置应该由应用程序特性决定,因此由导入EmailModule
的AppModule
提供。
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 { }
当应用程序运行时,它会遇到一个冲突问题,导致两个模块获得相同的配置值,在本例中为{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) { }
}
在将标识令牌转换为OpaqueTokens
而不改变任何其他内容之后,避免了冲突。 每个服务从根模块获取正确的配置对象,Angular现在能够区分使用相同字符串的两个令牌。
注入树
Angular 2注入器(一般)返回单例。 也就是说,在前面的示例中,应用程序中的所有组件都将接收相同的随机数。 在Angular1.x中只有一个注入器,并且所有服务都是单例。 Angular 2通过使用注入器树来克服这个限制。
在Angular 2中,每个应用程序不只有一个注入器,每个应用程序至少有一个注入器。 注入器被组织在与Angular 2的组件树平行的树中。
考虑下面的树,它是一个包含两个打开的聊天窗口和登录/注销小部件的聊天应用程序的模型。
Figure: Image of a Component Tree, and a DI Tree
在上图中,有一个根注入器,它通过@NgModule
的providers
数组建立。有一个LoginService
注册到根注入器。
- 根注入器下面是根
@Component
。这个特定的组件没有providers
数组,并将使用根注入器的所有依赖项。 - 还有两个子注入器,每个
ChatWindow
组件一个。每个组件都有自己的ChatService
实例。 - 第三个子组件是
Logout/Login
,但它没有注入器。 - 有几个没有注射器的孙子组件。每个
ChatWindow
有ChatFeed
和ChatInput
组件。还有LoginWidget
和LogoutWidget
组件,其中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;
}
}