每條日志消息都有一個LogLevel,它是由枚舉定義的:

export enum LogLevel {
DEBUG = 0,
INFO = 1,
ERROR = 2,
}

為了簡單起見,我們將 Logger 庫限制為只有三個日志級別。

我們會使用一個抽象LoggerConfig來定義可能的配置選項:

export abstract class LoggerConfig {
abstract level: LogLevel;
abstract formatter: Type<LogFormatter>;
abstract appenders: Type<LogAppender>[];
}

我們有意將其定義為抽象類,因為接口無法作為 DI 的令牌(token)。該類的一個常量定義了配置選項的默認(rèn)值:

export const defaultConfig: LoggerConfig = {
level: LogLevel.DEBUG,
formatter: DefaultLogFormatter,
appenders: [DefaultLogAppender],
};

LogFormatter用于在通過LogAppender發(fā)布日志消息之前對其進(jìn)行格式化:

export abstract class LogFormatter {
abstract format(level: LogLevel, category: string, msg: string): string;
}

LoggerConfiguration類似,LogFormatter是一個抽象類,可以用作令牌。Logger 庫的消費者可以通過提供自己的實現(xiàn)來調(diào)整格式,也可以使用庫提供的默認(rèn)實現(xiàn):

@Injectable()
export class DefaultLogFormatter implements LogFormatter {
format(level: LogLevel, category: string, msg: string): string {
const levelString = LogLevel[level].padEnd(5);
return [${levelString}] ${category.toUpperCase()} ${msg}; } }

LogAppender是另一個可替換的概念,它會負(fù)責(zé)將日志消息追加到日志中:

export abstract class LogAppender {
abstract append(level: LogLevel, category: string, msg: string): void;
}

默認(rèn)實現(xiàn)會將日志消息打印至控制臺。

@Injectable()
export class DefaultLogAppender implements LogAppender {
append(level: LogLevel, category: string, msg: string): void {
console.log(category + ' ' + msg);
}
}

盡管我們只能有一個LogFormatter,但是這個庫支持多個LogAppender。例如,第一個LogAppender可以將消息寫到控制臺,而第二個可以將消息發(fā)送至服務(wù)器。

為了實現(xiàn)這一點,各個LogAppender是通過多個提供者(provider)注冊的。所以,Injector 在一個數(shù)組中將它們?nèi)糠祷亍R驗閿?shù)組無法作為 DI 令牌,所以樣例使用了一個InjectionToken來代替:

export const LOG_APPENDERS = new InjectionToken<LogAppender[]>("LOG_APPENDERS");

LoggserService本身會通過 DI 來接收LoggerConfigLogFormatter和包含LogAppender的數(shù)組,并允許為多個LogLevel記錄日志信息:

@Injectable()
export class LoggerService {
private config = inject(LoggerConfig);
private formatter = inject(LogFormatter);
private appenders = inject(LOG_APPENDERS);

log(level: LogLevel, category: string, msg: string): void {
if (level < this.config.level) {
return;
}
const formatted = this.formatter.format(level, category, msg);
for (const a of this.appenders) {
a.append(level, category, formatted);
}
}
error(category: string, msg: string): void {
this.log(LogLevel.ERROR, category, msg);
}

info(category: string, msg: string): void {
this.log(LogLevel.INFO, category, msg);
}

debug(category: string, msg: string): void {
this.log(LogLevel.DEBUG, category, msg);
}
}

黃金法則

在開始介紹推斷出的模式之前,我想強調(diào)一下提供服務(wù)的黃金法則:

只要有可能,就使用 @Injectable({providedIn: ‘root’})

在庫中有些場景也應(yīng)該使用這種方式,它提供了一些我們想要的特征:簡單、支持搖樹(tree-shakable),并且能夠與懶加載協(xié)作。最后一項特征與其說是 Angular 的優(yōu)點,不如說是底層打包器的優(yōu)點:在懶加載包(bundle)中需要的所有內(nèi)容都會放在這里。

模式:提供者工廠(Provider Factory)

意圖

描述

提供者工廠是一個函數(shù),它會為給定的庫返回一個包含提供者的數(shù)組。這個數(shù)組會被轉(zhuǎn)換為 Angular 的EnvironmentProviders類型,以確保提供者只能在環(huán)境作用域內(nèi)使用,具體來講就是根作用域以及懶路由配置引入的作用域。

Angular 和 NGRX 將這些函數(shù)放在provider.ts文件中。

樣例

如下的提供者函數(shù)(Provider Function)provideLogger會接收一個LoggerConfiguration,并使用它來創(chuàng)建一些提供者:

