首页 / NestJS 入门教程 / 动态模块

NestJS 入门教程

动态模块

本教程共 47 篇 · 第 10 篇 · 更新于 2026-08-09 · 约 10 分钟阅读

NestJS动态模块forRootregisterConfigurableModuleBuilder模块配置

本节目标:理解动态模块的用途,学会用 forRoot/register 模式创建可配置的模块。

静态模块的局限

前面学的模块都是”静态”的——在 @Module() 装饰器里写死了 imports、providers、exports:

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

这种方式有个问题:模块的使用者没法影响模块的行为。

比如你写了一个 ConfigModule,想让别人能指定 .env 文件的路径。静态模块做不到——配置在编译时就写死了。

动态模块是什么

动态模块就是”运行时创建的模块”。它不是用 @Module() 装饰器定义的,而是用一个静态方法返回的。

使用时的区别:

// 静态导入——没法配置
@Module({
  imports: [ConfigModule],
})
export class AppModule {}

// 动态导入——可以传配置
@Module({
  imports: [ConfigModule.register({ folder: './config' })],
})
export class AppModule {}

ConfigModule.register() 返回一个 DynamicModule 对象。你在调用时可以传入配置参数,模块会根据这些参数动态生成 providers。

打个比方:静态模块像买成衣,尺码固定。动态模块像定制西装,你告诉裁缝你的尺寸,他给你量身定做。

DynamicModule 的结构

动态模块返回的对象和普通模块的 @Module() 配置几乎一样,多了一个 module 属性:

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

