模組化建築外牆,象徵 Angular Service 透過依賴注入組裝應用程式功能

← INSIGHTS & PERSPECTIVES | 前端開發

Angular Service 依賴注入教學:providers、InjectionToken 與分層注入

介紹 Angular Service 的依賴注入用法,包含 providers、useClass、useExisting、useValue、useFactory、InjectionToken、Optional 與 component 層級服務。

Angular Service 依賴注入(Dependency Injection,DI)的核心是:先把服務註冊到某一層 injector,再讓 component、service 或 directive 透過 constructor 或 inject() 取得該服務。服務註冊在哪一層,會決定注入時拿到的是全站共用實體、某個 component 專用實體,還是由 factory/token 產生的特殊實體。

Angular Service 的依賴注入是什麼?

Angular Service 依賴注入讓 class 不必自己 new 出相依物件,而是向 Angular injector 要服務實體。這樣可以集中管理建立方式、生命週期與替換規則。

依賴注入的好處,是把「使用服務」和「建立服務」拆開。component 只需要宣告自己需要 HeroService,不用知道 HeroService 要不要 logger、設定檔,或是否要依登入狀態切換行為。

一個最基本的 service 會用 @Injectable() 標記:

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

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

@Injectable() 是 Angular 用來標記可注入 class 的 decorator。新版 Angular 常見寫法會搭配 providedIn: 'root',而 Angular 5 時期的範例則常把 service 放進 providers 陣列。

providers 放在 NgModule 會發生什麼事?

Service 放在 NgModule 的 providers 時,該 module injector 會建立並保存服務實體。多個元件注入同一個 token 時,通常會拿到同一個共享物件。

在 NgModule 註冊 service:

providers: [
  UserService
],

接著就能在 component constructor 裡直接宣告需要的 service:

export class HeroListComponent {
  heroes: Hero[];

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

這種寫法很適合放共用狀態、API client、跨頁功能服務。要注意的是,同一個 service 若被放在更靠近 component 的 providers,Angular 會優先使用較近那一層的 provider,而不是外層 module 的共享實體。

providers 放在 Component 會有什麼差異?

Service 放在 @Component.providers 時,每個 component instance 都會有自己的服務實體。component 被銷毀時,該層 injector 與服務實體也會一起被銷毀。

NgModule.providers 註冊的服務,可以在該模組下任一元件透過 constructor 取得,不需要每個元件再宣告一次。可是如果某個服務只屬於單一元件,例如編輯表單的暫存狀態,就可以放進該元件的 providers

@Component({
  selector: 'app-hero-tax-return',
  templateUrl: './hero-tax-return.component.html',
  styleUrls: ['./hero-tax-return.component.css'],
  providers: [HeroTaxReturnService],
})
export class HeroTaxReturnComponent {
  constructor(private heroTaxReturnService: HeroTaxReturnService) {}
}

資訊增益:判斷 provider 層級時,我會先問一個問題:這份 service state 能不能被兄弟元件共用?如果答案是否定的,就放 component;如果答案是肯定的,就放 module、root 或 application config。

useClass 如何讓 token 與實際類別不同?

useClass 可以讓 Angular 用某個 token 注入服務時,實際建立另一個 class。這常用在替換實作、測試替身,或用進階版本取代基本服務。

最簡單的 provider 寫法:

providers: [Logger]

等同於完整 provider 物件:

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

在 constructor 裡,Logger 就是 Angular 做依賴注入時使用的 token:

constructor(private logger: Logger) {}

如果現有 component 需要注入 Logger,但專案裡有一個繼承 Logger 並覆寫部分功能的 BetterLogger,可以保留注入端不變,只替換 provider:

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

這樣 component 仍然寫 constructor(private logger: Logger),實際拿到的會是 BetterLogger 建立出的物件。

useExisting 和 useClass 差在哪裡?

useExisting 是替既有 provider 建立別名,兩個 token 會指向同一個實體。useClass 會建立新的 class 實體,因此不適合拿來做 alias。

以下寫法看起來像是把 OldLogger 指到 NewLogger,實際上會建立兩個不同的 NewLogger 實體:

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

正確的 alias provider 要用 useExisting

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

這種情境常出現在改名或重構服務時:新程式碼注入 NewLogger,舊程式碼暫時還注入 OldLogger,但兩邊必須共用同一份 logger 狀態。

useValue 適合注入什麼?

useValue 適合注入已經建立好的物件、常數設定、測試資料或 feature flags。Angular 不會替 useValue 建立 class,而是直接回傳指定的值。

例如先準備一個安靜版 logger:

export function SilentLoggerFn() {}

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

再用 Logger token 提供這個現成物件:

[{ provide: Logger, useValue: silentLogger }]

這樣 component 還是注入 Logger,但 Angular 回傳的是 silentLogger。單元測試常用這招把真正會打 API、寫 log 或存資料的服務換成固定假資料。

useFactory 什麼時候該用?

useFactory 適合在服務建立時需要依條件決定參數或實作的情境。factory function 可以接收其他已注入服務,再回傳真正要使用的物件。

假設 HeroService 需要根據使用者權限決定是否回傳秘密英雄,可以讓 constructor 接收 Logger 和授權狀態:

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

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

再把建立邏輯包成 factory:

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

provider 宣告如下:

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

useFactory 告訴 Angular 這個 provider 由 factory function 產生;deps 則列出 factory 需要的依賴。Angular 會依 deps 順序把 LoggerUserService 注入到 heroServiceFactory 參數。

InjectionToken 為什麼適合注入設定值?

InjectionToken 適合注入 interface、字串、布林值、設定物件等非 class 依賴。TypeScript 型別在編譯後會消失,因此不能直接拿 interface 當 DI token。

例如要注入一份 application config:

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

不能直接用 AppConfig 這個 interface 當 token:

[{ provide: AppConfig, useValue: HERO_DI_CONFIG }]

原因是 TypeScript interface 只存在於編譯期,轉成 JavaScript 後沒有可供 Angular injector 查找的 runtime token。比較穩定的寫法是建立 InjectionToken

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

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

註冊 provider:

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

注入設定值:

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

@Optional() 可以解決什麼問題?

@Optional() 讓某個依賴找不到 provider 時不拋錯,而是回傳 null。這適合可有可無的 logger、外掛功能或只在特定環境註冊的服務。

先從 Angular core 匯入 Optional

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

在 constructor 裡標記可選依賴:

constructor(@Optional() private logger: Logger) {
  if (this.logger) {
    this.logger.log(someMessage);
  }
}

沒有 @Optional() 時,如果 Angular 找不到 Logger provider,應用程式會在建立 class 時出錯。加上 @Optional() 後,程式可以自行判斷是否有 logger,再決定要不要執行相關行為。

Angular 分層注入系統怎麼隔離服務狀態?

Angular injector 是分層的,同一個 service 在不同層級註冊會產生不同實體。利用 component-level provider,可以讓每個 component instance 擁有互不干擾的狀態。

下面這張圖是來源筆記中的階層例子:HeroesListComponent 底下有三個 HeroTaxReturnComponent,每個子元件都需要自己的 HeroTaxReturnService 保存稅單資料。

HeroesListComponent 與 HeroTaxReturnComponent 的 component 階層

如果 HeroTaxReturnService 是 application-wide singleton,三個 HeroTaxReturnComponent 會共用同一份 tax return state,任何一個元件修改資料都可能覆蓋其他英雄的稅單。把 HeroTaxReturnService 放進 HeroTaxReturnComponent.providers,每個 component instance 就會拿到自己的服務實體:

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);
  }
}