export function provideLogger(
config: Partial<LoggerConfig>
): EnvironmentProviders {
// using default values for missing properties
const merged = { ...defaultConfig, ...config };

return makeEnvironmentProviders([
{
provide: LoggerConfig,
useValue: merged,
},
{
provide: LogFormatter,
useClass: merged.formatter,
},
merged.appenders.map((a) => ({
provide: LOG_APPENDERS,
useClass: a,
multi: true,
})),
]);
}

缺失的配置會使用默認(rèn)配置的值。Angular 的makeEnvironmentProviders會將Provider數(shù)組包裝到一個EnvironmentProviders實例中。

這個函數(shù)允許消費庫的應(yīng)用在引導(dǎo)過程中像使用其他庫(如HttpClientRouter)那樣設(shè)置 logger:

bootstrapApplication(AppComponent, {
providers: [

provideHttpClient(),

provideRouter(APP_ROUTES),

[...]

// Setting up the Logger:
provideLogger(loggerConfig),
]
}

使用場景和變種

模式:特性(Feature)

意圖

描述

提供者工廠會接收一個包含特性對象的可選數(shù)組。每個特性對象都有一個叫做kind的標(biāo)識符和一個providers數(shù)組。kind屬性允許校驗傳入特性的組合。比如,可能會存在互斥的特性,如為HttpClient同時提供配置 XSRF 令牌處理和禁用 XSRF 令牌處理的特性。

樣例

我們的樣例使用了一個著色的特性,為不同的LoggerLevel顯示不同的顏色:

為了對特性進(jìn)行分類,我們使用了一個枚舉:

export enum LoggerFeatureKind {
COLOR,
OTHER_FEATURE,
ADDITIONAL_FEATURE
}

每個特性都使用LoggerFeature對象來表示:

export interface LoggerFeature {
kind: LoggerFeatureKind;
providers: Provider[];
}

為了提供著色特性,引入了遵循with Feature命名模式的工廠函數(shù):

export function withColor(config?: Partial<ColorConfig>): LoggerFeature {
const internal = { ...defaultColorConfig, ...config };

return {
kind: LoggerFeatureKind.COLOR,
providers: [
{
provide: ColorConfig,
useValue: internal,
},
{
provide: ColorService,
useClass: DefaultColorService,
},
],
};
}

提供者工廠通過可選的第二個參數(shù)接收多個特性,它們定義為rest數(shù)組:

export function provideLogger(
config: Partial<LoggerConfig>,
...features: LoggerFeature[]
): EnvironmentProviders {
const merged = { ...defaultConfig, ...config };

// Inspecting passed features
const colorFeatures =
features?.filter((f) => f.kind === LoggerFeatureKind.COLOR)?.length ?? 0;

// Validating passed features
if (colorFeatures > 1) {
throw new Error("Only one color feature allowed for logger!");
}

return makeEnvironmentProviders([
{
provide: LoggerConfig,
useValue: merged,
},
{
provide: LogFormatter,
useClass: merged.formatter,
},
merged.appenders.map((a) => ({
provide: LOG_APPENDERS,
useClass: a,
multi: true,
})),

// Providing services for the features
features?.map((f) => f.providers),
]);
}

特性中kind屬性用來檢查和驗證傳入的特性。如果一切正常的話,特性中發(fā)現(xiàn)的提供者會被放到返回的EnvironmentProviders對象中。

DefaultLogAppender能夠通過依賴注入獲取著色特性提供的ColorService

export class DefaultLogAppender implements LogAppender {
colorService = inject(ColorService, { optional: true });

append(level: LogLevel, category: string, msg: string): void {
if (this.colorService) {
msg = this.colorService.apply(level, msg);
}
console.log(msg);
}
}

由于特性是可選的,DefaultLogAppenderoptional: true傳入到了inject中。如果特性不可用的話會遇到異常。除此之外,DefaultLogAppender還需要對null值進(jìn)行檢查。

使用場景和變種

模式:配置提供者工廠(Configuration Provider Factory)

描述

配置提供者工廠能夠擴(kuò)展現(xiàn)存服務(wù)的行為。它們可以提供額外的服務(wù),并使用ENVIRONMENT_INITIALIZER來獲取所提供的服務(wù)以及要擴(kuò)展的現(xiàn)存服務(wù)的實例。

樣例

我們假設(shè)有個擴(kuò)展版本的LoggerService,可以為每個日志類別定義一個額外的LogAppender

@Injectable()
export class LoggerService {

private appenders = inject(LOG_APPENDERS);
private formatter = inject(LogFormatter);
private config = inject(LoggerConfig);
[...]

// Additional LogAppender per log category
readonly categories: Record<string, LogAppender> = {};

log(level: LogLevel, category: string, msg: string): void {

if (level < this.config.level) {
return;
}

const formatted = this.formatter.format(level, category, msg);

// Lookup appender for this very category and use
// it, if there is one:
const catAppender = this.categories[category];

if (catAppender) {
catAppender.append(level, category, formatted);
}

// Also, use default appenders:
for (const a of this.appenders) {
a.append(level, category, formatted);
}

}

[...]
}

為了給某個類別配置LogAppender,可以引入另外一個提供者工廠:

export function provideCategory(
category: string,
appender: Type<LogAppender>
): EnvironmentProviders {
// Internal/ Local token for registering the service
// and retrieving the resolved service instance
// immediately after.
const appenderToken = new InjectionToken<LogAppender>("APPENDER_" + category);

return makeEnvironmentProviders([
{
provide: appenderToken,
useClass: appender,
},
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue: () => {
const appender = inject(appenderToken);
const logger = inject(LoggerService);

logger.categories[category] = appender;
},
},
]);
}