@Module({})
export class ConfigModule {
  static register(options: ConfigOptions): DynamicModule {
    return {
      module: ConfigModule,  // 必须有,指向模块类本身
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,  // 把传入的配置注册为 Provider
        },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}

module: ConfigModule 是必须的——告诉 NestJS 这是哪个模块。其他属性(providersimportsexportscontrollers)和静态模块一样。

Note

除了 module 属性是必须的,其他属性都可以省略。你可以返回一个只有 moduleproviders 的动态模块。

配置怎么传进去的

整个流程分三步:

第一步:使用方调用 register() 传入配置。

ConfigModule.register({ folder: './config' })

第二步register() 把配置对象注册为一个 Provider。

providers: [
  {
    provide: 'CONFIG_OPTIONS',
    useValue: options,  // 配置对象变成 Provider 的值
  },
  ConfigService,
]

第三步ConfigService 通过 @Inject() 拿到配置。

@Injectable()
export class ConfigService {
  constructor(
    @Inject('CONFIG_OPTIONS') private options: ConfigOptions,
  ) {
    // 现在可以用 options.folder 了
    const filePath = path.resolve(options.folder, '.env');
    this.envConfig = dotenv.parse(fs.readFileSync(filePath));
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}
Tip

核心思路:把传入的配置选项变成一个 Provider,然后通过依赖注入传给 Service。这就是 NestJS 处理动态配置的标准模式。

forRoot / register / forFeature

你可能见过不同的方法名:forRootregisterforFeature。它们没有技术上的区别,只是社区约定:

方法名用途使用次数
forRoot()全局配置,通常在根模块调用一次一次
register()注册一个可配置的模块多次
forFeature()forRoot 基础上,为特定模块做额外配置多次

举个例子:

// 数据库配置——全局一次
@Module({
  imports: [TypeOrmModule.forRoot({ type: 'mysql', ... })],
})
export class AppModule {}

// 用户模块——注册特定功能
@Module({
  imports: [HttpModule.register({ baseUrl: 'https://api.example.com' })],
})
export class UsersModule {}

// 特定模块的功能配置
@Module({
  imports: [TypeOrmModule.forFeature([User])],
})
export class UsersModule {}

每个方法都有对应的异步版本:forRootAsyncregisterAsyncforFeatureAsync

异步配置

有时候配置本身也需要异步获取——比如从远程服务拉取配置。这时候用异步版本:

@Module({
  imports: [
    ConfigModule.registerAsync({
      useFactory: async () => {
        const response = await fetch('https://config-service/api/config');
        return await response.json();
      },
    }),
  ],
})
export class AppModule {}

也可以注入其他 Provider 来获取配置:

@Module({
  imports: [
    ConfigModule.registerAsync({
      imports: [EnvModule],
      useFactory: (envService: EnvService) => {
        return {
          folder: envService.getConfigFolder(),
        };
      },
      inject: [EnvService],
    }),
  ],
})
export class AppModule {}

全局动态模块

动态模块也可以设置 global: true 变成全局模块:

static register(options: ConfigOptions): DynamicModule {
  return {
    module: ConfigModule,
    global: true,  // 全局可用
    providers: [
      {
        provide: 'CONFIG_OPTIONS',
        useValue: options,
      },
      ConfigService,
    ],
    exports: [ConfigService],
  };
}
Tip

@nestjs/configConfigModule.forRoot({ isGlobal: true }) 就是这么干的。

ConfigurableModuleBuilder

手动写 registerregisterAsync 挺繁琐的。NestJS 11 提供了 ConfigurableModuleBuilder 来简化这个过程。

第一步:定义配置接口。

export interface ConfigModuleOptions {
  folder: string;
}

第二步:创建模块定义文件。

// config.module-definition.ts
import { ConfigurableModuleBuilder } from '@nestjs/common';

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

第三步:让模块继承生成的基类。

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

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

就这样,ConfigModule 自动获得了 register()registerAsync() 两个方法。

第四步:在 Service 里注入配置。

@Injectable()
export class ConfigService {
  constructor(
    @Inject(MODULE_OPTIONS_TOKEN) private options: ConfigModuleOptions,
  ) {}
}

使用:

@Module({
  imports: [
    ConfigModule.register({ folder: './config' }),
    // 或者异步版本:
    // ConfigModule.registerAsync({
    //   useFactory: () => ({ folder: './config' }),
    // }),
  ],
})
export class AppModule {}
Note

ConfigurableModuleBuilder 默认生成 registerregisterAsync。如果你想用 forRoot,可以这样配置:

new ConfigurableModuleBuilder<ConfigModuleOptions>()
  .setClassMethodName('forRoot')
  .build();

额外选项

有些选项(比如 isGlobal)不应该出现在 MODULE_OPTIONS_TOKEN 里——Service 不需要知道模块是不是全局的。用 setExtras 处理:

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
  new ConfigurableModuleBuilder<ConfigModuleOptions>()
    .setExtras(
      { isGlobal: true },  // 默认值
      (definition, extras) => ({
        ...definition,
        global: extras.isGlobal,  // 映射到 DynamicModule 的 global 属性
      }),
    )
    .build();

使用时 isGlobal 不会出现在注入的 options 里:

ConfigModule.register({
  isGlobal: true,    // 控制模块是否全局
  folder: './config', // 传给 Service 的配置
})

实际案例:数据库模块

import { Module, DynamicModule } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

interface DatabaseOptions {
  type: 'mysql' | 'postgres';
  host: string;
  port: number;
  database: string;
  entities: any[];
  synchronize: boolean;
}

@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseOptions): DynamicModule {
    return {
      module: DatabaseModule,
      imports: [
        TypeOrmModule.forRoot({
          type: options.type,
          host: options.host,
          port: options.port,
          database: options.database,
          entities: options.entities,
          synchronize: options.synchronize,
        }),
      ],
      exports: [TypeOrmModule],
    };
  }

  static forFeature(entities: any[]): DynamicModule {
    return {
      module: DatabaseModule,
      imports: [TypeOrmModule.forFeature(entities)],
      exports: [TypeOrmModule],
    };
  }
}

使用:

@Module({
  imports: [
    DatabaseModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      database: 'mydb',
      entities: [User, Product],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

小结

动态模块让你的模块变成”可配置的插件”。

核心要点:

  • 动态模块就是一个返回 DynamicModule 对象的静态方法
  • module 属性必须指向模块类本身
  • 把配置选项注册为 Provider,通过 DI 传给 Service
  • forRoot 全局配置一次,forFeature 模块级别配置
  • ConfigurableModuleBuilder 自动生成 registerregisterAsync,减少样板代码

你日常开发中用 @nestjs/typeorm@nestjs/config 这些包时,用的 forRoot() 就是动态模块。现在你知道它底层是怎么回事了。