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

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

相关推荐
雨季mo浅忆16 小时前
记录Node.js安全漏洞及其内置Brotli解码器问题
node.js·安全性测试
tommyjiatong16 小时前
资安底座连环雷:从 PHP2Shell 到 Node2Shell
node.js·php
SEO_juper20 小时前
Bash 交叉验证 robots.txt 和 sitemap:两个文件自相矛盾,页面永远收录不了
开发语言·python·node.js·json·bash
SEO_juper21 小时前
Go 站点地图生成器:从首页爬全站,一键生成 sitemap.xml
xml·开发语言·人工智能·golang·node.js·seo·外贸独立站
Moshow郑锴1 天前
Ansible 批量部署 Node.js 服务:ZIP 直解与共享目录分发两种方案对比
node.js·ansible
少年的范儿2 天前
Windows 安装 Node.js 教程:nvm 版本管理 + pnpm 安装配置
windows·npm·node.js
peach2 天前
同样是装依赖,为什么 npm 长成 node_modules,Maven 长成 .m2
npm·node.js·maven
FungLeo2 天前
成为全栈·Go 后端篇·重写之前先锁基线:契约、Node 版本与行为清单
golang·node.js·go·openapi·契约测试·成为全栈
FungLeo3 天前
成为全栈·Go 后端篇·契约一致性与差分测试:两套实现如何互相校验
golang·node.js·go·合约测试·差分测试·成为全栈·api兼容
月夏3 天前
前端第一次接自己的后端:mock 切真接口踩了 2 个坑,组件只改了一行 import
前端·后端·node.js