Skip to content

模块 ​

ts
@Module({
  imports: [DataModule, CoreModule],   // 依赖哪些模块
  providers: [UserService, UserController],
  exports: [UserService],              // 允许别的模块看到什么
})
class UsersModule {}

模块是纯声明。它没有钩子、没有方法、没有生命周期——那些属于 provider。

控制器不是特殊东西:它就是普通的 provider,方法接受 Hono 的 Context。 所以 providers 里写它就行,没有单独的 controllers 字段。

作用域是真的 ​

每个模块一个容器:

  • imports 编译成指向对方容器的 alias
  • exports 是唯一能借出去的东西
  • 没 export 的 provider,别的模块看不见
ts
@Module({ providers: [UserRepository], exports: [UserRepository] })
class DataModule {}

@Module({ imports: [DataModule], providers: [UserService] })
class UsersModule {}

UserService 能注入 UserRepository,因为 DataModule 把它 export 了。 把 exports 删掉,createApp() 会在启动前抛 ProviderNotFoundError, 不是等某个请求打进来才炸。

alias 不是复制 ​

DataModule 的 UserRepository 单例,被十个模块 import 也是同一个实例。 alias 只是把解析请求转发回源容器,不会各造一份。

启动前的图校验 ​

createApp() 会把整张图校验干净,下面这些都不会漏到运行期:

情况错误
同一个模块里 provider 写了两遍DuplicateProviderError
自己提供的 token 与 import 撞名AmbiguousProviderError
exports 了不存在的东西UnresolvedExportError
模块 import 成环ModuleCycleError(带完整链路)
imports 里放了没 @Module() 的类InvalidModuleError
构造参数少标 @Inject()MissingInjectError
overrides 替换了不存在的 tokenUnknownOverrideError

错误信息都写清了「哪两个东西撞了」和「怎么改」。

动态模块 ​

需要把配置传进模块时,用 dynamicModule():

ts
const DB_URL = token<string>('dbUrl');

class DatabaseModule {
  static forRoot(options: { url: string; provide?: Token<Pool> }): DynamicModule {
    const token = options.provide ?? Pool;
    return dynamicModule(DatabaseModule, {
      providers: [
        { provide: DB_URL, useValue: options.url },
        { provide: token, useClass: Pool },
      ],
      exports: [token],
    });
  }
}

@Module({ imports: [DatabaseModule.forRoot({ url: 'postgres://main' })] })
class AppModule {}

工厂类不需要 @Module() 装饰器——装饰器是给静态模块用的。 忘了调用 forRoot() 直接写类名时,报错会点名可用的工厂方法:

DatabaseModule 不是一个模块。
imports 里只能放两种东西:被 @Module() 标记的类,或者 forRoot() 之类返回的 DynamicModule。

DatabaseModule 上有这些工厂方法,是不是忘了调用?
  DatabaseModule.forRoot(...)

每次 forRoot 都是独立实例 ​

模块图按引用去重,不按类。所以同一个类可以配置出多个并存:

ts
@Module({
  imports: [
    DatabaseModule.forRoot({ url: 'postgres://main', provide: PRIMARY }),
    DatabaseModule.forRoot({ url: 'postgres://replica', provide: REPLICA }),
  ],
})
class AppModule {}

两个节点、两个容器、两套单例。反过来,把同一个对象引到多处只会建一个节点, 单例照旧共享。

两条必须记住的规则 ​

两个 import 导出同一个 token → 报 AmbiguousImportError。

AppModule 从多个 import 里都拿到了 Pool。
两个模块导出同一个 token 时容器不知道该用哪个 ——
让它们用不同的 token(例如给动态模块传 provide),或者只 import 其中一个。

这就是上面例子里 provide 选项存在的原因:让调用方决定实例挂在哪个 token 上。

既 import 又本地提供同一个 token → 报 AmbiguousProviderError。 见下一节。

为什么不做覆盖 ​

NestJS 允许本地 provider 覆盖 import 进来的同名 token。这里直接报错:

provider 的顺序不该决定谁生效。 撞名就是设计问题,早报早改。

(测试里要换实现,用 createApp 的 overrides——那是显式的、会被校验的入口。)

模块的执行顺序 ​

graph.modules 是依赖在前、根模块在后的后序: AppModule 最后,被它依赖的模块最先。app.start() 按这个顺序跑生命周期。