系列第 02 篇 · 拆解
constructor(private readonly appService: AppService) {}这一行背后发生了什么
零、这篇文章讲什么
在 01 NestJS 是什么 里,我说 NestFactory.create() 是最外层的第一层工厂,它内部还藏着一层更重要的工厂——依赖注入容器(IoC Container)。这一篇就钻进去,把它讲透。
这一篇不是概念科普,而是对着代码逐行拆。看完你会明白:
- 你写的那句
constructor(private readonly appService: AppService)到底在干什么 - 谁负责
new AppService(),为什么不让你自己 new - Provider(提供者)是什么,为什么它不只是 Service
一、问题:为什么不让你手动 new
先看直觉做法。如果没有依赖注入,你的 controller 大概会这样写:
export class AppController {
appService: AppService;
constructor() {
this.appService = new AppService(); // 手动 new,紧紧耦合
}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
看起来没什么问题?三个隐患:
- 硬耦合:controller 必须知道
AppService怎么构造。哪天AppService构造器加了个数据库参数,这里全要改。 - 难以测试:想给 controller 喂假数据测逻辑,得先能替换
appService的实现,手动 new 的方式替换不了。 - 实例管理混乱:如果三个 controller 都用
AppService,谁来保证它是同一个实例?各自 new 一遍,状态就分裂了。
NestJS 的做法:你别 new,你只声明"我要什么"。
二、依赖注入的三件套
依赖注入不是一句口号,它在 NestJS 里由三处代码配合完成。你的 hello 项目里三件套正好齐全。
① 供给端:@Injectable() 标记"我交给容器管"
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
@Injectable() 告诉 Nest:这个类可以被注入,请把我登记进容器。 它是 provider 的"登记证"。
② 消费端:构造器里声明"我要这个"
export class AppController {
constructor(private readonly appService: AppService) {}
}
这里只做了一件事:声明依赖——"我这个类需要一份 AppService"。至于这份 AppService 怎么来的,我不管。
③ 容器端:启动时自动装配
const app = await NestFactory.create(AppModule);
Nest 启动时扫描到 AppController 的构造器参数是 AppService,就从容器里取一个实例注入进去。整个过程你一行 new 都没写过。
三件套缺一不可:没有 @Injectable(),容器不知道它可以被管;没有构造器声明,容器不知道该给谁;没有容器装配,前两件就是空话。
三、那行语法糖,拆开看
constructor(private readonly appService: AppService) {}
这一行其实是 TypeScript 的参数属性语法糖,等价于手写四行:
export class AppController {
appService: AppService; // ① 声明实例属性
constructor(appService: AppService) { // ② 构造器接收参数
this.appService = appService; // ③ 赋值给属性
}
}
逐字解释:
private—— 让appService成为实例的私有属性,外部访问不到readonly—— 只读,注入后不允许中途被换掉appService: AppService—— 参数名 + 类型,Nest 靠类型知道该注入谁
所以你的 getHello() 里才能直接用 this.appService.getHello()。这一行就是把"声明属性 + 构造器收参 + 赋值"三步压缩成一步的语法糖。
注意:NestJS 用的是构造器注入,但依赖注入不止这一种注入方式。还有属性注入、Setter 注入等,NestJS 中构造器注入是主流和推荐。
四、没有 DI vs 有 DI
把两种写法并排看,DI 的价值就一目了然:
// ❌ 手动 new:调用方负责创建,紧耦合
export class AppController {
constructor() {
this.appService = new AppService();
}
}
// ✅ 依赖注入:调用方只声明,容器负责创建
export class AppController {
constructor(private readonly appService: AppService) {}
}
| 手动 new | 依赖注入 | |
|---|---|---|
| 谁创建对象 | 调用方 | 容器 |
| 换实现 | 所有调用处都要改 | 只改容器配置 |
| 单元测试换 mock | 做不到 | 注入 mock 即可 |
| 实例唯一性 | 各 new 各的 | 容器统一管理(默认单例) |
对照 factory_demo/1.mjs 里你写的 MixueFactory.create("ice") —— 那是最朴素的工厂:手动传 type,工厂返回实例。DI 容器是更高级的工厂:它不用你传 type,而是靠反射读取类的元数据(@Injectable)自动决定怎么造、造几份。你之前学的工厂模式,在这里长成了它的成年形态。
五、Provider:提供者是什么
前面反复出现"provider",这一节给它正名。
中文译名
Provider 目前没有官方中文译名,最常见的翻译是 "提供者"(也有人译作"供应者""服务提供者")。但比译名更重要的是理解它指什么——它是一个分类概念,不是某一种具体的类。
本质:三个条件
一个对象要成为 NestJS 的 provider:
@Injectable() // ① 标记:我可以被容器管理
export class AppService { ... } // ② 是一个类
// ③ 被登记进 @Module 的 providers 数组
@Module({
providers: [AppService], // ③ 登记
})
一句话定义:
Provider = 被
@Injectable()标记、由 Nest 容器创建并管理、可以被注入到其他类里使用的对象。
为什么叫"提供者"
命名很讲究,它站在"被使用方"的角度:
- 你的 controller 说:"我需要一个 AppService"
- 容器说:"好,我这里有,我提供给你"
- 于是 AppService 就是那个"被提供的对象" → Provider
最大的误区:Provider 不只是 Service
Service 只是 provider 最常见的一种。 Provider 是一个更宽泛的集合:
| Provider 形态 | 作用 | 例子 |
|---|---|---|
| Service | 业务逻辑 | 你的 AppService |
| Repository | 数据访问 | UserRepository |
| Factory | 动态生产值 | 读取环境变量的 configFactory |
| Helper / 工具类 | 通用能力 | 加密、日期格式化 |
它们全都叫 provider,因为形态上是一回事:@Injectable() 标记 + 登记进模块 + 可被注入。区别只在"提供的能力是什么"。
六、DI 带来的三样东西
1. 解耦:改实现,不改调用方
controller 只认接口(类型),不认具体怎么来的。将来把 AppService 从返回字符串改成查数据库,controller 一行不用动,只改容器配置。
2. 可测试:注入 mock 就行
测试时把真实 AppService 换成假数据版本:
const controller = new AppController(mockService);
不用起服务器,不用连数据库。这也是为什么 NestJS 项目里天然带着 *.spec.ts 测试文件——DI 让单元测试变得极其自然。
3. 生命周期管理:默认单例
@Injectable() 的 provider 默认是单例——整个应用里只有一个实例,所有消费它的地方共用这一个。这是容器在替你管的"造几份"的问题。
生命周期有几种取值,会放在系列后面(模块作用域篇)详细讲。这里只需记住:默认单例,一个 provider 全应用一份。
用人才库比喻收尾这一节:
@Injectable() AppService → 登记进人才库(providers: [AppService])
│
│ 人才库(容器)持有它的实例
│
AppController 构造器说: │
constructor(appService) ←────────┘ 人才库把这号人才"派发"给 controller
- 人才库 = 容器(IoC Container)
- 登记的人才 = Provider(提供者)
- 派发人才 = 注入(Injection)
- 被派发到的人 = 消费方(Consumer)
七、小结
依赖注入这一篇的完整逻辑链:
为什么不让你手动 new?
└─ 因为:硬耦合、难测试、实例乱
怎么解决?三件套:
├─ @Injectable() 供给端:我交给容器管
├─ constructor(... 声明) 消费端:我要这个
└─ NestFactory.create() 容器端:我帮你注入
这一行背后是语法糖:
└─ private readonly 参数属性 = 声明属性 + 构造器收参 + 赋值
被注入的东西叫什么?
└─ Provider(提供者):可注入、可复用、默认单例,Service 只是其中一种
下一篇预告:DI 讲透了,就可以进入路由与参数了——@Get(':id')、@Body()、@Param()、@Query() 这些参数装饰器怎么把前端请求的每一块数据送到你的方法里。这是让 hello 项目从"返回固定字符串"走向"处理真实请求"的第一步。