NestJS 的"模块+依赖注入"机制是公共模块能跨服务复用的底层保证。 本文件讲清 5 个核心概念,每一个都在示例里落地。
NestJS 的模块默认是单例:每个 provider 在应用内只有一个实例。
但要把这个实例"给别的模块用",必须 exports 出去。
@Module({
providers: [CatsService], // 本模块可注入
exports: [CatsService], // 导入本模块的模块也可注入(同一实例)
})
export class CatsModule {}理解:exports 决定"可见性",单例决定"身份"。多个服务各自
imports同一 全局模块,拿到的是同一个实例 —— 这就是"共享"的来源。
被 @Global() 标记的模块,其 exports 对整个应用可见,无需在每个模块 import。
@Global()
@Module({ providers: [LoggerService], exports: [LoggerService] })
export class LoggerModule {}
// 任何地方: constructor(private logger: LoggerService) {}使用边界:只对真正"到处都要"的基础设施用(日志/配置/鉴权)。 业务模块滥用全局会导致依赖不明、难以测试。
注意:
@Global()+ 动态模块时,forRoot返回对象里要写global: true(Nest 不自动合并装饰器与运行时返回值),见下方动态模块。
这是微服务复用公共模块的核心。同一个公共模块,通过静态方法返回配置化的
DynamicModule,每个服务传入自己的参数。
// 定义
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseOptions): DynamicModule {
return {
module: DatabaseModule,
global: true, // 运行时声明全局
providers: [
{ provide: 'DATABASE_OPTIONS', useValue: options },
{ provide: 'DATABASE_CONNECTION', useFactory: (o) => create(o), inject: ['DATABASE_OPTIONS'] },
DatabaseService,
],
exports: [DatabaseService],
};
}
}
// 使用:两个服务拿不同配置
SharedModule.forRoot({ database: { type: 'memory' } }) // orders
SharedModule.forRoot({ database: { type: 'postgres', host } }) // billing| 方法 | 语义 |
|---|---|
register() |
常规注册(可能被多次调用) |
forRoot() / forRootAsync() |
应用级只初始化一次(通常全局) |
forFeature() |
按功能/实体扩展 |
当配置来自 ConfigModule(需要异步读取)时使用注入式工厂:
static forRootAsync(options: {
useFactory: (...args: unknown[]) => DatabaseOptions | Promise<DatabaseOptions>;
inject?: unknown[];
}): DynamicModule {
return { module: X, providers: [{
provide: 'DATABASE_OPTIONS',
useFactory: options.useFactory,
inject: options.inject,
}, ...] };
}
// 使用
DatabaseModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
host: config.get('DB_HOST'),
port: config.get<number>('DB_PORT'),
}),
})Nest 允许把「类」之外的任何东西注册为 provider,让公共层注入的"能力"更灵活:
@Module({
providers: [
// useClass —— 指定实现类(可换实现)
{ provide: CacheService, useClass: RedisCacheService },
// useValue —— 注入字面量/实例
{ provide: 'APP_NAME', useValue: 'orders' },
// useFactory —— 工厂函数(可异步、可依赖其他 provider)
{ provide: 'REDIS_CLIENT', useFactory: (cfg) => new Redis(cfg), inject: ['DB_OPTIONS'] },
// useExisting —— 别名,指向已有 provider
{ provide: 'BillingClient', useExisting: SomeClient },
],
})
export class DemoModule {}在共享库中,useValue(配置)、useFactory(连接/客户端)、useClass(可插拔实现)
用得最多;配合 TOKENS(Symbol 常量)避免魔法字符串。
把散落的公共模块编排进一个 SharedModule,对外只暴露一个 forRoot:
@Global()
@Module({})
export class SharedModule {
static forRoot(options: SharedModuleOptions): DynamicModule {
const imports = [LoggerModule.forRoot({ appName: options.appName }), HealthModule];
if (options.database) imports.push(DatabaseModule.forRoot(options.database));
if (options.auth) imports.push(AuthModule.forRoot(options.auth));
return { module: SharedModule, global: true, imports, exports: imports };
}
}收益:微服务根模块从"几十行样板"收敛到:
@Module({
imports: [SharedModule.forRoot({ appName: 'orders', database: {...}, auth: {...} })],
})
export class AppModule {}这就是"抽离公共模块"的最终形态:接入方几乎零样板代码。
| 作用域 | 说明 | 场景 |
|---|---|---|
Scope.DEFAULT(单例) |
应用级一个实例 | 默认;无状态服务、连接池 |
Scope.REQUEST |
每个请求一个实例 | 需按请求隔离状态(多租户) |
Scope.TRANSIENT |
每次注入独立实例 | 每次都要全新实例的工具类 |
注意:REQUEST 作用域 provider 会向上传播(依赖它的 provider 也变 REQUEST),
对性能有影响,按需使用。配合 ModuleRef.resolve() 可动态解析
(见 docs/05 的 ModuleRef)。
| 机制 | 解决什么 | 对应示例 |
|---|---|---|
exports |
跨模块共享单例 | SharedModule |
@Global() |
基础设施免导入 | LoggerModule |
| 动态模块 | 一套代码多配置 | DatabaseModule.forRoot |
| 自定义 provider | 注入连接/配置/可插拔实现 | ClientsModule、DatabaseModule |
| 聚合模块 | 一行接入 | SharedModule.forRoot |