广告位联系
返回顶部
分享到

Nest.js之依赖注入原理及实现过程介绍

JavaScript 来源:互联网 作者:佚名 发布时间:2024-02-14 13:00:17 人浏览
摘要

很久之前初学Java时就对注解及自动依赖注入这种方式感觉到不可思议,但是一直没有勇气(懒)去搞清楚。现在做前端了,发现 Nest.js 里面竟然也是这玩意,终究还是躲不过,那就趁着阳康了

很久之前初学 Java 时就对注解及自动依赖注入这种方式感觉到不可思议,但是一直没有勇气(懒)去搞清楚。现在做前端了,发现 Nest.js 里面竟然也是这玩意,终究还是躲不过,那就趁着“阳康”了搞清楚一下吧。

关于为什么要进行依赖注入这里就不展开了,下面直接进入正题,TypeScript 依赖注入的原理。

TypeScript 依赖注入的原理

TypeScript 中实现依赖注入离不开 Decorator 和 Metadata(需要引入第三方库 reflect-metadata),下面通过一个简单的例子来快速了解它的用途:

1

2

3

4

5

6

7

8

9

10

import 'reflect-metadata'

@Reflect.metadata('class', 'Class Data')

class Test {

  @Reflect.metadata('method', 'Method Data')

  public hello(): string {

    return 'hello world'

  }

}

console.log(Reflect.getMetadata('class', Test)) // Class Data

console.log(Reflect.getMetadata('method', new Test(), 'hello')) // Method Data

通过例子可以看到,我们通过 Reflect.metadata() 这个装饰器可以往类及其方法上面添加数据,然后通过 Reflect.getMetadata 可以取到这些数据。我们可以借助这一特性,实现简单的依赖注入:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

import 'reflect-metadata'

class TestService {}

@Reflect.metadata('params', [TestService])

class Test {

  constructor(testService: TestService) {}

  public hello(): string {

    return 'hello world'

  }

}

type Constructor<T = any> = new (...args: any[]) => T

const inject = <T>(target: Constructor<T>): T => {

  const providers = Reflect.getMetadata('params', target)

  const args = providers.map((provider: Constructor) => new provider())

  return new target(...args)

}

inject(Test).hello()

如上所示,我们通过 @Reflect.metadata('params', [TestService]) 在 Test 上添加了元数据,表示构造函数中需要用到 TestService,但 Nest.js 中好像不需要这样。怎么办呢?答案就是:

1

2

3

4

5

{

  "compilerOptions": {

    "emitDecoratorMetadata": true

  }

}

开启了这个参数后,我们就不需要手动添加元数据了:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

import 'reflect-metadata'

class TestService {}

const D = (): ClassDecorator => (target) => {}

@D()

class Test {

  constructor(testService: TestService) {}

  public hello(): string {

    return 'hello world'

  }

}

type Constructor<T = any> = new (...args: any[]) => T

const inject = <T>(target: Constructor<T>): T => {

  const providers = Reflect.getMetadata('design:paramtypes', target)

  const args = providers.map((provider: Constructor) => new provider())

  return new target(...args)

}

inject(Test).hello()

原因在于开启 emitDecoratorMetadata 后,TS 自动会在我们的装饰器前添加一些装饰器。比如,下面这段代码:

1

2

3

4

5

6

7

8

9

10

11

12

import 'reflect-metadata'

const D = (): ClassDecorator => (target) => {}

const methodDecorator = (): MethodDecorator => (target, key, descriptor) => {}

@D()

class Test {

  constructor(a: number) {}

  @methodDecorator()

  public hello(): string {

    return 'hello world'

  }

  public hi() {}

}

编译过后是这样子的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

var __decorate = ...

var __metadata = ...

import 'reflect-metadata'

const D = () => (target) => {}

const methodDecorator = () => (target, key, descriptor) => {}

let Test = class Test {

  constructor(a) {}

  hello() {

    return 'hello world'

  }

  hi() {}

}

__decorate(

  [

    methodDecorator(),

    __metadata('design:type', Function),

    __metadata('design:paramtypes', []),

    __metadata('design:returntype', String),

  ],

  Test.prototype,

  'hello',

  null

)

Test = __decorate(

  [D(), __metadata('design:paramtypes', [Number])],

  Test

)

可以看到,TS 自动会添加 design:type|paramtypes|returntype 三种类型的元数据,分别表示目标本身,参数以及返回值的类型。

我们把 inject 稍微改一下,支持递归的注入,这样一个简单的依赖注入就实现了:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

import 'reflect-metadata'

const D = (): ClassDecorator => (target) => {}

class OtherService {}

@D()

class TestService {

  constructor(otherService: OtherService) {}

}

@D()

class Test {

  constructor(testService: TestService) {}

  public hello(): string {

    return 'hello world'

  }

}

type Constructor<T = any> = new (...args: any[]) => T

