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

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

為了簡(jiǎn)單起見(jiàn),我們將 Logger 庫(kù)限制為只有三個(gè)日志級(jí)別。

我們會(huì)使用一個(gè)抽象LoggerConfig來(lái)定義可能的配置選項(xiàng):

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

我們有意將其定義為抽象類,因?yàn)榻涌跓o(wú)法作為 DI 的令牌(token)。該類的一個(gè)常量定義了配置選項(xiàng)的默認(rèn)值:

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

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

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

LoggerConfiguration類似,LogFormatter是一個(gè)抽象類,可以用作令牌。Logger 庫(kù)的消費(fèi)者可以通過(guò)提供自己的實(shí)現(xiàn)來(lái)調(diào)整格式,也可以使用庫(kù)提供的默認(rèn)實(shí)現(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是另一個(gè)可替換的概念,它會(huì)負(fù)責(zé)將日志消息追加到日志中:

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

默認(rèn)實(shí)現(xiàn)會(huì)將日志消息打印至控制臺(tái)。

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

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

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

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

LoggserService本身會(huì)通過(guò) DI 來(lái)接收LoggerConfigLogFormatter和包含LogAppender的數(shù)組,并允許為多個(gè)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);
}
}

黃金法則

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

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

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

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

意圖

描述

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

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

樣例

如下的提供者函數(shù)(Provider Function)provideLogger會(huì)接收一個(gè)LoggerConfiguration,并使用它來(lái)創(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,
})),
]);
}

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

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

bootstrapApplication(AppComponent, {
providers: [

provideHttpClient(),

provideRouter(APP_ROUTES),

[...]

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

使用場(chǎng)景和變種

模式:特性(Feature)

意圖

描述

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

樣例

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

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

export enum LoggerFeatureKind {
COLOR,
OTHER_FEATURE,
ADDITIONAL_FEATURE
}

每個(gè)特性都使用LoggerFeature對(duì)象來(lái)表示:

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,
},
],
};
}

提供者工廠通過(guò)可選的第二個(gè)參數(shù)接收多個(gè)特性,它們定義為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屬性用來(lái)檢查和驗(yàn)證傳入的特性。如果一切正常的話,特性中發(fā)現(xiàn)的提供者會(huì)被放到返回的EnvironmentProviders對(duì)象中。

DefaultLogAppender能夠通過(guò)依賴注入獲取著色特性提供的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中。如果特性不可用的話會(huì)遇到異常。除此之外,DefaultLogAppender還需要對(duì)null值進(jìn)行檢查。

使用場(chǎng)景和變種

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

描述

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

樣例

我們假設(shè)有個(gè)擴(kuò)展版本的LoggerService,可以為每個(gè)日志類別定義一個(gè)額外的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);
}

}

[...]
}

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

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

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

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

這樣,就能擴(kuò)展甚至來(lái)自父作用域的現(xiàn)有LoggerService。例如,如下的樣例假設(shè)LoggerService在根作用域中,而額外的日志級(jí)別是在懶加載路由中設(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,
},
[...]
],
},
];

使用場(chǎng)景和變種

模式:NgModule 橋

意圖

描述

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

樣例

如下的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 時(shí),這種方式是很常用的,所以消費(fèi)者可以利用現(xiàn)有的知識(shí)和慣例。

使用場(chǎng)景和變種

模式:服務(wù)鏈

意圖

描述

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

樣例

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

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,
},
[...]
],
},
];

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

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

@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來(lái)獲取父 LoggerService 時(shí),我們需要傳遞optional: true,避免祖先作用域在沒(méi)有提供LoggerService時(shí)出現(xiàn)異常。傳遞skipSelf: true能夠確保只有祖先作用域會(huì)被搜索。否則,Angular 會(huì)從當(dāng)前作用域開(kāi)始進(jìn)行搜索,因此會(huì)返回調(diào)用服務(wù)本身。

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

使用場(chǎng)景和變種

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

意圖

描述

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

樣例

在本例中,消費(fèi)者可以直接傳入一個(gè)函數(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類型來(lái)定義函數(shù)的簽名:

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

同時(shí),因?yàn)楹瘮?shù)不能用作令牌,所以需要引入InjectionToken

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

這個(gè)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注冊(cè)的,而對(duì)于函數(shù)式服務(wù),則需要使用useValue

此外,LogFormatter的消費(fèi)者需要為函數(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);

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

使用場(chǎng)景和變種

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

上一篇:

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

下一篇:

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

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

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

查看全部API→
??

熱門場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

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

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

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

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)