在前文(为什么要使用依赖注入)当中,我们简单的介绍了依赖注入的基本概念和 Angular
当中的一个简单的依赖注入实例,所以在本章,我们就来深入的了解一下 Angular
当中的依赖注入到底是什么东西
其实简单来说,依赖注入(DI
)就是一种设计模式,它也是 Angular
的核心,在 Angular
当中我们一般使用 Provider
来描述与 Token
关联的依赖对象的创建方式,创建方式有四种,它们分别是 useClass
,useValue
,useExisting
和 useFactory
(前两个也是使用较多的)
基本结构
在 @NgModule
的 providers: []
中,存放的是多个设定注入的元素,也可以称为 provider
的类型,每个 provider
的基本结构如下
1 | { |
这里就涉及到两个比较重要的概念
provide: SomeClass
,代表要提供的注入内容是什么,这时我们会把设定的类别当作是一个Token
,在之后则是选择要使用这个Token
,而这个Token
的具体内容到到底是什么?则是由useXXXX
来决定的useXXXX: ...
,useXXXX
其实是代表了多种设定,也就是上方我们提到的useClass
,useValue
,useExisting
和useFactory
下面我们就一个一个来进行介绍
useClass
我们先来看看它的接口定义,ClassProvider
接口定义如下
1 | export interface ClassProvider { |
这里有一个需要注意的地方,就是 Type<any>
这个类型,在 Angular
当中分为 Type
类型和非 Type
类似,两者的区别主要在于注入方式的不同,即非 Type
类型的参数只能用 @Inject(Something)
的方式注入,接口的定义是下面这样的
1 | export interface Type<T> extends Function { new (...args: any[]): T } |
下面我们再说回 useClass
,通常我们最常用的是 useClass
方法,代表的是使用某个类别,来当做产生 Token
的实体,如下
1 | @NgModule({ |
当程式内要注入 SomeClass
时,Angular
的核心程式就会改成以 AnotherClass
来建立新的实体,另外当 provide
和 useClass
相同时,可以直接简写,因此以下两段程式码是完全一样的
1 | @NgModule({ |
useValue
先来看看它的接口定义,ValueProvider
接口如下
1 | export interface ValueProvider { |
useValue
一般在设定 API
接口的时候使用较多,如下
1 | providers: [ |
如果需要使用的话,直接在当前组件的构造函数当中进行注入即可,需要注意使用 @Inject()
1 | constructor( |
还会遇到一种问题,如果在开发中引入第三方库以后,可能会引起比如上面的字符串 Token
(SEARCH_URL
)的冲突问题,这时可以使用 InjectionToken 来解决
useExisting
useExisting
的意思是使用已经注册的类型注入到这里(别名),比如下面示例意思是将 ApiService
起个叫 OtherApiService
的别名
1 | providers: [ |
useFactory
还是先来看看接口定义,如下
1 | export interface FactoryProvider { |
useFactory
一般用于比较复杂的情况,简单来说就是告诉 Injector
(注入器),通过调用 useFactory
对应的函数,返回 Token
对应的依赖对象,也就是下面这样
1 | @NgModule({ |
在上面的示例当中涉及到一个 multi
属性,关于 Multi providers
,它的作用是可以让我们使用相同的 Token
去注册多个 Provider
1 | const SOME_TOKEN: OpaqueToken = new OpaqueToken('SomeToken') |
我们使用 multi: true
来告诉 Angular
的依赖注入系统,我们设置的 provider
是 multi provider
,这样一来我们可以使用相同的 Token
值来注册不同的 provider
,当我们使用对应的 Token
去获取依赖项的时候,我们获取的是已注册的依赖对象列表
至于为什么要使用 multi provider
的原因,这是因为如果使用同一个 Token
去注册 provider
,后面注册的 provider
将会覆盖前面已经注册的 probider
,此外,Angular
使用 multi provider
这种机制,为我们提供了可插拔的钩子(pluggable hooks
)
另外需要注意的是
multi provider
是不能和普通的provider
混用的
使用 Provider
下面我们就来看看具体如何使用 provider
,步骤如下所示
- 创建
Token
- 根据实际需求来选择依赖对象的创建方式,如
useClass
,useValue
,useExisting
,useFactory
- 在
NgModule
或Component
中注册providers
- 使用构造注入的方式,注入与
Token
关联的依赖对象
下面是一个示例,封装 HTTP
服务,比如在每个 HTTP
的请求头中添加 Token
(类似拦截器)
1 | // 封装 http 为一个服务 |
下面是一些需要注意的地方
- 创建
Token
的时候为了避免命名冲突,尽量避免使用字符串作为Token
- 若要创建模块内通用的依赖对象,需要在
NgModule
中注册相关的provider
- 若在每个组件中都有唯一的依赖对象,就需要在
Component
中注册相关的provider
- 当
DI
解析Providers
时,都会对提供的每个provider
进行规范化处理,即转换成标准的形式
1 | function _normalizeProviders(providers: Provider[], res: Provider[]): Provider[] { |
Forward Reference
在开发过程中我们可能会遇到类似下面这样的问题
1 | @Injectable() |
这时因为我们编写的代码最终都会被转义为 ES5
来运行,所以在编译阶段『变量声明和函数声明会自动提升,而函数表达式不会自动提升』
如果要解决上面的问题,最简单的处理方式是交换类定义的顺序,或者还可以使用 Angular
提供的 forward reference
特性,Angular
通过引入 forwardRef
让我们可以在使用构造注入的时候,使用尚未定义的依赖对象类型,如果不使用 forwardRef
就会遇到上面那样的问题
1 | import { forwardRef } from '@angular2/core' |
forwardRef 原理分析
下面我们来看看 forwardRef
到底做了些什么,如下
1 | // @angular/core/src/di/forward_ref.ts |
inject 装饰器
在 Angular
中,Inject
是『参数装饰器』,主要用来在类的构造函数中描述非 Type
类型的参数对象,在 Angular
中的 Type
类型如下所示
1 | // Type类型 - @angular/core/src/type.ts |
Angular
中常用的非 Type
类型 Token
有字符串(常量)、OpaqueToken
对象、InjectionToken
对象等,后两者对象的构造如下
1 | /* |
下面是一个 Inject
的简单示例
1 | // 公共模块,主要作用是导入 providers,比如我们定义一个常量,用来保存一个地址 |
那么这里就会涉及到几个问题
为什么在构造函数中,非 Type 类型的参数只能用 @Inject(Something) 的方式注入
因为只有是 Type
类型的对象,才会被 TypeScript
编译器编译,即我们通过 class
关键字声明的服务,最终都会编译成 ES5
的函数对象
AppService(服务) 中的 @Injectable() 是必须的么
如下,一个简单的示例
1 | // 封装 http 为一个服务 |
如果所创建的服务不依赖于其他对象,是可以不用使用 @Injectable()
类装饰器,但当该服务需要在构造函数中注入依赖对象,就需要使用 @Injectable()
装饰器,因为只有声明了 @Injectable()
这个装饰器的服务才可以注入其他服务
推荐的做法不管是否有依赖对象,在创建服务时都使用 @Injectable()
类装饰器,这样所有服务都遵循同样的规则,一致性
在构造函数中,Type 类型的参数是否可以使用 @Inject(Type) 的方式注入
Type
类型的参数也能使用 @Inject(Type)
的方式注入,具体如下
1 | // 虽然可以正常使用,但是编辑器会有提示 [ts] Parameter 'http' implicitly has an 'any' type. |
如果是 Type
类型的参数,还是推荐使用下面这样的方式
1 | constructor(private http: Http) { } |
注入器和提供器
我们在使用的时候,一般会去定义一个变量用来接收对应的服务 ProductServicr
1 | constructor( |
而在定义的时候,需要在提供器的 providers
属性当中指明
1 | providers: [ProjectDirective] |
还可以使用如下方式
1 | providers: [{ |
提供器的作用域规则
下面我们来看看提供器的作用域规则,它有以下这些规则
- 当一个提供器声明在模块当中时(
Module
),对于该模块下的所有组件是可见的,所有组件都是可以注入的,即在当前模块的@NgModule
当中的providers
当中进行注入之后,当前模块下所有组件可用 - 当一个提供器声明在组件当中时(
Component
),只对声明它的组件及其子组件可见,其他组件不可注入 - 当在模块(
Module
)和组件(Component
)当中声明的提供器具有相同的Token
(key
)的时候,那么组件当中的提供器会覆盖声明在模块当中的提供器(就近原则)
一般情况下优先将服务提供器声明在模块当中
- 只有在服务只针对某个组件使用,并且对其他组件不可见的时候,才会声明在组件当中
- 这种情况十分少见,所以一般推荐声明在模块当中
因为服务最终能不能注入到其他地方,是由它在没有在当前模块的 providers
当中声明来决定的
如果所依赖的服务是动态加载的
我们从下面这个示例开始看起,一个随机生成的随机数动态加载对应服务
1 | // app.module.ts |
这里有一点需要注意,如果多个组件共用这个服务,那么生成的实例都是相同的,因为工厂方法创建的对象是一个单例对象,工厂方法只会在创建第一个对象的时候被调用一次,然后在整个应用当中所有被注入的服务的实例都是同一个对象
上面的实例中存在两个问题
第一个问题
在方法内部,我们手动实例化了一个 new LoggerService()
,意味着工厂方法与这个类是一种强耦合的关系,而我们又声明了 LoggerService
的提供器,所以我们可以采用下面这种方式来解耦,即利用第三个参数 deps
来声明工厂方法所依赖的参数
1 | @NgModule({ |
这样一来就不需要我们手动的去实例化(new LoggerService()
),这时的 Angular
将会使用 LoggerService
这个提供器来实例化一个 LoggerService
的实例,并将其注入到 ProductService
的工厂方法的参数当中
第二个问题
我们是根据一个随机数来决定实例化哪个对象,这样测试是可以的,但是在发布的时候就不适用了,通常在这种情况下,我们可以使用一个变量来决定调用哪个方法,然后同样的在 deps
当中进行依赖声明,然后在当作参数的时候传递进去
1 | @NgModule({ |
同样的,可以不单一的注入一个固定的值,也是可以注入一个对象,方便维护
1 | @NgModule({ |
注入器的层级关系
前面的提供器只负责实例化所需的依赖对象,将实例化好的对象注入所需组件的工作则是由注入器来完成的,在程序启动的时候, Angular
首先会创建一个应用级注入器,然后将模块中声明的提供器,都注册到这个注入器当中,被注册的提供器除了应用的主模块声明的以外,还包括所有被引用的模块中声明的提供器,比如
1 | // app.module.ts |
在应用级的注入器里面,除了 AppModule
本身声明的一些提供器(providers
)会被注册以外,它引入的部分(imports
)所有其他的模块,这些模块当中声明的提供器都会被注册到应用级注入器当中,然后 Angular
会创建启动模块指定的主组件(bootstrap
指定的模块),同时应用级别的注入器会为这个主组件创建一个组件级的注入器,并将组件中声明的提供器注册到这个组件级的注入器上
当子组件被创建的时候,它的父组件的注入器会为这个子组件也创建一个注入器,然后将子组件声明的提供器注册上去,以此类推,应用中会形成一组注入器,这些注入器会形成一个与组件的上下级关系一样的层级关系,不过在一般情况下 Angular
可以通过构造函数的参数自动注入所需的依赖
1 | constructor(private http: Http) { } |
同时也需要注意,Angular
的依赖注入点只有一个,就是它的构造函数,如果一个组件的构造函数为空,那么就可以断定,这个函数没有被注入任何东西,简单总结就是,Angular
当中的注入器层级关系分为
1 | 应用级的注入器 ==> 主组件注入器 ==> 子组件注入器 |
手动添加注入器(避免此类操作)
在前文当中我们介绍过了这种使用方式,但是需要注意的是,在实际的使用当中避免使用这种方式
1 | import { Component, OnInit, Injector } from '@angular/core' |