Пошук уроків, статей та іншого контенту
Побудуєте модулі з методами forRoot та forRootAsync, які змінюють провайдери залежно від конфігурації.
Звичайний модуль має фіксовані imports, providers, controllers та exports. Динамічний модуль може формувати частину цієї конфігурації під час імпорту.
Такий підхід корисний для модулів, які:
потребують налаштувань;
підключаються в різних частинах застосунку з різними параметрами;
мають залежності, які потрібно отримати асинхронно;
повинні створювати провайдери на основі конфігурації.
Динамічний модуль — це звичайний клас із методом, який повертає об’єкт DynamicModule.
import { DynamicModule } from '@nestjs/common';
@Module({})
export class ExampleModule {
static forRoot(): DynamicModule {
return {
module: ExampleModule,
};
}
}Властивість module є обов’язковою. Вона вказує NestJS, для якого модуля створюється динамічна конфігурація.
forRootЗа домовленістю метод forRoot використовують для синхронної конфігурації модуля.
Наприклад, створимо модуль аудиту. Він матиме префікс для повідомлень і прапорець, який дозволяє або вимикає аудит.
Для передачі параметрів у сервіс використаємо окремий токен:
export const AUDIT_OPTIONS = Symbol('AUDIT_OPTIONS');
export interface AuditModuleOptions {
prefix: string;
enabled: boolean;
}Токен потрібен тому, що інтерфейси TypeScript не існують під час виконання JavaScript. Впровадити AuditModuleOptions напряму через @Inject() неможливо, тому використовують рядок або Symbol.
forRootimport {
DynamicModule,
Inject,
Injectable,
Module,
} from '@nestjs/common';
export const AUDIT_OPTIONS = Symbol('AUDIT_OPTIONS');
export interface AuditModuleOptions {
prefix: string;
enabled: boolean;
}
@Injectable()
export class AuditService {
constructor(
@Inject(AUDIT_OPTIONS)
private readonly options: AuditModuleOptions,
) {}
record(message: string): void {
if (!this.options.enabled) {
return;
}
console.log(`[${this.options.prefix}] ${message}`);
}
}
@Module({})
export class AuditModule {
static forRoot(options: AuditModuleOptions): DynamicModule {
return {
module: AuditModule,
providers: [
{
provide: AUDIT_OPTIONS,
useValue: options,
},
AuditService,
],
exports: [AuditService],
};
}
}У цьому прикладі:
useValue створює провайдер із готовим значенням;
AuditService отримує це значення через токен AUDIT_OPTIONS;
AuditService експортується, тому його можна впровадити в модулі, які імпортують AuditModule.
Підключення модуля має такий вигляд:
@Module({
imports: [
AuditModule.forRoot({
prefix: 'orders',
enabled: true,
}),
],
})
export class AppModule {}NestJS обробить результат forRoot як частину метаданих модуля.
forRootAsyncforRootAsync використовують, коли параметри потрібно:
отримати з іншого провайдера;
прочитати з конфігурації;
обчислити асинхронно;
завантажити з зовнішнього джерела.
Замість useValue динамічний модуль створює провайдер через useFactory.
import { ModuleMetadata } from '@nestjs/common';
export interface AuditModuleAsyncOptions
extends Pick<ModuleMetadata, 'imports'> {
inject?: unknown[];
useFactory: (
...args: any[]
) => AuditModuleOptions | Promise<AuditModuleOptions>;
}Властивості:
imports — модулі, провайдери яких потрібні фабриці;
inject — токени залежностей, які потрібно передати фабриці;
useFactory — функція, що повертає параметри модуля.
forRootAsyncДо попереднього модуля додамо асинхронний метод:
import {
DynamicModule,
Inject,
Injectable,
Module,
} from '@nestjs/common';
export const AUDIT_OPTIONS = Symbol('AUDIT_OPTIONS');
export interface AuditModuleOptions {
prefix: string;
enabled: boolean;
}
export interface AuditModuleAsyncOptions {
imports?: any[];
inject?: any[];
useFactory: (
...args: any[]
) => AuditModuleOptions | Promise<AuditModuleOptions>;
}
@Injectable()
export class AuditService {
constructor(
@Inject(AUDIT_OPTIONS)
private readonly options: AuditModuleOptions,
) {}
record(message: string): void {
if (!this.options.enabled) {
return;
}
console.log(`[${this.options.prefix}] ${message}`);
}
}
@Module({})
export class AuditModule {
static forRoot(options: AuditModuleOptions): DynamicModule {
return {
module: AuditModule,
providers: [
{
provide: AUDIT_OPTIONS,
useValue: options,
},
AuditService,
],
exports: [AuditService],
};
}
static forRootAsync(
options: AuditModuleAsyncOptions,
): DynamicModule {
return {
module: AuditModule,
imports: options.imports ?? [],
providers: [
{
provide: AUDIT_OPTIONS,
useFactory: options.useFactory,
inject: options.inject ?? [],
},
AuditService,
],
exports: [AuditService],
};
}
}Тут провайдер AUDIT_OPTIONS створюється фабрикою:
{
provide: AUDIT_OPTIONS,
useFactory: options.useFactory,
inject: options.inject ?? [],
}NestJS спочатку виконає фабрику, дочекається результату, якщо він є Promise, і лише після цього створить AuditService.
Нижче наведено приклад модуля, який можна використати в NestJS-застосунку.
audit.module.tsimport {
DynamicModule,
Inject,
Injectable,
Module,
} from '@nestjs/common';
export const AUDIT_OPTIONS = Symbol('AUDIT_OPTIONS');
export interface AuditModuleOptions {
prefix: string;
enabled: boolean;
}
export interface AuditModuleAsyncOptions {
imports?: any[];
inject?: any[];
useFactory: (
...args: any[]
) => AuditModuleOptions | Promise<AuditModuleOptions>;
}
@Injectable()
export class AuditService {
constructor(
@Inject(AUDIT_OPTIONS)
private readonly options: AuditModuleOptions,
) {}
record(message: string): void {
if (!this.options.enabled) {
return;
}
console.log(`[${this.options.prefix}] ${message}`);
}
}
@Module({})
export class AuditModule {
static forRoot(options: AuditModuleOptions): DynamicModule {
return {
module: AuditModule,
providers: [
{
provide: AUDIT_OPTIONS,
useValue: options,
},
AuditService,
],
exports: [AuditService],
};
}
static forRootAsync(
options: AuditModuleAsyncOptions,
): DynamicModule {
return {
module: AuditModule,
imports: options.imports ?? [],
providers: [
{
provide: AUDIT_OPTIONS,
useFactory: options.useFactory,
inject: options.inject ?? [],
},
AuditService,
],
exports: [AuditService],
};
}
}app.controller.tsimport { Controller, Get } from '@nestjs/common';
import { AuditService } from './audit.module';
@Controller('orders')
export class AppController {
constructor(private readonly auditService: AuditService) {}
@Get()
getOrders(): string {
this.auditService.record('Отримано список замовлень');
return 'Orders';
}
}app.module.tsimport { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AuditModule } from './audit.module';
@Module({
imports: [
AuditModule.forRootAsync({
useFactory: async () => {
// Імітація асинхронного завантаження конфігурації
await new Promise((resolve) => setTimeout(resolve, 100));
return {
prefix: 'orders',
enabled: true,
};
},
}),
],
controllers: [AppController],
})
export class AppModule {}main.tsimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();Після запуску застосунку запит до GET /orders викличе:
[orders] Отримано список замовленьЯкщо змінити значення enabled на false, сервіс залишиться доступним, але не виводитиме повідомлення.
forRootAsyncФабрика може отримувати інші провайдери через inject.
Для цього провайдер має бути доступним у модулі, зазначеному в imports.
import { Injectable, Module } from '@nestjs/common';
@Injectable()
export class SettingsService {
getAuditOptions() {
return {
prefix: 'configured',
enabled: true,
};
}
}
@Module({
providers: [SettingsService],
exports: [SettingsService],
})
export class SettingsModule {}Тепер SettingsService можна передати у фабрику динамічного модуля:
@Module({
imports: [
AuditModule.forRootAsync({
imports: [SettingsModule],
inject: [SettingsService],
useFactory: async (settingsService: SettingsService) => {
return settingsService.getAuditOptions();
},
}),
],
})
export class AppModule {}Порядок роботи такий:
AppModule імпортує SettingsModule.
SettingsModule експортує SettingsService.
AuditModule.forRootAsync вказує SettingsModule у imports.
NestJS передає SettingsService у useFactory.
Результат фабрики стає значенням провайдера AUDIT_OPTIONS.
AuditService отримує це значення через @Inject(AUDIT_OPTIONS).
Якщо провайдер не додати до imports і inject, NestJS не зможе створити фабрику.
forRoot і forRootAsyncforRootВикористовуйте, коли конфігурація вже доступна під час опису модуля:
AuditModule.forRoot({
prefix: 'orders',
enabled: true,
});Типовий провайдер для параметрів:
{
provide: AUDIT_OPTIONS,
useValue: options,
}forRootAsyncВикористовуйте, коли конфігурацію потрібно отримати через фабрику:
AuditModule.forRootAsync({
useFactory: async () => {
return {
prefix: 'orders',
enabled: true,
};
},
});Типовий провайдер для параметрів:
{
provide: AUDIT_OPTIONS,
useFactory: options.useFactory,
inject: options.inject ?? [],
}Обидва методи можуть повертати однаковий набір провайдерів. Відмінність полягає в тому, як створюється провайдер із параметрами.
moduleВказує клас модуля, для якого створюється динамічна конфігурація:
return {
module: AuditModule,
};providersМістить провайдери, доступні всередині модуля:
providers: [
{
provide: AUDIT_OPTIONS,
useValue: options,
},
AuditService,
]exportsВизначає, що з модуля можна використовувати в модулях-імпортерах:
exports: [AuditService]Якщо не експортувати AuditService, інший модуль не зможе впровадити його залежністю, навіть якщо AuditModule імпортовано.
importsПотрібен для доступу фабрики до провайдерів інших модулів:
imports: [SettingsModule]Самого inject недостатньо. Модуль, який надає залежність, також має бути імпортований.
moduleДинамічний модуль повинен повертати об’єкт із властивістю module:
return {
module: AuditModule,
};Без неї NestJS не зможе коректно обробити результат методу.
Якщо сервіс потрібен за межами динамічного модуля, додайте його до exports:
exports: [AuditService]Токен під час створення провайдера і токен у @Inject() мають бути тим самим значенням:
provide: AUDIT_OPTIONS@Inject(AUDIT_OPTIONS)Не слід замінювати Symbol іншим символом із таким самим текстом.
importsЯкщо фабрика отримує SettingsService, але SettingsModule не зазначений у imports, залежність не буде доступною:
AuditModule.forRootAsync({
imports: [SettingsModule],
inject: [SettingsService],
useFactory: (settingsService: SettingsService) => {
return settingsService.getAuditOptions();
},
});Інтерфейс не можна використати як runtime-токен:
// Некоректно
constructor(private readonly options: AuditModuleOptions) {}Для цього потрібен явний токен:
constructor(
@Inject(AUDIT_OPTIONS)
private readonly options: AuditModuleOptions,
) {}Динамічний модуль повертає об’єкт типу DynamicModule.
Метод forRoot призначений для синхронної конфігурації.
Метод forRootAsync створює конфігурацію через фабрику.
Параметри модуля зазвичай передаються через окремий токен.
Для готового значення використовують useValue.
Для фабрики використовують useFactory.
Залежності фабрики потрібно зазначати в inject.
Модулі, які надають ці залежності, потрібно зазначати в imports.
Сервіси, доступні модулям-імпортерам, потрібно додати до exports.