Angular 中的依赖注入

Angular 中的依赖注入

在前文(为什么要使用依赖注入)当中,我们简单的介绍了依赖注入的基本概念和 Angular 当中的一个简单的依赖注入实例,所以在本章,我们就来深入的了解一下 Angular 当中的依赖注入到底是什么东西

其实简单来说,依赖注入(DI)就是一种设计模式,它也是 Angular 的核心,在 Angular 当中我们一般使用 Provider 来描述与 Token 关联的依赖对象的创建方式,创建方式有四种,它们分别是 useClassuseValueuseExistinguseFactory(前两个也是使用较多的)

基本结构

@NgModuleproviders: [] 中,存放的是多个设定注入的元素,也可以称为 provider 的类型,每个 provider 的基本结构如下

1
2
3
4
{
provide: SomeClass,
useXXXX: ...
}

这里就涉及到两个比较重要的概念

  • provide: SomeClass,代表要提供的注入内容是什么,这时我们会把设定的类别当作是一个 Token,在之后则是选择要使用这个 Token,而这个 Token 的具体内容到到底是什么?则是由 useXXXX 来决定的
  • useXXXX: ...useXXXX 其实是代表了多种设定,也就是上方我们提到的 useClassuseValueuseExistinguseFactory

下面我们就一个一个来进行介绍

useClass

我们先来看看它的接口定义,ClassProvider 接口定义如下

1
2
3
4
5
6
7
8
export interface ClassProvider {
// 用于设置与依赖对象关联的 Token 值
// Token 值可能是 Type、InjectionToken、OpaqueToken 的实例或字符串
provide: any
useClass: Type<any>
// 用于标识是否 multiple providers,若是 multiple 类型,则返回与 Token 关联的依赖对象列表
multi?: boolean
}

这里有一个需要注意的地方,就是 Type<any> 这个类型,在 Angular 当中分为 Type 类型和非 Type 类似,两者的区别主要在于注入方式的不同,即非 Type 类型的参数只能用 @Inject(Something) 的方式注入,接口的定义是下面这样的

1
2
3
4
5
export interface Type<T> extends Function { new (...args: any[]): T }

export function isType(v: any): v is Type<any> {
return typeof v === 'function'
}

下面我们再说回 useClass,通常我们最常用的是 useClass 方法,代表的是使用某个类别,来当做产生 Token 的实体,如下

1
2
3
4
5
@NgModule({
providers: [
{ provide: SomeClass, useClass: AnotherClass }
],
})

当程式内要注入 SomeClass 时,Angular 的核心程式就会改成以 AnotherClass 来建立新的实体,另外当 provideuseClass 相同时,可以直接简写,因此以下两段程式码是完全一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@NgModule({
providers: [
SomeClass
]
})

// ==> 等同于

@NgModule({
providers: [
{
provide: SomeClass,
useClass: SomeClass
}
]
})

useValue

先来看看它的接口定义,ValueProvider 接口如下

1
2
3
4
5
6
7
8
9
export interface ValueProvider {
// 用于设置与依赖对象关联的 Token 值
// Token 值可能是 Type、InjectionToken、OpaqueToken 的实例或字符串
provide: any
// 设置注入的对象
useValue: any
// 用于标识是否 multiple providers,若是 multiple 类型,则返回与 Token 关联的依赖对象列表
multi?: boolean
}

useValue 一般在设定 API 接口的时候使用较多,如下

1
2
3
providers: [
{ provide: 'SEARCH_URL', useValue: 'http://api.xxx.com'}
]

如果需要使用的话,直接在当前组件的构造函数当中进行注入即可,需要注意使用 @Inject()

1
2
3
constructor(
@Inject('SEARCH_URL') private searchUrl
) { }

还会遇到一种问题,如果在开发中引入第三方库以后,可能会引起比如上面的字符串 TokenSEARCH_URL)的冲突问题,这时可以使用 InjectionToken 来解决

useExisting

useExisting 的意思是使用已经注册的类型注入到这里(别名),比如下面示例意思是将 ApiService 起个叫 OtherApiService 的别名

1
2
3
providers: [
{ provide: 'OtherApiService', useExisting: ApiService }
]

useFactory

还是先来看看接口定义,如下

1
2
3
4
5
6
7
8
9
10
11
export interface FactoryProvider {
// 用于设置与依赖对象关联的 Token 值
// Token 值可能是 Type、InjectionToken、OpaqueToken 的实例或字符串
provide: any
// 设置用于创建对象的工厂函数
useFactory: Function
// 依赖对象列表
deps?: any[]
// 用于标识是否 multiple providers,若是 multiple 类型,则返回与 Token 关联的依赖对象列表
multi?: boolean
}

