Skip to content

模块系统

一、@Module 装饰器详解

typescript
@Module({
  imports: [],      // 导入其他模块(使其 exports 的 Provider 在本模块可用)
  controllers: [],  // 本模块的 Controller(自动注册路由)
  providers: [],    // 本模块的 Provider(Service、Repository、Guard 等)
  exports: [],      // 对外暴露的 Provider(其他模块 import 本模块后可注入这些 Provider)
})
export class UsersModule {}

四个字段的职责边界:

字段作用类比
providers注册本模块私有 Provider声明私有变量
exports将 Provider 公开给外部导出公有 API
imports使用其他模块的公开 Provider引用外部依赖
controllers注册路由处理器接口声明

二、模块间依赖传递规则

正确的依赖传递

AppModule
├── imports: [UsersModule, PostsModule]

UsersModule
├── providers: [UserService, UserRepository]
└── exports: [UserService]      ← 只暴露 UserService,不暴露 UserRepository

PostsModule
├── imports: [UsersModule]      ← 导入 UsersModule
└── providers: [PostService]
    └── constructor(
          private postRepo: PostRepository,
          private userService: UserService   ← ✅ 可以注入(UsersModule exports 了它)
        )

关键规则:

  1. 未导出的 Provider 是模块私有的,外部永远无法注入
  2. imports 导入的是模块(Module 类),注入的是Provider(Service 类)
  3. 只有出现在 exports 中的 Provider,才能被导入该模块的其他模块使用

常见误区

typescript
// ❌ 错误:exports 了 UserService,但忘记在 providers 中声明
@Module({
  exports: [UserService],  // 报错:UserService 不在本模块的 providers 中
})
export class UsersModule {}

// ✅ 正确:先在 providers 声明,再 exports
@Module({
  providers: [UserService],
  exports: [UserService],
})
export class UsersModule {}

三、根模块(AppModule)

AppModule 是应用的入口模块,负责组装所有子模块:

typescript
@Module({
  imports: [
    ConfigModule.forRoot(),      // 全局配置
    TypeOrmModule.forRoot({...}), // 数据库
    UsersModule,
    PostsModule,
    AuthModule,
  ],
})
export class AppModule {}

原则: AppModule 本身不应该有业务逻辑(不定义 providers 和 controllers),只负责把其他模块组合在一起。


四、全局模块

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

标记为 @Global() 后,其他模块无需 imports: [ConfigModule],直接注入即可。

何时使用全局模块:

  • ConfigService:几乎每个模块都需要读取配置
  • LoggerService:全局日志服务
  • I18nService:国际化服务

⚠️ 谨慎使用: 过多全局模块会破坏模块边界的可读性。其他开发者看到 constructor(private config: ConfigService) 时,无法从模块定义推断出它来自哪里。优先使用显式 imports 声明依赖关系。


五、特性模块(Feature Module)

特性模块是按业务域组织代码的最佳实践:

src/
├── app.module.ts              # 根模块
├── users/                     # 用户业务域
│   ├── users.module.ts
│   ├── users.controller.ts
│   ├── users.service.ts
│   ├── users.repository.ts
│   ├── dto/
│   │   ├── create-user.dto.ts
│   │   └── update-user.dto.ts
│   └── entities/
│       └── user.entity.ts
├── posts/                     # 文章业务域
│   ├── posts.module.ts
│   └── ...
└── shared/                    # 跨模块共享
    ├── database/
    │   └── database.module.ts
    └── config/
        └── config.module.ts

按域划分 vs 按层划分:

❌ 按技术层划分(难以找到相关代码):
src/
├── controllers/
│   ├── users.controller.ts
│   └── posts.controller.ts
├── services/
│   ├── users.service.ts
│   └── posts.service.ts
└── repositories/
    ├── users.repository.ts
    └── posts.repository.ts

✅ 按业务域划分(相关代码聚合):
src/
├── users/          ← 用户所有代码都在这里
└── posts/          ← 文章所有代码都在这里