這個工廠為LogAppender類創(chuàng)建了一個提供者。但是,我們并不需要這個類,而是需要它的一個實例。同時,我們還需要Injector解析該示例的依賴。這兩者均需要在通過注入檢索LogAppender時提供。

確切地講,這是通過ENVIRONMENT_INITIALIZER實現(xiàn)的,它是綁定到ENVIRONMENT_INITIALIZER令牌并指向某個函數(shù)的多個提供者。它能夠獲取注入的LogAppenderLoggerService。然后,LogAppender會被注冊到 logger 上。

這樣,就能擴(kuò)展甚至來自父作用域的現(xiàn)有LoggerService。例如,如下的樣例假設(shè)LoggerService在根作用域中,而額外的日志級別是在懶加載路由中設(shè)置的:

export const FLIGHT_BOOKING_ROUTES: Routes = [
{
path: '',
component: FlightBookingComponent,

// Providers for this route and child routes
// Using the providers array sets up a new
// environment injector for this part of the
// application.
providers: [
// Setting up an NGRX feature slice
provideState(bookingFeature),
provideEffects([BookingEffects]),

// Provide LogAppender for logger category
provideCategory('booking', DefaultLogAppender),
],
children: [
{
path: 'flight-search',
component: FlightSearchComponent,
},
[...]
],
},
];

使用場景和變種

模式:NgModule 橋

意圖

描述

NgModule 橋是一個通過提供者工廠衍生的 NgModule。為了讓調(diào)用者對服務(wù)有更多的控制權(quán),可以使用像forRoot這樣的靜態(tài)方法。這些方法也可以接收一個配置對象。

樣例

如下的NgModules以傳統(tǒng)的方式設(shè)置 Logger。

@NgModule({
imports: [/* your imports here */],
exports: [/* your exports here */],
declarations: [/* your delarations here */],
providers: [/* providers, you _always_ want to get, here */],
})
export class LoggerModule {
static forRoot(config = defaultConfig): ModuleWithProviders<LoggerModule> {
return {
ngModule: LoggerModule,
providers: [
provideLogger(config)
],
};
}

static forCategory(
category: string,
appender: Type<LogAppender>
): ModuleWithProviders<LoggerModule> {
return {
ngModule: LoggerModule,
providers: [
provideCategory(category, appender)
],
};
}
}

當(dāng)使用 NgModules 時,這種方式是很常用的,所以消費者可以利用現(xiàn)有的知識和慣例。

使用場景和變種

模式:服務(wù)鏈

意圖

描述

當(dāng)同一個服務(wù)被放在多個嵌套的環(huán)境 injector 中時,我們通常只能得到當(dāng)前作用域的服務(wù)實例。因此,在嵌套作用域中,對服務(wù)的調(diào)用無法反映到父作用域中。為了解決這個問題,服務(wù)可以在父作用域中查找自己的實例并將調(diào)用委托給它。

樣例

假設(shè)為一個懶加載的路由再次提供了日志庫:

export const FLIGHT_BOOKING_ROUTES: Routes = [
{
path: '',
component: FlightBookingComponent,
canActivate: [() => inject(AuthService).isAuthenticated()],
providers: [
// NGRX
provideState(bookingFeature),
provideEffects([BookingEffects]),

// Providing **another** logger for this part of the app:
provideLogger(
{
level: LogLevel.DEBUG,
chaining: true,
appenders: [DefaultLogAppender],
},
withColor({
debug: 42,
error: 43,
info: 46,
})
),

],
children: [
{
path: 'flight-search',
component: FlightSearchComponent,
},
[...]
],
},
];

在這里,我們在懶加載路由及其子路由中的環(huán)境 injector 中設(shè)置了另外一套 Logger 的服務(wù)。該服務(wù)會屏蔽掉父作用域中對應(yīng)的服務(wù)。因此,當(dāng)懶加載作用域中的組件調(diào)用LoggerService時,父作用域中的服務(wù)不會被觸發(fā)。