useFactory 一般用于比较复杂的情况,简单来说就是告诉 Injector(注入器),通过调用 useFactory 对应的函数,返回 Token 对应的依赖对象,也就是下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@NgModule({
...
providers: [
{
provide: APP_INITIALIZER,
useFactory: (config: AppConfig) => {
return () => config.load()
},
deps: [AppConfig],
// 关于这个属性,见下方
multi: true
}
]
})

在上面的示例当中涉及到一个 multi 属性,关于 Multi providers,它的作用是可以让我们使用相同的 Token 去注册多个 Provider

1
2
3
4
5
6
7
8
9
const SOME_TOKEN: OpaqueToken = new OpaqueToken('SomeToken')

var injector = ReflectiveInjector.resolveAndCreate([
provide(SOME_TOKEN, { useValue: 'dependency one', multi: true }),
provide(SOME_TOKEN, { useValue: 'dependency two', multi: true })
])

// dependencies == ['dependency one', 'dependency two']
var dependencies = injector.get(SOME_TOKEN)

我们使用 multi: true 来告诉 Angular 的依赖注入系统,我们设置的 providermulti provider,这样一来我们可以使用相同的 Token 值来注册不同的 provider,当我们使用对应的 Token 去获取依赖项的时候,我们获取的是已注册的依赖对象列表

