1. 使用DI
依赖注入是一个很重要的程序设计模式。 Angular 有自己的依赖注入框架,离开了它,我们几乎没法构建 Angular 应用。它使用得非常广泛,以至于几乎每个人都会把它简称为 DI。
我们来看一个简单的例子:
export class Animal { dogs; constructor() { var dog = new Dog(); } } |
我们的Animal在构造函数中手工创建所需的每样东西。问题在于,我们这个 Animal
类过于脆弱、缺乏弹性并且难以测试。
当我们的Animal 类需要一个 Dog,没有去请求一个现成的实例, 而是在构造函数中用具体的 Dog类新创建了一份只供自己用的副本。
如果 Dog类升级了,并且它的构造函数要求传入一个参数了,该怎么办? 我们这个Animal类就被破坏了,而且直到我们把创建引擎的代码重写为 Dog= new Dog(theNewParameter) 之前,它都是坏的。但是当Dog类的定义发生变化时,我们就不得不在乎了,Animal类也不得不跟着改变。 这就会让Animal类过于脆弱。
现在,每个Animal都有自己独特的Dog。他无法被其他Animal共享。我们的Animal缺乏必要的弹性,无法共享给其他的Animal类消费。
我们该如何让 Animal更强壮、有弹性以及可测试?
答案超级简单。我们把Animal的构造函数改造成使用 DI 的版本:
export class Animal { dogs; constructor(private dog:Dog) { } } |
发生了什么?我们把依赖的定义移到了构造函数中。 我们的Animal类不再创建Dog, 它仅仅“消费”它们。如果有人扩展了Dog类,那就不再是Animal类的烦恼了。
2. Angular DI
Angular 自带了它自己的依赖注入框架。此框架也能被当做独立模块用于其它应用和框架中。
2.1 注入器树
Angular 有一个多级依赖注入系统。实际上,应用程序中有一个与组件树平行的注入器树,我们可以在组件树中的任何级别上重新配置注入器。常见的注入器数的形式如下:
当一个底层的组件申请获得一个依赖时, Angular 先尝试用该组件自己的注入器来满足它。 如果该组件的注入器没有找到对应的提供商,它就把这个申请转给它父组件的注入器来处理。 如果那个注入器也无法满足这个申请,它就继续转给 它的父组件的注入器。 这个申请继续往上冒泡——直到我们找到了一个能处理此申请的注入器或者超出了组件树中的祖先位置为止。 如果超出了组件树中的祖先还未找到, Angular 就会抛出一个错误。
2.2 实现原理
Angular给依赖注入器提供令牌来获取服务。通常在构造函数里面,为参数指定类型,让 Angular 来处理依赖注入。该参数类型就是依赖注入器所需的 令牌 。 Angular 把该令牌传给注入器,然后把得到的结果赋给参数。
注入器从哪儿得到的依赖? 它可能在自己内部容器里已经有该依赖了。 如果它没有,也能在 提供商 的帮助下新建一个。 提供商 就是一个用于交付服务的配方,它被关联到一个令牌。Angular会根据该令牌根据供应商创建一个服务结果返回,并将其保存在注入器内部供以后调用。
2.3 令牌
当我们为注入器注册一个提供商时,实际上是把这个提供商和一个 DI 令牌关联起来了。 注入器维护一个内部的 令牌 - 提供商 映射表,这个映射表会在请求一个依赖时被引用到。令牌就是这个映射表中的键值 key 。
2.3.1 类依赖
一般情况下,依赖值都是一个类 实例 ,并且类的类型是它自己的查找键值。 这种情况下,我们实际上是直接从注入器中以 类型作为令牌,来获取一个 实例。
写一个需要基于类的依赖注入的构造函数对我们来说是很幸运的。我们只要以 类为类型,定义一个构造函数参数, Angular 就会知道把跟 类令牌关联的服务注入进来,大多数依赖值都是以类的形式提供的。
例如,其中Animal依赖Dog类,在构造函数中提供Dog类型,就可以依赖注入对应的Dog类实例。
export class Animal { dogs; constructor(private dog:Dog) { } } |
请注意,TypeScript 接口不是一个有效的令牌。
2.3.2 非类依赖
如果依赖值不是一个类呢?有时候我们想要注入的东西是一个字符串,函数或者对象。
应用程序经常为很多很小的因素 ( 比如应用程序的标题,或者一个网络 API 终点的地址 ) 定义配置对象,但是这些配置对象不总是类的实例。
但是这种情况下我们要把什么用作令牌呢? 解决方案是定义和使用一个 OpaqueToken。定义方式类似于这样:
import { OpaqueToken } from ‘@angular/core‘; export let APP_CONFIG = new OpaqueToken(‘app.config‘); |
我们使用这个 OpaqueToken
对象注册依赖的提供商:
providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }] |
现在,在 @Inject
的帮助下,我们可以把这个配置对象注入到任何需要它的构造函数中:
constructor(@Inject(APP_CONFIG) config: AppConfig) { this.title = config.title; } |
虽然 ConfigAppConfig
接口在依赖注入过程中没有任何作用,但它为该类中的配置对象提供了强类型信息。
2.4 提供商
提供商 提供 依赖注入的一个运行时版本。 注入器依靠 提供商们 来创建服务的实例,它会被注入器注入到组件或其它服务中。
Angular中使用provide对象来作为提供商,该 provide 对象需要一个令牌 和一个定义对象,该令牌通常是一个类,但并非一定是。
2.4.1 userValue-值提供商
该定义对象有一个主属性 ( 即userValue) ,用来标识该提供商会如何新建和返回依赖。
把一个固定的值 ,也就是该提供商可以将其作为依赖对象返回的值,赋给 userValue 属性。
通常使用该技巧来进行运行期常量设置 ,比如网站的基础地址和功能标志等。 我们在OpaqueToken中已经见识了一个例子,我们为APP_CONFIG提供了一个常量作为定义对象。
{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG } |
一个值提供商的值必须要立即定义。不能事后再定义它的值。很显然,标题字符串是立刻可用的。
2.4.2 useClass -类提供商
userClass 提供商创建并返回一个指定类的新实例。使用该技术来为公共或默认类 提供备选实现。一般来说,被新建的类同时也是该提供商的注入令牌,例如一个提供日志服务的提供商
{ provide: LoggerService, useClass:LoggerService } |
我们在依赖注入LoggerService时,会根据类LoggerService来创建一个默认的示例作为结果返回。
当依赖注入的类有其他类型依赖的情况下,例如LoggerService依赖于用户信息,我们同样用构造函数注入模式,来添加一个带有 LoggerService参数的构造函数。这种情况下就需要用到Injectable注解。
@Injectable() 标志着一个类可以被一个注入器实例化。当我们的LoggerService服务有了一个注入的依赖,我们需要使用@Injectable()来标识LoggerService,这样Angular 就可以使用构造函数参数的元数据来注入一个 用户服务
。
2.4.3 useExisting - 别名提供商
useExisting ,提供商可以把一个令牌映射到另一个令牌上。实际上,第一个令牌是第二个令牌所对应的服务的一个别名,创造了访问同一个服务对象的两种方法 。
{ provide: MinimalLogger, useClass:LoggerService } |
通过使用别名接口来把一个 API 变窄,是一个很重要的该技巧的使用例子。我们在这里就是为了这个目的使用的别名。 想象一下如果 LoggerService 有个很大的 API 接口 ( 虽然它其实只有三个方法,一个属性 ) ,通过使用 MinimalLogger 类 - 接口别名,就能成功的把这个 API 接口缩小到只暴露两个成员:
export abstract class MinimalLogger { logInfo: (msg: string) => void; logs: string[]; } |
2.4.4 useFactory工厂提供商
useFactory 提供商通过调用工厂函数来新建一个依赖对象,主要用来创建一个拥有参数的对象来作为提供者。
使用这项技术,可以用包含了一些 依赖服务和本地状态 输入的工厂函数来 建立一个依赖对象,该依赖对象不一定是一个类实例,它可以是任何东西。例如
export function factory(level){ return new Logger(level) } { provide: RUNNERS_UP, useFactory: factory, deps: [2] } |
2.5 配置注入器。
一般来说输入器的位置有两种,一种是在NgModule中注入,一种是在Component中注入。两种类型都是在元数据中的providers数组中注入,区别在在生效的范围不同,Component中注入只在当前组件以及子组件中生效。例如
Providers:[ UserService, { provide: Logger, useClass: EvenBetterLogger } ] |
其中UserService即是类提供商的简写
{ provide: UserService, useClass: UserService} |
2.6 使用DI
我们知道了如何配置服务提供商,现在我们来了解一下如何使用。
2.6.1 构造函数
通常情况下我们使用构造函数参数来注入对应的服务。一般来讲主要存在两种情况。
首先,是类型作为令牌的依赖注入,这种情况下,可以直接使用构造函数中的参数类型进行依赖注入,例如在Animal中使用Dog类型
export class Animal { dogs; constructor(private dog:Dog) { } } |
其次,可以使用@Inject(‘token’)的形式注入令牌的类型的对象或者服务,例如我们注入值类型的配置对象
constructor(@Inject(APP_CONFIG) config: AppConfig) { this.title = config.title; } |
2.6.2 获取父组件
通常来说获取父组件就是获取一个已经存在的组件类型,父组件必须通过提供一个与别名提供者来实现,例如
providers: [{provide: Parent, useExisting: forwardRef(() => ParentComponent) }] |
Parent 是该提供商的令牌, ParentComponent就是该别名提供商的类型,将该类型提供商注入到父组件的注入器中,则子组件可以使用Parent令牌作为构造函数参数类型来注入该服务,获取ParentComponent。 ParentComponent引用了自身,造成循环引用,必须使用 前向引用forwardRef 打破了该循环,查找当前或者父级的提供商。
2.6.3 跳过自身与可选
当我们不想从当前元素获取依赖的时候,可以使用@SkipSelf(),这样注入器从一个在自己 上一级 的组件开始搜索一个 Parent
依赖。同时,当无法确保依赖是否存在的情况下,而又为了避免抛出找不到依赖的错误情况,可以使用@Optional()注解,这样该依赖是可选的,例如引入父组件的构造函数可以写成如下的格式:
constructor( @SkipSelf() @Optional() public parent: Parent ) { } |