這段範例的重點不是稅單功能,而是 service state 的隔離:資料如果跟某個 component instance 綁在一起,就把 provider 放在該 component;資料如果需要跨頁共享,就放在更外層。

Angular DI provider 寫法怎麼選?

Angular DI provider 可以用「要提供的是 class、既有實體、固定值,還是動態建立邏輯」來判斷。選錯 provider 類型,最常見問題是多出不預期的 service instance。

需求建議寫法重點
注入一般 class serviceproviders: [Logger]等同 { provide: Logger, useClass: Logger }
用新實作取代舊 token{ provide: Logger, useClass: BetterLogger }會建立 BetterLogger 實體
讓舊 token 指向新 token{ provide: OldLogger, useExisting: NewLogger }兩個 token 共用同一個實體
注入固定物件或設定{ provide: APP_CONFIG, useValue: config }適合常數、mock、feature flags
依條件建立服務{ provide: HeroService, useFactory, deps }適合需要其他服務協助建立的物件
注入 interface 或非 class 依賴new InjectionToken<T>()interface 不存在於 JavaScript runtime

我自己在讀 Angular 舊專案時,會先搜尋 providers:,再檢查同一個 token 是否出現在 root、module、component 多個層級。這比單看 constructor 更快找出「為什麼這裡拿到的 service state 跟別處不一樣」。

常見問題

Angular Service 依賴注入常見問題通常不是語法,而是 provider 層級與 token 對應關係。以下整理實作時最容易混淆的幾個點。

QAngular Service 一定要加 @Injectable() 嗎?

Service class 若需要被 Angular 建立或本身還要注入其他依賴,建議加上 @Injectable()。新版 Angular 常搭配 @Injectable({ providedIn: 'root' }),舊版 NgModule 專案則常在 providers 陣列中註冊。

Qproviders: [Logger]{ provide: Logger, useClass: Logger } 有差嗎?

沒有本質差異。providers: [Logger] 是簡寫,Angular 會把它視為 { provide: Logger, useClass: Logger },也就是用 Logger token 建立 Logger class 實體。

Q什麼時候要把 Service 放在 Component providers?

當 service 的狀態只屬於某個 component instance,而且不該被兄弟元件共享時,就適合放在 @Component.providers。表單暫存、局部編輯狀態、每個列表項目自己的操作狀態,都是常見例子。

QuseClassuseExisting 最容易踩到什麼坑?

useClass 會建立指定 class 的新實體,useExisting 會回傳已存在 provider 的同一個實體。若只是要替 token 取別名,卻誤用 useClass,應用程式可能會多出兩份不同狀態的 service。

Q為什麼 interface 不能直接當 Angular DI token?

TypeScript interface 在編譯成 JavaScript 後會消失,Angular runtime 找不到可辨識的 token。要注入設定物件、字串或 interface 型別資料,應建立 InjectionToken<T> 再搭配 useValue 或 factory 使用。

Q@Optional() 找不到服務時會回傳什麼?

@Optional() 找不到對應 provider 時會讓注入值變成 null,而不是直接拋出錯誤。使用可選服務時,constructor 或後續方法都應檢查該值是否存在。

參考資料

延伸閱讀

最後更新

本文最後更新於 2026-08-28。2018-01-07 的 Angular 5 學習筆記保留 service DI、provider token、factory provider 與 component-level service 的範例,並補上 GEO 結構、FAQ、參考資料與新版 Angular 文件脈絡。

關於作者

Claire Chang | 企業 AI 導入與流程轉型顧問。專注於 AI Agent 架構設計、ERP 系統整合與企業 AI 治理。

首次發布:2018-01-07