# Динамические модули
Глава Модули охватывает основы модулей 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).