控制反转(IoC)html
控制反转的概念最先在2004年由Martin Fowler提出,是针对面向对象设计不断复杂化而提出的一种设计原则,是利用面向对象编程法则来下降应用耦合的设计模式。编程
IoC强调的是对代码引用的控制权由调用方转移到了外部容器,在运行是经过某种方式注入进来,实现了控制反转,这大大下降了程序之间的耦合度。依赖注入是最经常使用的一种实现IoC的方式,另外一种是依赖查找。bootstrap
依赖注入(Dependency Injection)canvas
固然,按照惯例咱们应该举个例子, 哦对,咱们主要说明的是依赖注入,依赖查找请自行查阅资料。设计模式
假设咱们有一个能作汉堡的设备(HRobot),须要用肉(meat)和一些沙拉(salad)做为原料,咱们能够这样实现:数组
export class HRobot { public meat: Meat; public salad: Salad; constructor() { this.meat = new Meat(); this.salad = new Salad(); } cook() {} }
看一下好像没有什么问题,可能你已经发现,咱们的原材料都是放在机器里面的,若是咱们想吃别的口味的汉堡恐怕就要去乡村基了。
为了能够吃到别的口味的汉堡,咱们不得不改造一下咱们的HRobot
:app
export class HRobot { public meat: Meat; public salad: Salad; constructor(public meat: Meat, public salad: Salad) { this.meat = meat; this.salad = salad; } cook() {} }
如今,只要要直接给它meat和salad就行了,咱们的HRobot()
并不须要知道给它的是什么样的meat
:框架
let hRobot = new HRobot(new Meat(), new Salad());
好比,咱们想吃鸡肉汉堡,只须要个它一块鸡肉就好:ide
class Chicken extends Meat { meat = 'chiken'; } let cRobot = new HRobot(new Chicken(), new Salad());
感受还不错,咱们不再会为了吃一个鸡肉汉堡大费周章的去改造一台机器,这太难以想象了。函数
我可能想到了,你仍是懒得弄块鸡肉给它,这时候可使用工厂函数:
export class HRobotFactory { createHRobot() { let robot = new HRobot(this.createMeat(), this.createSalad()); } createMeat() { return new Meat(); } creatSalad() { return new Salad(); } }
如今有了工厂,就有源源不断的汉堡能够吃了,开不开心,惊不惊喜?
好吧,没有最懒,只有更懒,连工厂都懒得管理我也是无话可说,幸运的是咱们有Angular
提供的依赖注入框架,它可让你伸手就有汉堡吃!
在介绍Angular依赖注入以前,先来理一下三个概念:
注入器(Injector
):就想制造工厂,提供了一系列的接口,用于建立依赖对象的实例。
提供商(Provider
):用于配置注入器,注入器经过它来建立被依赖对象的实例,Provider把令牌(Token
)映射到工厂方法,被依赖的对象就是经过这个方法建立的。
依赖(Denpendence
):指定了被依赖对象的类型,注入器会根据此类型建立对应的对象。
说了半天究竟是什么样的?
用代码示例以下:
var injector = new Injector(...); var robot = injector.get(HRobot); robot.cook();
Injector()
的实现以下:
import { ReflecttiveInjector } form '@angular/core'; var injector = ReflectiveInjector.resolveAndCreat([ {provide: HRobot, useClass: HRobot}, {provide: Meat, useClass: Meat}, {provide: Salad, useClass: Salad} ]);
还有注入器是这样知道知道初始化HRobot
须要依赖Meat
和Salad
:
export class Robot { //... consructor(public meat: Meat, public salad: Salad) {} //... }
固然,看了头大是应该的,由于上面的东西压根就不须要本身动手写,Angular
的依赖注入框架已经自动帮咱们完成了(注入器的生成和调用)。
1. 在组件中注入服务Angular
在底层作了大量的初始化工做,这极大地下降了咱们使用依赖注入的成本,如今要完成依赖注入,咱们只须要三步:
经过import
导入被依赖的对象服务
在组件中配置注入器。在启动组件时,Angular
会读取@Component
装饰器里的providers
元数据,它是一个数组,配置了该组件须要使用的全部依赖,Angular
的依赖注入框架会根据这个列表去建立对应的示例。
在组件构造函数中声明须要注入的依赖。注入器会根据构造函数上的声明,在组件初始化时经过第二步中的providers
元数据配置依赖,为构造函数提供对应的依赖服务,最终完成依赖注入。
例子来了:
// app.component.ts //... // 1. 导入被依赖对象的服务 import { MyService } from './my-service/my-service.service'; @Component({ //... // 2. 在组件中配置注入器 providers: [ MyService ] //... }) export class AppComponent { // 3. 在构造函数中声明须要注入的依赖 constructor(private myService: MyService) {} }
2. 在服务中注入服务
除了组件依赖服务,服务间依的相互调用也很寒常见。例如咱们想给咱们的汉堡机器人加上一个计数器,来记录它的生产情况,可是计数器又依靠电源来工做,咱们就能够用一个服务来实现:
// power.service.ts import { Injectable } from '@angular/core'; @Injectable() export class PowerService { // power come from here.. } // count.service.ts import { Injectable } from '@angular/core'; import { PowerService } from './power/power.service'; @Injectable() export class CountService { constructor(private power: PoowerService) {} } // app.component.ts 这里是当前组件,其实模块中的注入也同样,后面讲到 //... providers: [ CountService, PowerService ]
这里须要注意的是@Injectable
装饰器是非必须的,由于只有一个服务依赖其余服务的时候才必须须要使用@Injectable
显式装饰,来表示这个服务须要依赖,因此咱们的PowerService
并非必须加上@Injectable
装饰器的,但是,Angular
官方推荐是否依赖其余服务,都应该使用@Injectable
来装饰服务。
3. 在模块中注入服务
在模块中注册服务和在组件中注册服务的方法是同样的,只是在模块中注入的服务在整个组件中都是可用的。
// app.module.ts import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent, ], imports: [ BrowserModule ], providers: [CountService, PowerService], bootstrap: [AppComponent] }) export class AppModule { }
与在组件中注入不一样的是,在Angular
应用启动的时候,它好首先加载这个模块须要的全部依赖,,此时会生成一个全局的根注入器,由该依赖建立的依赖注入对象会再整个应用中可见,并共享一个实例。Angular
没有模块级做用域这个概念,只有应用程序级做用域和组件级做用域,这种设计主要是考虑模块的扩展性,一个应用一般由多个模块合并和成,在@NgModule
中注册的服务,默认在整个应用中可用。
下面说两种特殊状况:
假设在两个模块中使用一样的Token
注入了同一个服务,而且这两个模块前后导入到了根组件中:
// ... @NgModule({ imports: [ AModule, BModule ] // ... })
那么后面导入的模块中的服务会覆盖前面导入模块中的服务,也就是说BModule
中的服务会覆盖AModule
中的服务,即便是在AModule
中注入的服务,一样使用的是BMoudle
中提供的实例。
仍是假设两个模块一样使用同一个Token
注入了同一个服务,可是BModule
模块是导入在AModule
模块中的:
// a.module.ts // ... @NgModule({ imports: [BModule] })
那么这种状况下两个模块使用的都是AModule
中注入的服务。能够推断出在根模块中注入的服务是拥有最高优先级的,你能够在任何地方放心使用。
1. Provider的理解Provider
是有必要单独提出来一节的,上面第二节中咱们其实只是简单的使用了其中一种的provider
下面来详细说一下Provider
在Angular
中,Provider
描述了注入器(Injector)如何初始化令牌(Token)所对应的依赖服务。Provider
一个运行时的依赖,注入器依靠它来建立服务对象的实例。
好比咱们上面用到的例子:
// ... @Component({ //... // 2. 在组件中配置注入器 providers: [ MyService ] //... })
实际上它的完整形式应该是这样的:
@Component({ //... // 2. 在组件中配置注入器 providers: [ {provide: MyService, useClass: MyService} ] //... })
因此说咱们上面只使用了一种provider
: 类Provider(ClassProvider
)。
2. Provider注册方式
上面提到我只使用了其中一种注册方式,那么下面介绍Angular
中提供的四中常见的注册方式:
类Provider(ClassProvider
)
值Provider(ValueProvider
)
别名Provider(ExistingProvider
)
工厂Provider(FactoryProvider
)
1. 类Provider
类`Provider` 基于令牌(`Token`)指定依赖项,这种方式但是让依赖被动态指定为其余不一样的具体实现,只要接口不变,对于使用方就是透明的。好比数据渲染服务(`Render`),`Render`服务对上层提供的接口是固定的,却是底层的渲染方式能够不一样:
var inject = Injector.resolveAndCreate([ {provide: Render, useClass: DomRender} //{provide: Render, useClass: DomRender} // canvas 渲染方式 //{provide: Render, useClass: DomRender} // 服务的想染方式 ]) // 调用方不用作任何修改 class AppComponent { construtor(private render: Render) {} }
2. 值Provider
因为依赖的对象并不必定都是类,也能够是字符串、常量、对象等其余数据类型的,这能够方便用在全局变量、系统相关参数配置场景中。在建立`Provider`对象的时候,只须要使用`useValue`就能够声明一个值`Provider`:
let freeMan = { freeJob: boolen; live: () => {return 'do something u cant do'} }; @Component({ // ... providers: [ {provide: 'someone', useValue: freeMan} ] })
3. 别名Provider
有了别名`Provider`,咱们就能够在一个`Provider`中配置多个令牌(`Token`),其对于的对象指向同一个实例,从而实现了多个依赖、一个对象实例的做用:
// ... providers: [ {provider: Power1, useClass: PowerService}, {provider: Power2, useClass: PowerService} ] // ...
仔细想一想,这样对吗?
显然是不对的,若是两个都使用了useClass
那么按照令牌,将会建立两个不一样的实例出来,那么应该怎么实现两个令牌同一个实例呢?答案是使用useExistiong
:
// ... providers: [ {provider: Power1, useClass: PowerService}, {provider: Power2, useExisting: PowerService} ] // ...
4. 工厂Provider
工厂`Provider`容许咱们根据不一样的条件来实例化不一样的服务,好比,咱们在开发环境须要打印日志,可是在实际部署的时候可能并不须要打印这些东西,那么咱们总不可能去找到整个应用中全部的`console.log()`这样的方法吧,这个时候咱们可使用工厂`provider`来帮咱们处理,咱们只须要在工厂`provider`中设定一个条件,使其可以根据条件返回实例化咱们须要的服务就能够了。为了实现这样的功能咱们能够在根模块中这样注入:
// app.module.ts @NgModule({ // ... providers: [ HeroService, ConsoleService, { provide: LoggerService, useFactory: (consoleService) => { return new LoggerService(true, consoleService); }, deps: [ConsoleService] } ], bootstrap: [AppComponent] }) export class AppModule { }
哦哦,那两个服务是这样写的:
// console.service.ts // ... export class ConsoleService { log(message) { console.log(`ConsoleService: ${message}`); } } // logger.service.ts // ... export class LoggerService { constructor(private enable: boolean, consoleService: ConsoleService ) { } log(message: string) { if (this.enable) { console.log(`LoggerService: ${message}`); } } }
而后在组件构造函数中写上须要的服务就好。
想象一场景,你应用中的某个服务的provider
被当作无效代码删掉了,那么你的应用可能就会出问题。还好这个问题早在设计的时候就已经考虑到了,咱们可使用Angular
提供的@Optional
和@Host
装饰器来解决这个问题。Optional
能够兼容依赖不存在的状况,提升系统的健壮性;@Host
能够限定查找规则,明确实例化的位置,避免一些莫名的共享对象问题。
@Optional
借助@Optional
就能够实现可选注入:
// app.component.ts // ... import { Optional } from '@angular/core'; constructor(@Optional() private logger: LoggerService) { if (this.logger) { this.logger.log('i am choosed'); } }
像例子中的那样只须要在宿主组件(Host Component)的构造函数中增长@Optional
装饰器便可。
须要注意的是,上面例子中的LoggerService
并非不存在,只是并无根据providers
元数据中配置被实例化出来。
@HostAngular
中依赖查找的规则是按照注入器从当前组件向父组件查找,直到找到要注入的依赖位置,若是找不到就会报错。咱们可使用Angular
提供的@Host
装饰器来解决 这个问题。
宿主组件若是一个组件注入了依赖项,那么这个组件就是这个依赖的宿主组件;若是这个组件经过<ng-content>
被嵌入到了父组件,那这个父组件就是这个依赖的宿主组件。
宿主组件是当前组件
咱们给组件构造函数加上@Host
装饰器:
// ... @Component({ selector: 'parent', template: ` <h1>这里是父组件</h1> ` }) constructor( @Host() logger: LoggerService) {} // 加上@Host以后会报错,由于咱们并无在这个组件中注入LoggerService // 可是咱们能够加上@Optional来避免报错 //@Host() //@Optional() //logger: LoggerService) {} )
宿主组件是父组件
咱们修改一下上面的组件为父组件:
// parent.component.ts // ... @Component({ selector: 'parent', template: ` <h1>这里是父组件</h1> <ng-content></content> ` // 在父组件中注入 LoggerService providers: [LoggerService] }) constructor() {}
增长一个子组件:
// child.component.ts // ... @Component({ selector: 'child', template: ` <h1>这里是子组件</h1> ` }) constructor( @Host() @Optional() logger: LoggerService) ){}
固然<parent>
标签中应该这样写:
<parent> <child></child> </parent>
由于此时宿主组件是父组件,因此咱们在父组件中注入`LoggerService` `Angular`注入器会自动向上查找,找到`ParentComponet`中的配置,从而完成注入。