我的新書AI 職場超神助手:ChatGPT 與生成式 AI 一鍵搞定工作難題的教材投影片已製作完成
歡迎各位有需要的教師和博碩文化索取教材

[功能介紹-12] Angular裡Service的DI

何謂依賴注入

什麼是依賴注入呢?可以參考這篇文章:理解 Dependency Injection 實作原理

Service的DI

以下為一個service的範例

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

@Injectable()
export class HeroService {
  constructor() { }
}

@Injectable()是angular的service使用做依賴注入的裝飾詞,可以使Service成為可被注入的元件。
當我們在provider設定這個服務給這個module使用,如下:

providers: [
  UserService
],

這個service會變成是Singleton的,我們可以在許多的地方去從constructor直接宣告這個服務,在所有地方所取得的都會是同一個實體。

export class HeroListComponent {
  heroes: Hero[];

  constructor(heroService: HeroService) {
    this.heroes = heroService.getHeroes();
  }
}

component的provider

在ngModule裡provider設定的服務可以在該模組下任一元件裡去從建構子取得同一個物件的實體而不需要再在元件中額外去在provider宣告。
如果有一個服務只需要在某個元件下被使用,則可以直接在@Component.providers裡面註冊服務。
組件注入器是相互獨立的,每個組件都創建它自己的組件提供服務的實例。
當Angular銷毀其中一個組件實體時,它也會銷毀組件的注入器和注入器的服務實體

讓建立的類別與providers宣告的不同

providers: [Logger]

當angular在注入這個服務時,會用下面這行來取得實體

constructor(private logger: Logger) {  }

在這個例子中,Logger就是angular在做這個服務依賴注入的token,也可以寫成下面這樣。

providers: [{ provide: Logger, useClass: Logger }]

上面是token與實際物件實體類型相同的狀況。

但是有的時候,或許某一個元件寫好需注入的服務是Logger。
但因為在現有的模組中我們建立了一個繼承Logger並覆寫其部份功能的類別BetterLogger。
這時候可以用這個方式讓angular裡的token與實際產生的物件實體是不同的

providers: [{ provide: Logger, useClass: BetterLogger }]

Aliased class providers

以上面的例子,如果我們在根模組設定Logger為token,實際創建的物件為BetterLogger,會造成其他的子元件也都無法直接在constructor取得BetterLogger這類型的物件。
因此在這樣的狀況下,可以為這個服務取別名,下面這個是錯誤示範,因為事實上這樣的寫法會創建兩個不同的BetterLogger實體:

[ NewLogger,
  { provide: OldLogger, useClass: NewLogger}]

真正正確的寫法如下:

[ NewLogger,
  // 使用OldLogger會去取得NewLogger這個類別的實體
  { provide: OldLogger, useExisting: NewLogger}]

在provide提供現成的物件實體

有的時候我們會希望能夠直接使用一個已建立好的物件實體,可以用下面的方式:

// An object in the shape of the logger service
export function SilentLoggerFn() {}

const silentLogger = {
  logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
  log: SilentLoggerFn
};

下面的宣告方式,可以讓我們在元件下的建構子可以直接使用Logger來取得上面所建的物件實體

[{ provide: Logger, useValue: silentLogger }]

在provide服務時使用工廠模式

如果我們希望針對客戶的層級不同提供不同的服務,可以使用Factory providers
下面是src/app/heroes/hero.service.ts這個檔案的截錄內容

constructor(
  private logger: Logger,
  private isAuthorized: boolean) { }

getHeroes() {
  let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
  this.logger.log(`Getting heroes for ${auth} user.`);
  return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}

hero.service.provider.ts裡定義了factory的物件

let heroServiceFactory = (logger: Logger, userService: UserService) => {
  return new HeroService(logger, userService.user.isAuthorized);
};

然後在providers的宣告是這樣的

export let heroServiceProvider =
  { provide: HeroService,
    useFactory: heroServiceFactory,
    deps: [Logger, UserService]
  };

useFactory可讓angular知道provider是一個工廠函數,其實現是heroServiceFactory。
而deps屬性是這個工廠需要用到的服務。angular會將服務中的Logger和UserService注入符合的Factory function參數。

在注入一串文字做為服務

如果有時我們要注入的對象不是物件時,例如要注入下面這個常數

export const HERO_DI_CONFIG: AppConfig = {
  apiEndpoint: 'api.heroes.com',
  title: 'Dependency Injection'
};

我們不能使用下面這個方式來注入

[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]

這是因為javascript是一個弱型別的語言,即便typescript是強型別的,但最終會被轉回弱型別的javascript。
因此用這種方式angular會無法找到這個型別。

正確的使用方式如下:

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

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

然後用剛剛建的InjectionToken來註冊這個程序

providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]

使用這串字串的方式如下:

constructor(@Inject(APP_CONFIG) config: AppConfig) {
  this.title = config.title;
}

不用一定要被傳入的服務

有些service可以不一定要被建立,這時可以用這個方式宣告

import { Optional } from '@angular/core';
constructor(@Optional() private logger: Logger) {
  if (this.logger) {
    this.logger.log(some_message);
  }
}

如果使用@Optional(),並且找不到相應的服務時,就不會跳錯誤,而logger的值會是null。

分層注入系統

angular的服務是這樣的,每一次在providers裡註冊一次這個類型,就會產生一次物件實體。
我們可以利用這個特性來讓服務裡的資訊彼此間隔離不受干擾

例如下圖的HeroesListComponent下面有三個HeroTaxReturnComponent實體,裡面各字會有一個這個Hero的稅單資料(HeroTaxReturnService)

但若我們不希望彼此間的服務會受到彼此的干擾,則可以在HeroTaxReturnComponent再宣告一次HeroTaxReturnService,則可以達到這個目的

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { HeroTaxReturn }        from './hero';
import { HeroTaxReturnService } from './hero-tax-return.service';

@Component({
  selector: 'app-hero-tax-return',
  templateUrl: './hero-tax-return.component.html',
  styleUrls: [ './hero-tax-return.component.css' ],
  providers: [ HeroTaxReturnService ]
})
export class HeroTaxReturnComponent {
  message = '';
  @Output() close = new EventEmitter<void>();

  get taxReturn(): HeroTaxReturn {
    return this.heroTaxReturnService.taxReturn;
  }
  @Input()
  set taxReturn (htr: HeroTaxReturn) {
    this.heroTaxReturnService.taxReturn = htr;
  }

  constructor(private heroTaxReturnService: HeroTaxReturnService ) { }

  onCanceled()  {
    this.flashMessage('Canceled');
    this.heroTaxReturnService.restoreTaxReturn();
  };

  onClose()  { this.close.emit(); };

  onSaved() {
    this.flashMessage('Saved');
    this.heroTaxReturnService.saveTaxReturn();
  }

  flashMessage(msg: string) {
    this.message = msg;
    setTimeout(() => this.message = '', 500);
  }
}

17年資歷女工程師,專精於動畫、影像辨識以及即時串流程式開發。經常組織活動,邀請優秀的女性分享她們的技術專長,並在眾多場合分享自己的技術知識,也活躍於非營利組織,辦理活動來支持特殊兒及其家庭。期待用技術改變世界。

如果你認同我或想支持我的努力,歡迎請我喝一杯咖啡!讓我更有動力分享知識!