news 2026/8/12 13:04:43

NestJS 入门(6):Module 边界与导出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NestJS 入门(6):Module 边界与导出

上一篇:NestJS 入门(5):Pipe 与 DTO 校验 讲了入参怎么在进业务前拦住。
第二篇讲过依赖注入的写法;学到这里,最常见的启动报错往往是:

Nest can't resolve dependencies of the DocumentsService (?, PrismaService). Please make sure that the argument ProjectsService at index [0] is available in the DocumentsModule context.

这篇文章只讲清楚一件事:

Service 不是「写了@Injectable()就能到处用」。它只能在「看得见它的 Module」里被注入。

看得见 = 本模块providers里有,或从别的模块imports进来,且对方已经exports


1. Module 是能力的边界,不是文件夹

文件夹可以随便互相 import 文件;Nest 的注入容器不会。
每个@Module()都有自己的一块「可见范围」:

@Module({imports:[/* 从外面引进来的能力 */],controllers:[/* 本模块的 HTTP 入口 */],providers:[/* 本模块自己造的、可注入的东西 */],exports:[/* 愿意分享给别人的东西 */],})exportclassDocumentsModule{}

可以把它想成一个盒子:

DocumentsModule 盒子 里面有:DocumentsController、DocumentsService 需要外面的:PrismaService、ProjectsService 愿意拿出去的:DocumentsService

Controller 一般不导出——路由是这个模块自己的门面。
导出的通常是 Service、Guard 这类「别人构造函数里要注入」的东西。


2. 跨模块注入的三步清单

A 模块要用 B 模块的XxxService,三步缺一不可:

步骤写在哪含义
1. 注册B 的providersB 能创建这个类
2. 导出B 的exportsB 允许别人用
3. 引入A 的imports: [BModule]A 打开这扇门

认证模块把 Guard 分享出去,就是这三步:

