# Динамические модули
Глава Модули охватывает основы модулей Nest и включает краткое введение в динамические модули. Эта глава раскрывает тему динамических модулей. После ее завершения вы должны хорошо понимать, что это такое и как их использовать.
# Введение
Большинство примеров кода в разделе Обзор используют обычные, или статические, модули. Модули определяют группы компонентов, таких как провайдеры и контроллеры, которые подходят друг другу как модульная часть общего приложения. Они обеспечивают контекст выполнения, или область действия, для этих компонентов. Например, провайдеры, определенные в модуле, видны другим членам модуля без необходимости их экспорта. Когда провайдер должен быть виден за пределами модуля, он сначала экспортируется из своего главного модуля, а затем импортируется в потребляющий модуль.
Давайте рассмотрим знакомый пример.
Сначала мы определим UsersModule для предоставления и экспорта UsersService. UsersModule - это хост
модуль для UsersService.
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
Далее мы определим AuthModule, который импортирует UsersModule, делая экспортированные провайдеры UsersModule
доступными внутри AuthModule:
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
Эти конструкции позволяют нам внедрить UsersService, например, в AuthService, который размещен в AuthModule:
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
/*
Implementation that makes use of this.usersService
*/
}
Мы будем называть это статическим связыванием модулей. Вся информация, необходимая Nest для соединения модулей,
уже объявлена в принимающем и потребляющем модулях. Давайте разберем, что происходит во время этого процесса. Nest
делает UsersService доступным внутри AuthModule путем:
- Инстанцирует
UsersModule, включая импорт других модулей, которые потребляет самUsersModule, и разрешение любых зависимостей (см. Пользовательские провайдеры). - Инстанцирует
AuthModuleи предоставляет экспортируемые провайдерыUsersModuleкомпонентамAuthModule(так же, как если бы они были объявлены вAuthModule). - Инжектирует экземпляр
UsersServiceвAuthService.
# Пример использования динамического модуля
При статическом связывании модулей у потребляющего модуля нет возможности влиять на конфигурацию провайдеров модуля "хоста". Почему это важно? Рассмотрим случай, когда у нас есть модуль общего назначения, который должен вести себя по-разному в различных случаях использования. Это аналогично концепции "плагина" во многих системах, где общий модуль требует определенной конфигурации, прежде чем он может быть использован потребителем.
Хороший пример с Nest - это конфигурационный модуль. Многие приложения находят полезным внешнее представление деталей конфигурации с помощью модуля конфигурации. Это позволяет легко динамически изменять настройки приложения в различных средах развертывания: например, база данных разработки для разработчиков, база данных для среды тестирования и т. д. Делегируя управление параметрами конфигурации модулю конфигурации, исходный код приложения остается независимым от параметров конфигурации.
Проблема заключается в том, что сам модуль конфигурации, поскольку он является общим (подобно "плагину"), должен быть настроен потребляющим модулем. Именно здесь в игру вступают динамические модули. Используя возможности динамических модулей, мы можем сделать наш модуль конфигурации динамическим, чтобы потребляющий модуль мог использовать API для управления настройкой модуля конфигурации в момент его импорта.
Другими словами, динамические модули предоставляют API для импорта одного модуля в другой и настройки свойств и поведения этого модуля при импорте, в отличие от использования статических привязок, которые мы рассматривали до сих пор.
на русский язык!
Если документация помогла вам и оказалась полезна - вы можете поддержать
автора, угостив его парой бокалов пивка 🍺 через сервис
donatty.
Либо внести свой вклад в перевод пул реквестом на
гитхабе

