引言
依赖注入,有后端背景的童鞋(尤其是熟悉java spring框架的)应该不会陌生,提到依赖注入,就不得不说一下控制反转,因为依赖注入实际上是控制反转概念的一种实现模式。
控制反转(inversion of control,IoC)原则的非正式称谓是“好莱坞法则”,它来自好莱坞的一句常用语“别打给我们,我们会打给你(don't call us, we'll call you)”,从控制权的角度来看,控制权从我被反转到了别人,我从打电话这个行为的发出者,变成了打电话这个行为的接受者。
控制“正”转可以理解为面向过程编程,定义一个main函数,在main函数里面定义所有的参与者与他们之间的交互过程,一点不能偷懒。需要用到某个对象某个类,自己去new,需要某个库函数,自己去主动调用。
控制“反”转可以理解为面向对象编程,我们有了各种适用于不同场景下的框架和设计模式,这些事物帮我们搭建好了一个整体架构,这个架构用于准备一些基本而普遍需要的基础物资。
控制反转的设计目标
- 将执行任务这个动作和任务(具体实现)解耦
- 让模块专注于它自己的设计目标。不需要考虑别的模块实现了什么,是如何实现的,模块之间依赖于接口
- 让符合相同契约的模块能够互相替代
遵循控制反转的几个事物
- 框架:有些太繁琐太基础的事情我已经帮你做好了,我做不了的那部分以坑的形式留给你了,填完坑你的工作也就完成了,后边的事情我来处理
- 回调、调度器、事件循环:把坑填好,等到时机成熟我就来和你相会
策略模式:坑的游戏规则我已经定义好(输入输出),你只需要按规则填坑就好 - 模板方式模式:坑的种类和踩坑的顺序我已定义好,你只需要负责填坑
- 工厂模式和依赖注入类似,下面详细分析
依赖注入
在任何一个有请求作用的系统当中,至少需要有两个类互相配合工作,在一个入口类下使用new关键字创建另一个类的对象实例,“我”充当一个入口类,在这个入口类中,我每次吃饭的时候都要买一双一次性筷子(每一次使用都要new一次),在这样的关系下,是“我”(即调用者)每次都要“主动”去买一次性筷子(另一个类),是我控制了筷子,在这种控制正转的关系下,放在现实生活当中,肯定是不现实的,而且人是懒惰的,他总会去创造出更加方便自己生活的想法,更确切的做法是,买一双普通的筷子(非一次性),把他放在一个“框架”当中,你需要使用的时候就对“框架”说:我想要用筷子(向框架发出请求),接着筷子就会"注入"到你的手上,而在这个过程当中,你不再是控制方,反而演变成一名请求者(虽然本身还是调用者),依赖于框架给予你资源,控制权落到了框架身上,这就是依赖注入的设计原理。
要理解依赖注入,就必须搞清楚几个问题:
参与者:都有谁
依赖:谁依赖于谁,为什么需要依赖
注入:谁注入谁,到底注入什么
控制:谁控制谁,控制什么
参与者:
一般有三方参与者,参与者1:某个对象,参与者2:DI框架,参与者3:对象所需的外部资源(类,文件等等)
依赖:
对象依赖于DI框架
为什么需要依赖:
“对象(参与者1)”需要“DI框架(参与者2)”提供所需的“外部资源(参与者3)”
谁注入谁:
DI框架注入“对象(参与者1)”
注入什么:
注入“对象(参与者1)”所需的“外部资源(参与者3)”
谁控制谁:
“DI框架(参与者2)”控制对象(参与者1)”
控制什么:
主要是控制对象所需外部资源实例的创建,管理所需重要对象的生命周期
反转:
在传统方式下,也就是正向,如果部件A需要依赖部件B,那就意味着A在内部要创建一个B的实例,也就是A依赖于B。在依赖注入机制也就是反向,如果在部件A中用到部件B,我们就应该期待B被传给A
依赖注入:它的作用是让框架帮你处理重要对象的生命周期的管理,不需要你显式地进行管理(对象构造和销毁)
优势:架构松耦合,测试更简单
Angular 依赖注入
在angular中,依赖注入包括三部分
- 提供商:负责把一个令牌(可能是字符串也可能是类)映射到一个依赖的列表,它告诉angular该如何根据指定的令牌创建对象
- 注入器:负责持有一组绑定,当外界要求创建对象时,解析这些依赖并注入它们。我们不需要创建angular注入器,angular在启动过程中会自动为我们创建一个应用级注入器(
platformBrowserDynamic().bootstrapModule(AppModule)
) - 依赖:被用于注入的对象
注入器的提供商
- 类提供商 useClass
providers: [Logger],这其实是注册提供商的简写表达式,完整的应该是providers: [{provide: Logger,useClass: Logger}]
provide是令牌,用于定位依赖值和注册提供商
useClass是提供商,用于定义对象,它用来指出注入什么以及如何注入
某些时候,我们会请求一个不同的类来提供服务:
providers: [{provide: Logger,useClass: BetterLogger}]
- 别名提供商 useExisting
制造一个别名来引用以前注册过的令牌,比如:
某个旧组件依赖一个OldLogger类,OldLogger和NewLogger具有相同的接口,但是由于某些原因, 我们不能升级这个旧组件并使用它。当旧组件想使用OldLogger记录消息时,我们希望改用NewLogger的单例对象来记录,不管组件请求的是新的还是旧的日志服务,依赖注入器注入的都应该是同一个单例对象。 也就是说,OldLogger应该是NewLogger的别名。如果使用useClass应用中会有两个不同的NewLogger实例,这显然不是我们想看到的,这时候就可以使用useExisting
providers: [
NewLogger,
{provide: OldLogger,useClass: NewLogger} //创建NewLogger的两个实例
]
NewLogger,
{provide: OldLogger,useExisting: NewLogger} //只创建NewLogger的一个实例
]
- 值提供商 useValue
当我们需要一个常量,而它可能会根据应用的其他部分甚至环境进行重定义时,这种方式非常重要。
官方推荐使用InjectionToken作为令牌
import { InjectionToken } from '@angular/core';
export const TITLE = new InjectionToken <string>('title');
providers:[
{provide:TITLE, useValue: 'Hero of the Month'}
]
- 工厂提供商 useFactory
使用工厂提供商进行注入,需要写一个返回任意对象的函数,工厂是创建可诸如对象的最强方式,因为我们可以在工厂函数中“为所欲为”。
providers:[{
provide: MyComponent,
useFactory: ()=> {
if(loggedIn) {
return new MyloggedComponent();
}
return new MyComponent();
}
}]
依赖注入令牌
- 类
我们最常用的
providers: [{provide: Logger,useClass: BetterLogger}]
- 类-接口
使用没有被继承的抽象类作为依赖注入令牌,这种用法的类叫做:类-接口,它的好处是:提供了接口的强类型,能像正常类一样把它当做提供商令牌使用。类-接口应该只定义允许它消费者调用的成员,窄的接口有助于解耦该类的具体实现和它的消费者。
不能使用接口作为依赖注入令牌,因为在JavaScript中并没有接口的概念,编译后angular无法识别
更详细内容参考angular官方文档 - InjectionToken
有时候依赖对象并不是一个类,它可能是一个简单的值,比如日期,数字和字符串,或者一个无形的对象,比如数组和函数。
这样的对象没有应用程序接口,所以不能用一个类来表示,更适合表示它们的是:唯一的和符号性的令牌,一个JavaScript对象,拥有一个友好的名字,但不会与其它的同名令牌发生冲突。
官方推荐使用InjectionToken实现(其实直接使用字符串也可以,最好还是按照官方推荐方式O(∩_∩)O)
import { InjectionToken } from '@angular/core';
export const TITLE = new InjectionToken <string>('title');
export const RUNNERS_UP= new InjectionToken <string>('RunnersUP');
providers:[
{provide:TITLE, useValue: 'Hero of the Month'},
{RUNNERS_UP, useFactory: runnersUpFactory(2),dep[Hero, HeroService]}
]
注册提供商
可以在NgModule或者组件中注册提供商
通常推荐在对应的模块(NgModule)中注册提供商,除非你必须把服务实例的范围限制到某个组件及其子组件树
- 在NgModule中注册提供商
@NgModule({
imports:[],
declarations:[],
providers:[ HeroService ]
})
2.在组件中注册提供商
@Component({
selector:'my-heros',
template:`<h2></h2>`
providers:[ HeroService ]
})
服务
在写一个服务时必须注意@Injectable() 是必不可少的,除非你不打算注入这个服务,因为@Injectable()相当于C++中的new()
import { Injectable } from '@angular/core';
@Injectable()
export class HeroService {
}
如何使用已注册的服务?在组件或者服务的构造函数中注入即可
constructor(
public heroService: HeroService
) {}
所有被注入的服务在angular中总是单例的,肯定有童鞋要问了,那我需要多实例的怎么办?
angular应用程序有多个依赖注入器,组成了一个与组件树平行的树状结构,所以可以在任何组件级别提供和建立服务,当组件申请一个依赖时,angular会从该组件本身的注入器开始,沿着依赖注入器的树向上查找,所以要实现多实例,在组件内注入服务即可
@host @Optional
有时候我们想限定依赖查找方式,比如只想让组件向上搜索到宿主组件,可以使用@host,当组件一层层向上查找没有找到注册的服务时,就会抛出错误,如果不想抛出错误,可以使用@Optional,它会把注入参数设置为null
import { Host } from '@angular/core';
constructor( @Host heroService: HeroService ) {}
import { Optional } from '@angular/core';
constructor( @Optional heroService: HeroService ) {}
总结
angularjs中依赖注入涉及的知识面很多,本文只是作了一个简单介绍,更详细内容参考官方文档和API