第 3 章 模块系统:AppModule 与应用的装配图
学习目标
- 理解 Nest"一切皆是模块"的组织哲学
- 逐行读懂
@Module装饰器的四个元数据字段 - 理解动态模块(
forRoot)的模式 - 初步认识
@nestjs/observe可观测性模块
涉及核心文件
src/app.module.ts
一、完整代码与逐段讲解
import { Module } from "@nestjs/common";
import { createObserveModule } from "@nestjs/observe";
import { AppController } from "./app.controller.js";
import { AppService } from "./app.service.js";
export const { ObserveModule, ObserveInstrument } = createObserveModule();
@Module({
imports: [
// Distributed tracing, auto-correlated logs, request/job metrics, error
// telemetry, alarms, and more — out of the box. Sign up at https://observe.nestjs.com
ObserveModule.forRoot({
appKey: process.env.OBSERVE_APP_KEY ?? "",
appSecret: process.env.OBSERVE_APP_SECRET ?? "",
runtimeMetrics: !Boolean(process.versions?.["webcontainer"]),
serviceId: "nest-typescript-starter",
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}1. createObserveModule() 工厂调用
export const { ObserveModule, ObserveInstrument } = createObserveModule();这一行做了两件事并export出去:
ObserveModule:一个 Nest 模块类,供imports使用;ObserveInstrument:一个插桩器对象,供main.ts里NestFactory.create(AppModule, { instrument })使用。
注意它被定义并导出在 app.module.ts 里,而 main.ts 从 ./app.module.js 同时导入 AppModule 和 ObserveInstrument——这是模板的巧妙安排:把可观测性的"创建"集中在模块文件,入口文件只管消费。
2. @Module() 装饰器的四个字段
@Module 接收一个元数据对象,告诉 Nest 这个模块由什么组成:
| 字段 | 本项目内容 | 语义 |
|---|---|---|
imports | [ObserveModule.forRoot({...})] | 本模块依赖哪些其他模块 |
controllers | [AppController] | 本模块注册哪些控制器(接收 HTTP 请求) |
providers | [AppService] | 本模块注册哪些提供者(可被注入的服务) |
exports | (本项目未用) | 把本模块的 provider 暴露给其他模块用 |
可以把 @Module 理解为一张装配清单:
providers: [AppService]——"把 AppService 放进 DI 容器,谁需要就给谁";controllers: [AppController]——"实例化这个控制器,并登记它上面的路由";imports: [...]——"先把这些子模块装配好,它们导出的东西我也能用"。
3. ObserveModule.forRoot({...})——动态模块模式
注意这里 imports 的不是一个类,而是一个方法调用的返回值。这是 Nest 极重要的**动态模块(Dynamic Module)**模式:
- 静态模块:
imports: [SomeModule]——原样引入,无配置; - 动态模块:
imports: [SomeModule.forRoot(config)]——引入时传入配置,模块根据配置定制自己的行为。
配置项解读:
ObserveModule.forRoot({
appKey: process.env.OBSERVE_APP_KEY ?? "", // 从环境变量读密钥,没配则为空串
appSecret: process.env.OBSERVE_APP_SECRET ?? "", // 同上(空配置 = 本地禁用上报)
runtimeMetrics: !Boolean(process.versions?.["webcontainer"]),
serviceId: "nest-typescript-starter",
});appKey/appSecret:Nest Observe 云平台的凭证,通过环境变量注入(密钥不入库,process.env.X ?? ''兜底)。runtimeMetrics:是否采集 CPU/内存/事件循环等运行时指标。process.versions['webcontainer']用于检测是否运行在 StackBlitz WebContainer 环境(该环境不支持某些指标采集),是普通 Node 就开启。process.versions?.的可选链是防御性写法。serviceId:该服务在可观测平台上的标识名,与package.json的name对应。
forRoot vs forFeature(约定):forRoot 在根模块调用一次做全局配置;forFeature 在业务模块中注册局部内容(如 TypeORM 的实体)。本项目只有 forRoot。
4. export class AppModule {}
类体是空的——模块类本身通常不写任何逻辑,它的全部意义就是身上那层 @Module 元数据。框架读取元数据完成装配,类只是元数据的"载体"。
二、关键概念:模块是组织边界
Nest 应用是一棵模块树,AppModule 是根。真实项目中你会按业务拆分:
AppModule
├── UsersModule (controllers: [UsersController], providers: [UsersService])
├── OrdersModule
├── AuthModule
└── ObserveModule / ConfigModule / TypeOrmModule ...(基础设施模块)规则:一个类(Service)默认只在声明它的模块内可注入;想跨模块用,该模块必须把它列入 exports,使用方再 imports 该模块。这就是 Nest 的封装与共享机制。
三、动手实践
实践 1:观察模块元数据
在 main.ts 的 NestFactory.create 之前临时加:
import "reflect-metadata";
console.log(Reflect.getMetadata("controllers", AppModule));
console.log(Reflect.getMetadata("providers", AppModule));运行后能看到 @Module 装饰器实际上就是把配置数组存成了类的元数据——装饰器并不神秘。
实践 2:故意制造一个错误
把 providers: [AppService] 中的 AppService 删掉再启动,观察报错:
Nest can't resolve dependencies of the AppController (?).
Please make sure that the argument AppService is available in the AppModule context.这是 Nest 初学者最常遇到的错误。读懂它:Controller 声明了需要 AppService,但模块没把它注册进容器——"用到什么,就要在 providers 里登记什么"。修复后恢复。
实践 3(思考)
如果把 AppService 写进另一个模块的 providers,AppController 还能注入到吗?需要哪两个额外步骤?(提示:exports + imports)
本章小结
@Module是装配清单:imports(依赖谁)、controllers(谁收请求)、providers(谁可被注入)、exports(暴露给谁)。- 动态模块
forRoot(config)让模块可配置,是 Nest 生态库的通行模式。 @nestjs/observe通过动态模块 +instrument双入口接入应用。- 下一章聚焦
controllers里登记的那个类——AppController,看路由是怎么定义的。