依赖注入(DI)

如果你是第一次接触 依赖注入(Dependency Injection)NestJS,感觉概念比较抽象、难以理解是很正常的。下面我会用更通俗、分步骤的方式,来帮助你理清 NestJS DI 容器和 TypeORM 的集成原理。希望能让你对"为什么要这样做"和"怎么做"有更直观的认识。


1. 为什么要用依赖注入(DI)?

1.1 传统做法(手动 new 对象)

在没有 DI 的情况下,如果一个类 A 需要用到类 B,我们通常会在 A 里面手动写 const b = new B()。问题是:

  1. 如果 B 还依赖 C,那就要在 B 里写 new C()。层层嵌套会变得很乱。
  2. 想测试 A 的时候,如果想换个"假" B(比如一个 mock),就要改代码,把 new B() 换成 new FakeB()

这样会导致耦合度很高,代码难以维护和测试。

1.2 有了 DI 的好处

  1. 解耦 :在类 A 中只需要"声明"自己需要 B,而不需要手动创建 B。框架(NestJS)会帮我们找来 B 的实例。
  2. 易测 :测试时,我们可以给 A 注入一个假的 B(mock),而不用改 A 的代码。
  3. 灵活 :一旦想换成另一个实现,只需要在配置或模块里改一下提供者,就能把 B 换成 B2

2. NestJS DI 容器做了什么?

你可以把 NestJS DI 容器 想象成一个"大管家":

  1. 扫描 :NestJS 启动时,会扫描所有标记了 @Injectable()@Controller() 等装饰器的类,把它们当做可以被"管理"的对象(Provider)。
  2. 登记:管家会记下"这些类是 Provider,分别叫什么名字(令牌/Token),需要用什么东西来创建"。
  3. 检查依赖 :如果某个 Provider(比如 A)的构造函数里写了 constructor(private b: B) {}, 管家就知道 A 需要 B
  4. 实例化 :管家会先去创建(或获取)B 的实例,然后再创建 A 的实例,并把 B 塞给 A
  5. 缓存 :默认情况下,NestJS 会把创建好的对象缓存起来(单例模式)。后面要用到 A 的地方,直接复用已经创建好的 A

一个极简的例子

typescript 复制代码
@Injectable()
class B {
  sayHello() {
    return 'Hello from B';
  }
}

@Injectable()
class A {
  constructor(private b: B) {}

  greet() {
    return this.b.sayHello();
  }
}
  • 当 NestJS 看到 A 需要 B 时,就会先创建 B,然后创建 A,并把 B 的实例注入到 A 里面。
  • 于是 A.greet() 就能调用 B.sayHello() 了,但我们自己从来没有写 new B()

3. 那 TypeORM 又是怎么回事?

3.1 TypeORM 和 NestJS 的集成

  • TypeORM 是一个数据库操作库,可以帮我们用面向对象的方式(Entity/Repository)来增删改查数据。
  • NestJS 提供了一个官方模块 @nestjs/typeorm,把 TypeORM 的"连接"、"实体(Entity)"、"仓库(Repository)"也当做 Provider 注册进了 NestJS 的 DI 容器。

3.2 Repository 是怎样被注入的?

  1. 你在某个模块里写 TypeOrmModule.forFeature([User]),NestJS 就会为 User 这个实体创建一个 UserRepository Provider。

  2. 当你的服务(Service)中需要用到 UserRepository 时,你只要写:

    typescript 复制代码
    @Injectable()
    export class UserService {
      constructor(
        @InjectRepository(User) private userRepository: Repository<User>,
      ) {}
    
      findAll() {
        return this.userRepository.find();
      }
    }
    • NestJS 看到 @InjectRepository(User),就知道要注入对应的 UserRepository
    • 因为在同一个模块(或导入的模块)里,TypeOrmModule.forFeature([User]) 已经把这个仓库注册到 DI 容器了。
    • 最终,NestJS 会把已经创建好的 UserRepository 对象塞到 userRepository 这个参数里。

3.3 为什么要把 Repository 也放进 DI 容器?

  • 这样一来,Service 只需要"声明"需要哪个 Repository,而不用自己写 new Repository().
  • 另外,如果想在测试中换个假的 Repository,也很容易,只要在测试模块里提供一个"假仓库"就行了。

