依赖性注入( Dependency Injection )其实不是 Angular 独有的概念,这是一个已经存在很长时间的设计模式,也能够叫作控制反转 ( Inverse of Control )。咱们从下面这个简单的代码片断入手来看看什么是依赖性注入以及为何要使用依赖性注入。javascript
class Person {
constructor() {
this.address = new Address('北京', '北京', '朝阳区', 'xx街xx号');
this.id = Id.getInstance(ID_TYPES.IDCARD);
}
}复制代码
上面的代码中,咱们在 Person
这个类的构造函数中初始化了咱们构建 Person
所须要的依赖类: Address
和 Id
,其中 Address
是我的的地址对象,而 Id
是我的身份对象。这段代码的问题在于除了引入了内部所需的依赖以外, 它知道了这些依赖建立的细节 ,好比它知道 Address
的构造函数须要的参数(省、市、区和街道地址)和这些参数的顺序,它还知道 Id
的工厂方法和其参数(取得身份证类型的 Id
)。html
但这样作的问题到底是什么呢?首先这样的代码是很是难以进行单元测试的,由于在测试的时候咱们每每须要构造一些不一样的测试场景(好比咱们想传入护照类型的 Id
),但这种写法致使你没办法改变其行为。其次,咱们在代码的可维护性和扩展性方面有了很大的障碍,设想一下若是咱们改变了 Address
的构造函数或 Id
的工厂方法的话,咱们不得不去更改 Person
类。一个类还好,但若是几十个类都依赖 Address
或 Person
的话,这会形成多大的麻烦?前端
那么解决的方法呢?也很简单,那就是咱们把 Person
的构造改造一下:java
class Person {
constructor(address, id) {
this.address = address;
this.id = id;
}
}复制代码
咱们在构造中接受已经建立的 Address
和 Id
对象,这样在这段代码中就没有任何关于它们的具体实现了。换句话说,咱们把建立这些依赖性的职责向上一级传递了出去(噗~~推卸责任啊)。如今咱们在生产代码中能够这样构造 Person
:编程
const person = new Person(
new Address('北京', '北京', '朝阳区', 'xx街xx号'),
Id.getInstance(ID_TYPES.IDCARD)
);复制代码
而在测试时,能够方便的构造各类场景,好比咱们将地区改成辽宁:设计模式
const person = new Person(
new Address('辽宁', '沈阳', '和平区', 'xx街xx号'),
Id.getInstance(ID_TYPES.PASSPORT)
);复制代码
其实这就是依赖性注入了,这个概念是否是很简单?但有的同窗问了,那上一级要是单元测试不仍是有问题吗?是的,若是上一级须要测试,就得『推卸责任』到再上一级了。这样一级一级的最后会推到最终的入口函数,但这也不是办法啊,并且靠人工维护也很容易出错,这时候就须要有一个依赖性注入的框架来解决了,这种框架通常叫作 DI 框架或者 IoC 框架。这种框架对于熟悉 Java 和 .Net 的同窗不会陌生,鼎鼎大名的 Spring 最初就是一个这样的框架,固然如今功能丰富多了,远不止这个功能了。api
Angular 中的依赖性注入框架主要包含下面几个角色:数组
可能看到这里仍是有些云里雾里,不要紧,咱们仍是用例子来讲明:数据结构
import { ReflectiveInjector } from '@angular/core';
const injector = RelfectiveInjector.resolveAndCreate([
// providers 数组定义了多个提供者,provide 属性定义令牌
// useXXX 定义怎样建立的方法
{ provide: Person, useClass: Person },
{ provide: Address, useFactory: () => {
if(env.testing)
return new Address('辽宁', '沈阳', '和平区', 'xx街xx号');
return new Address('北京', '北京', '朝阳区', 'xx街xx号');
}
},
{ provide: Id, useFactory: (type) => {
if(type === ID_TYPES.PASSPORT)
return Id.getInstance(ID_TYPES.PASSPORT, someparam);
if(type === ID_TYPES.IDCARD)
return Id.getInstance(ID_TYPES.IDCARD);
return Id.getDefaultInstance();
}
}
]);
class Person {
// 经过 @Inject 修饰器告诉 DI 这个参数须要什么样类型的对象
// 请在 injector 中帮我找到并注入到对应参数中
constructor(@Inject(Address) address, @Inject(Id) id) {
// 省略
}
}
// 经过 injector 获得对象
const person = injector.get(Person);复制代码
上述代码中,Angular 提供了 RelfectiveInjector
来解析和建立依赖的对象,你能够看到咱们把这个应用中须要的 Person
、 Id
和 Address
都放在里面了。谁须要这些对象就能够向 injector 请求,好比: injector.get(Person)
,固然也能够 injector.get(Address)
等等。能够把它理解成一个依赖性的池子,想要什么就取就行了。app
可是问题来了,首先 injector 怎么知道如何建立你须要的对象呢?这个是靠 Provider 定义的,在刚刚的 RelfectiveInjector.resolveAndCreate()
中咱们发现它是接受一个数组做为参数,这个数组就是一个 Provider 的数组。Provider 最多见的属性有两个。第一个是 provide
,这个属性其实定义的是令牌,令牌的做用是让框架知道你要找的依赖是哪一个而后就能够在 useXXX
这个属性定义的构建方式中将你须要的对象构建出来了。
那么 constructor(@Inject(Address) address, @Inject(Id) id)
这句怎么理解呢?因为咱们在 const person = injector.get(Person);
想取得 Person ,但 Person 又须要两个依赖参数: address 和 id 。 @Inject(Address) address
是告诉框架我须要的是一个令牌为 Address 的对象,这样框架就又到 injector 中寻找令牌为 Address 对应的工厂函数,经过工厂函数构造好对象后又把对象赋值到 address 。
因为这里咱们是用对象的类型来作令牌,上面的注入代码也能够写成下面的样子。利用 Typescript 的类型定义,框架看到有依赖的参数就会去 Injector 中寻找令牌为该类型的工厂函数。
class Person {
constructor(address: Address, id: Id) {
// 省略
}
}复制代码
而对于令牌为类型的而且是 useClass
的这种形式,因为先后都同样,对于这种 Provider 咱们有一个语法糖:能够直接写成 { Person }
,而不用完整的写成 { provide: Person, useClass: Person }
这种形式。固然还要注意 Token 不必定非得是某个类的类型,也能够是字符串, Angular 中还有 InjectionToken
用于建立一个能够避免重名的 Token。
那么其实除了 useClass
和 useFactory
,咱们还可使用 useValue
来提供一些简单数据结构,好比咱们可能但愿把系统的 API 基础信息配置经过这种形式让全部想调用 API 的类均可以注入。以下面的例子中,基础配置就是一个简单的对象,里面有多个属性,这种状况用 useValue
便可。
{
provide: 'BASE_CONFIG',
useValue: {
uri: 'https://dev.local/1.1',
apiSecret: 'blablabla',
apiKey: 'nahnahnah'
}
}复制代码
可能你注意到,上面提到的依赖性注入有一个特色,那就是须要注入的参数若是在 Injector 中找不到对应的依赖,那么就会发生异常了。但确实有些时候咱们是须要这样的特性:该依赖是可选的,若是有咱们就这么作,若是没有就那样作。遇到这种状况怎么办呢?
Angular 提供了一个很是贴心的 @Optional
修饰器,这个修饰器用来告诉框架后面的参数须要一个可选的依赖。
constructor(@Optional(ThirdPartyLibrary) lib) {
if (!lib) {
// 若是该依赖不存在的状况
}
}复制代码
须要注意的是,Angular 的 DI 框架建立的对象都是单件( Singleton )的,那么若是咱们须要每次都建立一个新对象怎么破呢?咱们有两个选择,第一种:在 Provider 中返回工厂而不是对象,像下面例子这样:
{
provide: Address,
useFactory: () => {
// 注意:这里返回的是工厂,而不是对象
return () => {
if(env.testing)
return new Address('辽宁', '沈阳', '和平区', 'xx街xx号');
return new Address('北京', '北京', '朝阳区', 'xx街xx号');
}
}
}复制代码
第二种:咱们建立一个 child injector
(子注入者): Injector.resolveAndCreateChild()
const injector = ReflectiveInjector.resolveAndCreate([Person]);
const childInjector = injector.resolveAndCreateChild([Person]);
// 此时父 Injector 和子 Injector 获得的 Person 对象是不一样的
injector.get(Person) !== childInjector.get(Person);复制代码
并且子 Injector 还有一个特性:若是在 childInjector
中找不到令牌对应的工厂,它会去父 Injector 中寻找。换句话说,这父子关系(多重的)是构成了一棵依赖树,框架会从最下面的子 Injector 开始寻找,一直找到最上面的父 Injector。看到这里相信你就知道为何父组件声明的 providers 对于子组件是可见的,由于子组件中在本身 constructor 中若是发现有找不到的依赖就会到父组件中去找。
在实际的 Angular 应用中咱们其实不多会直接显式使用 Injector 去完成注入,而是在对应的模块、组件等的元数据中提供 providers 便可,这是因为 Angular 框架帮咱们完成了这部分代码,它们其实在元数据配置后由框架放入 Injector 中了。
有问题的童鞋能够加入个人小密圈讨论:t.xiaomiquan.com/jayRnaQ (该连接7天内(5月14日前)有效)
个人 《Angular 从零到一》纸书出版了,欢迎你们围观、订购、提出宝贵意见。
下面是书籍的内容简介:
本书系统介绍Angular的基础知识与开发技巧,可帮助前端开发者快速入门。共有9章,第1章介绍Angular的基本概念,第2~7章从零开始搭建一个待办事项应用,而后逐步增长功能,如增长登陆验证、将应用模块化、多用户版本的实现、使用第三方样式库、动态效果制做等。第8章介绍响应式编程的概念和Rx在Angular中的应用。第9章介绍在React中很是流行的Redux状态管理机制,这种机制的引入可让代码和逻辑隔离得更好,在团队工做中强烈建议采用这种方案。本书不只讲解Angular的基本概念和最佳实践,并且分享了做者解决问题的过程和逻辑,讲解细腻,风趣幽默,适合有面向对象编程基础的读者阅读。
京东连接:item.m.jd.com/product/120…