Skip to content

第 3 章 模块系统:AppModule 与应用的装配图 ​

学习目标 ​

  • 理解 Nest"一切皆是模块"的组织哲学
  • 逐行读懂 @Module 装饰器的四个元数据字段
  • 理解动态模块(forRoot)的模式
  • 初步认识 @nestjs/observe 可观测性模块

涉及核心文件 ​

  • src/app.module.ts

一、完整代码与逐段讲解 ​

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() 工厂调用 ​

ts
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)] ——引入时传入配置,模块根据配置定制自己的行为。

配置项解读:

ts
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 之前临时加:

ts
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,看路由是怎么定义的。