3. NestJS 项目初始化:使用 CLI 创建项目、模块化架构设计、依赖注入与装饰器原理

本章是 NestJS 路由设计与性能调优的起点。你将通过 NestJS CLI 快速搭建项目骨架,理解模块化架构的核心设计模式,并深入掌握依赖注入(DI)与装饰器(Decorator)的底层原理。这些知识是后续实现高效路由、中间件、守卫和性能压测的基础。

3.1 使用 NestJS CLI 创建项目

NestJS 官方 CLI 提供了标准化的项目脚手架,确保目录结构、构建配置和开发体验的一致性。执行以下命令即可初始化一个 TypeScript 项目:

npm i -g @nestjs/cli
nest new go-contract-benchmark
cd go-contract-benchmark

CLI 会询问包管理器(npm / yarn / pnpm),选择后自动生成以下核心文件:

文件/目录 作用
src/main.ts 应用入口,使用 NestFactory.create() 启动 HTTP 服务
src/app.module.ts 根模块,声明全局依赖和子模块导入
src/app.controller.ts 根控制器,定义路由处理函数
src/app.service.ts 根服务,封装业务逻辑
tsconfig.json TypeScript 编译配置,启用装饰器与元数据反射
nest-cli.json NestJS 构建与开发配置
性能提示: 在 tsconfig.json 中确保 experimentalDecorators 和 emitDecoratorMetadata 均为 true,这是装饰器与 DI 正常工作的前提。

3.2 模块化架构设计

NestJS 的模块化架构借鉴了 Angular 的模块系统,每个功能单元封装为一个 @Module。模块是组织代码、管理依赖和隔离作用域的基本单位。

3.2.1 模块的核心组成

一个标准模块包含以下四个元数据属性:

属性 说明 典型值
controllers 该模块下定义的控制器列表 [UserController]
providers 该模块下可被注入的服务、工厂、仓库等 [UserService]
imports 导入其他模块,共享其导出的 providers [DatabaseModule]
exports 将本模块的 providers 暴露给其他模块 [UserService]

3.2.2 功能模块拆分示例

以“用户管理”和“订单管理”为例,模块化架构如下:

src/
├── app.module.ts          // 根模块,导入 UserModule 和 OrderModule
├── user/
│   ├── user.module.ts     // @Module({ controllers: [UserController], providers: [UserService], exports: [UserService] })
│   ├── user.controller.ts
│   └── user.service.ts
└── order/
    ├── order.module.ts
    ├── order.controller.ts
    └── order.service.ts
架构原则: 每个模块只负责一个业务领域,通过 exports 控制可见性。这种高内聚低耦合的设计在压测场景下便于独立扩缩容和性能分析。

3.2.3 模块依赖关系 SVG 图

NestJS 模块化依赖关系图 AppModule (根) UserModule OrderModule DatabaseModule imports imports imports imports 模块通过 imports 建立依赖,通过 exports 控制可见性,形成可测试、可压测的架构

3.3 依赖注入原理

依赖注入是 NestJS 的核心 IoC(控制反转)机制。它允许你将对象的创建和依赖关系的管理交给框架,而不是在类内部手动 new 实例。这带来了松耦合、易测试和单例管理等优势。

3.3.1 DI 工作流程

  1. 注册 Provider: 在 @Module 的 providers 数组中声明类或令牌。
  2. 注入请求: 在构造函数中通过参数类型或 @Inject() 装饰器声明依赖。
  3. 实例化与缓存: NestJS IoC 容器扫描所有模块,创建依赖图,按需实例化并缓存为单例(默认作用域)。
  4. 注入完成: 将实例化后的对象注入到目标类的构造函数中。
// user.service.ts
@Injectable()
export class UserService {
  findAll(): string[] {
    return ['Alice', 'Bob'];
  }
}

// user.controller.ts
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {} // DI 注入

  @Get()
  getUsers(): string[] {
    return this.userService.findAll();
  }
}
性能注意: 默认作用域为 DEFAULT(单例),适合无状态服务。如果使用 REQUEST 或 TRANSIENT 作用域,每次请求或每次注入都会创建新实例,增加 GC 压力,在压测场景下需谨慎评估。

3.3.2 Provider 注册方式对比

方式 语法 适用场景
类 Provider { provide: UserService, useClass: UserService } 标准类注入,最常用
值 Provider { provide: 'CONFIG', useValue: { port: 3000 } } 注入常量、配置对象或第三方库实例
工厂 Provider { provide: 'DB', useFactory: (config) => createDb(config), inject: ['CONFIG'] } 需要动态创建或异步初始化的依赖
别名 Provider { provide: 'AliasService', useExisting: UserService } 为已有 provider 创建别名,避免重复实例

3.4 装饰器原理

装饰器是 TypeScript 的一项实验性特性,NestJS 大量使用类装饰器、方法装饰器、属性装饰器和参数装饰器来声明元数据。这些元数据在运行时被 NestJS 的反射机制读取,从而决定路由、依赖注入、守卫、拦截器等行为。

3.4.1 常用装饰器分类

类别 装饰器 作用
类装饰器 @Module(), @Controller(), @Injectable() 标记类为模块、控制器或可注入服务
方法装饰器 @Get(), @Post(), @Put(), @Delete() 定义 HTTP 路由方法和路径
参数装饰器 @Param(), @Query(), @Body(), @Headers() 从请求中提取特定参数
属性装饰器 @Inject() 显式指定注入的令牌

3.4.2 装饰器执行顺序

在 NestJS 中,多个装饰器组合时,执行顺序遵循以下规则:

  • 参数装饰器 先于 方法装饰器 执行。
  • 方法装饰器 先于 类装饰器 执行。
  • 同一类型的装饰器,从下往上 执行(离声明近的先执行)。
@Controller('example')          // 类装饰器(后执行)
export class ExampleController {
  @Get('data')                  // 方法装饰器(先执行)
  getData(@Query('id') id: string) {  // 参数装饰器(最先执行)
    return `id: ${id}`;
  }
}

3.4.3 装饰器与元数据反射

NestJS 利用 reflect-metadata 库在运行时读取装饰器附加的元数据。例如,@Get('users') 会在原型上存储路径 'users' 和 HTTP 方法 GET,路由解析器在启动时扫描这些元数据并注册路由。

性能洞察: 装饰器元数据的解析发生在应用启动阶段(bootstrap),不影响运行时请求处理性能。但过多的装饰器组合会增加启动时间,在微服务或大规模路由场景下需关注启动耗时。

3.5 本章小结