Angular4學習之依賴注入

DJL簫氏發表於2017-12-26

在一個專案中,元件和服務之間存在錯綜複雜的關係,為了最小程度的耦合,我們需要來管理組織這種關係,依賴注入就是管理這種關係的一種方式。

為什麼要使用依賴注入

在學習一個概念之前,我們必須要知道我們為什麼要學習這個東西,這個東西究竟解決了什麼問題。就好比這裡講到的,依賴注入究竟解決了什麼問題。要解決這個問題,我們先來看看示例程式碼:

export class Car {
  public engine: Engine;
  public tires: Tires;
  public description = 'No DI';

  constructor() {
    this.engine = new Engine();
    this.tires = new Tires();
  }

  // Method using the engine and tires
  drive() {
    return `${this.description} car with ` +
      `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
  }
}
複製程式碼

以上是來自angular官網的一段程式碼,我們可以看到一個Car類依賴於EngineTires這兩個類,我們在Car的建構函式中去例項這兩個依賴類。這有什麼問題?如果有一天我們的Tires建構函式需要一個引數,那麼我們必須要在Car的建構函式中去更改程式碼。

// ...
constructor() {
   this.engine = new Engine();
   this.tires = new Tires(params);
 }
]
// ...
複製程式碼

這種程式碼是非常不靈活的。雖然我們可以進行如下結構調整

export class Car {
  public engine: Engine;
  public tires: Tires;
  public description = 'No DI';

  constructor(engine, tires) {
    this.engine = engine;
    this.tires = tires;
  }

  // Method using the engine and tires
  drive() {
    return `${this.description} car with ` +
      `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
  }
}

const car = new Car(new Engine(), new Tires())
複製程式碼

這樣似乎解決了不靈活的問題,但是如果依賴項很多的話,我們都要去手動建立這些例項,也不太方便。其實建立依賴例項的過程完全可以交給一個專門的'工廠'來做,這就是angular裡面的Injector。

基本使用

  • 在元件中使用
@Component({
  selector: 'app-heroes',
  providers: [Engine, Tires],
  template: `
    <h2>Heroes</h2>
    <app-hero-list></app-hero-list>
  `
})
export class HeroesComponent {
  construtor(private engine: Engine) {
    this.engine.start();
  }
}
複製程式碼

在Angular中,一般我們將這些公共的依賴都會一些一個服務裡面。在上面的用法我們可以看到多了一個providers,另外就是在類的建構函式中增加了private engine: Engine我們就可以去使用engine這個例項了,在這個過程中,我們並沒有去手動去建立依賴項的例項。這是因為angular的Injector幫我們自動建立了。在這裡有一個比較形象的比喻就是,一個廚子(Injector)根據菜譜(providers)去做菜(依賴的例項),但是究竟做哪些菜呢,客人說了算(private engine: Engine也就是建構函式中的)

  • 在服務中使用
import { Injectable } from '@angular/core';

@Injectable()
export class HeroService {
  constructor(private engine: Engine) { }
}
複製程式碼

如果我們的一個服務本身就依賴於其他依賴項,那麼我們使用@Injectable()裝飾器(即使一個服務並沒有依賴於其他服務,我們也推薦加上@Injectable()裝飾器),我們依然要提供providers。這裡由於服務通常跟檢視是沒有具體的關係,所以這裡我們不會引入@component裝飾器,那麼我們在哪裡確定這個providers呢?我們可以在一個module中的providers屬性中去定義,那麼這個module中的所有元件都會去共用這一個例項,但是我們有時候我們不希望共用一個例項,而是一個新的例項,那麼我們可以在這個元件中的providers中重新定義,這樣我們就會得到一個新的例項。實際上這就是層級注入。利用層級注入我們既可以共用例項,也可以不共用例項非常方便。一般全域性使用的服務,我們會註冊在app.module模組之下,這樣在整個應用中都可以使用。

在上面我們說過通過依賴注入建立的例項是可以實現共享的,我們證明一下。

import { Component, OnInit, ReflectiveInjector } from '@angular/core';
import {DependenceComponent} from './dependence.component';

@Component({
  selector: 'app-service',
  templateUrl: './service.component.html',
  styleUrls: ['./service.component.scss'],
})


