Angular4 - 依赖注入
1. 例子 (不是很恰当)
第一版
//car.ts
export class Car {
engine: Engine;
doors: Doors;
body: Body;
constructor() {
this.engine = new Engine();
this.body = new Body();
this.doors = new Doors();
}
run() {
this.engine.start();
}
}
/*车身*/
export class Body { }
/*车门*/
export class Doors { }
/*引擎*/
export class Engine {
start() {
console.log('start run');
}
}
上面的这个文件只是一个简单的文件,内部输出四个类文件,汽车,车身,车门,引擎。然后将这个文件中的四个对象当作是一个资源对象在Angular应用中使用,为什么叫做资源对象,因为这个文件的四个对象是为了Angular程序运行过程中独立存在的一种对象,不依赖于模块组件。
然后我们在Angular程序中去引用创建car对象
//angular.component.ts
import {Component, OnInit} from '@angular/core';
import { Car } from '../../common/class/car';
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
})
export class AngularComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
let car1 = new Car(); // 创建Car对象
let car2 = new Car(); // 创建Car对象
console.log(car1 === car2); //false
}
}
第一个问题是什么呢?假如我想在调用的时候自定义一些参数,调用这决定自己的汽车使用那些配件,那么上面的代码就需要改了。
第二版
//car.ts
export class Car {
engine: Engine;
doors: Doors;
body: Body;
constructor(engine, body, doors) {
this.engine = engine;
this.body = body;
this.doors = doors;
}
run() {
this.engine.start();
}
}
/*车身*/
export class Body { }
/*车门*/
export class Doors { }
/*引擎*/
export class Engine {
start() {
console.log('start run');
}
}
//angular.component.ts
import {Component, OnInit} from '@angular/core';
import {Body, Car, Doors, Engine} from '../../common/class/car';
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
})
export class AngularComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
let engine = new Engine();
let body = new Body();
let doors = new Doors();
let car = new Car(engine, body, doors);
car.run();
}
}
现在的问题是什么吗?刚才Car 类发生了变化,Angular程序中所有调用的地方都需要变化。对于Angular程序来说,按照Car的接口创建对象也就认了。可是Car对象发生变化了,Angular程序也要发生变化。对于追求开发效率来说,不能忍啊,有本事就Car你发生了变化,就变化,Angular程序不需要发生变化才牛逼嘛。好像是这个道理,我们现在看到当我们想要自定义传入car构造函数的参数时,我们在调用的地方就更复杂了。好像这二者之间有点不对头,想要自定义,还想调用的地方不变化。先来解决第一个问题: 就是调用的地方不发生变化。
第三版
//car.ts
import {Injectable} from '@angular/core';
/*车身*/
@Injectable()
export class Body { }
/*车门*/
@Injectable()
export class Doors { }
/*引擎*/
@Injectable()
export class Engine {
start() {
console.log('start run');
}
}
@Injectable()
export class Car {
constructor(
private engine: Engine,
private body: Body,
private doors: Doors) {}
run() {
this.engine.start();
}
}
//angular.component.ts
import {Component, OnInit, ReflectiveInjector} from '@angular/core';
import {Body, Car, Doors, Engine} from '../../common/class/car';
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
})
export class AngularComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
let injector = ReflectiveInjector.resolveAndCreate([Engine, Body, Doors, Car]);
let car = injector.get(Car);
car.run();
}
}
通过调用 ReflectiveInjector 抽象类的 resolveAndCreate() 方法,创建注入器。然后通过调用注入器的 get() 方法,获取 Token 对应的对象。注入到什么地方,当然是IOC容器。
继续变化,上面这种方式不是常用的手段,我们现在使用常用的手法。
第四版
//angular.component.ts
import {Component, OnInit, ReflectiveInjector} from '@angular/core';
import {Body, Car, Doors, Engine} from '../../common/class/car';
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
providers: [Body, Car, Doors, Engine]
})
export class AngularComponent implements OnInit {
constructor(private car: Car) {
}
ngOnInit(): void {
this.car.run();
}
}
这个调用方式是最经常见到的方式。走到了这里,现在我们需要将依赖注入说一下了。
2. 依赖注入
我们先说上面第一版的情况如上图所示: Car在被由angular程序(使用Car对象的模块组件创建,然后调用者需要follow Car对象的构造函数)。这是使用者自身控制对象的产生。
然后我们看第三版,就是用注入器的方式,现在使用者不在直接去创建对象了,而是通过注入器来产生新的对象,第三版就是在通知angular IOC容器我需要哪些对象,你去帮我创建吧。
第四版其实已经将通知的内容都省略,IOC容器自己会去根据对象来产生相应的对象。前提是我们angular程序提供了这些可被注入的对象。
控制反转是相对于应用程序(也就是angular.component.ts)来说的,它需要谁就创建谁。现在不一样,创建的事情交给了IOC容器来做。这也就叫做控制反转。
然后再说依赖注入:相对于IOC容器和资源对象来说的(Car),理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”:
谁依赖于谁:当然是应用程序依赖于IoC容器;
为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源;
谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;
注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。
相对于angular来说,Angular IOC容器来控制这些资源对象,维护,创建完对象之后注入到应用程序中去。但是IOC本身不知道我可以控制的对象有哪些,就需要通过providers 来告知。这也是上面第四版中加了providers 元数据的原因。
走到这里,其实之前说的两个问题已经说了一个,但是另外一个还没有解决,就是使用者想要自定义参数创建对象。其实大家反过来想一下,现在使用者都已经不在乎外部资源怎么创建了,还需要纠结这个问题吗?因为我们的这个例子是创建一个对象,就会出现自定义传入的参数,这也是为什么在上面我会表明不太恰当的原因。angular的服务其实更多的是一种应用到多个模块,多个组件的对象,很少回事这种创建一个实体对象。
至于说我们通过IOC容器创建的对象是不是单例的呢?其实也是需要考虑的,关于这方面请看文章Angular4 - 共享模块 。
现在我们知道了providers 的作用,接着看一下providers吧。
3. providers
(1)Provider
import {NgModule, TemplateRef} from '@angular/core';
import { AngularComponent } from './angular.component';
import {RouterModule, Routes} from '@angular/router';
import {CommonModule as CommonPrivateModule} from '../../common/common.module';
import {CommonModule} from '@angular/common';
import { RouteComponent } from './route/route.component';
import {NgZorroAntdModule} from 'ng-zorro-antd';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {HttpServiceService} from '../../common/service/http-service.service';
const routes: Routes = [
{path: 'module', component: AngularComponent},
{path: 'route', component: RouteComponent}
];
@NgModule({
imports: [
CommonModule,
RouterModule.forChild(routes),
CommonPrivateModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
NgZorroAntdModule
],
declarations: [AngularComponent, RouteComponent],
providers: [{provide: HttpServiceService, useClass: HttpServiceService}]
})
export class AngularModule { }
providers: [{provide: HttpServiceService, useClass: HttpServiceService}]
第一个是令牌 (token),它作为键值 (key) 使用,用于定位依赖值和注册提供商。
第二个是一个提供商定义对象。 可以把它看做是指导如何创建依赖值的配方。 有很多方式创建依赖值,也有很多方式可以写配方。
一般我们会直接写providers: [HttpServiceService],这种书写方式和上面的方式一样,token与class同名。
(2). 四种提供方式
a) 类提供商(useClass)
providers: [{provide: HttpServiceService, useClass: HttpServiceService}]
这种方式是token为HttpServiceService, 使用class HttpServiceService进行实例化。
当然这个地方的useClass可以使用别名类提供商,假如现在HttpServiceService现在不能满足新的开发需求,但是这个来在其他组件中还在使用,所以我们可以新开发一个新的类HttpServiceService1,与HttpServiceService实现同样的接口,然后只在这个地方使用,那我们就可以使用如下:
providers: [{provide: HttpServiceService, useClass: HttpServiceService1}]
b) 值提供商(userValue)
有时,提供一个预先做好的对象会比请求注入器从类中创建它更容易。import { Component, Inject, InjectionToken, OnInit} from '@angular/core';
export let APP_CONFIG = new InjectionToken<string>('injectionToken');
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
providers: [
{ provide: APP_CONFIG, useValue: 'Test' }
]
})
export class AngularComponent implements OnInit {
constructor(@Inject(APP_CONFIG) config: string) {
console.log(config);
}
ngOnInit(): void {
}
}
c) 工厂提供商(useFactory)
有时,我们需要动态创建这个依赖值,因为它所需要的信息直到最后一刻才能确定。 也许这个信息会在浏览器的会话中不停地变化。还假设这个可注入的服务没法通过独立的源访问此信息。这种情况下,请调用工厂提供商。FactoryProvider 用于告诉 Injector (注入器),通过调用 useFactory 对应的函数,返回 Token 对应的依赖对象。
FactoryProvider 的使用
function serviceFactory() {
return new Service();
}
const provider: FactoryProvider = {
provide: 'someToken', useFactory: serviceFactory, deps: []
};
FactoryProvider 接口
export interface FactoryProvider {
// 用于设置与依赖对象关联的Token值,Token值可能是Type、InjectionToken、
// OpaqueToken的实例或字符串
provide: any;
// 设置用于创建对象的工厂函数
useFactory: Function;
// 依赖对象列表
deps?: any[];
// 用于标识是否multiple providers,若是multiple类型,则返回与Token关联的依赖对象列表
multi?: boolean;
}
d) 别名-提供商(useExisting)
使用useExisting,提供商可以把一个令牌映射到另一个令牌上。实际上,第一个令牌是第二个令牌所对应的服务的一个别名,创造了访问同一个服务对象的两种方法。 { provide: OldLogger, useExisting: NewLogger}]
(3). Multi Providers
我们以值提供器作为例子:import {Component, Inject, InjectionToken, Injector, OnInit} from '@angular/core';
export let APP_CONFIG = new InjectionToken<string>('injectionToken');
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
providers: [
{ provide: APP_CONFIG, useValue: 'Test' },
{ provide: APP_CONFIG, useValue: 'Test Second'}
]
})
export class AngularComponent implements OnInit {
constructor(@Inject(APP_CONFIG) config: string) {
console.log(config);
}
ngOnInit(): void {
}
}
此时的输出值为‘Test Second’, 因为使用同一个 token 注册 provider,后面注册的 provider 将会覆盖前面已注册的 provider。如果我们加上multi,那么我们得到的就是一个数组。
import {Component, Inject, InjectionToken, Injector, OnInit} from '@angular/core';
export let APP_CONFIG = new InjectionToken<string>('injectionToken');
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
providers: [
{ provide: APP_CONFIG, useValue: 'Test', multi: true },
{ provide: APP_CONFIG, useValue: 'Test Second', multi: true }
]
})
export class AngularComponent implements OnInit {
constructor(@Inject(APP_CONFIG) config: string) {
console.log(config); //["Test", "Test Second"]
}
ngOnInit(): void {
}
}