Angular 提供兩種方式,讓服務可供注入:
- 自動提供:使用
@Injectable裝飾器中的providedIn、@Service裝飾器,或在InjectionToken設定中提供工廠 - 手動提供:使用元件、指令、路由或應用程式設定中的
providers陣列
在上一篇指南中,你學到了如何使用 providedIn: 'root' 建立服務,這涵蓋了最常見的使用情境。本指南將探討自動與手動提供者設定的其他模式。
非類別依賴的自動提供
帶有 providedIn: 'root' 的 @Injectable 裝飾器對服務(類別)很好用,但你可能需要全域提供其他類型的值,例如設定物件、函式或原始值。Angular 為此提供了 InjectionToken。
什麼是 InjectionToken?
InjectionToken 是 Angular 依賴注入系統用來唯一識別可注入值的物件。可以把它想成一把特殊的鑰匙,讓你能在 Angular 的 DI 系統中儲存和擷取任何類型的值:
import {InjectionToken} from '@angular/core';
// Create a token for a string value
export const API_URL = new InjectionToken<string>('api.url');
// Create a token for a function
export const LOGGER = new InjectionToken<(msg: string) => void>('logger.function');
// Create a token for a complex type
export interface Config {
apiUrl: string;
timeout: number;
}
export const CONFIG_TOKEN = new InjectionToken<Config>('app.config');
NOTE: 字串參數(例如 'api.url')只是一個純粹用於除錯的描述,Angular 是以物件參考來識別 token,而不是這個字串。
搭配 providedIn: 'root' 的 InjectionToken
帶有 factory 的 InjectionToken,預設即為 providedIn: 'root'(但可以透過 providedIn 屬性覆寫)。
/app/config.token.ts
import {InjectionToken, Component, inject} from '@angular/core';
export interface AppConfig {
apiUrl: string;
version: string;
features: Record<string, boolean>;
}
// Globally available configuration using providedIn
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config', {
providedIn: 'root',
factory: () => ({
apiUrl: 'https://api.example.com',
version: '1.0.0',
features: {
darkMode: true,
analytics: false,
},
}),
});
// No need to add to providers array - available everywhere!
@Component({
selector: 'app-header',
template: `<h1>Version: {{ config.version }}</h1>`,
})
export class Header {
config = inject(APP_CONFIG); // Automatically available
}
何時該使用帶工廠函式的 InjectionToken
當你無法使用類別、但需要全域提供依賴時,帶工廠函式的 InjectionToken 是理想選擇:
/app/logger.token.ts
import {InjectionToken, inject} from '@angular/core';
import {APP_CONFIG} from './config.token';
// Logger function type
export type LoggerFn = (level: 'log' | 'warn' | 'error', message: string) => void;
// Global logger function with dependencies
export const LOGGER_FN = new InjectionToken<LoggerFn>('logger.function', {
providedIn: 'root',
factory: () => {
const config = inject(APP_CONFIG);
return (level: 'log' | 'warn' | 'error', message: string) => {
if (config.features.logging !== false) {
console[level](`[${new Date().toISOString()}] ${message}`);
}
};
},
});
/app/storage.token.ts
// Providing browser APIs as tokens
export const LOCAL_STORAGE = new InjectionToken<Storage>('localStorage', {
// providedIn: 'root' is configured as the default
factory: () => window.localStorage,
});
export const SESSION_STORAGE = new InjectionToken<Storage>('sessionStorage', {
providedIn: 'root',
factory: () => window.sessionStorage,
});
/app/feature-flags.token.ts
// Complex configuration with runtime logic
export const FEATURE_FLAGS = new InjectionToken<Map<string, boolean>>('feature.flags', {
providedIn: 'root',
factory: () => {
const flags = new Map<string, boolean>();
// Parse from environment or URL params
const urlParams = new URLSearchParams(window.location.search);
const enableBeta = urlParams.get('beta') === 'true';
flags.set('betaFeatures', enableBeta);
flags.set('darkMode', true);
flags.set('newDashboard', false);
return flags;
},
});
這種做法有幾個優點:
- 不需要手動設定提供者:運作方式就像服務的
providedIn: 'root'一樣 - 可搖樹:只有在實際被使用時才會包含進來
- 型別安全:對非類別值提供完整的 TypeScript 支援
- 可以注入其他依賴:工廠函式可以使用
inject()來存取其他服務
了解手動提供者設定
當你需要比 providedIn: 'root' 更強的控制時,可以手動設定提供者。在以下情況,透過 providers 陣列手動設定很有用:
- 服務沒有
providedIn:沒有自動提供的服務必須手動提供 - 你想要新的實例:在元件/指令層級建立獨立的實例,而不是用共用的那個
- 你需要執行階段設定:當服務行為取決於執行階段的值時
- 你提供的是非類別值:設定物件、函式或原始值
範例:沒有 providedIn 的服務
import {Injectable, Component, inject} from '@angular/core';
// Service without providedIn
@Injectable()
export class LocalDataStore {
private data: string[] = [];
addData(item: string) {
this.data.push(item);
}
}
// Component must provide it
@Component({
selector: 'app-example',
// A provider is required here because the `LocalDataStore` service has no providedIn.
providers: [LocalDataStore],
template: `...`,
})
export class Example {
dataStore = inject(LocalDataStore);
}
範例:建立元件專屬的實例
帶有 providedIn: 'root' 的服務可以在元件層級被覆寫。這會把服務實例與元件的生命週期綁在一起。因此,當元件被銷毀時,所提供的服務也會一併被銷毀。
import {Injectable, Component, inject} from '@angular/core';
@Injectable({providedIn: 'root'})
export class DataStore {
private data: string[] = [];
}
// This component gets its own instance
@Component({
selector: 'app-isolated',
// Creates new instance of `DataStore` rather than using the root-provided instance.
providers: [DataStore],
template: `...`,
})
export class Isolated {
dataStore = inject(DataStore); // Component-specific instance
}
Angular 中的注入器階層
Angular 的依賴注入系統是階層式的。當元件請求某個依賴時,Angular 會從該元件的注入器開始,沿著樹往上尋找,直到找到該依賴的提供者為止。應用程式樹中的每個元件都可以有自己的注入器,這些注入器形成一個與元件樹相對應的階層。
這個階層帶來以下能力:
- 限定範圍的實例:應用程式的不同部分可以擁有同一個服務的不同實例
- 覆寫行為:子元件可以覆寫父元件的提供者
- 記憶體效率:服務只在需要的地方被實例化
在 Angular 中,任何帶有元件或指令的元素,都可以把值提供給它的所有後代。
在上面的範例中:
SocialApp可以把值提供給UserProfile和FriendListFriendList可以把值提供給FriendEntry注入,但無法提供給UserProfile注入,因為它不在同一棵樹中
宣告提供者
可以把 Angular 的依賴注入系統想成雜湊表或字典。每個提供者設定物件都定義了一組鍵值對:
- 鍵(提供者識別項):你用來請求依賴的唯一識別項
- 值:當該 token 被請求時,Angular 應該傳回的內容
手動提供依賴時,你通常會看到這種簡寫語法:
import {Component} from '@angular/core';
import {LocalService} from './local-service';
@Component({
selector: 'app-example',
providers: [LocalService], // Service without providedIn
})
export class Example {}
這其實是一種更詳細的提供者設定的簡寫:
{
// This is the shorthand version
providers: [LocalService],
// This is the full version
providers: [
{ provide: LocalService, useClass: LocalService }
]
}
提供者設定物件
每個提供者設定物件都有兩個主要部分:
- 提供者識別項:Angular 用來取得依賴的唯一鍵(透過
provide屬性設定) - 值:你想讓 Angular 取得的實際依賴,依所需的類型用不同的鍵來設定:
useClass:提供一個 JavaScript 類別useValue:提供一個靜態值useFactory:提供一個傳回值的工廠函式useExisting:提供一個既有提供者的別名
提供者識別項
提供者識別項讓 Angular 的依賴注入(DI)系統能透過唯一 ID 擷取依賴。你可以用兩種方式產生提供者識別項:
類別名稱
類別名稱直接使用匯入的類別作為識別項:
import {Component} from '@angular/core';
import {LocalService} from './local-service';
@Component({
selector: 'app-example',
providers: [{provide: LocalService, useClass: LocalService}],
})
export class Example {
/* ... */
}
類別同時充當識別項和實作,這就是 Angular 提供 providers: [LocalService] 這種簡寫的原因。
Injection token
Angular 提供內建的 InjectionToken 類別,用來為可注入的值建立唯一的物件參考,或在你想為同一個介面提供多個實作時使用。
/app/tokens.ts
import {InjectionToken} from '@angular/core';
import {DataService} from './data-service.interface';
export const DATA_SERVICE_TOKEN = new InjectionToken<DataService>('DataService');
NOTE: 字串 'DataService' 只是一個純粹用於除錯的描述。Angular 是以物件參考來識別 token,而不是這個字串。
在你的提供者設定中使用這個 token:
import {Component, inject} from '@angular/core';
import {LocalDataService} from './local-data-service';
import {DATA_SERVICE_TOKEN} from './tokens';
@Component({
selector: 'app-example',
providers: [{provide: DATA_SERVICE_TOKEN, useClass: LocalDataService}],
})
export class Example {
private dataService = inject(DATA_SERVICE_TOKEN);
}
TypeScript 介面可以作為注入的識別項嗎?
TypeScript 介面無法用於注入,因為它們在執行階段並不存在:
// ❌ This won't work!
interface DataService {
getData(): string[];
}
// Interfaces disappear after TypeScript compilation
@Component({
providers: [
{provide: DataService, useClass: LocalDataService}, // Error!
],
})
export class Example {
private dataService = inject(DataService); // Error!
}
// ✅ Use InjectionToken instead
export const DATA_SERVICE_TOKEN = new InjectionToken<DataService>('DataService');
@Component({
providers: [{provide: DATA_SERVICE_TOKEN, useClass: LocalDataService}],
})
export class Example {
private dataService = inject(DATA_SERVICE_TOKEN); // Works!
}
InjectionToken 提供了一個 Angular DI 系統可以在執行階段使用的值,同時透過 TypeScript 的泛型型別參數維持型別安全。
提供者值的類型
useClass
useClass 把一個 JavaScript 類別提供為依賴。使用簡寫語法時,這是預設行為:
// Shorthand
providers: [DataService];
// Full syntax
providers: [{provide: DataService, useClass: DataService}];
// Different implementation
providers: [{provide: DataService, useClass: MockDataService}];
// Conditional implementation
providers: [
{
provide: StorageService,
useClass: environment.production ? CloudStorageService : LocalStorageService,
},
];
實務範例:替換 Logger
你可以替換實作來擴充功能:
import {Injectable, Component, inject} from '@angular/core';
// Base logger
@Injectable()
export class Logger {
log(message: string) {
console.log(message);
}
}
// Enhanced logger with timestamp
@Injectable()
export class BetterLogger extends Logger {
override log(message: string) {
super.log(`[${new Date().toISOString()}] ${message}`);
}
}
// Logger that includes user context
@Injectable()
export class EvenBetterLogger extends Logger {
private userService = inject(UserService);
override log(message: string) {
const name = this.userService.user.name;
super.log(`Message to ${name}: ${message}`);
}
}
// In your component
@Component({
selector: 'app-example',
providers: [
UserService, // EvenBetterLogger needs this
{provide: Logger, useClass: EvenBetterLogger},
],
})
export class Example {
private logger = inject(Logger); // Gets EvenBetterLogger instance
}
useValue
useValue 把任何 JavaScript 資料型別提供為靜態值:
providers: [
{provide: API_URL_TOKEN, useValue: 'https://api.example.com'},
{provide: MAX_RETRIES_TOKEN, useValue: 3},
{provide: FEATURE_FLAGS_TOKEN, useValue: {darkMode: true, beta: false}},
];
IMPORTANT: TypeScript 型別和介面不能作為依賴值。它們只存在於編譯時期。
實務範例:應用程式設定
useValue 的一個常見使用情境是提供應用程式設定:
// Define configuration interface
export interface AppConfig {
apiUrl: string;
appTitle: string;
features: {
darkMode: boolean;
analytics: boolean;
};
}
// Create injection token
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
// Define configuration
const appConfig: AppConfig = {
apiUrl: 'https://api.example.com',
appTitle: 'My Application',
features: {
darkMode: true,
analytics: false,
},
};
// Provide in bootstrap
bootstrapApplication(AppComponent, {
providers: [{provide: APP_CONFIG, useValue: appConfig}],
});
// Use in component
@Component({
selector: 'app-header',
template: `<h1>{{ title }}</h1>`,
})
export class Header {
private config = inject(APP_CONFIG);
title = this.config.appTitle;
}
useFactory
useFactory 提供一個函式,用來產生供注入的新值:
export const loggerFactory = (config: AppConfig) => {
return new LoggerService(config.logLevel, config.endpoint);
};
providers: [
{
provide: LoggerService,
useFactory: loggerFactory,
deps: [APP_CONFIG], // Dependencies for the factory function
},
];
你可以把工廠的依賴標記為可選:
import {Optional} from '@angular/core';
providers: [
{
provide: MyService,
useFactory: (required: RequiredService, optional?: OptionalService) => {
return new MyService(required, optional || new DefaultService());
},
deps: [RequiredService, [new Optional(), OptionalService]],
},
];
實務範例:以設定為基礎的 API 用戶端
以下是一個完整範例,示範如何使用工廠來建立帶有執行階段設定的服務:
// Service that needs runtime configuration
class ApiClient {
constructor(
private http: HttpClient,
private baseUrl: string,
private rateLimitMs: number,
) {}
async fetchData(endpoint: string) {
// Apply rate limiting based on user tier
await this.applyRateLimit();
return this.http.get(`${this.baseUrl}/${endpoint}`);
}
private async applyRateLimit() {
// Simplified example - real implementation would track request timing
return new Promise((resolve) => setTimeout(resolve, this.rateLimitMs));
}
}
// Factory function that configures based on user tier
import {inject, Component} from '@angular/core';
import {HttpClient} from '@angular/common/http';
const apiClientFactory = () => {
const http = inject(HttpClient);
const userService = inject(UserService);
// Assuming userService provides these values
const baseUrl = userService.getApiBaseUrl();
const rateLimitMs = userService.getRateLimit();
return new ApiClient(http, baseUrl, rateLimitMs);
};
// Provider configuration
export const apiClientProvider = {
provide: ApiClient,
useFactory: apiClientFactory,
};
// Usage in component
@Component({
selector: 'app-dashboard',
providers: [apiClientProvider],
})
export class Dashboard {
private apiClient = inject(ApiClient);
}
useExisting
useExisting 為已經定義的提供者建立別名。兩個 token 都會傳回同一個實例:
providers: [
NewLogger, // The actual service
{provide: OldLogger, useExisting: NewLogger}, // The alias
];
IMPORTANT: 不要把 useExisting 和 useClass 混淆。useClass 會建立各自獨立的實例,而 useExisting 確保你取得的是同一個單例實例。
多重提供者
當多個提供者要為同一個 token 貢獻值時,使用 multi: true 旗標:
export const INTERCEPTOR_TOKEN = new InjectionToken<Interceptor[]>('interceptors');
providers: [
{provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true},
{provide: INTERCEPTOR_TOKEN, useClass: LoggingInterceptor, multi: true},
{provide: INTERCEPTOR_TOKEN, useClass: RetryInterceptor, multi: true},
];
當你注入 INTERCEPTOR_TOKEN 時,你會收到一個陣列,其中包含全部三個攔截器的實例。
你可以在哪些地方指定提供者?
Angular 提供多個可以註冊提供者的層級,每個層級對範圍、生命週期和效能都有不同的影響:
- 應用程式啟動:隨處可用的全域單例
- 在元素上(元件或指令):特定元件樹的隔離實例
- 路由:延遲載入模組的功能專屬服務
應用程式啟動
在以下情況,把應用程式層級的提供者放在 bootstrapApplication 中:
- 服務跨多個功能區域使用:例如 HTTP 用戶端、記錄或驗證等應用程式許多部分都需要的服務
- 你想要真正的單例:由整個應用程式共用的一個實例
- 服務沒有元件專屬的設定:在任何地方運作方式都相同的通用工具
- 你提供的是全域設定:API 端點、功能旗標或環境設定
main.ts
bootstrapApplication(App, {
providers: [
{provide: API_BASE_URL, useValue: 'https://api.example.com'},
{provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true},
LoggingService, // Used throughout the app
{provide: ErrorHandler, useClass: GlobalErrorHandler},
],
});
優點:
- 單一實例可減少記憶體用量
- 隨處可用,不需要額外設定
- 更容易管理全域狀態
缺點:
- 永遠會包含在你的 JavaScript bundle 中,即使該值從未被注入
- 無法輕易地依功能客製化
- 較難單獨測試個別元件
為什麼要在啟動時提供,而不是使用 providedIn: 'root'?
在以下情況,你可能會想在啟動時提供提供者:
- 提供者有副作用(例如安裝用戶端路由器)
- 提供者需要設定(例如路由)
- 你使用的是 Angular 的
provideSomething模式(例如provideRouter、provideHttpClient)
元件或指令提供者
在以下情況,使用元件或指令提供者:
- 服務有元件專屬的狀態:表單驗證器、元件專屬的快取或 UI 狀態管理器
- 你需要隔離的實例:每個元件需要自己的服務副本
- 服務只被一棵元件樹使用:不需要全域存取的專門服務
- 你在建立可重複使用的元件:應該能以自己的服務獨立運作的元件
// Specialized form component with its own validation service
@Component({
selector: 'app-advanced-form',
providers: [
FormValidationService, // Each form gets its own validator
{provide: FORM_CONFIG, useValue: {strictMode: true}},
],
})
export class AdvancedForm {}
// Modal component with isolated state management
@Component({
selector: 'app-modal',
providers: [
ModalStateService, // Each modal manages its own state
],
})
export class Modal {}
優點:
- 更好的封裝與隔離
- 更容易單獨測試元件
- 多個實例可以以不同設定共存
缺點:
- 每個元件都會建立新實例(記憶體用量較高)
- 元件之間沒有共用狀態
- 需要在每個需要的地方提供
- 永遠會和元件或指令包含在同一個 JavaScript bundle 中,即使該值從未被注入
NOTE: 如果同一個元素上的多個指令提供了同一個 token,其中一個會勝出,但哪一個會勝出並未定義。
路由提供者
路由層級的提供者適用於:
- 功能專屬的服務:只在特定路由或功能模組中需要的服務
- 延遲載入模組的依賴:應該只隨特定功能一起載入的服務
- 路由專屬的設定:依應用程式區域而不同的設定
routes.ts
export const routes: Routes = [
{
path: 'admin',
providers: [
AdminService, // Only loaded with admin routes
{provide: FEATURE_FLAGS, useValue: {adminMode: true}},
],
loadChildren: () => import('./admin/admin.routes'),
},
{
path: 'shop',
providers: [
ShoppingCartService, // Isolated shopping state
PaymentService,
],
loadChildren: () => import('./shop/shop.routes'),
},
];
在路由層級提供的服務,可供該路由內的所有元件和指令使用,也可供它的守衛和解析器使用。
由於這些服務是在路由的元件之外獨立實例化的,它們無法直接存取路由專屬的資訊。
函式庫作者模式
建立 Angular 函式庫時,你經常需要為使用者提供彈性的設定選項,同時維持乾淨的 API。Angular 自己的函式庫就示範了達成此目標的強大模式。
provide 模式
函式庫作者可以匯出傳回提供者設定的函式,而不是要求使用者手動設定複雜的提供者:
/libs/analytics/src/providers.ts
import {InjectionToken, Provider, inject} from '@angular/core';
// Configuration interface
export interface AnalyticsConfig {
trackingId: string;
enableDebugMode?: boolean;
anonymizeIp?: boolean;
}
// Internal token for configuration
const ANALYTICS_CONFIG = new InjectionToken<AnalyticsConfig>('analytics.config');
// Main service that uses the configuration
export class AnalyticsService {
private config = inject(ANALYTICS_CONFIG);
track(event: string, properties?: any) {
// Implementation using config
}
}
// Provider function for consumers
export function provideAnalytics(config: AnalyticsConfig): Provider[] {
return [{provide: ANALYTICS_CONFIG, useValue: config}, AnalyticsService];
}
main.ts
// Usage in consumer app
bootstrapApplication(App, {
providers: [
provideAnalytics({
trackingId: 'GA-12345',
enableDebugMode: !environment.production,
}),
],
});
帶選項的進階提供者模式
對於更複雜的情境,你可以結合多種設定方式:
/libs/http-client/src/provider.ts
import {Provider, InjectionToken, inject} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
// Feature flags for optional functionality
export enum HttpFeatures {
Interceptors = 'interceptors',
Caching = 'caching',
Retry = 'retry',
}
// Configuration interfaces
export interface HttpConfig {
baseUrl?: string;
timeout?: number;
headers?: Record<string, string>;
}
export interface RetryConfig {
maxAttempts: number;
delayMs: number;
}
// Internal tokens
const HTTP_CONFIG = new InjectionToken<HttpConfig>('http.config');
const RETRY_CONFIG = new InjectionToken<RetryConfig>('retry.config');
const HTTP_FEATURES = new InjectionToken<Set<HttpFeatures>>('http.features');
// Core service
class HttpClientService {
private config = inject(HTTP_CONFIG, {optional: true});
private features = inject(HTTP_FEATURES);
get(url: string) {
// Use config and check features
}
}
// Feature services
class RetryInterceptor {
private config = inject(RETRY_CONFIG);
// Retry logic
}
class CacheInterceptor {
// Caching logic
}
// Main provider function
export function provideHttpClient(config?: HttpConfig, ...features: HttpFeature[]): Provider[] {
const providers: Provider[] = [
{provide: HTTP_CONFIG, useValue: config || {}},
{provide: HTTP_FEATURES, useValue: new Set(features.map((f) => f.kind))},
HttpClientService,
];
// Add feature-specific providers
features.forEach((feature) => {
providers.push(...feature.providers);
});
return providers;
}
// Feature configuration functions
export interface HttpFeature {
kind: HttpFeatures;
providers: Provider[];
}
export function withInterceptors(...interceptors: any[]): HttpFeature {
return {
kind: HttpFeatures.Interceptors,
providers: interceptors.map((interceptor) => ({
provide: INTERCEPTOR_TOKEN,
useClass: interceptor,
multi: true,
})),
};
}
export function withCaching(): HttpFeature {
return {
kind: HttpFeatures.Caching,
providers: [CacheInterceptor],
};
}
export function withRetry(config: RetryConfig): HttpFeature {
return {
kind: HttpFeatures.Retry,
providers: [{provide: RETRY_CONFIG, useValue: config}, RetryInterceptor],
};
}
// Consumer usage with multiple features
bootstrapApplication(App, {
providers: [
provideHttpClient(
{baseUrl: 'https://api.example.com'},
withInterceptors(AuthInterceptor, LoggingInterceptor),
withCaching(),
withRetry({maxAttempts: 3, delayMs: 1000}),
),
],
});
為什麼要用提供者函式,而不是直接設定?
提供者函式為函式庫作者帶來幾個優點:
- 封裝:內部 token 和實作細節保持私密
- 型別安全:TypeScript 確保編譯時期的設定正確
- 彈性:用
with*模式輕鬆組合功能 - 面向未來:內部實作可以變更,而不會破壞使用者的程式
- 一致性:與 Angular 自己的模式一致(例如
provideRouter、provideHttpClient等)
這個模式在 Angular 自己的函式庫中被廣泛使用,被視為需要提供可設定服務的函式庫作者的最佳實務。