前端应用在不断壮大的过程中,内部模块间的依赖可能也会随之越来越复杂,模块间的低复用性导致应用难以维护,不过我们可以借助计算机领域的一些优秀的编程理念来一定程度上解决这些问题
接下来我们要介绍的依赖注入就是其中之一,也是 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
| 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() }) } }
new App({ onReady() { }, })
|
看起来似乎没什么问题,但是实际应用中需求是非常多变的,比如需要给路由新增新的功能(比如实现 history
模式)或者更新配置,这样一来就不得不在 App
内部去修改这两个模块,这是一个 INNER BREAKING
的操作,而对于之前测试通过了的 App
来说,也必须重新测试
很明显这不是一个好的应用结构,高层次的模块 App
依赖了两个低层次的模块 Router
和 Track
,对低层次模块的修改都会影响高层次的模块 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
| 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() }) } }
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.router
和 this.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
| import Router from 'path/to/Router' export default { init(app) { app.router = new Router(app.options.router) app.router.to('home') } }
import Track from 'path/to/Track' export default { init(app) { app.track = new Track(app.options.track) app.track.tracking() } }
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
| import Share from 'path/to/Share' export default { init(app) { app.share = new Share() app.setShare = data => app.share.setShare(data) } }
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
类,它依赖于 Id
和 Address
两个类
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() { 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') }} ])
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 } }
class Person { id: Id address: 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({ 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') }} ])
const childInjector = injector.resolveAndCreateChild([Person])
const personFromChild = childInjector.get(Person) const person = injector.get(Person) console.log(person === personFromChild) }
|
以上就是 Angular
当中的依赖注入简单的使用方式,我们通过一个基本的示例了解了为什么要使用依赖注入,再到引出 Angular
当中的依赖注入,但是 Angular
当中的依赖注入的内容远远不止上文介绍到的这些,所以我们会在下一部分 Angular 中的依赖注入 来深入的了解 Angular
当中的依赖注入