模块系统
一、@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 了它)
)关键规则:
- 未导出的 Provider 是模块私有的,外部永远无法注入
imports导入的是模块(Module 类),注入的是Provider(Service 类)- 只有出现在
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 | 全局模块只在根模块注册一次 |
providers 和 exports 不一致 | export 了未在 providers 声明的 token | exports 的内容必须是 providers 的子集 |