# Пример модуля Config
В этом разделе мы будем использовать базовую версию кода примера из главы configuration chapter. Завершенная версия на момент окончания этой главы доступна в виде рабочего примера здесь (opens new window).
Наша задача состоит в том, чтобы ConfigModule принимал объект options для его настройки. Вот функционал, который мы хотим
создать. Базовый пример жестко кодирует расположение файла .env в корневой папке проекта. Предположим, мы хотим
сделать это настраиваемым, чтобы вы могли управлять файлами .env в любой папке по вашему выбору. Например, представьте,
что вы хотите хранить различные файлы .env в папке под корнем проекта под названием config (т.е. в папке, родственной
папке src). Вы хотели бы иметь возможность выбирать разные папки при использовании ConfigModule в разных проектах.
Динамические модули дают нам возможность передавать параметры в импортируемый модуль, чтобы мы могли изменять
его поведение. Давайте посмотрим, как это работает. Будет полезно, если мы начнем с конечной цели - как это может
выглядеть с точки зрения потребляющего модуля, а затем будем работать в обратном направлении. Во-первых, давайте
быстро рассмотрим пример статического импорта ConfigModule (т.е. подход, который не имеет возможности влиять
на поведение импортируемого модуля). Обратите пристальное внимание на массив imports в декораторе @Module():
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
@Module({
imports: [ConfigModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Давайте рассмотрим, как может выглядеть импорт динамического модуля, в котором мы передаем объект конфигурации.
Сравните разницу в массиве imports между этими двумя примерами:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
@Module({
imports: [ConfigModule.register({ folder: './config' })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Давайте посмотрим, что происходит в приведенном выше динамическом примере. Каковы движущиеся части?
ConfigModule- это обычный класс, поэтому мы можем сделать вывод, что у него должен быть статический метод под названиемregister(). Мы знаем, что он статический, потому что мы вызываем его на классеConfigModule, а не на экземпляре класса. Примечание: этот метод, который мы вскоре создадим, может иметь любое произвольное имя, но по соглашению мы должны называть его либоforRoot(), либоregister().- Метод
register()определен нами, поэтому мы можем принимать любые входные аргументы. В данном случае мы будем принимать простой объектoptionsс подходящими свойствами, что является типичным случаем. - Мы можем сделать вывод, что метод
register()должен возвращать что-то вродемодуля, поскольку его возвращаемое значение появляется в знакомом спискеimports, который, как мы уже видели, включает список модулей.
На самом деле, метод register() вернет нам DynamicModule. Динамический модуль - это не что иное, как модуль,
созданный во время выполнения, с теми же свойствами, что и статический модуль, плюс одно дополнительное свойство
под названием module. Давайте быстро рассмотрим пример объявления статического модуля, обращая пристальное внимание
на параметры модуля, передаваемые в декоратор:
@Module({
imports: [DogsModule],
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]
})
Динамические модули должны возвращать объект с точно таким же интерфейсом, плюс одно дополнительное свойство module.
Свойство module служит именем модуля и должно быть таким же, как имя класса модуля, как показано в примере ниже.
Для динамического модуля все свойства объекта module options являются необязательными за исключением
module.
А как насчет статического метода register()? Теперь мы видим, что его задача - вернуть объект, который имеет интерфейс
DynamicModule. Когда мы вызываем его, мы предоставляем модуль в список imports, подобно тому, как мы сделали
бы это в статическом случае, указав имя класса модуля. Другими словами, API динамического модуля просто возвращает модуль,
но вместо того, чтобы фиксировать свойства в декораторе @Module, мы задаем их программно.
Осталось еще несколько деталей, которые помогут сделать картину полной:
- Теперь мы можем утверждать, что свойство
@Module()декоратораimportsможет принимать не только имя класса модуля (например,imports: [UsersModule]), но и функцию возвращающую динамический модуль (например,imports: [ConfigModule.register(...)]). - Динамический модуль может сам импортировать другие модули. Мы не будем делать этого в данном примере, но если
динамический модуль зависит от провайдеров из других модулей, вы будете импортировать их с помощью необязательного
свойства
imports. Опять же, это в точности аналогично тому, как вы объявляете метаданные для статического модуля с помощью декоратора@Module().
Вооруженные этим знанием, мы можем теперь посмотреть, как должно выглядеть наше динамическое объявление ConfigModule.
Давайте попробуем это сделать.
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({})
export class ConfigModule {
static register(): DynamicModule {
return {
module: ConfigModule,
providers: [ConfigService],
exports: [ConfigService],
};
}
}
Теперь должно быть понятно, как эти части связаны друг с другом. Вызов ConfigModule.register(...) возвращает объект
DynamicModule со свойствами, которые по сути те же самые, что до сих пор мы предоставляли в качестве метаданных
через декоратор @Module().
Импортируйте
DynamicModuleиз@nestjs/common.
Однако наш динамический модуль пока не очень интересен, поскольку мы не представили никакой возможности конфигурировать его, как мы собирались сделать. Давайте займемся этим дальше.
# Конфигурация модуля
Очевидным решением для настройки поведения ConfigModule является передача ему объекта options в статическом методе
register(), как мы догадались выше. Давайте еще раз посмотрим на свойство imports нашего потребляющего модуля:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
@Module({
imports: [ConfigModule.register({ folder: './config' })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Этот вариант прекрасно справляется с передачей объекта options нашему динамическому модулю. Как мы затем используем этот
объект options в ConfigModule? Давайте рассмотрим это. Мы знаем, что наш ConfigModule - это, по сути,
хост для предоставления и экспорта инжектируемого сервиса - ConfigService - для использования другими провайдерами.
На самом деле именно нашему ConfigService необходимо читать объект options для настройки своего поведения. Давайте
пока предположим, что мы знаем, как каким-то образом получить options из метода register() в ConfigService. Исходя
из этого предположения, мы можем внести несколько изменений в сервис, чтобы настроить его поведение на основе свойств
объекта options. (Примечание: на данный момент, поскольку мы не определили, как его передавать, мы просто жестко
закодируем options. Мы исправим это через минуту).
import { Injectable } from '@nestjs/common';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { EnvConfig } from './interfaces';
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
constructor() {
const options = { folder: './config' };
const filePath = `${process.env.NODE_ENV || 'development'}.env`;
const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
this.envConfig = dotenv.parse(fs.readFileSync(envFile));
}
get(key: string): string {
return this.envConfig[key];
}
}
Теперь наш ConfigService знает, как найти файл .env в папке, которую мы указали в options.
Наша оставшаяся задача - каким-то образом внедрить объект options из шага register() в наш ConfigService.
И конечно же, для этого мы будем использовать инъекцию зависимостей. Это ключевой момент, поэтому убедитесь,
что вы его понимаете. Наш ConfigModule предоставляет ConfigService. ConfigService в свою очередь зависит
от объекта options, который предоставляется только во время выполнения. Поэтому во время выполнения нам нужно
сначала связать объект options с IoC-контейнером Nest, а затем заставить Nest внедрить его в наш ConfigService.
Помните из главы Custom providers, что провайдеры могут
включать любое значение,
а не только сервисы, поэтому мы вполне можем использовать инъекцию зависимостей для работы с простым объектом options.
Давайте сначала займемся привязкой объекта options к IoC-контейнеру. Мы сделаем это в нашем статическом методе register().
Помните, что мы динамически конструируем модуль, а одним из свойств модуля является список провайдеров. Поэтому нам нужно
определить наш объект options как провайдер. Это сделает его инжектируемым в ConfigService, чем мы воспользуемся в следующем
шаге. В приведенном ниже коде обратите внимание на массив providers:
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({})
export class ConfigModule {
static register(options): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
ConfigService,
],
exports: [ConfigService],
};
}
}
Теперь мы можем завершить процесс инъекцией провайдера 'CONFIG_OPTIONS' в ConfigService. Напомним, что когда мы определяем
провайдера с помощью неклассового токена, нам нужно использовать декоратор @Inject() как описано здесь.
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { Injectable, Inject } from '@nestjs/common';
import { EnvConfig } from './interfaces';
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
constructor(@Inject('CONFIG_OPTIONS') private options) {
const filePath = `${process.env.NODE_ENV || 'development'}.env`;
const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
this.envConfig = dotenv.parse(fs.readFileSync(envFile));
}
get(key: string): string {
return this.envConfig[key];
}
}
И последнее замечание: для простоты мы использовали строковый маркер ('CONFIG_OPTIONS'), но лучше всего
определить его как константу (или Symbol) в отдельном файле и импортировать этот файл. Например:
export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';
# Пример
Полный пример кода из этой главы можно найти здесь (opens new window).