為了防止這種情況,可以從父作用域中獲取LoggerService。更準(zhǔn)確地說,這不一定是父作用域,而是提供LoggerService的“最近的祖先作用域”。隨后,該服務(wù)可以委托給它的父服務(wù)。這樣,服務(wù)就被鏈結(jié)起來了。

@Injectable()
export class LoggerService {
private appenders = inject(LOG_APPENDERS);
private formatter = inject(LogFormatter);
private config = inject(LoggerConfig);

private parentLogger = inject(LoggerService, {
optional: true,
skipSelf: true,
});
[...]

log(level: LogLevel, category: string, msg: string): void {

// 1. Do own stuff here
[...]

// 2. Delegate to parent
if (this.config.chaining && this.parentLogger) {
this.parentLogger.log(level, category, msg);
}
}
[...]
}

當(dāng)使用inject來獲取父 LoggerService 時,我們需要傳遞optional: true,避免祖先作用域在沒有提供LoggerService時出現(xiàn)異常。傳遞skipSelf: true能夠確保只有祖先作用域會被搜索。否則,Angular 會從當(dāng)前作用域開始進(jìn)行搜索,因此會返回調(diào)用服務(wù)本身。

另外,上述的樣例允許通過LoggerConfiguration中的新標(biāo)記chaining啟用或停用這種行為。

使用場景和變種

模式:函數(shù)式服務(wù)

意圖

描述

庫能夠避免強迫消費者按照給定的接口實現(xiàn)基于類的服務(wù),而是允許使用函數(shù)。在內(nèi)部,它們可以使用useValue注冊服務(wù)。

樣例

在本例中,消費者可以直接傳入一個函數(shù),作為LogFormatter傳遞給provideLogger

bootstrapApplication(AppComponent, {
providers: [
provideLogger(
{
level: LogLevel.DEBUG,
appenders: [DefaultLogAppender],

// Functional CSV-Formatter
formatter: (level, cat, msg) => [level, cat, msg].join(";"),
},
withColor({
debug: 3,
})
),
],
});

為了允許這樣做,Logger 需要使用LogFormatFn類型來定義函數(shù)的簽名:

export type LogFormatFn = (
level: LogLevel,
category: string,
msg: string
) =>

同時,因為函數(shù)不能用作令牌,所以需要引入InjectionToken

export const LOG_FORMATTER = new InjectionToken<LogFormatter | LogFormatFn>(
"LOG_FORMATTER"
);

這個InjectionToken既支持基于類的LogFormatter,也支持函數(shù)式的LogFormatter

這可以防止破壞現(xiàn)有的代碼。為了支持這兩種情況,providerLogger需要以稍微不同的方式處理這兩種情況:

export function provideLogger(config: Partial<LoggerConfig>, ...features: LoggerFeature[]): EnvironmentProviders {

const merged = { ...defaultConfig, ...config};

[...]

return makeEnvironmentProviders([
LoggerService,
{
provide: LoggerConfig,
useValue: merged
},

// Register LogFormatter
// - Functional LogFormatter: useValue
// - Class-based LogFormatters: useClass
(typeof merged.formatter === 'function' ) ? {
provide: LOG_FORMATTER,
useValue: merged.formatter
} : {
provide: LOG_FORMATTER,
useClass: merged.formatter
},

merged.appenders.map(a => ({
provide: LOG_APPENDERS,
useClass: a,
multi: true
})),
[...]
]);
}

基于類的服務(wù)是用useClass注冊的,而對于函數(shù)式服務(wù),則需要使用useValue

此外,LogFormatter的消費者需要為函數(shù)式和基于類的方式進(jìn)行調(diào)整:

@Injectable()
export class LoggerService {
private appenders = inject(LOG_APPENDERS);
private formatter = inject(LOG_FORMATTER);
private config = inject(LoggerConfig);

[...]

private format(level: LogLevel, category: string, msg: string): string {
if (typeof this.formatter === 'function') {
return this.formatter(level, category, msg);
}
else {
return this.formatter.format(level, category, msg);
}
}

log(level: LogLevel, category: string, msg: string): void {
if (level < this.config.level) {
return;
}

const formatted = this.format(level, category, msg);

[...]
}
[...]
}

使用場景和變種

原文鏈接: Patterns for Custom Standalone APIs in Angular

上一篇:

七步將Chatbot AI聊天機器人添加到您的網(wǎng)站

下一篇:

Claude注冊使用指南:穩(wěn)定不封的方法
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊

多API并行試用

數(shù)據(jù)驅(qū)動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費