@Module({imports:[PrismaModule,PassportModule,JwtModule.register({/* ... */})],controllers:[AuthController],providers:[AuthService,JwtStrategy,JwtAuthGuard],exports:[AuthService,JwtAuthGuard],// 第 2 步:愿意分享})exportclassAuthModule{}

其它业务模块要挂@UseGuards(JwtAuthGuard)、或注入AuthService时,需要:

@Module({imports:[AuthModule],// 第 3 步:引进来// ...})exportclassProjectsModule{}

少任何一步,启动时就会报文章开头那种can't resolve dependencies

排查顺序建议固定:

  1. 这个类有没有进某个模块的providers
  2. 那个模块有没有exports它?
  3. 当前模块有没有imports那个模块?
  4. 构造函数里的类型名是否写对、有没有循环依赖?

3. 根模块imports不等于「全局都能注入」

根模块常会把业务模块都挂上:

@Module({imports:[PrismaModule,AuthModule,ProjectsModule,DocumentsModule,PromptTemplatesModule,TaskPromptsModule,// ...],controllers:[HealthController],})exportclassAppModule{}

这只表示:应用启动时要加载这些模块(路由、Provider 会被创建)。
不表示DocumentsModule里可以直接注入ProjectsService

DocumentsModule要用项目能力,自己还得写:

@Module({imports:[PrismaModule,ProjectsModule],controllers:[DocumentsController],providers:[DocumentsService],exports:[DocumentsService],})exportclassDocumentsModule{}

可以记:

AppModule.imports= 「这些模块存在于应用中」
业务模块自己的imports= 「我这个盒子要用谁的导出」


4.@Global():少写 imports,但别滥用

数据库、日志这种几乎处处都要,可以做成全局模块:

@Global()@Module({providers:[PrismaService],exports:[PrismaService],})exportclassPrismaModule{}

只要根模块imports: [PrismaModule]一次,其它模块通常不必再写imports: [PrismaModule],也能注入PrismaService

日志模块同理:

@Global()@Module({providers:[LoggerService,MetricsService/* ... */],exports:[LoggerService,MetricsService],})exportclassObservabilityModule{}

适合全局化的:

  • 数据库客户端
  • 日志 / 指标
  • 配置读取

不适合全局化的:

  • 具体业务 Service(项目、文档、模板……)

业务一全局,模块边界就糊了:谁依赖谁从文件上看不出来,后面拆分、测试都会变难。
@Global()仍然要exports——全局只是「自动帮你 imports」,不是「不用导出」。


5. 只导出需要被注入的,不导出 Controller

一张对照表:

成员通常导出?原因
Service常常导出别的模块构造函数要注入
Guard / Strategy按需导出别的 Controller 要@UseGuards
Controller基本不导出路由属于本模块;导出也注入不到 HTTP 层
内部工具类能不导出就不导出缩小边界,避免外人依赖实现细节

项目模块会导出多个 Service,因为别的模块确实要用:

@Module({imports:[PrismaModule,AuthModule,forwardRef(()=>DocumentsModule),forwardRef(()=>PromptTemplatesModule),forwardRef(()=>TaskPromptsModule),],controllers:[ProjectsController],providers:[ProjectsService,ChapterPipelineService,ComplianceCheckService],exports:[ProjectsService,ChapterPipelineService,ComplianceCheckService],})exportclassProjectsModule{}

ProjectsController留在盒子里;拿出去的是可复用的业务能力。


6. 循环依赖:两个盒子互相要对方

真实业务里很容易出现:

  • 文档要问项目:这个projectId合法吗?
  • 项目要问文档:删项目时先清文档

两边构造函数互相注入,Nest 解析依赖图时会卡住。这时会看到forwardRef

模块层:

// documents.module.tsimports:[forwardRef(()=>ProjectsModule)]// projects.module.tsimports:[forwardRef(()=>DocumentsModule)]

构造函数层:

constructor(privatereadonlyprisma:PrismaService,@Inject(forwardRef(()=>DocumentsService))privatereadonlydocumentsService:DocumentsService,){}

forwardRef(() => Xxx)的含义是:

现在先别急着解析这个类,等模块图搭完再接线。

入门阶段的建议:

  1. 能改成单向依赖就改(A 依赖 B,B 不要依赖 A)
  2. 实在改不了再用forwardRef
  3. 模块imports和构造函数@Inject两边都要包,只改一处常常仍报错

循环依赖往往是领域边界没切干净的信号,能拆就拆。


7. 用一张图看「谁能看见谁」

AppModule imports: PrismaModule(全局)、AuthModule、ProjectsModule、DocumentsModule ... PrismaModule (@Global) providers/exports: PrismaService → 各业务模块都能注入,不必再 imports AuthModule providers: AuthService, JwtAuthGuard, JwtStrategy exports: AuthService, JwtAuthGuard → ProjectsModule imports AuthModule 后才能用 Guard ProjectsModule imports: AuthModule、DocumentsModule(forwardRef)、... exports: ProjectsService、... → DocumentsModule imports 之后才能注入 ProjectsService DocumentsModule imports: ProjectsModule(forwardRef) exports: DocumentsService → ProjectsModule 才能注入 DocumentsService(形成环,所以才要 forwardRef)

问自己一句就够:

当前这个类的构造函数里写的依赖,是在「本模块 providers」里,还是在「已 imports 且对方已 exports」里?

答不上来,就不要指望 Nest 能变出实例。


8. 最小反例与改法

假设DocumentsService要注入ProjectsService,但启动失败。

反例 1:没导出

// projects.module.tsproviders:[ProjectsService],exports:[],// 忘了导出

改法:exports: [ProjectsService]

反例 2:导出了但对方没引入

// documents.module.tsimports:[],// 只写了 PrismaModule,没有 ProjectsModuleproviders:[DocumentsService],

改法:imports: [ProjectsModule]

反例 3:只在 AppModule 引入了双方,以为就能互相注入

// app.module.tsimports:[ProjectsModule,DocumentsModule]

两边模块自己的imports仍是空的 → 仍然失败。

改法:在真正注入的那个模块里写imports


9. 小结

  • Module 是注入可见范围,不是普通文件夹
  • 跨模块三步:providers 注册 → exports 分享 → imports 引入
  • 根模块挂上 ≠ 子模块能互相注入
  • @Global()适合基础设施;业务模块保持显式 imports
  • Controller 通常不导出;导出 Service / Guard
  • 循环依赖用forwardRef救急,优先考虑拆单向依赖

对照本系列:

  1. Controller / Service / Module?
  2. 依赖从哪注入?
  3. 有没有 Guard / Pipe / 统一信封?
  4. 这个依赖所在模块导出了吗?当前模块引入了吗?会不会成环?

下一篇会讲:生命周期钩子——OnModuleInit里适合做什么(连数据库、灌种子数据),以及和构造函数的差别。

系列导航

  • 上一篇:NestJS 入门(5):Pipe 与 DTO 校验
  • 第四篇:NestJS 入门(4):统一响应与异常处理
  • 第三篇:NestJS 入门(3):Guard 如何挡住未登录请求?
  • 第二篇:NestJS 入门(2):依赖注入到底解决了什么问题?
  • 第一篇:NestJS 入门(1):先搞懂 Module、Controller、Service
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/12 12:59:58

Python实战:基于ERA5数据计算与可视化整层水汽通量及散度

1. 项目概述:从气象数据到直观洞察在气象分析和气候研究中,我们常常需要理解大气中水分的“来龙去脉”。水分是能量传输和天气过程(如暴雨、台风)的核心载体,但仅知道某个高度上的湿度或风速是远远不够的。我们需要一个…

作者头像 李华
网站建设 2026/8/12 12:59:26

基于YOLOv8与强化学习的麻将AI实战:从视觉识别到智能决策

1. 项目概述:当AI坐上麻将桌“AI打麻将”这个事儿,听起来像是科幻电影里的桥段,但今天,它已经是一个可以动手实现的、充满挑战和趣味的实战项目。这不仅仅是让电脑学会“碰杠吃胡”的规则那么简单,它背后融合了计算机视…

作者头像 李华
网站建设 2026/8/12 12:58:12

Celandine取色器插件:数字绘画高效色彩管理方案

1. 项目概述:Celandine取色器插件的核心价值数字绘画创作过程中,色彩管理一直是影响效率的关键环节。传统工作流中,设计师往往需要频繁切换软件或手动记录色值,这种打断创作连贯性的操作平均每小时会浪费8-12分钟。Celandine取色器…

作者头像 李华
网站建设 2026/8/12 12:56:44

Git与Gerrit协同工作流:从版本控制到代码审查的完整实践指南

1. 从版本控制到代码审查:为什么需要 Git 与 Gerrit 的组合?如果你是一名刚入行的开发者,或者是从 SVN 时代转型过来的“老兵”,第一次听到 Git 和 Gerrit 这两个词时,可能会有点懵。Git 我知道,是现在最流…

作者头像 李华
网站建设 2026/8/12 12:56:07

15分钟搞定完美黑苹果:OpCore-Simplify图形化工具完全指南

15分钟搞定完美黑苹果:OpCore-Simplify图形化工具完全指南 【免费下载链接】OpCore-Simplify A tool designed to simplify the creation of OpenCore EFI 项目地址: https://gitcode.com/GitHub_Trending/op/OpCore-Simplify 还在为复杂的黑苹果OpenCore配置…

作者头像 李华
网站建设 2026/8/12 12:56:05

深入Linux USB驱动:从架构、URB机制到实战开发与调试

1. 从“插上就能用”到“为什么能用”:USB驱动的幕后世界作为一名在嵌入式Linux领域摸爬滚打多年的开发者,我见过太多工程师对USB设备的态度:插上,能用,就完事了。直到有一天,你需要在板子上接入一个非标准…

作者头像 李华