全局模块与动态模块


文档摘要

2.4 全局模块与动态模块 三大零件的标准装法已经齐了,本节处理两种特殊形态:全局模块解决"每个工位都要用的公共零件怎么供",动态模块解决"同一零件按不同参数供货"。两者都是柔性改造——柔性意味着好用也容易用歪,本节把正确姿势和滥用信号一起交给你。 全局模块:车间级的公告栏 按 2.1 节的规则,想用别家服务就得 import 它的模块。但有些零件几乎每个工位都要:日志、配置、公共工具。让三十个模块挨个 import 日志模块显然是重复劳动,@Global 就是为此准备的: 在根模块 imports 里登记一次 LoggerModule,全车间可见。

2.4 全局模块与动态模块

三大零件的标准装法已经齐了,本节处理两种特殊形态:全局模块解决"每个工位都要用的公共零件怎么供",动态模块解决"同一零件按不同参数供货"。两者都是柔性改造——柔性意味着好用也容易用歪,本节把正确姿势和滥用信号一起交给你。

全局模块:车间级的公告栏

按 2.1 节的规则,想用别家服务就得 import 它的模块。但有些零件几乎每个工位都要:日志、配置、公共工具。让三十个模块挨个 import 日志模块显然是重复劳动,@Global 就是为此准备的:

@Global() @Module({ providers: [AppLoggerService], exports: [AppLoggerService], }) export class LoggerModule {}
// 任何模块无需 import LoggerModule,直接注入 @Injectable() export class OrdersService { constructor(private readonly logger: AppLoggerService) {} }

在根模块 imports 里登记一次 LoggerModule,全车间可见。规则很简单,纪律更重要:全局名额留给真正的车间级基础设施,我的上限是三到五个——日志、配置、公共工具类,再多就说明模块间出现了该用 imports 表达的正常依赖,被图省事塞进了公告栏。全局化还有一个隐性代价:依赖关系从代码结构里消失。别人读到 OrdersService 时,看不出 logger 从哪来,工程一大,这种"看不见的供给"会让排错成本翻倍。业务模块之间,请老老实实走 imports。

一条自查办法值得配进代码评审清单:全局模块的导出对象,命名上带不带业务含义?AppLoggerService 这种纯技术命名的可以进公告栏;一旦出现 OrderAuditLogger 这类业务命名的导出,它就绝对不该是全局的——业务能力走公告栏,等于宣告"任何模块都能绕过工位画线直接取用",2.1 节辛苦建立的可见性规则名存实亡。

动态模块:可配置的供货方案

静态模块的 providers 是写死的。可有些零件必须由使用方决定参数——数据库连接串、缓存过期时间、第三方密钥。动态模块允许导入方在 import 时传入配置,模块内部按参数装配:

// 缓存模块:接收配置的动态模块 @Module({}) export class CacheModule { static register(options: { ttl: number; maxItems: number }): DynamicModule { return { module: CacheModule, providers: [ { provide: 'CACHE_OPTIONS', useValue: options }, CacheService, ], exports: [CacheService], }; } } // 使用方按自己的参数供货 @Module({ imports: [CacheModule.register({ ttl: 300, maxItems: 1000 })], }) export class ProductsModule {}

静态的 CacheModule 变成了函数调用 CacheModule.register(...),返回的 DynamicModule 里带着按参数造好的 providers。这个模式你会在整个 Nest.js 生态里反复见到——TypeORM 的 forRoot、配置模块的 forRoot、微服务传输器的 register,全是同一副骨架。

register 与 forRoot 的名字约定值得记一下:forRoot 表示全应用一份的全局配置(通常配 @Global,在根模块调用一次),register 表示各业务域各配各的(可以多次调用,每次生成独立的实例组)。选错形态的症状很典型:明明只想配一份连接,却因为用了 register 被三个模块各自实例化出三份连接池。

图 2-3:柔性产线——动态模块装配视图

图 2-3:柔性产线——动态模块装配视图

异步配置的伏笔

动态模块还有一档进化形态 registerAsync:配置本身来自环境变量或配置中心,需要在启动时异步读取后再装配。它引入 import 数组把配置模块先接进来、再用 useFactory 异步产出 options——语法与 2.3 节的工厂供应一脉相承,只是套上了模块级的外壳。完整代码到 5.3 节配置管理再拆,这里先记住判断:参数编译期可知用 register,参数要运行时读环境用 registerAsync

registerAsync 的完整形态

把伏笔展开。假设缓存模块的参数存在配置中心,启动时要异步读取。三步改造:静态 register 之外加一个 registerAsync 静态方法;用 useFactory 异步产出 options;用 inject 声明工厂依赖配置服务。完整骨架:

import { DynamicModule, Module } from '@nestjs/common'; @Module({}) export class CacheModule { static registerAsync(options: { isGlobal?: boolean; }): DynamicModule { return { module: CacheModule, global: options.isGlobal ?? false, providers: [ { provide: 'CACHE_OPTIONS', inject: [ConfigService], useFactory: async (config: ConfigService) => ({ ttl: await config.get('CACHE_TTL', 300), maxItems: await config.get('CACHE_MAX', 1000), }), }, CacheService, ], exports: [CacheService], }; } }

三个细节值得圈出来。其一,DynamicModule 的 global 字段让"是否全局"也成为调用方可选的参数——官方配置模块的 isGlobal 选项就是这个用法。其二,工厂里 await 配置读取,容器会等它完成再装配 CacheService,与 3.3 节异步工厂的语义一致。其三,CacheService 内部用 @Inject('CACHE_OPTIONS') 领取参数,参数对象与零件解耦——想改默认值,动配置不动代码。

同样的骨架还能接第三种来源:配置不来自配置服务,而来自异步函数直接返回(useFactory 直接写异步逻辑,连 inject 都省了),或来自别的动态模块导出的令牌。形态千变万化,内核只有一句话:把"参数怎么来"从"零件怎么造"里剥离出去。这一句话你会在第 5 章的数据库集成里再次验证——TypeORM 的 forRootAsync 与这里的结构几乎逐行对应。

本节要点收纳

  • 全局模块是公告栏:留给日志、配置等车间级基础设施,数量控制在个位数;
  • 全局的代价:依赖关系从结构中消失,业务模块间坚持走 imports;
  • 动态模块是参数化图纸:register 按参数生成 providers,返回 DynamicModule;
  • forRoot 与 register 的分工:全应用一份用 forRoot,各域各配用 register;
  • 异步配置的判断:参数要读环境就用 registerAsync,细节在第 5 章展开。

零件与柔性改造都认识了。下一章拆开最核心的那台设备——IoC 容器,看零件到底是怎么被装配起来的。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U