六、共享模块

当多个模块需要共用同一个 Service 时,将其提取为共享模块

typescript
// shared/database/database.module.ts
@Module({
  providers: [DatabaseService, UserRepository, PostRepository],
  exports: [DatabaseService, UserRepository, PostRepository],
})
export class DatabaseModule {}

// users/users.module.ts
@Module({
  imports: [DatabaseModule],  // 导入共享模块
  providers: [UserService],
  controllers: [UsersController],
})
export class UsersModule {}

共享模块的实例是单例的:

typescript
// 无论被多少模块导入,DatabaseService 只有一个实例
// 这是安全的(NestJS 默认单例模式保证)
UsersModule 导入 DatabaseModule → DatabaseService 实例 A
PostsModule 导入 DatabaseModule → DatabaseService 实例 A(同一个)

七、模块重新导出

模块可以重新导出它导入的模块,让消费者只需导入一个入口模块:

typescript
// infrastructure/infrastructure.module.ts
@Module({
  imports: [DatabaseModule, CacheModule, MailModule],
  exports: [DatabaseModule, CacheModule, MailModule],  // 重新导出
})
export class InfrastructureModule {}

// 其他模块只需导入 InfrastructureModule
@Module({
  imports: [InfrastructureModule],
  // 可以直接注入 DatabaseService、CacheService、MailService
})
export class UsersModule {}

八、模块懒加载

对于不常用的功能(如管理后台、批处理),可以按需加载:

typescript
@Injectable()
export class AppService {
  constructor(private lazyModuleLoader: LazyModuleLoader) {}

  async processAdminTask() {
    // 只在需要时才加载 AdminModule(减少启动时间)
    const { AdminModule } = await import('./admin/admin.module');
    const moduleRef = await this.lazyModuleLoader.load(() => AdminModule);

    const adminService = moduleRef.get(AdminService);
    await adminService.process();
  }
}

适用场景: Serverless 函数(冷启动优化)、后台任务模块、管理功能。


九、模块生命周期钩子

typescript
@Module({...})
export class UsersModule implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    // 模块初始化完成后执行
    // 适合:启动后台任务、预热缓存、建立长连接
    console.log('UsersModule initialized');
  }

  async onModuleDestroy() {
    // 应用关闭前执行
    // 适合:释放资源、关闭连接、刷新缓冲区
    console.log('UsersModule destroyed');
  }
}

完整的生命周期顺序:

onModuleInit()           ← 各模块按依赖顺序初始化
onApplicationBootstrap() ← 所有模块初始化后,开始监听请求前
  ...应用运行中...
onModuleDestroy()        ← 收到关闭信号(SIGTERM)时
beforeApplicationShutdown(signal) ← 即将关闭
onApplicationShutdown(signal)     ← 最终关闭

十、循环模块依赖

模块 A 导入模块 B,模块 B 又导入模块 A:

typescript
// ❌ 循环依赖
@Module({ imports: [AuthModule] }) class UsersModule {}
@Module({ imports: [UsersModule] }) class AuthModule {}

// ✅ 使用 forwardRef 临时解决
@Module({ imports: [forwardRef(() => AuthModule)] }) class UsersModule {}
@Module({ imports: [forwardRef(() => UsersModule)] }) class AuthModule {}

根本解决方案: 提取公共依赖。例如:把 UserService 中被 AuthModule 使用的部分提取到 CoreModule,两个模块都依赖 CoreModule,而不是互相依赖。


可运行 Demo: practice/07-extensions — 多模块组合示例,AppModule 注册 7 个功能模块


常见错误

错误原因解决
Cannot find module 启动报错模块未加入 AppModule.imports检查根模块的 imports 数组
全局模块重复注册报错@Global() 模块被多次 imports全局模块只在根模块注册一次
providersexports 不一致export 了未在 providers 声明的 tokenexports 的内容必须是 providers 的子集

NestJS 深度学习体系