@Injectable()
export class ServiceComponent implements OnInit {
  
  constructor() {
    let injector = ReflectiveInjector.resolveAndCreate([Dependence]);
    let dependence1 = injector.get(Dependence);
    let dependence2 = injector.get(Dependence);
    console.log('dependence1 === dependence2', dependence1 === dependence2); // true
  }
  
  ngOnInit() {}
}
複製程式碼

在這裡我們可以看見列印出來的是true,這裡我們採用的是手動建立例項,所以我們並不需要在providers中提供“菜譜”,實際上resolveAndCreate的引數就是一個providers

Providers

我們有四種配置注入過程,即使用類、使用工廠、使用值、使用別名

  • 使用類
{provide: MyService, useClass: MyService}
複製程式碼

這是我們最常見的情形在angular中,通常如果provide的值和useclass的值一樣,我們可以簡化為[MyService]

  • 使用值 顯然並不是每種情況,我們都需要注入一個類,有時候可以僅僅是一個值
{provide: MyValue, useValue: 12345}
複製程式碼
  • 使用別名
{provide: OldService, useClass: NewService}
複製程式碼

如果我們有兩個服務OldServiceNewService介面都一致,出於某種原因,我們不得不使用OldService作為Token,但是我們又想使用NewService中的介面,那麼我們就可以使用別名。

  • 使用存在的值
[ NewLogger,
  // Not aliased! Creates two instances of `NewLogger`
  { provide: OldLogger, useClass: NewLogger}]
複製程式碼

這種情況下會建立兩個NewLogger的例項,這顯然不是我們想要的結果,這時我們就可以使用存在的

[ NewLogger,
  // Alias OldLogger w/ reference to NewLogger
  { provide: OldLogger, useExisting: NewLogger}]
複製程式碼
  • 使用工廠 如果我們的服務需要根據不同的輸入值,做出不同的響應,那麼就必須要接受一個引數,那麼我們就必須使用工廠
{provide: MyService, useFactory: (user: User) => {
    user.isAdmin ? new adminService : customService,
    deps: [User]
}}
複製程式碼

當使用工廠時,我們可以通過變數的不同值,去例項不同的類。也就是說我們需要根據不同的值返回不同的依賴例項的時候,那麼我們就需要使用工廠。

@Options 、@Host

目前為止我們的依賴都是存在的,但是實際情況並不是總是這樣。那麼我們可以通過@Optional裝飾器來解決這個問題。

import { Optional } from '@angular/core';
// ....
constructor(
    @Optional() private dependenceService: DependenceService
) {}
複製程式碼

但是這裡DependenceService這個服務類的定義還是存在的,只是沒有準備好,例如沒有在providers中使用

依賴查詢的規則是按照注入器從當前元件向父級元件查詢,直到找到這個依賴為止,但是如果限定查詢路徑截止在宿主元件,那麼如果宿主元件中沒有就會報錯,我們可以通過@Host修飾器達到這一功能。

如果一個元件注入了依賴項,那麼這個元件就是這個依賴項的宿主元件,但是如果這個元件通過ng-content被嵌入到宿主元件,那麼這個宿主元件就是該依賴項的宿主元件。

Token

當我們在建構函式中使用private dependenceService: DependenceService,injector就可以正確的知道我們要例項哪一個類,這是因為在這裡DependenceService充當了Token的角色(也就是說類名是可以充當Token的),我們只需要在providers中去尋找具有相同Token的值就行,但是往往我們注入不是一個類,而是一個字串,function或者物件。而這裡string、方法名和物件是不能夠充當Token的,那麼這時我們就需要來手動建立一個Token:

import { InjectionToken } from '@angular/core';

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]
複製程式碼
constructor(@Inject(APP_CONFIG) config: AppConfig) {
  this.title = config.title;
}
複製程式碼

Inject 裝飾器顯示的宣告所依賴物件的型別

@Injectable()
class A {
    constructor(private buffer: Buffer) {}
}
複製程式碼

等同於

class A {
    constructor(@Inject(Buffer) private buffer: Buffer) {}
}
複製程式碼

更過精彩

相關文章