const inject = <T>(target: Constructor<T>): T => {

  const providers = Reflect.getMetadata('design:paramtypes', target)

  if (providers) {

    const args = providers.map((provider: Constructor) => {

      return inject(provider)

    })

    return new target(...args)

  }

  return new target()

}

inject(Test).hello()

接下来,我们浅看一下 Nest.js 大概是怎么实现的。

浅析 Nest.js 实现依赖注入的过程

我们通过官方脚手架生成一个 Demo 项目,可以发现其中 tsconfig.json 中的 emitDecoratorMetadata 确实是开启的。我们先用一个最简单的例子来说明:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

// app.module.ts

import {Injectable, Module} from '@nestjs/common'

@Injectable()

class TestService {

  hello() {

    return 'hello world'

  }

}

@Module({

  providers: [TestService],

})

export class AppModule {

  constructor(testService: TestService) {

    testService.hello()

  }

}

// main.ts

import {NestFactory} from '@nestjs/core'

import {AppModule} from './app.module'

async function bootstrap() {

  await NestFactory.create(AppModule)

}

bootstrap()

为了更加直观的理解流程,这里暂时先把源码核心部分扒下来,我们把 await NestFactory.create(AppModule) 替换成我们自己的代码:

1

2

const injector = new Injector()

await injector.inject(AppModule)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

import {Type} from '@nestjs/common'

import {MODULE_METADATA} from '@nestjs/common/constants'

import {ApplicationConfig, NestContainer} from '@nestjs/core'

import {InstanceLoader} from '@nestjs/core/injector/instance-loader'

export default class Injector {

  container: NestContainer

  public async inject(module: any) {

    const applicationConfig = new ApplicationConfig()

    this.container = new NestContainer(applicationConfig)

    const moduleInstance = await this.container.addModule(module, null)

    // 1 resolve dependencies

    const {token, metatype} = moduleInstance

    this.reflectProviders(metatype, token)

    // 2 create instance

    const instanceLoader = new InstanceLoader(this.container)

    instanceLoader.createInstancesOfDependencies()

  }

  public reflectProviders(module: Type<any>, token: string) {

    const providers = [

      ...this.reflectMetadata(MODULE_METADATA.PROVIDERS, module),

    ]

    providers.forEach((provider) => {

      return this.container.addProvider(provider as Type<any>, token)

    })

  }

  public reflectMetadata(metadataKey: string, metatype: Type<any>) {

    return Reflect.getMetadata(metadataKey, metatype) || []

  }

}

这里大概分成两部分:

  • 处理 module 的依赖,也就是 @Module 装饰器所声明的,我们这里暂时只考虑 providers。这一步执行完后,NestContainer 中数据如下(注意到 Module 本身也作为自己的 provider):

1

2

3

4

5

6

7

8

9

10

11

{

  modules: {

    '19bb8f429cacdbcc18fc1afcaac891a4606578aa': Module {

      _metatype: class AppModule {...},

      _providers: {

        class AppModule {...}: InstanceWrapper {}, // Module 本身也作为自己的 provider

        class TestService {...}: InstanceWrapper {}

      }

    }

  }

}

  • 实例化 Module。这一部分需要稍微看一下源码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public async createInstancesOfDependencies(

  modules: Map<string, Module> = this.container.getModules(),

) {

  ...

  await this.createInstances(modules);

}

...

private async createInstances(modules: Map<string, Module>) {

  await Promise.all(

    [...modules.values()].map(async moduleRef => {

      await this.createInstancesOfProviders(moduleRef);

      ...

    }),

  );

}

这里的意思是实例化所有的 Module,实例化 Module 前,我们需要先实例化它的依赖,具体到这里就是实例化 providers:

1

2

3

4

5

6

7

private async createInstancesOfProviders(moduleRef: Module) {

  const { providers } = moduleRef;

  const wrappers = [...providers.values()];

  await Promise.all(

    wrappers.map(item => this.injector.loadProvider(item, moduleRef)),

  );

}

最后会到 loadInstance 这个函数:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

public async loadInstance<T>(

  wrapper: InstanceWrapper<T>,

  collection: Map<InstanceToken, InstanceWrapper>,

  moduleRef: Module,

  contextId = STATIC_CONTEXT,

  inquirer?: InstanceWrapper,

) {

  ...

  try {

    const callback = async (instances: unknown[]) => {

      const properties = await this.resolveProperties(

        wrapper,

        moduleRef,

        inject as InjectionToken[],

        contextId,

        wrapper,

        inquirer,

      );

      const instance = await this.instantiateClass(

        instances,

        wrapper,

        targetWrapper,

        contextId,

        inquirer,

      );

      this.applyProperties(instance, properties);

      done();

    };

    await this.resolveConstructorParams<T>(

      wrapper,

      moduleRef,

      inject as InjectionToken[],

      callback,

      contextId,

      wrapper,

      inquirer,

    );

  } catch (err) {

    done(err);

    throw err;

  }

}

