跳转至

定义动态模块

到目前为止,我们已经定义了许多可以组提供和控制器的不同模块。 不过,他们都不是可定制的。 在本文中,我们学习了如何创建可自定义的动态模块,从而使它们更易于重复使用。在这样做的过程中,我们重写了一些旧代码以使用动态模块。

如果您想进一步了解有关模块的更多信息,请使用 Nestjs#6 查看 API。研究依赖注入和模块

为了更好地说明为什么我们可能想使用动态模块,让我们看一下使用 Nestjs#25 在 API 中定义的电子邮件模块。 发送带有 Cron 和 NodeMailer 文章的预定电子邮件。

email.module.ts
import { Module } from '@nestjs/common';
import EmailService from './esmail.service';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule],
  controllers: [],
  providers: [EmailService],
  exports: [EmailService],
})
export class EmailModule {}

以上模块包含电子邮件服务提供商并将其导出。让我们在引擎盖下看:

email.service.ts
import { Injectable } from '@nestjs/common';
import { createTransport } from 'nodemailer';
import * as Mail from 'nodemailer/lib/mailer';
import { ConfigService } from '@nestjs/config';

@Injectable()
export default class EmailService {
  private nodemailerTransport: Mail;

  constructor(private readonly configService: ConfigService) {
    this.nodemailerTransport = createTransport({
      service: configService.get('EMAIL_SERVICE'),
      auth: {
        user: configService.get('EMAIL_USER'),
        pass: configService.get('EMAIL_PASSWORD'),
      },
    });
  }

  sendMail(options: Mail.Options) {
    return this.nodemailerTransport.sendMail(options);
  }
}

上面要注意的关键之处在于,我们的电子邮件服务始终具有相同的配置。 因此,例如,我们不能为密码确认和新闻通讯单独的电子邮件提供单独的电子邮件。 为了解决这个问题,我们可以创建一个动态模块。

创建动态模块

要创建动态模块,让我们在电子邮件模块中添加静态方法。

email.module.ts
import { DynamicModule, Module } from '@nestjs/common';
import EmailService from './email.service';
import { EMAIL_CONFIG_OPTIONS } from './constants';
import EmailOptions from './emailOptions.interface';

@Module({})
export class EmailModule {
  static register(options: EmailOptions): DynamicModule {
    return {
      module: EmailModule,
      providers: [
        {
          provide: EMAIL_CONFIG_OPTIONS,
          useValue: options,
        },
        EmailService,
      ],
      exports: [EmailService],
    };
  }
}

