创建日期:2026-09-08 | 最近更新:2026-09-08 基于 NestJS 12(
@nestjs/core12.0.1)。本文的「模块接线 / 依赖注入」在本机跑通(见下方路由日志),作用域与生命周期属框架级概念,写法以官方 modules / providers 文档为准。
NestJS 模块化与 Provider 作用域:把应用拆成可复用的「域」
一句话:Nest 的
@Module不是装饰器仪式,它是依赖注入的边界——「谁 import 谁、谁 exports 谁」决定了容器里谁能注入谁。掌握 Module 的imports/exports/providers/controllers四要素和 Provider 的三种作用域,你才算真正会组织 Nest 工程。
1. 回顾:Module 就是「一个可复用的依赖域」
@Module({
imports: [], // 引入别的模块 → 本模块能注入对方 exports 的 provider
controllers: [CatsController], // 本模块的路由
providers: [CatsService], // 本模块的 provider(可注入)
exports: [CatsService], // 允许「imports 我的模块」也注入 CatsService
})
export class CatsModule {}
四要素一句话版:
| 字段 | 意思 |
|---|---|
controllers | 这个域里谁接收 HTTP |
providers | 这个域里谁干活(可被注入) |
imports | 我要用哪些「别的域」 |
exports | 我允许谁把「我的 provider」拿出去用 |
默认规则:provider 只在声明它的模块内可见。 想让 A 模块里的 XxxService 被 B 使用,得 A exports 它 + B imports A。这不是麻烦,是刻意做封装——大项目靠这个保持「依赖边界清晰」。
上一篇(入门)里那个 cats demo 就是这样:
AppModule imports CatsModule,启动日志能直接看到Mapped {/cats, POST} route——一个特性模块接进去,路由就有了。
2. 组织习惯:一个业务域一个 Module
src/
├─ app.module.ts # 根模块:只做「组装 + 全局」的薄壳
├─ cats/
│ ├─ cats.module.ts # 业务域
│ ├─ cats.controller.ts
│ ├─ cats.service.ts
│ └─ dto/create-cat.dto.ts
├─ common/ # 跨域共享(guard/filters/interceptors/decorators)
└─ config/ # 全局配置模块
- 根模块尽量薄:只 import 特性模块、注册全局管道/过滤器/守卫;
- 一个文件一个类:controller / service / dto / entity 分开,别堆;
- DTO/接口靠近它所属的域,而不是放一个中央
types/大杂烩。
3. 动态模块:为什么配置、DB 的 Module 都长得像工厂
你天天 TypeOrmModule.forRoot(...)、ConfigModule.forRoot(...)——forRoot 就是「动态模块」:模块不是写死的,而是根据你传的选项「生成一个带不同 providers 的模块实例」。
// 极简动态模块:调用方能自定义 token
@Module({})
export class CacheModule {
static forRoot(ttl: number): DynamicModule {
return {
module: CacheModule,
providers: [
{ provide: 'CACHE_TTL', useValue: ttl },
CacheService,
],
exports: [CacheService],
};
}
}
// 使用方:CacheModule.forRoot(3600)
看到价值了:同一套模块逻辑,按不同配置产出不同实例——ConfigModule 读不同 env、TypeOrmModule 连不同库,都是它。
@Global:真要全局共享时才用
默认封装严格,但有些 provider(日志、配置、全局工具)谁都要用,@Global() 让模块只注册一次、处处可注入(不用到处 import):
@Global()
@Module({ providers: [LoggerService], exports: [LoggerService] })
export class CommonModule {}
克制使用:
@Global用多了等于把「封装」又拆了。经验法则——只对「真·基础设施」全局化,业务 provider 老老实实走 imports/exports。
4. Provider 三种作用域
默认 provider 是单例(整个应用一份)。还可以:
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.DEFAULT }) // 单例(默认)
export class DefaultSvc {}
@Injectable({ scope: Scope.REQUEST }) // 每个请求一份(可读请求上下文)
export class RequestSvc {}
@Injectable({ scope: Scope.TRANSIENT }) // 每次注入都 new 一份(不共享)
export class TransientSvc {}
| scope | 生命周期 | 典型用途 | 代价 |
|---|---|---|---|
DEFAULT | 应用级单例 | 绝大多数 service | 无 |
REQUEST | 每个 HTTP 请求 | 想读 req.user、按请求隔离状态 | 注入链上所有依赖都要 REQUEST,性能/测试变复杂 |
TRANSIENT | 每次注入各一份 | 有内部状态、不想要单例共享的类 | 每次实例化开销 |
REQUEST 作用域的坑:它要求整条依赖链也都是 REQUEST(REQ → A → B → C 全要标),否则会报错或拿到错误实例。能用构造器参数拿不到请求时,优先考虑把「请求相关数据」作为方法参数传入,而不是把 service 设成 REQUEST。
5. 生命周期钩子:在「启动/关闭」时干点正事
provider / controller / module 都能实现以下接口,容器在对应时机调用:
import { OnModuleInit, Injectable, Logger } from '@nestjs/common';
@Injectable()
export class AppService implements OnModuleInit {
private readonly logger = new Logger(AppService.name);
onModuleInit() {
this.logger.log('服务初始化:这里适合连 DB、预热缓存');
}
}
| 接口 | 时机 |
|---|---|
OnModuleInit | 该模块的依赖解析完后 |
OnApplicationBootstrap | 所有模块都 init 完、应用将监听前 |
OnModuleDestroy / BeforeApplicationShutdown / OnApplicationShutdown | 关闭阶段(优雅停机、断连) |
要在关闭时收到系统信号(SIGINT/SIGTERM),在 main.ts 开:
app.enableShutdownHooks(); // 配合容器/PM2 发信号做优雅停机
这正是连接 DB/Redis 这类「有连接生命周期」资源的正确位置——比在 controller 里偷偷建连接靠谱得多。
6. 自定义 Provider:别只会写 providers: [X]
providers: [X] 是 { provide: X, useClass: X } 的简写。拆开能表达更多:
@Module({
providers: [
{ provide: 'CONFIG', useValue: { env: 'prod' } }, // 值(用字符串/符号 token)
{ provide: 'DB', useFactory: (cfg) => createDb(cfg), // 工厂(惰性 + 可注入)
inject: ['CONFIG'] },
{ provide: CatsService, useClass: MockCatsService }, // 换实现(测试常用)
],
})
注入方:
constructor(
@Inject('CONFIG') private readonly config: { env: string },
) {}
自定义 token + useFactory 是「对接第三方库」的标准姿势(把非 Nest 的对象包装成可注入 provider);useClass 换实现是单测里 mock 服务的利器。
7. 小结
- 写模块:先问「这个 provider 该谁用」——决定了放哪个 Module、要不要 exports;
- 拆域:按业务域建 Module,根模块做薄壳;
- 要配置化:动态模块
forRoot/forFeature;要全局基础设施:才@Global; - 默认单例,别没事开 REQUEST;真要就整条链一致;
- 资源生命周期(连库、连 Redis、优雅停机)交给
OnModuleInit/enableShutdownHooks。
关联
- 上一篇:NestJS 入门指南
- 下一篇:数据层:TypeORM 与 Prisma 怎么选、怎么接
- 参考官方:Modules / Providers / Custom providers