十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

NestJS 入门(6):Module 边界与导出 上一篇NestJS 入门5Pipe 与 DTO 校验 讲了入参怎么在进业务前拦住。第二篇讲过依赖注入的写法学到这里最常见的启动报错往往是Nest cant 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 愿意拿出去的DocumentsServiceController 一般不导出——路由是这个模块自己的门面。导出的通常是 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{}少任何一步启动时就会报文章开头那种cant resolve dependencies。排查顺序建议固定这个类有没有进某个模块的providers那个模块有没有exports它当前模块有没有imports那个模块构造函数里的类型名是否写对、有没有循环依赖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 要UseGuardsController基本不导出路由属于本模块导出也注入不到 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)的含义是现在先别急着解析这个类等模块图搭完再接线。入门阶段的建议能改成单向依赖就改A 依赖 BB 不要依赖 A实在改不了再用forwardRef模块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()适合基础设施业务模块保持显式 importsController 通常不导出导出 Service / Guard循环依赖用forwardRef救急优先考虑拆单向依赖对照本系列Controller / Service / Module依赖从哪注入有没有 Guard / Pipe / 统一信封这个依赖所在模块导出了吗当前模块引入了吗会不会成环下一篇会讲生命周期钩子——OnModuleInit里适合做什么连数据库、灌种子数据以及和构造函数的差别。系列导航上一篇NestJS 入门5Pipe 与 DTO 校验第四篇NestJS 入门4统一响应与异常处理第三篇NestJS 入门3Guard 如何挡住未登录请求第二篇NestJS 入门2依赖注入到底解决了什么问题第一篇NestJS 入门1先搞懂 Module、Controller、Service
返回列表