为什么要使用依赖注入

为什么要使用依赖注入

前端应用在不断壮大的过程中,内部模块间的依赖可能也会随之越来越复杂,模块间的低复用性导致应用难以维护,不过我们可以借助计算机领域的一些优秀的编程理念来一定程度上解决这些问题

接下来我们要介绍的依赖注入就是其中之一,也是 Angular 当中比较重要的一部分,但是在展开之前我们先来看看 IOC 的概念

IOC

IOC 的全称叫做 Inversion of Control,可翻译为为『控制反转』或『依赖倒置』,它主要包含了以下三个准则

  • 高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象
  • 抽象不应该依赖于具体实现,具体实现应该依赖于抽象
  • 面向接口编程 而不要面向实现编程

概念总是抽象的,所以下面我们用一个例子来解释上述的概念,假设我们需要构建一款应用叫 App,它包含一个路由模块 Router 和一个页面监控模块 Track,一开始我们可能会这么实现

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
// app.js
import Router from './modules/Router'
import Track from './modules/Track'

class App {
constructor(options) {
this.options = options
this.router = new Router()
this.track = new Track()
this.init()
}

init() {
window.addEventListener('DOMContentLoaded', () => {
this.router.to('home')
this.track.tracking()
this.options.onReady()
})
}
}

// index.js
new App({
onReady() {
// ...
},
})

看起来似乎没什么问题,但是实际应用中需求是非常多变的,比如需要给路由新增新的功能(比如实现 history 模式)或者更新配置,这样一来就不得不在 App 内部去修改这两个模块,这是一个 INNER BREAKING 的操作,而对于之前测试通过了的 App 来说,也必须重新测试