接下来就到了最重要的 this.resolveConstructorParams 这个函数了,我们以 class AppModule 这个 provider 为例来分析:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

public async resolveConstructorParams<T>(

    wrapper: InstanceWrapper<T>,

    moduleRef: Module,

    inject: InjectorDependency[],

    callback: (args: unknown[]) => void | Promise<void>,

    contextId = STATIC_CONTEXT,

    inquirer?: InstanceWrapper,

    parentInquirer?: InstanceWrapper,

  ) {

    // dependencies 返回的就是 AppModule 构造函数的参数类型,本例为: [TestService]

    const [dependencies, optionalDependenciesIds] = isFactoryProvider

      ? this.getFactoryProviderDependencies(wrapper)

      : this.getClassDependencies(wrapper);

    let isResolved = true;

    const resolveParam = async (param: unknown, index: number) => {

      ...

    };

    // 这里的 instances 就是通过 dependencies 实例化后的对象,具体到本例,可以理解为这样: [new TestService()]

    const instances = await Promise.all(dependencies.map(resolveParam));

    isResolved && (await callback(instances));

  }

其中调用 this.getClassDependencies(wrapper) 最终会调用 reflectConstructorParams:

1

2

3

4

5

6

public reflectConstructorParams<T>(type: Type<T>): any[] {

  const paramtypes = Reflect.getMetadata(PARAMTYPES_METADATA, type) || [];

  const selfParams = this.reflectSelfParams<T>(type);

  selfParams.forEach(({ index, param }) => (paramtypes[index] = param));

  return paramtypes;

}

这里的 PARAMTYPES_METADATA 就是 design:paramtypes。

终于看到了我们想要的结果,那本文暂时就分析到这里吧,这样一次带着一个问题看源码,目标明确,不至于陷入源码的汪洋大海之中。

总结

本文先通过几个简单的例子揭示了 TS 中如何实现依赖注入,核心原理在于通过 Decorator 及 Metadata 两大特性可以在类及其方法上存储一些数据,并且开启了 emitDecoratorMetadata 后,TS 还可以自动添加三种类型的数据。

然后简单地调试了 Nest.js 的初始化过程,发现原理与我们分析的类似。


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。

您可能感兴趣的文章 :

原文链接 :
相关文章
  • 独立使用umi的核心插件模块示例介绍
    今天我们做一个有趣的尝试,将 umi 的核心插件模块独立出来作为另一个框架的基础架构,这里我们将它称为 konos。 介于 umi 自身的源码的独
  • Nest.js之依赖注入原理及实现过程介绍
    很久之前初学Java时就对注解及自动依赖注入这种方式感觉到不可思议,但是一直没有勇气(懒)去搞清楚。现在做前端了,发现 Nest.js 里面
  • 20个拿来就能用的JavaScript技巧分享给大家
    1. 解构魔法:轻松提取值 解构允许你轻松地从数组或对象中解包值。以下是一个例子: 1 2 3 4 const person = { name: Alice, age: 30 }; const { name, ag
  • javascript入门教程基础篇介绍

    javascript入门教程基础篇介绍
    一、 简介 1、 什么是javascript JavaScript 是网景(Netscape)公司开发的一种基于客户端浏览器、面向(基于)对象、事件驱动式的网页脚本语言
  • 手把手教你从0搭建前端脚手架的全部详细教程

    手把手教你从0搭建前端脚手架的全部详细教程
    先来看一眼实现的效果。 从图上来看这个脚手架的功能非常的简单只有一个创建的命令,其他都是帮助和显示版本号的。 也就是上图这句,
  • 让chatgpt将html中的图片转为base64的方法

    让chatgpt将html中的图片转为base64的方法
    故事要从我们公司的新官网说起,新官网是叫外包做的,前后端没有分离,对,你没听错,都到了 2023 年的今天,新项目依然是前后端混在
  • JavaScript深拷贝方法structuredClone使用介绍
    对于深拷贝,最容易也应该是常见的方法是使用JSON.parse() + JSON.stringify(),还有一个借助第三方脚本库 lodash ,其中方法cloneDeep可以实现深拷
  • requestAnimationFrame使用示例介绍

    requestAnimationFrame使用示例介绍
    requestAnimationFrame--use是什么 告诉浏览器用来执行一个动画,并且在下一次重绘之前调用其指定的回调函数取更新动画,所以该方法的参数就
  • 不可变数据方案之immer.js原理介绍

    不可变数据方案之immer.js原理介绍
    本篇文章是JavaScript 函数式编程 学习系列第三篇 前一篇JavaScript数据类型对函数式编程的影响讲到了不可变数据的重要性,而让数据不可变的
  • WebComponent使用教程介绍

    WebComponent使用教程介绍
    WebComponent 是官方定义的自定义组件实现方式,它可以让开发者不依赖任何第三方框架(如Vue,React)来实现自定义页面组件;达到组件复用
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计