4. 结合起来:一步一步发生了什么?

  1. NestJS 启动
    • 读取所有模块和类,发现哪些类用了 @Injectable()、哪些实体放进了 TypeOrmModule.forFeature(...)
  2. 注册 Provider
    • DI 容器把每个"可注入"的类和 Repository 都当做一个 Provider 记录下来。
  3. 分析依赖
    • 哪些服务需要哪些 Repository?哪些控制器需要哪些服务?
  4. 实例化
    • 从底层依赖开始往上实例化。
    • 先创建数据库连接 -> 创建 Repository -> 创建 Service -> 注入到 Controller。
  5. 缓存并复用
    • 全部默认单例,如果别的地方也需要同一个 Service,就直接拿缓存好的实例。
  6. 响应请求
    • 当请求进来时,Controller 被调用 -> 调用 Service -> Service 调用 Repository -> 操作数据库 -> 返回结果。
    • 全过程里,你都没写 new Service()new Repository();都是由 NestJS 的 DI 容器自动完成。

5. 我还是觉得抽象,能再直观一点吗?

5.1 "大管家" 比喻

  • 管家手里有个"字典":记下"某个名称" -> "对应的类" -> "怎么创建它"。
  • 有人来问管家:"我要一个 UserService 的对象"。
  • 管家先看字典:UserService 需要 UserRepository,对吗?那就去看"UserRepository"的条目;UserRepository 需要数据库连接,对吗?那就先把数据库连接弄好,再生成一个 UserRepository。
  • 有了 UserRepository 后,就能生成 UserService。
  • 生成好后管家会说:"以后谁要 UserService,就直接拿这份现成的。"

5.2 如果要测试?

  • 你可以告诉管家:"在测试环境里,不要用真的 UserRepository,用一个假的 FakeUserRepository"。只要管家"字典"里的映射改了,就可以把假对象注入到 Service 中,测试起来就很方便。

6. 需要多少预备知识?

  1. TypeScript 装饰器和反射
    • 了解 @Injectable()@Controller()@InjectRepository() 等装饰器的作用原理。
    • 了解 reflect-metadata 在 NestJS 中是如何用来读取构造函数的参数类型的。
  2. 模块化概念
    • 明白 NestJS 中"模块"(Module)是怎么把一堆相关的 Provider(服务、控制器、仓库)组织到一起的。
  3. 面向对象和设计模式
    • 依赖注入(DI)本质是"控制反转(IoC)"的一种实现,需要对面向对象、单例模式等有些基本理解。

如果这些概念还很陌生,建议先花点时间熟悉一下 面向对象编程DI / IoCTypeScript 装饰器 等,这样再回过头看 NestJS 的依赖注入,就会豁然开朗。


结语

  • DI 容器 = 一个专门管理对象实例创建和依赖关系的"管家"。
  • TypeORM 集成 = NestJS 把 TypeORM 的仓库也纳入管家管理,通过 TypeOrmModule.forFeature([Entity]) 注册,之后任何服务想用仓库,只要声明依赖就行。
  • 理解关键 :你不用再写 new 去手动创建对象,NestJS 帮你干了这件事;而你只要专注于"我需要什么"------在构造函数里写上即可。

如果现在还是有点懵,不用急,这些概念最初确实比较抽象,多写几个示例、动手调试看看,就会慢慢明白"哦,原来 NestJS 启动的时候就已经替我把那些对象都创建好了,我只管在代码里声明需求就行。" 这是依赖注入带来的巨大好处,也是 NestJS 之所以容易扩展、易测试、结构清晰的根本原因。

相关推荐
拉不动的猪7 分钟前
刷刷题30(vue3常规面试题)
前端·javascript·面试
狂炫一碗大米饭17 分钟前
面试小题:写一个函数实现将输入的数组按指定类型过滤
前端·javascript·面试
最胖的小仙女18 分钟前
通过动态获取后端数据判断输入的值打小
开发语言·前端·javascript
GoldenaArcher21 分钟前
[MERN] 使用 socket.io 实现即时通信功能
websocket·mongodb·node.js·reactjs·express
yzhSWJ36 分钟前
Vue 3 中,将静态资源(如图片)转换为 URL
前端·javascript·vue.js
Moment39 分钟前
🏞 JavaScript 提取 PDF、Word 文档图片,非常简单,别再头大了!💯💯💯
前端·javascript·react.js
初心丨哈士奇1 小时前
基于大模型的GitLab CodeReview 技术调研
前端·人工智能·node.js
鱼樱前端1 小时前
基于Vue3+Ts+Vant的高级图片上传组件
前端·javascript·vue.js
ChangYan.1 小时前
electron builder打包时,出现errorOut=ERROR: Cannot create symbolic link
前端·javascript·electron
冴羽1 小时前
SvelteKit 最新中文文档教程(1)—— 入门指南
前端·javascript·svelte