至于为什么要使用 multi provider 的原因,这是因为如果使用同一个 Token 去注册 provider,后面注册的 provider 将会覆盖前面已经注册的 probider,此外,Angular 使用 multi provider 这种机制,为我们提供了可插拔的钩子(pluggable hooks

另外需要注意的是 multi provider 是不能和普通的 provider 混用的

使用 Provider

下面我们就来看看具体如何使用 provider,步骤如下所示

  1. 创建 Token
  2. 根据实际需求来选择依赖对象的创建方式,如 useClassuseValueuseExistinguseFactory
  3. NgModuleComponent 中注册 providers
  4. 使用构造注入的方式,注入与 Token 关联的依赖对象

下面是一个示例,封装 HTTP 服务,比如在每个 HTTP 的请求头中添加 Token(类似拦截器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 封装 http 为一个服务
@Injectable()
export class ApiService {
constructor(
public http: Http
) {}
...
}

// 然后在使用的 Module 当中进行注入
@NgModule({
providers: [
{ provide: ApiService, usdClass: ApiService }
],
})

// 然后在组件当中进行注入后使用
export class HomePage {
constructor(
public apiService: ApiService
) {}

ngOnInit(): void {
this.apiService.get(url)
.map(res => res.json())
.subscribe(v => {
console.log(v)
})
}
}

下面是一些需要注意的地方

  • 创建 Token 的时候为了避免命名冲突,尽量避免使用字符串作为 Token
  • 若要创建模块内通用的依赖对象,需要在 NgModule 中注册相关的 provider
  • 若在每个组件中都有唯一的依赖对象,就需要在 Component 中注册相关的 provider
  • DI 解析 Providers 时,都会对提供的每个 provider 进行规范化处理,即转换成标准的形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function _normalizeProviders(providers: Provider[], res: Provider[]): Provider[] {
providers.forEach(b => {
// 支持简洁的语法,转换为标准格式
if (b instanceof Type) {
res.push({ provide: b, useClass: b })
} else if (b && typeof b == 'object' && (b as any).provide !== undefined) {
res.push(b as NormalizedProvider)
} else if (b instanceof Array) {
// 如果是数组,进行递归处理
_normalizeProviders(b, res)
} else {
throw invalidProviderError(b)
}
})
return res
}

Forward Reference

在开发过程中我们可能会遇到类似下面这样的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Injectable()
class Socket {
constructor(private buffer: Buffer) { }
}

console.log(Buffer) // undefined

@Injectable()
class Buffer {
constructor(@Inject(BUFFER_SIZE) private size: Number) { }
}

console.log(Buffer) // [Function: Buffer]

// 运行后报错
// Error: Cannot resolve all parameters for Socket(undefined).
// Make sure they all have valid type or annotations

这时因为我们编写的代码最终都会被转义为 ES5 来运行,所以在编译阶段『变量声明和函数声明会自动提升,而函数表达式不会自动提升』

如果要解决上面的问题,最简单的处理方式是交换类定义的顺序,或者还可以使用 Angular 提供的 forward reference 特性,Angular 通过引入 forwardRef 让我们可以在使用构造注入的时候,使用尚未定义的依赖对象类型,如果不使用 forwardRef 就会遇到上面那样的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
import { forwardRef } from '@angular2/core'

@Injectable()
class Socket {
constructor(
@Inject(forwardRef(() => Buffer))
private buffer
) { }
}

class Buffer {
constructor(@Inject(BUFFER_SIZE) private size: Number) { }
}

forwardRef 原理分析

下面我们来看看 forwardRef 到底做了些什么,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// @angular/core/src/di/forward_ref.ts

/**
* Allows to refer to references which are not yet defined.
*/
export function forwardRef(forwardRefFn: ForwardRefFn): Type<any> {
// forwardRefFn: () => Buffer
// 当调用 forwardRef 方法时,我们只是在 forwardRefFn 函数对象上,增加了一个私有属性__forward_ref__
(<any>forwardRefFn).__forward_ref__ = forwardRef
// 然后覆写了函数的 toString 方法
(<any>forwardRefFn).toString = function () { return stringify(this()) }
return (<Type<any>><any>forwardRefFn)
}

/**
* Lazily retrieves the reference value from a forwardRef.
*/
// 用来解析通过 forwardRef 包装过的引用值
// 由依赖注入系统调用,当解析 Provider 和创建依赖对象的时候,会自动调用该函数
// 如下
export function resolveForwardRef(type: any): any {
if (typeof type === 'function' && type.hasOwnProperty('__forward_ref__') &&
type.__forward_ref__ === forwardRef) {
return (<ForwardRefFn>type)() // Call forwardRefFn get Buffer
} else {
return type
}
}

/********************************************************************/

// @angular/core/src/di/reflective_provider.ts

/**
* 解析Provider
*/
function resolveReflectiveFactory(provider: NormalizedProvider): ResolvedReflectiveFactory {
let factoryFn: Function
let resolvedDeps: ReflectiveDependency[]
...
if (provider.useClass) {
const useClass = resolveForwardRef(provider.useClass)
factoryFn = reflector.factory(useClass)
resolvedDeps = _dependenciesFor(useClass)
}
}

/********************************************************************/

/**
* 构造依赖对象
*/
export function constructDependencies(
typeOrFunc: any, dependencies: any[]): ReflectiveDependency[] {
if (!dependencies) {
return _dependenciesFor(typeOrFunc)
} else {
const params: any[][] = dependencies.map(t => [t])
return dependencies.map(t => _extractToken(typeOrFunc, t, params))
}
}

/**
* 抽取Token
*/
function _extractToken(
typeOrFunc: any, metadata: any[] | any, params: any[][]): ReflectiveDependency {

token = resolveForwardRef(token)
if (token != null) {
return _createDependency(token, optional, visibility)
} else {
throw noAnnotationError(typeOrFunc, params)
}
}

inject 装饰器

Angular 中,Inject 是『参数装饰器』,主要用来在类的构造函数中描述非 Type 类型的参数对象,在 Angular 中的 Type 类型如下所示

1
2
3
4
5
6
7
8
// Type类型 - @angular/core/src/type.ts
export const Type = Function

export function isType(v: any): v is Type<any> {
return typeof v === 'function'
}

export interface Type<T> extends Function { new (...args: any[]): T }

Angular 中常用的非 Type 类型 Token 有字符串(常量)、OpaqueToken 对象、InjectionToken 对象等,后两者对象的构造如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* 用于创建 OpaqueToken 实例
* export const CONFIG = new OpaqueToken('config')
*/
export class OpaqueToken {
constructor(protected _desc: string) {}
toString(): string { return `Token ${this._desc}` }
}

/*
* 用于创建 InjectionToken 实例,使用泛型描述该 Token 所关联的依赖对象的类型
* const API_URL = new InjectionToken<string>('apiUrl')
*/
export class InjectionToken<T> extends OpaqueToken {
private _differentiate_from_OpaqueToken_structurally: any
constructor(desc: string) { super(desc) }

toString(): string { return `InjectionToken ${this._desc}` }
}

下面是一个 Inject 的简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 公共模块,主要作用是导入 providers,比如我们定义一个常量,用来保存一个地址
@NgModule({
providers: [
{
provide: 'BASE_CONFIG',
useValue: {
uri: 'http://localhost:3002'
}
}
]
})

// 然后在需要使用的地方使用 @Inject 导入进来即可
// 而对于 Http 这种 Type 类型则不需要使用 @Inject
constructor(
@Inject('BASE_CONFIG') private config,
private http: Http
) {
// ...
}

那么这里就会涉及到几个问题

为什么在构造函数中,非 Type 类型的参数只能用 @Inject(Something) 的方式注入

因为只有是 Type 类型的对象,才会被 TypeScript 编译器编译,即我们通过 class 关键字声明的服务,最终都会编译成 ES5 的函数对象

AppService(服务) 中的 @Injectable() 是必须的么

如下,一个简单的示例

1
2
3
4
5
6
7
8
// 封装 http 为一个服务
@Injectable()
export class ApiService {
constructor(
public http: Http
) {}
...
}

如果所创建的服务不依赖于其他对象,是可以不用使用 @Injectable() 类装饰器,但当该服务需要在构造函数中注入依赖对象,就需要使用 @Injectable() 装饰器,因为只有声明了 @Injectable() 这个装饰器的服务才可以注入其他服务

推荐的做法不管是否有依赖对象,在创建服务时都使用 @Injectable() 类装饰器,这样所有服务都遵循同样的规则,一致性

在构造函数中,Type 类型的参数是否可以使用 @Inject(Type) 的方式注入

Type 类型的参数也能使用 @Inject(Type) 的方式注入,具体如下

1
2
3
4
5
// 虽然可以正常使用,但是编辑器会有提示 [ts] Parameter 'http' implicitly has an 'any' type.
constructor(@Inject(Http) private http) { }

// 也可以这样使用,但是有些冗余
constructor(@Inject(Http) private http: Http) { }

如果是 Type 类型的参数,还是推荐使用下面这样的方式

1
constructor(private http: Http) { }

注入器和提供器

我们在使用的时候,一般会去定义一个变量用来接收对应的服务 ProductServicr

1
2
3
constructor(
private productServicr: ProductServicr
) { }

而在定义的时候,需要在提供器的 providers 属性当中指明

1
2
3
4
5
6
7
8
9
10
11
12
providers: [ProjectDirective]

// 等价于下面这种写法,因为 provide 和 useClass 两者的属性皆为 ProductServicr,所以可以简写成上面那种形式
providers: [{
// provide 属性指定了提供器的 token(key),就是根据这个属性来匹配注入的对象和提供器
// 然后才会根据提供器的 useClass 属性再去具体实例化某个类
provide: ProductServicr,
// 指明实例化的方式为 new 操作(因为定义的为 useClass)
// 比如上方在 constructor 当中注入的 ProductServicr 就是经过 new 操作之后的
// 如果这里定义为 useClass: AnotherProductServicr 的话就不能使用简写的方式,因为两者不一致
useClass: ProductServicr
}]

还可以使用如下方式

1
2
3
4
5
providers: [{
provide: ProductServicr,
// 在这种情况下就不再是直接去执行 new 一个类的操作了,而是通过一个工厂方法返回一个实例,再将这个实例注入到 providers 当中,比如做一些初始化的工作
useFactory: () => { ... }
}]

提供器的作用域规则

下面我们来看看提供器的作用域规则,它有以下这些规则

  • 当一个提供器声明在模块当中时(Module),对于该模块下的所有组件是可见的,所有组件都是可以注入的,即在当前模块的 @NgModule 当中的 providers 当中进行注入之后,当前模块下所有组件可用
  • 当一个提供器声明在组件当中时(Component),只对声明它的组件及其子组件可见,其他组件不可注入
  • 当在模块(Module)和组件(Component)当中声明的提供器具有相同的 Tokenkey)的时候,那么组件当中的提供器会覆盖声明在模块当中的提供器(就近原则)

一般情况下优先将服务提供器声明在模块当中

  • 只有在服务只针对某个组件使用,并且对其他组件不可见的时候,才会声明在组件当中
  • 这种情况十分少见,所以一般推荐声明在模块当中

因为服务最终能不能注入到其他地方,是由它在没有在当前模块的 providers 当中声明来决定的

如果所依赖的服务是动态加载的

我们从下面这个示例开始看起,一个随机生成的随机数动态加载对应服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// app.module.ts
@NgModule({
// ...

providers: [{
provide: ProductService,
useFactory: () => {
let logger = new LoggerService()
let dev = Math.random() > 0.5
if (dev) {
return new ProductService(logger)
} else {
return new AnotherProductService(logger)
}
}
}, LoggerService]
})

// logger.service.ts,一个简单的打印日志的服务
import { Injectable } from '@angular/core'

@Injectable()
export class LoggerService {
constructor() { }
log (messgae: string) {
console.log(messgae)
}
}

这里有一点需要注意,如果多个组件共用这个服务,那么生成的实例都是相同的,因为工厂方法创建的对象是一个单例对象,工厂方法只会在创建第一个对象的时候被调用一次,然后在整个应用当中所有被注入的服务的实例都是同一个对象

上面的实例中存在两个问题

第一个问题

在方法内部,我们手动实例化了一个 new LoggerService(),意味着工厂方法与这个类是一种强耦合的关系,而我们又声明了 LoggerService 的提供器,所以我们可以采用下面这种方式来解耦,即利用第三个参数 deps 来声明工厂方法所依赖的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@NgModule({
// ...

providers: [{
provide: ProductService,
useFactory: (logger: LoggerService) => {
let dev = Math.random() > 0.5
if (dev) {
return new ProductService(logger)
} else {
return new AnotherProductService(logger)
}
},
deps: [LoggerService]
}, LoggerService]
})

这样一来就不需要我们手动的去实例化(new LoggerService()),这时的 Angular 将会使用 LoggerService 这个提供器来实例化一个 LoggerService 的实例,并将其注入到 ProductService 的工厂方法的参数当中

第二个问题

我们是根据一个随机数来决定实例化哪个对象,这样测试是可以的,但是在发布的时候就不适用了,通常在这种情况下,我们可以使用一个变量来决定调用哪个方法,然后同样的在 deps 当中进行依赖声明,然后在当作参数的时候传递进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@NgModule({
// ...

providers: [
{
provide: ProductService,
useFactory: (logger: LoggerService, isDev) => {
if (isDev) {
return new ProductService(logger)
} else {
return new AnotherProductService(logger)
}
},
deps: [LoggerService, 'IS_DEV_ENV']
},
LoggerService,
{
provide: 'IS_DEV_ENV',
useValue: false
}
]
})

同样的,可以不单一的注入一个固定的值,也是可以注入一个对象,方便维护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@NgModule({
// ...

providers: [
{
provide: ProductService,
useFactory: (logger: LoggerService, appConfig) => {
if (appConfig.isDev) {
return new ProductService(logger)
} else {
return new AnotherProductService(logger)
}
},
deps: [LoggerService, 'APP_CONFIG']
},
LoggerService,
{
provide: 'APP_CONFIG',
useValue: {
isDev: false
}
}
]
})

注入器的层级关系

前面的提供器只负责实例化所需的依赖对象,将实例化好的对象注入所需组件的工作则是由注入器来完成的,在程序启动的时候, Angular 首先会创建一个应用级注入器,然后将模块中声明的提供器,都注册到这个注入器当中,被注册的提供器除了应用的主模块声明的以外,还包括所有被引用的模块中声明的提供器,比如

1
2
3
4
5
6
7
8
9
10
11
12
// app.module.ts
@NgModule({
// ...
imports: [
BrowserModule,
FormsModule,
HttpModule
],
providers: [
// ...
]
})

在应用级的注入器里面,除了 AppModule 本身声明的一些提供器(providers)会被注册以外,它引入的部分(imports)所有其他的模块,这些模块当中声明的提供器都会被注册到应用级注入器当中,然后 Angular 会创建启动模块指定的主组件(bootstrap 指定的模块),同时应用级别的注入器会为这个主组件创建一个组件级的注入器,并将组件中声明的提供器注册到这个组件级的注入器上

当子组件被创建的时候,它的父组件的注入器会为这个子组件也创建一个注入器,然后将子组件声明的提供器注册上去,以此类推,应用中会形成一组注入器,这些注入器会形成一个与组件的上下级关系一样的层级关系,不过在一般情况下 Angular 可以通过构造函数的参数自动注入所需的依赖

1
constructor(private http: Http) { }

同时也需要注意,Angular 的依赖注入点只有一个,就是它的构造函数,如果一个组件的构造函数为空,那么就可以断定,这个函数没有被注入任何东西,简单总结就是,Angular 当中的注入器层级关系分为

1
应用级的注入器  ==>  主组件注入器  ==>  子组件注入器

手动添加注入器(避免此类操作)

在前文当中我们介绍过了这种使用方式,但是需要注意的是,在实际的使用当中避免使用这种方式

1
2
3
4
5
6
7
8
9
10
import { Component, OnInit, Injector } from '@angular/core'
import { ProductService } from './product.service'

export class ProductComponent implements OnInit {
private productService: ProductService

constructor(private injector: Injector) {
this.productService = injector.get(this.productService)
}
}

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×