欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Angular4 - 依赖注入

程序员文章站 2022-06-01 13:02:38
...

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. 依赖注入

我们先说上面第一版的情况
Angular4 - 依赖注入
如上图所示: Car在被由angular程序(使用Car对象的模块组件创建,然后调用者需要follow Car对象的构造函数)。这是使用者自身控制对象的产生。

然后我们看第三版,就是用注入器的方式,现在使用者不在直接去创建对象了,而是通过注入器来产生新的对象,第三版就是在通知angular IOC容器我需要哪些对象,你去帮我创建吧。

Angular4 - 依赖注入


第四版其实已经将通知的内容都省略,IOC容器自己会去根据对象来产生相应的对象。前提是我们angular程序提供了这些可被注入的对象。

Angular4 - 依赖注入

控制反转是相对于应用程序(也就是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 {
  }
}

3. @Injectable() 是必须的么

如果所创建的服务不依赖于其他对象,是可以不用使用 Injectable 类装饰器。但当该服务需要在构造函数中注入依赖对象,就需要使用 Injectable 装饰器。不过比较推荐的做法不管是否有依赖对象,在创建服务时都使用 Injectable 类装饰器。