很明显这不是一个好的应用结构,高层次的模块 App 依赖了两个低层次的模块 RouterTrack,对低层次模块的修改都会影响高层次的模块 App,那么如何解决这个问题呢?解决方案就是我们接下将要介绍到的依赖注入(Dependency Injection

使用依赖注入

所谓的依赖注入,简单来说就是把高层模块所依赖的模块通过传参的方式把依赖『注入』到模块内部,上面的代码可以通过依赖注入的方式改造成如下方式

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
// app.js
class App {
constructor(options) {
this.options = options
this.router = options.router
this.track = options.track
this.init()
}

init() {
window.addEventListener('DOMContentLoaded', () => {
this.router.to('home')
this.track.tracking()
this.options.onReady()
})
}
}

// index.js
import App from 'path/to/App'
import Router from './modules/Router'
import Track from './modules/Track'

new App({
router: new Router(),
track: new Track(),
onReady() {
// ...
},
})

我们将依赖提升到了入口处的 new App({}) 当中,可以看到,通过依赖注入解决了上面所说的 INNER BREAKING 的问题,这时我们可以直接在 App 外部对各个模块进行修改而不影响内部,但是这样是不是就万事大吉了呢?仔细观察的话,如果我们现在想给 App 添加一个分享模块 Share,那么这样就又回到了之前所提到的 INNER BREAKING 的问题上,也就是我们不得不对 App 模块进行修改加上一行 this.share = options.share,这明显不是我们所期望的

虽然 App 通过依赖注入的方式在一定程度上解耦了与其他几个模块的依赖关系,但是还不够彻底,其中的 this.routerthis.track 等属性其实都还是对『具体实现』的依赖,明显违背了 IOC 思想的准则,那我们该如何进一步的抽象 App 模块呢?往下看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class App {
static modules = []
constructor(options) {
this.options = options
this.init()
}
init() {
window.addEventListener('DOMContentLoaded', () => {
this.initModules()
this.options.onReady(this)
})
}
static use(module) {
Array.isArray(module) ? module.map(item => App.use(item)) : App.modules.push(module)
}
initModules() {
App.modules.map(module => module.init && typeof module.init == 'function' && module.init(this))
}
}

现在,我们把依赖保存在了 App.modules 属性中,等待后续初始化模块的时候被调用,而 initModules() 方法就是遍历 App.modules 中所有的模块,判断模块是否包含 init 属性且该属性必须是一个函数,如果判断通过的话,该方法就会去执行模块的 init 方法并把 App 的实例 this 传入其中,以便在模块中引用它

从这个方法中可以看出,要实现一个可以被 App.use() 的模块,就必须满足两个『约定』

  • 模块必须包含 init 属性
  • init 必须是一个函数

这其实就是 IOC 思想中对『面向接口编程,而不要面向实现编程』这一准则的很好的体现,App 不关心模块具体实现了什么,只要满足对接口 init 的『约定』就可以了,下面我们在来看看如何使用 App 来管理我们的依赖,如下

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
// modules/Router.js
import Router from 'path/to/Router'
export default {
init(app) {
app.router = new Router(app.options.router)
app.router.to('home')
}
}

// modules/Track.js
import Track from 'path/to/Track'
export default {
init(app) {
app.track = new Track(app.options.track)
app.track.tracking()
}
}

// index.js
import App from 'path/to/App'
import Router from './modules/Router'
import Track from './modules/Track'

App.use([Router, Track])

new App({
router: {
mode: 'history',
},
track: {
// ...
},
onReady(app) {
// ...
},
})

可以发现 App 模块在使用上也非常的方便,通过 App.use() 方法来『注入』依赖,在 ./modules/some-module.js 中按照一定的『约定』去初始化相关配置,比如此时需要新增一个 Share 模块的话,无需到 App 内部去修改内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// modules/Share.js
import Share from 'path/to/Share'
export default {
init(app) {
app.share = new Share()
app.setShare = data => app.share.setShare(data)
}
}

// index.js
App.use(Share)

new App({
// ...
onReady(app) {
app.setShare({
title: 'Hello IOC.',
description: '',
// ...
})
}
})

我们只需要直接在 App 外部去 use 这个 Share 模块即可,对模块的注入和配置极为方便,App 模块此时应该称之为『容器』比较合适了,跟业务已经没有任何关系了,它仅仅只是提供了一些方法来辅助管理注入的依赖和控制模块如何执行

简单总结就是控制反转(Inversion of Control)是一种思想,而依赖注入(Dependency Injection)则是这一思想的一种具体实现方式,这里的 App 则是辅助依赖管理的一个容器

在了解完上面的内容以后,下面我们就来看看 Angular 当中的依赖注入是什么样子的

Angular 当中的依赖注入

同样的,我们也是通过一个示例开始看起,比如下面这个简单的 Person 类,它依赖于 IdAddress 两个类

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
class Id {
static getId(type: string): Id {
return new Id()
}
}

class Address {
constructor(city, street) { }
}

class Person {
id: Id
address: Address
constructor(id: Id, address: Address) {
this.id = id
this.address = address
}
}

// 在某个类当中调用的时候
main() {
const id = Id.getId('123')
const address = new Address('北京', '北京')
const person = new Person(id, address)
}

和我们开头部分介绍的示例十分类似,我们也将依赖提升到了入口处的 main() 当中,但是在当下这种形式中,我们已经知道如果有新的需求变动,我们还是需要去模块的内部来进行修改,下面我们就来看看如何在 Angular 当中来解决这个问题的

Angular 的依赖注入中主要有三个概念

  • Injector,注入者,利用其提供的 API 去创建依赖的实例
  • Provider,告诉 Injector 如何去创建一个实例(构造这个对象)
  • Object,创建好的对象,也就是当前所处的模块或者组件需要的依赖(某种类型的对象,依赖本身也是一种类型)

在这里我们先不利用 Angular 提供的现成方法,而是借住框架手动的来完善整个依赖注入的流程,最后再来和 Angular 当中提供的原生方法来做一个对比,话不多说,先来改写上面的例子

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
import { Component, ReflectiveInjector, Inject } from '@angular/core'
import { environment } from '../environments/environment'

export class AppComponent {

constructor() {
// resolveAndCreate() 方法接收一个 provider 数组
// ReflectiveInjector 利用 resolveAndCreate() 方法会根据输入的一个 provider 数组来构建一个可以提供依赖性的池子
// 池子当中含有多个 provider 对象,该对象有两个属性
// 一个为 provide,可以简单的理解为 key/令牌 等,作用是唯一性的一个标识
// 另外一个为类型,类型有多种,比如 useClass, useFactory 等
// 简单来说,池子的作用就是把在这个类中所有想使用的依赖都存放到里面
const injector = ReflectiveInjector.resolveAndCreate([

// 针对于 { provide: Person, useClass: Person }, 这种 useClass 的方式可以直接简写为 Person
// 可以直接使用类来去代替,因为本质上就是提供了这个类的构造,key/令牌 也是这个类的本身
Person,
{
provide: Address, useFactory: () => {
if (environment.production) {
return new Address('北京', '北京')
} else {
return new Address('西藏', '拉萨')
}
}
},
{ provide: Id, useFactory: () => {
return Id.getId('123')
}}
])

// 注入之后就可以来进行使用了,利用 injector 的 get() 方法来获取 Person 对象
const person = injector.get(Person)
console.log(JSON.stringify(person))
}
}

class Id {
static getId(type: string): Id {
return new Id()
}
}

class Address {
city: string
street: string
constructor(city, street) {
this.city = city
this.street = street
}
}

// 依赖 Id 和 Address 两个类
class Person {
id: Id
address: Address
// 需要两个依赖性,一个是 Id,一个是 Address
// 这里执行的流程就是会去上面的池子当中进行寻找
constructor(@Inject(Id) id, @Inject(Address) address) {
this.id = id
this.address = address
}
}

我们使用 resolveAndCreate() 方法根据输入的一个 provider 数组来构建一个可以提供依赖性的池子,而池子的作用就是把在这个类中所有想使用的依赖都存放到里面,这样一来我们在需要使用的地方就可以直接使用 @Inject() 的方式去池子当中寻找我们所需要的依赖

上面就是我们手动的来实现 Angular 当中的依赖注入的流程,但是在绝大部分的情况下,我们并不需要显性的去手写这么一个 provider 对象,因为 Angular 已经帮我们都已经封装好了,并且提供给了我们一些便利,而我们就可以直接利用这些便利来完成上面这样复杂的操作

使用依赖注入

下面我们就来看看如何在项目当中使用 Angular 提供的 provider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 一个公共模块
// 在 @NgModule 当中注入 BASE_CONFIG 这个静态变量
@NgModule({
// ...
providers: [
{ provide: 'BASE_CONFIG', useValue: 'http://localhost:3000'}
]
})

// 使用
import { Inject } from '@angular/core'

constructor(@Inject('BASE_CONFIG') config) {
console.log(config)
}

我们在 Module 或者 Component 当中,只需要在 providers 数组当中提供这个 provide 对象,Angular 便会自动的帮我们注册到 Inject 这个池子当中,所以在我们使用过程当中,只需要在 constructor 当中使用 @Inject() 注入进来即可,但是需要注意的一点就是,我们这样操作的话,在池子当中取的数据都是单例的,如果想要每次取的都是一个新的实例的话,比如之前的实例,只需在此基础上稍作调整,让其返回一个函数即可

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
export class AppComponent {
constructor() {
const injector = ReflectiveInjector.resolveAndCreate([
Person,
{
provide: Address, useFactory: () => {
// 修改这里
return () => {
if (environment.production) {
return new Address('北京', '北京')
} else {
return new Address('西藏', '拉萨')
}
}
}
},
{ provide: Id, useFactory: () => {
return Id.getId('123')
}}
])

const person = injector.get(Person)
console.log(JSON.stringify(person))
}
}

这是一种方式,另外一种方式就是利用父子传递的概念,这种情况一般使用较少,可以考虑使用返回一个函数的方式

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
constructor() {

const injector = ReflectiveInjector.resolveAndCreate([
Person,
{
provide: Address, useFactory: () => {
if (environment.production) {
return new Address('北京', '北京')
} else {
return new Address('西藏', '拉萨')
}
}
},
{ provide: Id, useFactory: () => {
return Id.getId('123')
}}
])

// 创建一个子注入者,传入 Person
// 这里有一点需要注意,在创建子注入者的时候,我们仅仅只传入了 Person,并没有传入 Id 和 Address
// 这里的情况是 当在子池子中找不到的情况下就回去父池子当中进行查找
const childInjector = injector.resolveAndCreateChild([Person])

const personFromChild = childInjector.get(Person)
const person = injector.get(Person)

console.log(person === personFromChild) // false
}

以上就是 Angular 当中的依赖注入简单的使用方式,我们通过一个基本的示例了解了为什么要使用依赖注入,再到引出 Angular 当中的依赖注入,但是 Angular 当中的依赖注入的内容远远不止上文介绍到的这些,所以我们会在下一部分 Angular 中的依赖注入 来深入的了解 Angular 当中的依赖注入

评论

Your browser is out-of-date!

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

×