NestJS 从入门到企业级实战(二):依赖注入(DI)深度拆解

上一篇我们跑通了第一个接口,但有个问题没讲透: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)的东西。你可以把它理解成一个"大管家":

  1. 注册 :你告诉管家"我有哪些服务"(通过 @Injectable() + providers)。
  2. 创建 :管家在应用启动时,自动帮你 new 所有服务的实例。
  3. 注入:当某个类需要某个服务时,管家自动把实例塞进去。

整个过程

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.tsproviders 数组,确保所有用到的 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。
  • 依赖注入 :你不用自己 new B,框架帮你创建好 B,直接塞给 A。
  • DI 容器:幕后的大管家,负责注册、创建、注入所有被管理的对象。
  • @Injectable():告诉容器"这个类可以被管理"。
  • providers: [XxxService]:告诉容器"请帮我管理这个类"。
  • 构造函数注入:声明"我需要什么",容器自动塞进来。
  • 默认单例:整个应用只有一个实例,全局共享。

一句话总结:DI 就是"你只管声明要什么,框架帮你搞定一切"。

相关推荐
Larru-Sun6 小时前
从零开始用 Docker 部署 Node.js 后端服务
docker·容器·node.js
__zRainy__7 小时前
Node系列 · Express:nodemon
node.js·express
__zRainy__8 小时前
Node系列 · Express:中间件
中间件·node.js·express
2601_962122979 小时前
Node.js的解释
node.js
屋昂仼9 小时前
NestJS 从入门到企业级实战(一):环境搭建与项目结构全解析
node.js
coderCN1 天前
Nodejs 响应头和请求头
后端·node.js
__zRainy__1 天前
Node系列 · ORM:log4js 日志记录
node.js·log4j
秋秋小事1 天前
node 导入与导出
node.js
秋秋小事1 天前
node event模块
node.js