Пошук уроків, статей та іншого контенту
Зареєструєте залежності через useValue, useClass і useExisting для гнучкого налаштування DI.
NestJS зазвичай створює залежність, використовуючи клас:
@Module({
providers: [ReportsService],
})
export class ReportsModule {}У цьому випадку NestJS:
знаходить клас ReportsService;
аналізує його конструктор;
створює всі необхідні залежності;
передає їх у конструктор.
Користувацький provider дає змогу змінити спосіб створення залежності. Замість автоматичного створення класу можна:
передати готове значення через useValue;
вказати клас-реалізацію через useClass;
створити псевдонім для вже зареєстрованої залежності через useExisting.
Загальний синтаксис:
{
provide: TOKEN,
useValue: value,
}або:
{
provide: TOKEN,
useClass: SomeClass,
}або:
{
provide: TOKEN,
useExisting: EXISTING_TOKEN,
}TOKEN — це ідентифікатор залежності, за яким NestJS зможе її знайти.
Клас може одночасно бути і токеном, і реалізацією:
providers: [ReportsService]Але для інтерфейсів, конфігураційних об’єктів або взаємозамінних реалізацій потрібен окремий токен.
Як токен можна використовувати:
рядок;
Symbol;
клас;
абстрактний клас.
Для уникнення конфліктів зручно використовувати Symbol:
export const APP_CONFIG = Symbol('APP_CONFIG');
export const LOGGER = Symbol('LOGGER');Під час отримання залежності потрібно використати декоратор @Inject():
@Injectable()
export class ReportsService {
constructor(
@Inject(APP_CONFIG) private readonly config: AppConfig,
) {}
}Тип TypeScript AppConfig сам по собі не існує під час виконання програми, тому NestJS не може використати його як токен автоматично.
useValueuseValue реєструє вже створене значення. Це може бути:
рядок;
число;
об’єкт;
масив;
готовий екземпляр класу;
тестова заміна справжньої залежності.
useValue{
provide: APP_CONFIG,
useValue: {
applicationName: 'Reports API',
environment: 'development',
pageSize: 20,
},
}Коли інший provider запитує APP_CONFIG, NestJS передає саме цей об’єкт.
Приклад використання:
@Injectable()
export class ReportsService {
constructor(
@Inject(APP_CONFIG)
private readonly config: AppConfig,
) {}
printInfo(): void {
console.log(this.config.applicationName);
console.log(this.config.environment);
}
}NestJS не створює копію об’єкта для кожного споживача. Зареєстроване значення використовується як одна залежність контейнера.
useValue особливо корисний для:
конфігурації модуля;
значень із змінних середовища;
констант;
моків під час тестування;
готових об’єктів, створених за межами NestJS.
useClassuseClass вказує клас, екземпляр якого потрібно створити для певного токена:
{
provide: LOGGER,
useClass: ConsoleLogger,
}Тепер при запиті залежності з токеном LOGGER NestJS створить ConsoleLogger.
Клас може мати власні залежності в конструкторі. NestJS також спробує їх розв’язати через контейнер залежностей.
Припустімо, застосунку потрібен логер, але конкретну реалізацію потрібно змінювати залежно від оточення:
{
provide: LOGGER,
useClass: ConsoleLogger,
}Для іншого середовища можна зареєструвати:
{
provide: LOGGER,
useClass: SilentLogger,
}Код сервісу при цьому не змінюється. Він залежить від токена LOGGER, а не від конкретного класу:
@Injectable()
export class ReportsService {
constructor(
@Inject(LOGGER)
private readonly logger: LoggerLike,
) {}
}Це дає змогу замінювати реалізації без зміни споживачів залежності.
useExistinguseExisting створює псевдонім для вже зареєстрованого provider:
{
provide: LOGGER,
useClass: ConsoleLogger,
},
{
provide: AUDIT_LOGGER,
useExisting: LOGGER,
}Тепер токени LOGGER і AUDIT_LOGGER посилаються на той самий екземпляр ConsoleLogger.
Це важлива відмінність від повторного використання useClass:
{
provide: LOGGER,
useClass: ConsoleLogger,
},
{
provide: AUDIT_LOGGER,
useClass: ConsoleLogger,
}У такому випадку NestJS створює окремі provider-и. Вони матимуть різні екземпляри.
З useExisting екземпляр спільний:
const first = moduleRef.get(LOGGER);
const second = moduleRef.get(AUDIT_LOGGER);
console.log(first === second); // trueuseExisting корисний, коли:
одна залежність повинна бути доступна під кількома токенами;
потрібно зберегти одну спільну інстанцію;
різні частини застосунку використовують різні назви для тієї самої реалізації.
У цьому прикладі:
APP_CONFIG реєструється через useValue;
LOGGER реєструється через useClass;
AUDIT_LOGGER є псевдонімом LOGGER через useExisting;
ReportsService отримує всі ці залежності через токени.
import { Inject, Injectable, Module } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
const APP_CONFIG = Symbol('APP_CONFIG');
const LOGGER = Symbol('LOGGER');
const AUDIT_LOGGER = Symbol('AUDIT_LOGGER');
interface AppConfig {
applicationName: string;
environment: string;
pageSize: number;
}
interface LoggerLike {
log(message: string): void;
}
@Injectable()
class ConsoleLogger implements LoggerLike {
log(message: string): void {
console.log(`[console] ${message}`);
}
}
@Injectable()
class ReportsService {
constructor(
@Inject(APP_CONFIG)
private readonly config: AppConfig,
@Inject(LOGGER)
private readonly logger: LoggerLike,
@Inject(AUDIT_LOGGER)
private readonly auditLogger: LoggerLike,
) {}
printReportInfo(): void {
this.logger.log(
`Запущено ${this.config.applicationName} у середовищі ` +
`${this.config.environment}`,
);
this.auditLogger.log(
`Розмір сторінки звіту: ${this.config.pageSize}`,
);
console.log(
'LOGGER і AUDIT_LOGGER використовують один екземпляр:',
this.logger === this.auditLogger,
);
}
}
@Module({
providers: [
{
provide: APP_CONFIG,
useValue: {
applicationName: 'Reports API',
environment: 'development',
pageSize: 20,
},
},
{
provide: LOGGER,
useClass: ConsoleLogger,
},
{
provide: AUDIT_LOGGER,
useExisting: LOGGER,
},
ReportsService,
],
})
class AppModule {}
async function bootstrap(): Promise<void> {
const app = await NestFactory.createApplicationContext(AppModule);
const reportsService = app.get(ReportsService);
reportsService.printReportInfo();
await app.close();
}
void bootstrap();Результат буде приблизно таким:
[console] Запущено Reports API у середовищі development
[console] Розмір сторінки звіту: 20
LOGGER і AUDIT_LOGGER використовують один екземпляр: trueuseClassРозглянемо логер, який не виводить повідомлення:
@Injectable()
class SilentLogger implements LoggerLike {
log(_message: string): void {
// Повідомлення навмисно ігнорується
}
}Щоб вимкнути виведення логів, достатньо змінити provider:
{
provide: LOGGER,
useClass: SilentLogger,
}ReportsService залишиться без змін, оскільки він залежить від токена LOGGER та інтерфейсу LoggerLike, а не від ConsoleLogger:
@Injectable()
class ReportsService {
constructor(
@Inject(LOGGER)
private readonly logger: LoggerLike,
) {}
}Це один із практичних способів реалізувати залежність від абстракції, а не від конкретного класу.
Користувацькі providers додаються до масиву providers модуля так само, як і звичайні класи:
@Module({
providers: [
{
provide: APP_CONFIG,
useValue: {
applicationName: 'Reports API',
},
},
ReportsService,
],
})
export class ReportsModule {}Якщо provider потрібно використовувати в іншому модулі, його необхідно експортувати:
@Module({
providers: [
{
provide: APP_CONFIG,
useValue: {
applicationName: 'Reports API',
},
},
],
exports: [APP_CONFIG],
})
export class ConfigurationModule {}Після імпорту ConfigurationModule інший модуль зможе отримати APP_CONFIG через @Inject(APP_CONFIG).
Експортується саме токен, а не ключ useValue, useClass чи useExisting.
useValue{
provide: TOKEN,
useValue: value,
}передає готове значення;
не створює клас;
зручно для конфігурації та моків.
useClass{
provide: TOKEN,
useClass: Implementation,
}створює екземпляр указаного класу;
розв’язує залежності цього класу;
дає змогу замінювати реалізації.
useExisting{
provide: ALIAS,
useExisting: TOKEN,
}не створює новий екземпляр;
посилається на вже зареєстрований provider;
забезпечує спільну інстанцію під кількома токенами.
Якщо provider не додано до providers, NestJS не зможе його знайти:
@Injectable()
class ReportsService {
constructor(
@Inject(APP_CONFIG)
private readonly config: AppConfig,
) {}
}Для цього класу в модулі має бути відповідний запис:
{
provide: APP_CONFIG,
useValue: {
applicationName: 'Reports API',
},
}Токен у @Inject() повинен точно відповідати токену в provide:
{
provide: APP_CONFIG,
useValue: {},
}constructor(@Inject(APP_CONFIG) config: AppConfig) {}Інший Symbol або інший рядок буде вже іншим токеном.
useExisting посилається на незареєстрований tokenТакий provider не працюватиме:
{
provide: AUDIT_LOGGER,
useExisting: LOGGER,
}якщо LOGGER не зареєстрований у тому самому модулі або доступному імпортованому модулі.
Спочатку потрібно зареєструвати основний provider:
{
provide: LOGGER,
useClass: ConsoleLogger,
}useExistinguseExisting не створює новий об’єкт. Він повертає вже зареєстрований provider.
Якщо потрібні окремі екземпляри, слід зареєструвати класи окремо через useClass, а не створювати псевдонім через useExisting.
@Inject()Інтерфейси TypeScript видаляються під час компіляції, тому NestJS не може автоматично використати їх як runtime-токени:
constructor(private readonly logger: LoggerLike) {}Для provider-а, зареєстрованого через Symbol, потрібно явно вказати токен:
constructor(
@Inject(LOGGER)
private readonly logger: LoggerLike,
) {}Користувацький provider описує, як NestJS має створити або знайти залежність.
useValue передає готове значення.
useClass створює вказаний клас для заданого токена.
useExisting створює псевдонім для вже зареєстрованого provider-а.
useClass може створити окремий екземпляр для кожного токена, а useExisting використовує той самий екземпляр.
Для інтерфейсів, конфігурації та абстракцій зручно використовувати Symbol разом із @Inject().
Provider, потрібний в інших модулях, потрібно додати до exports.