FORMA

模块系统与依赖注入

@Module 组织控制器与 Provider;依赖注入由 IoC 容器管理。见 导读基础

一、模块(@Module

模块是用 @Module() 装饰器声明的类,它将一组控制器、提供者等组织在一起。每个 Nest 应用至少有一个根模块(AppModule)。

1. 4 个核心属性

属性类型描述
importsArray<ModuleType>导入其他模块中导出的提供者(如服务、存储库)
controllersArray<ControllerType>该模块中需要实例化的控制器类
providersArray<ProviderType>该模块中可注入的服务、工厂等提供者
exportsArray<string | symbol | ProviderType>导出本模块的提供者,供其他模块使用(需在 imports 中导入本模块)
ts
import { Module } from "@nestjs/common";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";

@Module({
  imports: [], // 可导入其他模块
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService], // 允许其他模块使用 UsersService
})
export class UsersModule {}

2. 全局模块(@Global

使用 @Global() 装饰器的模块称为全局模块,其导出的提供者可在所有模块中直接注入,无需在 imports 中声明。

ts
@Global()
@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class CommonModule {}

// 任意模块中都可以直接注入 ConfigService

适用场景:通用基础设施服务(配置模块、日志模块、数据库连接池等),避免在每个模块中重复导入。

3. 模块重导出

模块可以重新导出其他模块的提供者,实现聚合导出。

ts
@Module({
  imports: [DatabaseModule, CacheModule],
  exports: [DatabaseModule, CacheModule], // 将这两个模块的提供者一并导出
})
export class DataAccessModule {}

这样其他模块只需导入 DataAccessModule 即可获得 DatabaseModuleCacheModule 的所有导出。

4. 动态模块(DynamicModule)

动态模块允许在导入时传递配置参数,从而动态生成模块的定义。

ts
export class ConfigModule {
  static forRoot(options: ConfigOptions): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: "CONFIG_OPTIONS",
          useValue: options,
        },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}

// 导入时
@Module({
  imports: [ConfigModule.forRoot({ path: "./config.json" })],
})
export class AppModule {}

常见的动态模块方法命名有:forRootforFeatureregister 等(如 TypeORM、Mongoose 模块)。

二、依赖注入(DI)

NestJS 使用 IoC 容器管理提供者的生命周期和依赖关系。

1. 构造函数注入(推荐) vs 属性注入

  • 构造函数注入:通过构造函数的参数声明依赖,清晰且易于测试。
ts
@Injectable()
export class UsersService {
  constructor(private readonly userRepository: UserRepository) {}
}
  • 属性注入:使用 @Inject() 装饰器直接注入到属性,适用于可选依赖或基类场景。
ts
@Injectable()
export class UsersService {
  @Inject("CONNECTION")
  private readonly connection: Connection;
}

2. 自定义提供者

Nest 提供多种方式定义提供者,超越简单的类注入。

类型描述示例
useClass指定替代类{ provide: Logger, useClass: FileLogger }
useValue注入常量/对象{ provide: 'PORT', useValue: 3000 }
useFactory工厂函数,可注入其他提供者{ provide: 'DB', useFactory: (config) => new DB(config), inject: [ConfigService] }
useExisting别名(用于重新映射提供者){ provide: MyService, useExisting: BetterService }
ts
@Module({
  providers: [
    { provide: "DB_NAME", useValue: "postgres" },
    {
      provide: "CONNECTION",
      useFactory: (dbName: string) => createConnection(dbName),
      inject: ["DB_NAME"],
    },
    { provide: Logger, useClass: FileLogger },
    { provide: OriginalService, useExisting: EnhancedService }, // 别名
  ],
})
export class AppModule {}

3. 异步提供者

useFactory 可以返回 Promise,使提供者异步创建。其他依赖该提供者的服务会等待其解析完成。

ts
{
  provide: 'ASYNC_DB',
  useFactory: async (config: ConfigService) => {
    const connection = await createDatabaseConnection(config);
    return connection;
  },
  inject: [ConfigService],
}

4. 循环依赖解决方案

当两个类互相依赖时,Nest 无法决定实例化顺序。解决方案:使用 forwardRef() 函数。

ts
@Injectable()
export class ServiceA {
  constructor(@Inject(forwardRef(() => ServiceB)) private serviceB: ServiceB) {}
}

@Module({
  imports: [forwardRef(() => ModuleB)],
})
export class ModuleA {}

最佳实践:尽量避免循环依赖,若无法避免,使用 forwardRef 并确保只在模块级别使用(不要过于深入)。

三、作用域(Scope)

提供者的生命周期由作用域决定。

作用域描述生命周期
SINGLETON(默认)整个应用只有一个实例,所有请求共享应用启动时创建,应用关闭时销毁
REQUEST每个 HTTP 请求创建一个新实例(请求处理完后销毁)针对每个请求单独创建
TRANSIENT每次注入(每个消费者)创建一个全新实例按需创建,不共享

使用 @Injectable({ scope: Scope.REQUEST })@Injectable({ scope: Scope.TRANSIENT }) 指定。

性能影响与适用场景

  • SINGLETON:性能最好,无额外开销。适合大多数无状态服务(如数据库服务、工具类、仓储)。
  • REQUEST:每个请求都会创建实例,会增加内存分配和 GC 压力。适合需要携带请求上下文的实例(如请求级缓存、用户会话数据)。
  • TRANSIENT:每次注入创建新实例,开销最大。适合状态独立、不应共享的服务(如某个特定计算对象、临时数据处理器)。

注意:REQUEST 和 TRANSIENT 作用域的提供者不能注入到 SINGLETON 提供者中(因为 SINGLETON 实例不会在请求结束后释放,会导致内存泄漏或状态混乱),除非通过 @Inject(forwardRef(...)) 或使用 Request 作用域的代理模式(NestJS 内部有处理,但需谨慎)。通常,尽量保持默认 SINGLETON 作用域。

总结

模块特性作用
imports / exports模块间依赖共享
@Global全局模块,避免重复导入
重导出聚合导出多个模块
动态模块带配置参数的模块导入
DI 特性描述
构造函数注入推荐注入方式,便于测试
自定义提供者useValueuseClassuseFactoryuseExisting
异步提供者工厂可返回 Promise,依赖会等待
forwardRef解决循环依赖
作用域适用场景
SINGLETON(默认)无状态服务
REQUEST需要请求级状态
TRANSIENT每次需全新实例

掌握这些概念,可以设计出清晰、可测试、高性能的 NestJS 应用。

参考文献

以下链接在编写时均可正常访问:

资料说明
NestJS 文档官方
Modules本章主题
Request lifecycle执行顺序