第 5 章:模块
学习目标
- 理解 Module 的组织作用
- 掌握 imports/exports/providers/controllers
- 学会全局模块和动态模块
- 避开 4 个模块拆分坑
一、Module 是干嘛的
NestJS 用 Module 把 Controller、Service、其它 Module 组织起来,一个应用就是 Module 树。
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // 暴露给其它 Module
})
export class UsersModule {}根模块:
// app.module.ts
@Module({
imports: [UsersModule],
})
export class AppModule {}二、四大配置项
| 配置 | 含义 |
|---|---|
imports | 引入其他 Module,才能用它们导出的 Provider |
controllers | 当前 Module 的 Controller |
providers | 当前 Module 的 Provider |
exports | 当前 Module 哪些 Provider 可被其他 Module 注入 |
三、模块导出与共享
// common.module.ts
@Module({
providers: [LoggerService],
exports: [LoggerService], // 关键:不导出别人用不了
})
export class CommonModule {}
// users.module.ts
@Module({
imports: [CommonModule], // 拉进来才能注入
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
// users.service.ts
@Injectable()
export class UsersService {
constructor(private readonly logger: LoggerService) {}
// ↑ 来自 CommonModule,因为 CommonModule exports 了它
}⚠️ 坑 1:Service 写在 A 模块,要从 A 模块
exports出来,B 模块imports: [A],B 才能注入。
Provider 消费路径(谁可以注入谁):
- 同 Module 的 Controller/Service —— 自动注入,不用任何配置(
providers注册即可) - 跨 Module 的 Controller/Service —— 需
exports(源 Module) +imports(目标 Module) - Controller 本身 —— 不需要在
providers注册,通过构造器注入消费 Service 即可
┌─ users.module.ts ─────────────────┐
│ providers: [UsersService] │
│ exports: [UsersService] ← 跨模块开关│
└────────────────────────────────────┘
↓ exports
┌─ orders.module.ts ────────────────┐
│ imports: [UsersModule] ← 拉进来│
│ providers: [OrdersService] │
└────────────────────────────────────┘
↓ OrdersService 可注入 UsersService反例:logger.module.ts 没 exports: [LoggerService],其他 module 即使 imports: [LoggerModule] 也注入不了,NestJS 会报错:Nest can't resolve dependencies of the XxxService。
四、全局模块(@Global)
import { Global, Module } from '@nestjs/common';
@Global()
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}全局模块的 Provider 不用每个 Module 都 imports,直接注入即可。
⚠️ 坑 2:
@Global用太多会让依赖关系混乱 → 只给确实到处用的东西加(配置、日志、Redis Client)。
五、动态模块(传参配置)
// config.module.ts
@Module({})
export class ConfigModule {
static register(options: { env: 'dev' | 'prod' }): DynamicModule {
return {
module: ConfigModule,
providers: [
{ provide: 'CONFIG_OPTIONS', useValue: options },
ConfigService,
],
exports: [ConfigService],
};
}
}
// app.module.ts
@Module({
imports: [
ConfigModule.register({ env: 'prod' }),
],
})
export class AppModule {}常用于 TypeOrmModule.forRoot({...})、JwtModule.register({...}) 这类需要传参的模块。
providers 数组里能放什么? —— 两类:
providers: [
// 简写:直接写类(隐式 = { provide: UsersService, useClass: UsersService })
UsersService,
// 完整写法:对象,显式指定 provide + 来源
{ provide: 'CONFIG_OPTIONS', useValue: options },
]对象形式的 4 种组合:
providers: [
// 1. useValue —— 注入固定值(配置、Mock、第三方实例)
{ provide: 'API_KEY', useValue: 'sk-xxxxx' },
// 2. useClass —— 注入类(可换实现,常用于测试)
{ provide: UsersService, useClass: process.env.NODE_ENV === 'test' ? MockUsersService : UsersService },
// 3. useFactory —— 工厂函数返回(可注入其他 Provider)
{ provide: 'DB', useFactory: (cfg: ConfigService) => createPool(cfg.get('DB_URL')), inject: [ConfigService] },
// 4. useExisting —— 复用已有 Provider(换个名字)
{ provide: 'ALIAS_USERS', useExisting: UsersService },
]为什么需要对象形式? 因为不是所有 Provider 都是类 —— 配置、字符串 token、已 new 好的 axios 实例、Mock 对象,这些都不是类,必须用对象形式才能注入。
记忆口诀:
[类名]—— 简单场景,注入自己的类[{ provide, useValue/useClass/useFactory }]—— 复杂场景,需要字符串 token / 换实现 / 工厂
动态模块里常见用法 —— 传参 + useValue 注入配置:
{
module: ConfigModule,
providers: [
{ provide: 'CONFIG_OPTIONS', useValue: options }, // ← 把外部传的 options 包成 Provider
ConfigService, // ← ConfigService 才能注入它
],
exports: [ConfigService],
}完整使用示例:
// config.service.ts
@Injectable()
export class ConfigService {
// 注入动态模块里 useValue 包好的配置
constructor(
@Inject('CONFIG_OPTIONS') private readonly options: { env: 'dev' | 'prod' },
) {}
get isProd(): boolean {
return this.options.env === 'prod';
}
get env(): string {
return this.options.env;
}
}
// config.module.ts
@Module({})
export class ConfigModule {
static register(options: { env: 'dev' | 'prod' }): DynamicModule {
return {
module: ConfigModule,
providers: [
{ provide: 'CONFIG_OPTIONS', useValue: options },
ConfigService,
],
exports: [ConfigService], // 导出,其他模块才能用
};
}
}
// app.module.ts
@Module({
imports: [
ConfigModule.register({ env: 'prod' }), // ← 注册时传参
],
})
export class AppModule {}
// users.module.ts
@Module({
imports: [ConfigModule.register({ env: 'prod' })], // 需要的话再注册一次
providers: [UsersService],
})
export class UsersModule {}
// users.service.ts
@Injectable()
export class UsersService {
constructor(private readonly config: ConfigService) {}
someMethod() {
if (this.config.isProd) { // ← 拿到动态模块里的配置
// 生产环境逻辑
}
}
}六、模块懒加载(可选)
import { LazyModuleLoader } from '@nestjs/core';
constructor(private readonly loader: LazyModuleLoader) {}
async onModuleInit() {
const modRef = await this.loader.load(() => import('./heavy.module').then(m => m.HeavyModule));
// 只在第一次调用时才初始化 HeavyModule
}适合启动慢的模块,按需加载。
完整使用示例:
// heavy.module.ts —— 一个启动慢的模块(比如连多个外部服务)
@Module({
providers: [HeavyService],
exports: [HeavyService],
})
export class HeavyModule {}
// app.service.ts —— 在某个 Service 里按需加载
@Injectable()
export class AppService implements OnModuleInit {
constructor(private readonly loader: LazyModuleLoader) {}
private heavyServiceRef: ModuleRef | null = null;
async onModuleInit() {
// 应用启动时不会执行,只有第一次调用时才加载
console.log('AppModule 已就绪');
}
async useHeavyFeature() {
if (!this.heavyServiceRef) {
// 第一次调用时,才异步加载 HeavyModule
this.heavyServiceRef = await this.loader.load(() =>
import('./heavy.module').then(m => m.HeavyModule),
);
}
// 从加载好的 ModuleRef 里取出 HeavyService
const heavyService = this.heavyServiceRef.get(HeavyService);
return heavyService.doSomething();
}
}关键点:
import('./heavy.module')—— 用动态import(),代码会被 Webpack/Vite 打成单独 chunk- 缓存在
heavyServiceRef—— 第二次调用直接复用,不再重新加载 ModuleRef.get(HeavyService)—— 从懒加载的模块里拿 Provider,跟普通模块一样注入
适用场景:
- 重报表模块(初始化要跑几分钟)
- 多租户场景(每个租户独立模块)
- 插件化系统(不同客户启用不同模块)
- 微前端 / 微服务编排
对比 imports: [HeavyModule]:
| 方式 | 启动时间 | 首次调用 | 适用 |
|---|---|---|---|
imports: [HeavyModule] | 慢(一起加载) | 快 | HeavyModule 必须立即用 |
LazyModuleLoader.load() | 快(按需) | 慢(首次要加载) | 重型模块、插件系统 |
七、模块拆分原则
按业务域拆分,而不是按技术层。
src/
├── modules/
│ ├── users/ # 用户域
│ │ ├── users.module.ts
│ │ ├── users.controller.ts
│ │ ├── users.service.ts
│ │ └── dto/
│ ├── orders/ # 订单域
│ └── products/ # 商品域
├── common/ # 公共(全局)
│ ├── logger.module.ts
│ └── config.module.ts
└── app.module.ts⚠️ 坑 3:按
controllers/services/repositories分目录 → 改一个功能要跳 5 个文件夹。
八、特性模块(Feature Module)
把每个业务当独立特性模块,通过 imports 拼装。
// app.module.ts
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRoot({ /* ... */ }),
UsersModule,
OrdersModule,
ProductsModule,
],
})
export class AppModule {}⚠️ 坑 4:
imports顺序不影响,但循环引用 Module 会启动失败 → 用forwardRef。
九、实战:组织一个电商骨架
// app.module.ts
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({ useFactory: () => ({ /* ... */ }) }),
UsersModule,
ProductsModule,
OrdersModule,
],
})
export class AppModule {}每个业务子模块各自 controllers + providers + imports 子依赖,导出对外的 Service。
十、本章小结
| 要点 | 关键 |
|---|---|
| 作用 | 组织代码、隔离依赖、暴露 Provider |
| 导出 | exports: [Service] 才能跨模块用 |
| 全局 | @Global() 跳过 imports,但要慎用 |
| 动态模块 | XxxModule.register({}) 用于传参 |
| 拆分 | 按业务域,不按技术层 |
动手练习
- 拆模块:把 Users 拆到
src/modules/users/,根模块 import 它 - 全局 Config:写一个
ConfigModule,用@Global暴露ConfigService - 动态模块:让
ConfigModule.register({ env })把 env 注入 Service
下一章:第 6 章:中间件 →