Bootstrap

Nest Dynamic modules 笔记

Nest Dynamic modules 文档地址👈

记录Dynamic modules是因为确实抽象,文档并没有很详细的指出不同方式创建动态模块的区别

静态模块

静态模块非常好理解,就是使用imports关键字导入其他的模块,并在@Moduleimports属性中加上,代码如下:

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 {}

导入之后便可以使用UsersModule内的Server,或者说注入,代码如下:

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中,如果只是在AuthService 文件内使用import通过路径导入UsersService ,没有在模块内导入UsersModule模块,那么在构造函数中加入UsersService也是报错的,因为Nest的机制是以模块为单位

换句话说,想在某个服务下引用其他模块的服务,必须先导入该服务所在的模块

传统动态模块方式实现

动态模块的使用场景,或者说为什么要有动态模块的出现,原因在于开发场景的不同,例如一个系统可能会在不同周期使用不同的数据库?就好比开发也有生产环境、开发环境、测试环境,不同环境下需要导入的.env或许有些不同

所以动态模块就是可以根据不同的条件去导入不同的Module,进而,使用不同的Service

在文档中,讲述的方式是一种倒叙的形式,Let's see how this works. It's helpful if we start from the end-goal of how this might look from the consuming module's perspective, and then work backwards.

  1. 假设需要导入一个用于配置的模块
    静态模式下的代码如下:
@Module({
  imports: [ConfigModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  1. 因为是动态的,所以这个ConfigModule就应该有一个接口方便开发人员去替换不同的配置,例如可以传入一个配置文件的路径,然后通过fs便可以读取里面的内容,代码如下:
@Module({
  imports: [ConfigModule.register({ folder: './config' })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  1. 想象一下,一个模块包含哪些内容?有importscontrollersprovidersexports,所以调用ConfigModule.register({ folder: './config' })返回的就是一个包含上述内容的东西。而ConfigModule内部包含有一个静态方法register,代码如下:
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],
    };
  }
}
  1. 传递的{ folder: './config' }如何进行消费?代码如下:
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
  static register(options: Record<string, any>): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}

注:在TypeScript中,Record<K, T>是一个内置的工具类型,它用于构建一个对象类型,该对象的所有属性键都属于类型K,而对应的属性值则属于类型T

注:在Nest文档中推荐将'CONFIG_OPTIONS'定义为单独文件中的常量,也就是唯一值

静态方法register接收一个配置选项,然后作为提供者'CONFIG_OPTIONS'的值,被注入到了ConfigService中,代码如下:

import * as dotenv from 'dotenv';
import * as fs from 'fs'; // 前面提到的fs
import * as path from 'path';
import { Injectable, Inject } from '@nestjs/common';
import { EnvConfig } from './interfaces';

@Injectable()
export class ConfigService {
  private readonly envConfig: EnvConfig;
  // 注入'CONFIG_OPTIONS',拿到{ folder: './config' }
  constructor(@Inject('CONFIG_OPTIONS') private options: Record<string, any>) {
  	// 根据运行环境找到.env文件
    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];
  }
}

这里有一个细节就是为什么要用 dotenv.parse(),很简单,想想.env文件是怎么存储内容的,KEY=value

那么至此,就完成了可以通过传入不同的options.folder,去形成不同的ConfigService,其他的模块使用的便是动态ConfigService

三种不同的方法命名

在上面的静态方法中,方法的命名为register,这是一个社区规范,或者说默认情况下就是这个名字,此外还有两种,分别是forRootforFeature

forRoot很好理解,Root是根的意思,该方法是用于全局或者基础配置,是用在AppModule中的,一般只会调用一次

forFeature则用于特定功能或者子模块的配置

总的来说,就是通过命名向其他的开发者传达当前这个动态模块是用于什么场景的

使用ConfigurableModuleBuilder

Nest的官方推荐新手使用ConfigurableModuleBuilder去搭建动态模块

  1. 定义接收参数的类型,例如{ folder: './config' },就需要定义folder:string,代码如下:
export interface ConfigModuleOptions {
  folder: string;
}
  1. 调用ConfigurableModuleBuilder,构造动态模块,代码如下:
// config.module-definition
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { ConfigModuleOptions } from './interfaces/config-module-options.interface';

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
  new ConfigurableModuleBuilder<ConfigModuleOptions>().build();
  1. config.module.ts里利用生成的动态模块,代码如下:
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ConfigurableModuleClass } from './config.module-definition';