上面,我们没有将模块定义放入`@module({})decorator''中,而是从寄存器方法返回。 我们允许用户提供电子邮件将作为寄存器功能的参数。

emailOptions.interface.ts
1
2
3
4
5
6
7
interface EmailOptions {
  service: string;
  user: string;
  password: string;
}

export default EmailOptions;

我们还在单独的文件中定义了唯一的 EMAIL_CONFIG_OPTIONS 常数。

constants.ts
export const EMAIL_CONFIG_OPTIONS = 'EMAIL_CONFIG_OPTIONS';

我们也可以使用一个符号。

register()方法中,我们将EMAIL_CONFIG_OPTIONS'添加到提供商列表中,以及选项的值。 多亏了上述内容,该配置将通过@inject()装饰器在电子邮件服务中提供。

email.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { createTransport } from 'nodemailer';
import { EMAIL_CONFIG_OPTIONS } from './constants';
import Mail from 'nodemailer/lib/mailer';
import EmailOptions from './emailOptions.interface';

@Injectable()
export default class EmailService {
  private nodemailerTransport: Mail;

  constructor(@Inject(EMAIL_CONFIG_OPTIONS) private options: EmailOptions) {
    this.nodemailerTransport = createTransport({
      service: options.service,
      auth: {
        user: options.user,
        pass: options.password,
      },
    });
  }

  sendMail(options: Mail.Options) {
    return this.nodemailerTransport.sendMail(options);
  }
}

由于我们的方法,我们现在可以在导入该模块时配置该模块。

emailConfirmation.module.ts
import { Module } from '@nestjs/common';
import { EmailConfirmationService } from './emailConfirmation.service';
import { ConfigModule } from '@nestjs/config';
import { EmailModule } from '../email/email.module';
import { JwtModule } from '@nestjs/jwt';
import { EmailConfirmationController } from './emailConfirmation.controller';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [
    ConfigModule,
    EmailModule.register({
      service: 'gmail',
      user: 'email.account@gmail.com',
      password: 'mystrongpassword',
    }),
    JwtModule.register({}),
    UsersModule,
  ],
  providers: [EmailConfirmationService],
  exports: [EmailConfirmationService],
  controllers: [EmailConfirmationController],
})
export class EmailConfirmationModule {}

异步配置

上述方法存在重大缺点,因为在设置我们的电子邮件提供商时,我们不再可以使用 ConfigService。 幸运的是,我们可以通过创建寄存器方法的异步版本来解决这个问题。它将可以访问 Nestjs 内置的依赖项注入机制。

我们的 registerync 方法还将接收选项,但是其参数的类型与寄存器略有不同。

1
2
3
4
5
6
7
8
9
EmailModule.registerAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (configService: ConfigService) => ({
    service: configService.get('EMAIL_SERVICE'),
    user: configService.get('EMAIL_USER'),
    password: configService.get('EMAIL_PASSWORD'),
  }),
}),

让我们浏览以上所有属性: Let’s go through all of the properties above:

  • imports – 我们希望将 emailModule 导入的模块列表,因为我们需要它们在 useFactory 中,
  • inject – 我们希望 Nestjs 注入 useFactory 函数的上下文的提供商列表,
  • useFactory – 返回我们的 email_config_options 提供商的值的函数。

使用上述属性创建类型的一种直接方法是使用 Modulemetadata 和 Factory -Provider 接口。

emailOptions.type.ts
1
2
3
4
5
6
7
8
import { ModuleMetadata } from '@nestjs/common';
import EmailOptions from './emailOptions.interface';
import { FactoryProvider } from '@nestjs/common/interfaces/modules/provider.interface';

type EmailAsyncOptions = Pick<ModuleMetadata, 'imports'> &
  Pick<FactoryProvider<EmailOptions>, 'useFactory' | 'inject'>;

export default EmailAsyncOptions;

请注意,FactoryProvider 是通用的,我们将电子邮件发送给它以执行正确的配置。

一旦拥有以上内容,我们就可以定义我们的 registerync 方法。

email.module.ts
import { DynamicModule, Module } from '@nestjs/common';
import EmailService from './email.service';
import { EMAIL_CONFIG_OPTIONS } from './constants';
import EmailOptions from './emailOptions.interface';
import EmailAsyncOptions from './emailAsyncOptions.type';

@Module({})
export class EmailModule {
  static register(options: EmailOptions): DynamicModule {
    return {
      module: EmailModule,
      providers: [
        {
          provide: EMAIL_CONFIG_OPTIONS,
          useValue: options,
        },
        EmailService,
      ],
      exports: [EmailService],
    };
  }

  static registerAsync(options: EmailAsyncOptions): DynamicModule {
    return {
      module: EmailModule,
      imports: options.imports,
      providers: [
        {
          provide: EMAIL_CONFIG_OPTIONS,
          useFactory: options.useFactory,
          inject: options.inject,
        },
        EmailService,
      ],
      exports: [EmailService],
    };
  }
}

在引擎盖下,我们的 registerync 方法与登记非常相似。区别在于它使用 USEFACTORY 和 INDECT 而不是 UseValue。它还接受了一系列其他模块以通过 options.imports 导入。

由于创建了一种使 EmailModule 异步配置的方法,因此我们现在可以根据用例使用各种配置,并且仍然使用依赖项注入机制。

1
2
3
4
5
6
7
8
9
EmailModule.registerAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (configService: ConfigService) => ({
    service: configService.get('NEWSLETTER_EMAIL_SERVICE'),
    user: configService.get('NEWSLETTER_EMAIL_USER'),
    password: configService.get('NEWSLETTER_EMAIL_PASSWORD'),
  }),
}),

命名约定

到目前为止,我们仅定义了寄存器和寄存器方法。值得注意的是,Nestjs 没有执行任何命名惯例,但有一些准则。

注册和注册

到目前为止,我们在本文中使用的寄存器和寄存器方法应该将模块配置为仅与导入其导入的模块一起使用。

为了说明这一点,让我们看一下电子邮件 schedulingmodule。

emailScheduling.module.ts
import { Module } from '@nestjs/common';
import EmailSchedulingService from './emailScheduling.service';
import { EmailModule } from '../email/email.module';
import EmailSchedulingController from './emailScheduling.controller';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    EmailModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) =&gt; ({
        service: configService.get('EMAIL_SERVICE'),
        user: configService.get('EMAIL_USER'),
        password: configService.get('EMAIL_PASSWORD'),
      }),
    }),
  ],
  controllers: [EmailSchedulingController],
  providers: [EmailSchedulingService],
})
export class EmailSchedulingModule {}

上面,我们为电子邮件模块提供了仅在 EmailSchedulingModule 中使用的 EmailModule 的配置。

forRoot 和 forRootAsync

使用 Forroot 和 Forrootasync 方法,我们旨在一次配置一个动态模块,并在多个位置重复使用此配置。因此,它对全球模块很有意义。

一个很好的例子是 Nestjs 提供的 TypeModule 模块。

database.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import Address from '../users/address.entity';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        username: configService.get('POSTGRES_USER'),
        password: configService.get('POSTGRES_PASSWORD'),
        entities: [Address],
        autoLoadEntities: true,
        // ...
      }),
    }),
  ],
})
export class DatabaseModule {}

forFeature 和 forFeatureAsync

使用 FortFeature 和 Forfeatureasync 方法,我们可以更改使用 ForroTasync 指定的配置。同样,一个很好的例子是 typeormmodule。

categories.module.ts
import { Module } from '@nestjs/common';
import CategoriesController from './categories.controller';
import CategoriesService from './categories.service';
import Category from './category.entity';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([Category])],
  controllers: [CategoriesController],
  providers: [CategoriesService],
})
export class CategoriesModule {}

上面,我们使用仅适用于类别模块的 Forfeature()指定其他配置。

Configurable 模块构建器

配置动态模块可能很困难,尤其是使用异步方法。 幸运的是,Nestjs 拥有一个 ConfigurableModuleBuilder 类,为我们完成了许多工作。要使用它,让我们创建一个单独的文件。

email.module-definition.ts
1
2
3
4
5
6
7
import { ConfigurableModuleBuilder } from '@nestjs/common';
import EmailOptions from './emailOptions.interface';

export const {
  ConfigurableModuleClass: ConfigurableEmailModule,
  MODULE_OPTIONS_TOKEN: EMAIL_CONFIG_OPTIONS,
} = new ConfigurableModuleBuilder<EmailOptions>().build();

Now, we need our EmailModule to extend the ConfigurableEmailModule class.

email.module.ts
1
2
3
4
5
6
7
8
9
import { Module } from '@nestjs/common';
import { ConfigurableEmailModule } from './email.module-definition';
import EmailService from './email.service';

@Module({
  providers: [EmailService],
  exports: [EmailService],
})
export class EmailModule extends ConfigurableEmailModule {}

多亏了上述内容,我们的电子邮件模块允许我们同时使用 register 和 registerync 类。

为了使其按预期工作,我们需要记住使用我们从 email.module-definition.ts 文件中获得的 email_config_options 常数。

扩展自动生成的方法

如果我们需要其他逻辑,我们可以扩展自动生成的方法。这样做时,configurablemoduleasyncoptions 可能会派上用场。

import {
  ConfigurableModuleAsyncOptions,
  DynamicModule,
  Module,
} from '@nestjs/common';
import { ConfigurableEmailModule } from './email.module-definition';
import EmailService from './email.service';
import EmailOptions from './emailOptions.interface';

@Module({
  providers: [EmailService],
  exports: [EmailService],
})
export class EmailModule extends ConfigurableEmailModule {
  static register(options: EmailOptions): DynamicModule {
    return {
      // you can put additional configuration here
      ...super.register(options),
    };
  }

  static registerAsync(
    options: ConfigurableModuleAsyncOptions<EmailOptions>,
  ): DynamicModule {
    return {
      // you can put additional configuration here
      ...super.registerAsync(options),
    };
  }
}

使用寄存器和登记册除外的方法

如果我们要使用寄存器和 registerync 以外的其他方法,则需要调用 setClassMethodname 函数。

email.module-definition.ts
1
2
3
4
5
6
7
8
9
import { ConfigurableModuleBuilder } from '@nestjs/common';
import EmailOptions from './emailOptions.interface';

export const {
  ConfigurableModuleClass: ConfigurableEmailModule,
  MODULE_OPTIONS_TOKEN: EMAIL_CONFIG_OPTIONS,
} = new ConfigurableModuleBuilder<EmailOptions>()
  .setClassMethodName('forRoot')
  .build();

概括

在本文中,我们经历了动态模块的想法。 为了说明它,我们手动实施了它,并了解了它在引擎盖下的工作原理。 然后,我们学会了如何使用内置在 Nestjs 中的实用程序,这些实用程序可以显着简化此过程。 动态模块是一个方便的功能,它使我们能够在利用依赖性注入机制的同时使我们的模块可自定义。