文章

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

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

本文由作者按照 CC BY 4.0 进行授权