@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule extends ConfigurableModuleClass {}

这里非常抽象,但我们先看其他的步骤

  1. 在根模块内,或者说导入动态模块的模块内,代码如下:
@Module({
  imports: [
    ConfigModule.register({ folder: './config' }),
    // or alternatively:
    // ConfigModule.registerAsync({
    //   useFactory: () => {
    //     return {
    //       folder: './config',
    //     }
    //   },
    //   inject: [...any extra dependencies...]
    // }),
  ],
})
export class AppModule {} // 这里是根模块
  1. configService内,需要将'CONFIG_OPTIONS'替换为在文件config.module-definition导出的'MODULE_OPTIONS_TOKEN',代码如下:
@Injectable()
export class ConfigService {
  constructor(@Inject(MODULE_OPTIONS_TOKEN) private options: ConfigModuleOptions) { ... }
}

对比在哪?第一个是在config.module.ts,从

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
  static register(options: Record<string, any>): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}

变成了👇

import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ConfigurableModuleClass } from './config.module-definition';

@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule extends ConfigurableModuleClass {}

减去了return里面的代码和静态方法return笔者不觉得这样更适合新手,非常抽象

第二个变化就是'CONFIG_OPTIONS'变成了'MODULE_OPTIONS_TOKEN'

所以ConfigurableModuleBuilder的作用就是,直接帮开发者省去了构造register方法和return的流程,但抽象的是在ConfigModule里的register哪来的?所以这就是抽象,在阅读官方文档时真的绕

从这里也可以看出,使用ConfigurableModuleBuilder后,在AppModule 使用的方法是register,所以说明这是默认的命名,如果要改名怎么办?比方说使用forRoot,需要调用.setClassMethodName(),代码如下:

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

异步动态模块

在上面的例子中,是主动的传入一个配置项,例如路径,但如果配置需要异步获取呢?需要等待Promise解析后才能得到呢?

Nest提供了registerAsync(或者forRootAsync)方法,代码如下:

@Module({
  imports: [
    ConfigModule.registerAsync({
      useClass: ConfigModuleOptionsFactory,
    }),
  ],
})
export class AppModule {}

这里抽象的是,ConfigModuleOptionsFactory必须要有一个create方法,如果没有,那么会在解析的过程中出现问题,并且文档并没有给出ConfigModuleOptionsFactory究竟是怎么样的,但可以模拟一下,代码如下:

import { Injectable } from '@nestjs/common';
import { ConfigModuleOptions } from './config.module-definition';

@Injectable()
export class ConfigModuleOptionsFactory {
  async create(): Promise<ConfigModuleOptions> {
    // 这里可以进行异步操作,例如从数据库或文件系统加载配置
    return {
      folder: 'path/to/config/folder',
    };
  }
}

这个create方法可以自定义取名,例如改成createConfigOptions,代码如下:

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
  new ConfigurableModuleBuilder<ConfigModuleOptions>().setFactoryMethodName('createConfigOptions').build();

一切的目的,都是让开发者通过命名清楚这个动态模块是干什么的,见名知意

如果想要把动态模块全局化

代码如下:

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<ConfigModuleOptions>()
  .setExtras(
    {
      isGlobal: true,
    },
    (definition, extras) => ({
      ...definition,
      global: extras.isGlobal,
    }),
  )
  .build();

.setExtras()的第一个形参名字是extras,所以第二个函数参数里的extras.isGlobal就是true,这样设置之后,就可以在AppModule内加多一个isGlobal属性,代码如下:

@Module({
  imports: [
    ConfigModule.register({
      isGlobal: true,
      folder: './config',
    }),
  ],
})
export class AppModule {}

延缓执行动态模块

Dynamic modules的最后一个章节中,提供了延缓执行动态模块的方法,代码如下:

import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ConfigurableModuleClass, ASYNC_OPTIONS_TYPE, OPTIONS_TYPE } from './config.module-definition';

@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule extends ConfigurableModuleClass {
  static register(options: typeof OPTIONS_TYPE): DynamicModule {
    return {
      // your custom logic here
      ...super.register(options),
    };
  }

  static registerAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
    return {
      // your custom logic here
      ...super.registerAsync(options),
    };
  }
}

options传进来之后,在返回模块之前,可以执行想要的逻辑,例如日志记录,调用了哪个动态模块,就可以在这里加上了

;