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 图
3.3 依赖注入原理
依赖注入是 NestJS 的核心 IoC(控制反转)机制。它允许你将对象的创建和依赖关系的管理交给框架,而不是在类内部手动 new 实例。这带来了松耦合、易测试和单例管理等优势。
3.3.1 DI 工作流程
- 注册 Provider: 在
@Module的providers数组中声明类或令牌。 - 注入请求: 在构造函数中通过参数类型或
@Inject()装饰器声明依赖。 - 实例化与缓存: NestJS IoC 容器扫描所有模块,创建依赖图,按需实例化并缓存为单例(默认作用域)。
- 注入完成: 将实例化后的对象注入到目标类的构造函数中。
// 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),不影响运行时请求处理性能。但过多的装饰器组合会增加启动时间,在微服务或大规模路由场景下需关注启动耗时。