上一篇我们跑通了第一个接口,但有个问题没讲透:Controller 里的
appService是从哪来的?为什么不用new AppService()?本篇目标:彻底搞懂依赖注入(DI),这是 NestJS 最核心的机制,搞懂它,后面所有概念都迎刃而解。
一、先搞懂"依赖"是什么
看这段代码:
TypeScript
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
问题:Controller 需要调用 Service 的方法,Controller 就"依赖"了 Service。
前端类比:你在 Vue 组件里用了 Pinia 的 store,组件就"依赖"了这个 store。
传统写法(没有 DI 时):
TypeScript
@Controller()
export class AppController {
// 手动 new 一个 Service 实例
private readonly appService = new AppService();
@Get()
getHello(): string {
return this.appService.getHello();
}
}
这种写法的问题:
- Controller 和 Service 强耦合了------Controller 必须知道 Service 怎么创建。
- 如果 Service 的构造函数变了(比如要传参数),所有用到 Service 的地方都要改。
- 无法复用实例------每次
new都是一个新的对象。
二、依赖注入(DI)解决了什么问题
一句话定义:DI 就是"你不用自己创建依赖对象,框架帮你创建好,直接塞给你"。
前端类比 :Vue 的 provide/inject。父组件 provide 一个值,子组件 inject 就能用,子组件不需要知道这个值从哪来、怎么创建的。
用了 DI 之后:
TypeScript
@Controller()
export class AppController {
// 你只需要声明"我需要一个 AppService"
// 框架会自动帮你创建并注入
constructor(private readonly appService: AppService) {}
}
对比:
表格
| 方式 | 写法 | 问题 |
|---|---|---|
| 手动 new | new AppService() |
强耦合,难维护 |
| 依赖注入 | constructor(appService: AppService) |
框架自动管理,解耦 |
三、DI 容器:幕后的"大管家"
DI 的核心是一个叫 DI 容器(IoC Container)的东西。你可以把它理解成一个"大管家":
- 注册 :你告诉管家"我有哪些服务"(通过
@Injectable()+providers)。 - 创建 :管家在应用启动时,自动帮你
new所有服务的实例。 - 注入:当某个类需要某个服务时,管家自动把实例塞进去。
整个过程:
TypeScript
1. 你在 AppService 上贴 @Injectable()
→ 告诉容器:"这个类可以被管理"
2. 你在 AppModule 的 providers 里注册 AppService
→ 告诉容器:"请帮我管理 AppService"
3. 你在 AppController 的构造函数里声明需要 AppService
→ 容器看到后,自动把 AppService 的实例塞进来
四、动手:从零理解 DI 的完整流程
第一步:创建 Service
TypeScript
// app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable() // ← 关键:告诉 DI 容器"我可以被管理"
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
@Injectable() 做了什么?
- 它给
AppService类打了一个"标签",告诉 NestJS:"这个类可以被 DI 容器管理"。 - 不加这个装饰器,容器不知道它的存在,也就不会帮你创建实例。
第二步:在 Module 中注册 Service
TypeScript
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService], // ← 关键:把 Service 注册到容器中
})
export class AppModule {}
providers: [AppService] 做了什么?
- 告诉 DI 容器:"请帮我创建并管理
AppService的实例"。 - 容器启动时,会自动
new AppService(),并把这个实例存起来。
第三步:在 Controller 中注入 Service
TypeScript
// app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
// ← 关键:声明"我需要一个 AppService"
// 容器看到后,自动把实例塞进来
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
构造函数里的 appService: AppService 做了什么?
- 你只是声明了类型,容器看到类型后,去自己的"仓库"里找到对应的实例,自动注入。
- 你不需要
new,不需要import实例,框架全帮你搞定。
五、DI 的三种生命周期(作用域)
DI 容器创建实例时,有三种模式:
表格
| 模式 | 含义 | 前端类比 |
|---|---|---|
| DEFAULT(默认) | 单例,整个应用只有一个实例 | Pinia store |
| TRANSIENT | 每次注入都创建新实例 | 普通函数调用 |
| REQUEST | 每个 HTTP 请求创建一个新实例 | 请求级别的临时状态 |
默认就是单例 ,这也是最常用的。也就是说,不管你注入多少次 AppService,拿到的都是同一个实例。
TypeScript
// 这两个拿到的 appService 是同一个实例
constructor(
private readonly appService1: AppService,
private readonly appService2: AppService,
) {
console.log(appService1 === appService2); // true
}
六、动手:多 Service 注入实战
创建一个 UserService,然后在 Controller 里同时注入两个 Service:
user.service.ts
TypeScript
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
findAll() {
return [
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 },
];
}
}
app.module.ts
TypeScript
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserService } from './user.service';
@Module({
controllers: [AppController],
providers: [AppService, UserService], // 注册两个 Service
})
export class AppModule {}
app.controller.ts
TypeScript
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { UserService } from './user.service';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly userService: UserService,
) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('users')
getUsers() {
return this.userService.findAll();
}
}
访问 http://localhost:3000/users,你会看到:
TypeScript
[
{ "id": 1, "name": "张三", "age": 25 },
{ "id": 2, "name": "李四", "age": 30 }
]
七、常见报错与解决方案
报错 1:Nest can't resolve dependencies of the AppController (?, ...)
原因:你在 Controller 里注入了某个 Service,但没有在 Module 的 providers 里注册它。
解决:检查 app.module.ts 的 providers 数组,确保所有用到的 Service 都注册了。
报错 2:Cannot read properties of undefined (reading 'xxx')
原因:Service 没有被 @Injectable() 装饰,容器不认识它。
解决:确保 Service 类上有 @Injectable() 装饰器。
报错 3:循环依赖(Circular dependency)
原因:A 依赖 B,B 又依赖 A,形成死循环。
解决:使用 forwardRef() 打破循环,或者重新设计架构避免循环依赖。
TypeScript
// 示例:打破循环依赖
constructor(
@Inject(forwardRef(() => BService))
private readonly bService: BService,
) {}
八、本篇总结
- 依赖:A 需要用 B,A 就依赖 B。
- 依赖注入 :你不用自己
newB,框架帮你创建好 B,直接塞给 A。 - DI 容器:幕后的大管家,负责注册、创建、注入所有被管理的对象。
@Injectable():告诉容器"这个类可以被管理"。providers: [XxxService]:告诉容器"请帮我管理这个类"。- 构造函数注入:声明"我需要什么",容器自动塞进来。
- 默认单例:整个应用只有一个实例,全局共享。
一句话总结:DI 就是"你只管声明要什么,框架帮你搞定一切"。