天天看點

Angular4 - 依賴注入Angular4 - 依賴注入

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 - 依賴注入Angular4 - 依賴注入

如上圖所示: Car在被由angular程式(使用Car對象的子產品元件建立,然後調用者需要follow Car對象的構造函數)。這是使用者自身控制對象的産生。

然後我們看第三版,就是用注入器的方式,現在使用者不在直接去建立對象了,而是通過注入器來産生新的對象,第三版就是在通知angular IOC容器我需要哪些對象,你去幫我建立吧。

Angular4 - 依賴注入Angular4 - 依賴注入

第四版其實已經将通知的内容都省略,IOC容器自己會去根據對象來産生相應的對象。前提是我們angular程式提供了這些可被注入的對象。

Angular4 - 依賴注入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 類裝飾器。