模块 Module
本教程共 47 篇 · 第 5 篇 · 更新于 2026-08-09 · 约 10 分钟阅读
本节目标:掌握 NestJS 模块系统的核心机制,学会用模块组织代码、控制依赖、共享服务。
模块是什么
模块是 NestJS 组织代码的基本单元。
打个比方:你的应用是一栋大楼,模块就是里面的一个个房间。每个房间有自己的功能(用户管理、订单处理、认证鉴权),房间之间通过门(导入导出)互通。
每个 NestJS 应用至少有一个模块——根模块 AppModule。它是整个应用的起点,NestJS 从它开始构建依赖关系图。
@Module 装饰器
用 @Module() 装饰器来定义一个模块,它接收一个配置对象:
@Module({
imports: [], // 导入其他模块
controllers: [], // 注册本模块的控制器
providers: [], // 注册本模块的服务
exports: [], // 导出服务,让别的模块能用
})
export class AppModule {}
四个属性,各有各的分工:
| 属性 | 作用 | 比喻 |
|---|---|---|
imports | 导入别的模块 | 打开门,借用别的房间的东西 |
controllers | 注册控制器 | 这个房间的前台接待 |
providers | 注册服务 | 这个房间里的干活人 |
exports | 导出服务 | 把房间里的某个人借给别的房间用 |
Note模块默认是”封装”的——别的模块不能直接用你的服务,除非你显式
exports出去。这和 Angular 的设计思路一样。
创建功能模块
小项目只有一个 AppModule 就够了。项目一大,你得按功能拆分模块。
用 CLI 创建一个用户模块:
nest g module users
生成的代码很简单:
import { Module } from '@nestjs/common';
@Module({})
export class UsersModule {}
然后把控制器和服务加进去:
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
最后在根模块里导入它:
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
@Module({
imports: [UsersModule],
})
export class AppModule {}
Tip用
nest g module创建模块时,CLI 会自动帮你把UsersModule加到AppModule的imports里。手动创建的话别忘了这一步。
模块的导入与导出
这是模块系统最核心的机制。
假设你有一个 AuthService,它需要用 UsersService 来查询用户信息。UsersService 在 UsersModule 里,AuthService 在 AuthModule 里。怎么让 AuthModule 用上 UsersService?
第一步:UsersModule 导出 UsersService。
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // 关键:导出
})
export class UsersModule {}
第二步:AuthModule 导入 UsersModule。
@Module({
imports: [UsersModule], // 关键:导入
providers: [AuthService],
})
export class AuthModule {}
这样 AuthService 就能通过构造函数注入 UsersService 了:
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
}
整个流程可以这样理解:
UsersModule AuthModule
┌──────────────────┐ ┌──────────────────┐
│ providers: │ │ imports: │
│ UsersService │──导出──→│ [UsersModule] │
│ exports: │ │ providers: │
│ [UsersService] │ │ AuthService │
└──────────────────┘ └──────────────────┘
↓
AuthService 可以
注入 UsersService
Note如果
UsersModule没有exports: [UsersService],AuthModule是拿不到UsersService的。很多人刚开始会忘记导出,然后发现注入报错——八成就是这个问题。
模块是单例
NestJS 里的模块都是单例的。什么意思呢?
不管有多少个模块导入了 UsersModule,UsersModule 在整个应用中只有一个实例。同理,它导出的 UsersService 也只有一个实例,所有模块共享同一个。
这带来两个好处:
- 省内存——不会重复创建多个实例
- 状态一致——所有模块操作的是同一份数据
Tip这也是为什么你不应该在 Service 里用
private users = []这种内存数组来存数据——虽然只有一个实例,但重启就没了。后面会接真正的数据库。
模块重导出
模块可以”转发”别的模块。比如你有一个 SharedModule,把常用的模块打包在一起:
@Module({
imports: [CommonModule, UtilsModule],
exports: [CommonModule, UtilsModule], // 重导出
})
export class SharedModule {}
别的模块只需要导入 SharedModule,就能同时使用 CommonModule 和 UtilsModule 导出的服务:
@Module({
imports: [SharedModule], // 一个 imports 搞定两个
})
export class UsersModule {}
这在大型项目里很实用。把公共依赖打包成一个共享模块,避免到处重复导入。
全局模块
每次用 UsersService 都要导入 UsersModule,写多了有点烦。能不能一次注册,到处使用?
可以,用 @Global() 装饰器:
import { Module, Global } from '@nestjs/common';
@Global()
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}
全局模块的服务在所有模块中都能直接注入,不需要在 imports 里声明:
@Injectable()
export class UsersService {
// 不用导入 ConfigModule,直接用
constructor(private configService: ConfigService) {}
}
全局模块只需要在根模块里注册一次:
@Module({
imports: [ConfigModule], // 注册一次,全局可用
})
export class AppModule {}
Tip全局模块很方便,但别滥用。只有那些真正”到处都要用”的服务才适合做成全局的,比如配置服务、日志服务。如果什么都全局,模块的封装意义就没了。
模块里也能注入服务
模块类本身也能通过构造函数注入服务。这在需要动态配置模块时很有用:
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
constructor(private catsService: CatsService) {}
}
Note模块类不能把自己注入到自己里面——这会导致循环依赖。模块里注入的服务必须是它自己的 providers 或从 imports 的模块中导出的。
循环依赖怎么办
有时候会出现 A 依赖 B、B 又依赖 A 的情况。比如 UsersModule 需要 AuthService,AuthModule 需要 UsersService。
NestJS 提供了 forwardRef 来解决这个问题:
// users.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [forwardRef(() => AuthModule)],
exports: [UsersService],
})
export class UsersModule {}
// auth.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { UsersModule } from '../users/users.module';
@Module({
imports: [forwardRef(() => UsersModule)],
exports: [AuthService],
})
export class AuthModule {}
服务层面也要用 forwardRef:
@Injectable()
export class UsersService {
constructor(
@Inject(forwardRef(() => AuthService))
private authService: AuthService,
) {}
}
Tip循环依赖通常说明设计有问题。如果两个模块互相依赖,考虑是不是该把它们合并,或者抽出一个公共模块来打破循环。
forwardRef是应急方案,不是长久之计。
模块生命周期
NestJS 的模块和应用都有生命周期钩子,让你在特定时机执行逻辑。
| 钩子 | 触发时机 |
|---|---|
onModuleInit() | 模块初始化完成后 |
onModuleDestroy() | 模块销毁前 |
onApplicationBootstrap() | 整个应用启动完成后 |
onApplicationShutdown() | 应用关闭前 |
使用方式:
import { Module, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
@Module({})
export class UsersModule implements OnModuleInit, OnModuleDestroy {
onModuleInit() {
console.log('UsersModule 初始化完成');
// 比如:预热缓存、建立数据库连接
}
onModuleDestroy() {
console.log('UsersModule 即将销毁');
// 比如:清理资源、关闭连接
}
}
Note生命周期钩子在 Provider(Service)上也能用,而且用得更多。模块级别的钩子适合做一些模块级别的初始化或清理工作。
模块组织建议
项目大了之后,模块怎么划分是个实际问题。几个原则:
按业务功能划分
src/modules/
├── users/ # 用户模块
├── auth/ # 认证模块
├── products/ # 商品模块
├── orders/ # 订单模块
└── payments/ # 支付模块
公共能力单独放
src/common/
├── database/ # 数据库连接
├── logger/ # 日志服务
├── config/ # 配置服务
└── guards/ # 全局守卫
每个模块职责单一
一个模块只管一件事。UsersModule 只管用户相关的逻辑,AuthModule 只管认证相关的逻辑。不要搞一个”万能模块”什么都往里塞。
小结
模块是 NestJS 的骨架,它决定了你的应用怎么组织。
核心就四件事:
imports:我要用别的模块的东西controllers:我有哪些控制器providers:我有哪些服务exports:我有哪些服务可以给别人用
记住几个要点:
- 模块默认封装,想共享必须
exports - 模块是单例,服务也是单例
- 全局模块方便但别滥用
- 循环依赖用
forwardRef应急,但最好重新设计