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

目标效果

某个类(TestService)中依赖的其他类(UnitService),不需要在TestService的构造函数中显式的实例化。
只需要使用TS的语法,用private、public、readonly、protected声明构造函数的入参,TestService能自动实例化入参中涉及的类。

被private、public、readonly、protected声明的构造函数参数,会被TS自动放入到类的属性上,和单独声明等效。参考👇问题。

bookmark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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元数据)
  2. 将入参逐个实例化,并且将实例化后的对象放在类中

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,效果如下:

1
2
const testService = DiFactory(TestService) as TestService
testService.test() // 输出:hello world!

NestJS中的Injectable实现

@nestjs@7.0.0 版本中,Injectable() 函数实现和上面的实现一样:

Untitled.png

这里还存储了options元信息,去掉defineMetadata的逻辑,就是一个裸的类装饰器。

元信息是怎么注入的?

这里涉及到元编程的概念。

在编译成es5代码的时候,要先处理成语法树。而处理成语法树的过程中,就能拿到参数的具体类型。然后将其保存下来,提供接口支持用户读取即可。

参考链接中的知乎文章中,有翻译后的es5代码。其中,DemoService和上面的TestService一样,InjectService和上面的UnitService一样(被注入对象)。可以看到,翻译后的es5代码,已经有了参数的类型信息。(2019年)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 此处省略了__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代码已经看不到显式的参数类型了。

1
2
3
4
5
6
7
8
9
10
11
12
var TestService = /** @class */ (function () {
function TestService(unitService) {
this.unitService = unitService;
}
TestService.prototype.test = function () {
this.unitService.sayHello();
};
TestService = __decorate([
Injectable
], TestService);
return TestService;
}());

参考链接

bookmark