NestJS源码:实现依赖注入(DI)

NestJS源码:实现依赖注入(DI)

Tags
DI
Nest.js
TypeScript
元编程
装饰器模式
ES6
CreatedTime
Aug 21, 2022 02:33 PM
Slug
2020-12-02-nestjs-di
UpdatedTime
Last updated August 21, 2022

目标效果

某个类(TestService)中依赖的其他类(UnitService),不需要在TestService的构造函数中显式的实例化。 只需要使用TS的语法,用private、public、readonly、protected声明构造函数的入参,TestService能自动实例化入参中涉及的类。
被private、public、readonly、protected声明的构造函数参数,会被TS自动放入到类的属性上,和单独声明等效。参考👇问题。
 
export class UnitService { public sayHello() { console.log('hello world!') } } @Injectable() export class TestService { constructor( private readonly unitService: UnitService ) { } public test() { this.unitService.sayHello() // 打印hello world! } }
👆代码中,在TestService的test方法里面,就能通过this访问到UnitService的实例unitService。

实现原理

  1. 使用Reflect Metadata 获取类的构造函数的入参类型(参考TS元数据)
  1. 将入参逐个实例化,并且将实例化后的对象放在类中

实现代码

import "reflect-metadata"; // 为什么需要一个什么也不做的装饰器? // 被装饰过的类,才能读到构造函数的元信息。 const Injectable = () => { return (target: ClassConstructorType) => {} }; // 依赖注入工厂函数 const DiFactory = (target: ClassConstructorType) => { // 获取入参类型 const providers = Reflect.getMetadata('design:paramtypes', target) // 逐个实例化 const args = providers.map((provider: ClassConstructorType) => new provider()); // 将实例化后的对象,作为参数,按照顺序传给类的构造函数 return new target(...args) }
使用DiFactory来创建TestService,效果如下:
const testService = DiFactory(TestService) as TestService testService.test() // 输出:hello world!

NestJS中的Injectable实现

@nestjs@7.0.0 版本中,Injectable() 函数实现和上面的实现一样:
notion image
这里还存储了options元信息,去掉defineMetadata的逻辑,就是一个裸的类装饰器。

元信息是怎么注入的?

这里涉及到元编程的概念。
在编译成es5代码的时候,要先处理成语法树。而处理成语法树的过程中,就能拿到参数的具体类型。然后将其保存下来,提供接口支持用户读取即可。
参考链接中的知乎文章中,有翻译后的es5代码。其中,DemoService和上面的TestService一样,InjectService和上面的UnitService一样(被注入对象)。可以看到,翻译后的es5代码,已经有了参数的类型信息。(2019年)
// 此处省略了__decorate和__metadata的实现代码 var DemoService = /** @class */ (function() { function DemoService(injectService) { this.injectService = injectService; } DemoService.prototype.test = function() { console.log(this.injectService.a); }; DemoService = __decorate( [Injectable(), __metadata('design:paramtypes', [InjectService])], DemoService ); return DemoService; })();
在ts新版本中(2021年),由于装饰器提案一直在推进,翻译后的es5代码已经看不到显式的参数类型了。
var TestService = /** @class */ (function () { function TestService(unitService) { this.unitService = unitService; } TestService.prototype.test = function () { this.unitService.sayHello(); }; TestService = __decorate([ Injectable ], TestService); return TestService; }());

参考链接

基于 TypeScript 的 IoC 与 DI
在使用 Angular或者 Nestjs 时,你可能会遇到下面这种形式的代码: 上述代码中使用了 Component的装饰器,并在模块的 providers中注入了需要使用的服务。这个时候,在 AppComponent中 otherService将会自动获取到 OtherService 实例。 你可能会比较好奇, Angular是如何实现这种神奇操作的呢?实现的过程简而言之,就是 Angular在底层使用了IoC设计模式,并利用 TypeScript 强大的装饰器特性,完成了依赖注入。下面我会详细介绍IoC与DI,以及简单的DI实例。 理解了IoC与DI的原理,有助于我们更好的理解和使用Angular及Nestjs。 IoC 英文全称为 Inversion of Control,即 控制反转 。控制反转是面向对象编程中的一种原则,用于降低代码之间的耦合度。传统应用程序都是在类的内部主动创建依赖对象,这样将导致类与类之间耦合度非常高,并且不容易测试。有了 IoC 容器之后,可以将创建和查找依赖对象的控制权交给了容器,这样对象与对象之间就是松散耦合了,方便测试与功能复用,整个程序的架构体系也会变得非常灵活。 正常方式的引用模块是通过直接引用,就像下面这个例子一样: 这么做会造成 ModuleC依赖于 ModuleA和 ModuleB,产生了模块间的耦合。为了解决模块间的强耦合性, IoC 的概念就产生了。 我们通过使用一个容器来管理我们的模块,这样模块之间的耦合性就降低了(下面这个例子只是模仿 IoC 的过程,Container 需要另外实现): 为了让大家更清楚 IoC 的过程,我举一个例子,方便大家理解。 当我要找工作的时候,我会去网上搜索想要的工作岗位,然后去投递简历,这个过程叫做控制正转,也就是说控制权在我的手上。而对于控制反转,找工作的过程就变成了,我把简历上传到拉钩这样的第三方平台(容器),第三方平台负责管理很多人的简历。此时HR(其他模块)如果想要招人,就会按照条件在第三方平台查询到我,然后再联系安排面试。 DI 英文全称为 Dependency Injection,即 依赖注入 。依赖注入是控制反转最常见的一种应用方式,即通过控制反转,在对象创建的时候,自动注入一些依赖对象。 在 Nestjs或 Angular中,我们需要通过装饰器@Injectable()让我们依赖注入到类实例中。而理解他们如何实现依赖注入,我们需要先对
基于 TypeScript 的 IoC 与 DI