聊聊 nestjs 中的依赖注入

共 12760字,需浏览 26分钟

 ·

2022-03-17 11:05

本文首发于政采云前端团队博客:聊聊 nestjs 中的依赖注入

https://zoo.team/article/nestjs


前言

首先 nestjs 是什么?引用其官网的原话 A progressive Node.js framework for building efficient, reliable and scalable server-side applications.,翻译一下就是:“一个可以用来搭建高效、可靠且可扩展的服务端应用的 node 框架”。目前在 github 上有 42.4k 的 star 数,人气还是很高的。

在使用过程中会发现 nest 框架和后端同学使用的 Springboot 以及前端三大框架之一的 Angular 都有很多相似之处。没错这三个框架都有相似的设计,并都实现了依赖注入。

可能对大部分前端同学来说,依赖注入这个词还比较陌生,本文就围绕依赖注入这个话题,展开讨论一下依赖注入是什么?以及在 nestjs 中详细的实现过程。

重要概念

概念解释

先来看看几个重要概念的解释

  • 依赖倒置原则( DIP ):抽象不应该依赖实现,实现也不应该依赖实现,实现应该依赖抽象。
  • 依赖注入(dependency injection,简写为 DI):依赖是指依靠某种东西来获得支持。将创建对象的任务转移给其他class,并直接使用依赖项的过程,被称为“依赖项注入”。
  • 控制反转(Inversion of Control, 简写为 IoC):指一个类不应静态配置其依赖项,应由其他一些类从外部进行配置。

结合代码

光看上面的解释可能并不好理解?那么我们把概念和具体的代码结合起来看。

  1. 根据 nest 官网教程,用脚手架创建一个项目,创建好的项目中有 main.ts 文件为入口文件,引入了 app.module.ts 文件,而 app.module.ts 文件引入了 app.controller.ts。先看一下代码的逻辑:
   // src/main.ts文件
   import { NestFactory } from '@nestjs/core';
   import { AppModule } from './app.module';
   
   async function bootstrap({
     const app = await NestFactory.create(AppModule);
     await app.listen(3000);
   }
   bootstrap();
   // src/app.module.ts文件
   import { Module } from '@nestjs/common';
   import { AppController } from './app.controller';
   import { AppService } from './app.service'
   
   @Module({
     imports: [],
     controllers: [AppController],
     providers: [AppService],
   })
   export class AppModule {}
   // src/app.controller.ts文件
   import { Controller, Get } from '@nestjs/common';
   import { AppService } from './app.service';
   
   @Controller()
   export class AppController {
     constructor(private readonly appService: AppService) {}
   
     @Get()
     getHello(): string {
       return this.appService.getHello();
     }
   }
   // src/app.service.ts文件
   import { Injectable } from '@nestjs/common';
   
   @Injectable()
   export class AppService {
     getHello(): string {
       return 'Hello World!';
     }
   }
   

现在我们执行 npm start 启动服务,访问 localhost:3000 就会执行这个 AppController 类中的 getHello 方法了。我们来看 app.controller.ts 文件。可以看到构造函数的参数签名中第一个参数 appService 是 AppService 的一个实例。

constructor(private readonly appService: AppService){}

但是在代码里并有没有看到实例化这个 AppService 的地方。这里其实是把创建这个实例对象的工作交给了 nest 框架,而不是 AppController 自己来创建这个对象,这就是所谓的控制反转。而把创建好的 AppService 实例对象作为 AppController 实例化时的参数传给构造器就是依赖注入了。

依赖注入的方式

依赖注入的实现主要有三种方式

  1. 构造器注入:依赖关系通过 class 构造器提供;
  2. setter 注入:用 setter 方法注入依赖项;
  3. 接口注入:依赖项提供一个注入方法,该方法将把依赖项注入到传递给它的任何客户端中。客户端必须实现一个接口,该接口的 setter 方法接收依赖;在 nest 中采用了第一种方式——构造器注入。

优点

那么 nestjs 框架用了依赖注入控制反转有什么好处呢?

其实 DIIoC 是实现依赖倒置原则的具体手段。依赖倒置原则是设计模式五大原则(SOLID)中的第五项原则,也许上面这个 AppController 的例子还看不出 DIP 有什么用,因为 DIP 也不是今天的重点,这里就不多赘述了,但是通过上面的例子我们至少能体会到以下两个优点:

  1. 减少样板代码,不需要再在业务代码中写大量实例化对象的代码了;
  2. 可读性和可维护性更高了,松耦合,高内聚,符合单一职责原则,一个类应该专注于履行其职责,而不是创建履行这些职责所需的对象。

元数据反射

我们都知道 ts 中的类型信息是在运行时是不存在的,那运行时是如何根据参数的类型注入对应实例的呢?

答案就是:元数据反射

先说反射,反射就是在运行时动态获取一个对象的一切信息:方法/属性等等,特点在于动态类型反推导。不管是在 ts 中还是在其他类型语言中,反射的本质在于元数据。在 TypeScript 中,反射的原理是通过编译阶段对对象注入元数据信息,在运行阶段读取注入的元数据,从而得到对象信息。

元数据反射(Reflect Metadata) 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它。要在 ts 中启用元数据反射相关功能需要:

  • npm i reflect-metadata --save
  • tsconfig.json 里配置 emitDecoratorMetadata 选项为true

定义元数据

Reflect.defineMetadata(metadataKey, data, target)

可以定义一个类的元数据;

获取元数据

Reflect.getMetadata(metadataKey, target)Reflect.getMetadata(metadataKey, instance, methodName)

可以获取类或者方法上定义的元数据。

内置元数据

TypeScript 结合自身语言的特点,为使用了装饰器的代码声明注入了 3 组元数据:

  • design:type:成员类型
  • design:paramtypes:成员所有参数类型
  • design:returntype:成员返回类型

示例一:元数据的定义与获取

import 'reflect-metadata';

class A {
  sayHi() {
    console.log('hi');
  }
}

class B {
  sayHello() {
    console.log('hello');
  }
}

function Module(metadata{
  const propsKeys = Object.keys(metadata);
  return (target) => {
    for (const property in metadata) {
      if (metadata.hasOwnProperty(property)) {
        Reflect.defineMetadata(property, metadata[property], target);
      }
    }
  };
}

@Module({
  controllers: [B],
  providers: [A],
})
class C {}

const providers = Reflect.getMetadata('providers', C);
const controllers = Reflect.getMetadata('controllers', C);

console.log(providers, controllers); // [ [class A] ] [ [class B] ]



(new (providers[0])).sayHi(); // 'hi'

在这个例子里,我们定义了一个名为 Module 的装饰器,这个装饰器的主要作用就是往装饰的类上添加一些元数据。然后用装饰器装饰 C 类。我们就可以获取到这个参数中的信息了;

示例二:依赖注入的简单实现

import 'reflect-metadata';

type Constructorany> = new (...args: any[]) => T;

const Test = ()ClassDecorator => (target) => {};

class OtherService {
  a = 1;
}

@Test()
class TestService {
  constructor(public readonly otherService: OtherService) {}

  testMethod() {
    console.log(this.otherService.a);
  }
}

const Factory = (target: Constructor): T => {
  // 获取所有注入的服务
  const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
};

Factory(TestService).testMethod(); // 1

这里例子就是依赖注入简单的示例,这里 Test 装饰器虽然什么都没做,但是如上所说,只要使用了装饰器,ts 就会默认给类或对应方法添加design:paramtypes的元数据,这样就可以通过Reflect.getMetadata('design:paramtypes', target)拿到类型信息了。

nest 中的实现

下面来看 nest 框架内部是怎么来实现的

执行逻辑

在入口文件 main.ts 中有这样一行代码

const app = await NestFactory.create(AppModule);

在源码 nest/packages/core/nest-application.ts 找到 NestFactory.create 方法,这里用注释解释说明了与依赖注入相关的几处代码(下同)。

public async createextends INestApplication = INestApplication>(
    module: any,
    serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
    options?: NestApplicationOptions,
  ): Promise {
    const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
      ? [serverOrOptions, options]
      : [this.createHttpAdapter(), serverOrOptions];

    const applicationConfig = new ApplicationConfig();
    // 1. 实例化 IoC 容器,这个容器就是用来存放所有对象的地方
    const container = new NestContainer(applicationConfig); 
    this.setAbortOnError(serverOrOptions, options);
    this.registerLoggerConfiguration(appOptions);

    // 2. 执行初始化逻辑,是依赖注入的核心逻辑所在
    await this.initialize(module, container, applicationConfig, httpServer); 
    
    // 3. 实例化 NestApplication 类
    const instance = new NestApplication(     
      container,
      httpServer,
      applicationConfig,
      appOptions,
    );
    const target = this.createNestInstance(instance);
    // 4. 生成一个 Proxy 代理对象,将对 NestApplication 实例上部分属性的访问代理到 httpServer,在 nest 中httpServer 默认就是 express 实例对象,所以默认情况下,express 的中间件都是可以使用的
    return this.createAdapterProxy(target, httpServer); 
  }

IoC 容器

在目录 nest/packages/core/injector/container.ts,找到了 NestContainer 类,里面有很多成员属性和方法,可以看到其中的私有属性 modules 是一个 ModulesContainer 实例对象,而 ModulesContainer 类是 Map 类的一个子类。


export class NestContainer {   
  ...
  private readonly modules = new ModulesContainer();
  ...
}
export class ModulesContainer extends Map<string, Module> {
   private readonly _applicationId = uuid();
  
   get applicationId(): string {
     return this._applicationId;
   }
}

依赖注入过程

先来看 this.initialize 方法:

  private async initialize(
    module: any,
    container: NestContainer,
    config = new ApplicationConfig(),
    httpServer: HttpServer = null,
  ) {
  // 1. 实例加载器
    const instanceLoader = new InstanceLoader(container);  
    const metadataScanner = new MetadataScanner();     
    // 2. 依赖扫描器
    const dependenciesScanner = new DependenciesScanner(   
      container,
      metadataScanner,
      config,
    );
    container.setHttpAdapter(httpServer);

    const teardown = this.abortOnError === false ? rethrow : undefined;
    await httpServer?.init();
    try {
      this.logger.log(MESSAGES.APPLICATION_START);

      await ExceptionsZone.asyncRun(
        async () => {
          // 3. 扫描依赖
          await dependenciesScanner.scan(module); 
          // 4. 生成依赖的实例
          await instanceLoader.createInstancesOfDependencies(); 
          dependenciesScanner.applyApplicationProviders();
        },
        teardown,
        this.autoFlushLogs,
      );
    } catch (e) {
      this.handleInitializationError(e);
    }
  }
  • new InstanceLoader()实例化 InstanceLoader 类,并把刚才的 IoC 容器作为参数传入,这个类是专门用来生成需要注入的实例对象的;
  • 实例化 MetadataScanner 类和 DependenciesScanner 类,MetadataScanner 类是一个用来获取元数据的工具类,而 DependenciesScanner 类是用来扫描出所有 modules 中的依赖项的。上面的 app.module.ts 中 Module 装饰器的参数中传入了controllersproviders等其他选项,这个 Module 装饰器的作用就是标明 AppModule 类的一些依赖项;
   @Module({
     imports: [],
     controllers: [AppController],
     providers: [AppService],
   })
   export class AppModule {}
  • 调用依赖扫描器的 scan 方法,扫描依赖;
   public async scan(module: Type) {
     await this.registerCoreModule(); // 1. 把一些内建module添加到IoC容器中
     await this.scanForModules(module); // 2. 把传入的module添加到IoC容器中
     await this.scanModulesForDependencies(); // 3. 扫描当前IoC容器中所有module的依赖
     this.calculateModulesDistance();
   
     this.addScopedEnhancersMetadata();
     this.container.bindGlobalScope();
   }

这里所说的 module 可以理解为是模块,但并不是 es6 语言中的模块化的 module,而是 app.module.ts 中定义的类, 而 nest 内部也有一个内建的 Module 类,框架会根据 app.module.ts 中定义的 module 类去实例化一个内建的 Moudle 类。下面 addModule 方法是把 module 添加到 IoC 容器的方法,可以看到,这里针对每个 module 会生成一个 token,然后实例化内建的 Module 类,并放到容器的 modules 属性上,token 作为 Map 结构的 key,Module 实例作为值。

 public async addModule(
   metatype: Type<any> | DynamicModule | Promise,
   scope: Type<any>[],
 ): Promiseundefined> {
   // In DependenciesScanner#scanForModules we already check for undefined or invalid modules
   // We still need to catch the edge-case of `forwardRef(() => undefined)`
   if (!metatype) {
     throw new UndefinedForwardRefException(scope);
   }
   const { type, dynamicMetadata, token } = await this.moduleCompiler.compile(
     metatype,
   ); // 生成token
   if (this.modules.has(token)) {
     return this.modules.get(token);
   }
   const moduleRef = new Module(typethis); // 实例化内建Module类
   moduleRef.token = token;
   this.modules.set(token, moduleRef); // 添加在modules上

   await this.addDynamicMetadata(
     token,
     dynamicMetadata,
     [].concat(scope, type),
   );

   if (this.isGlobalModule(type, dynamicMetadata)) {
     this.addGlobalModule(moduleRef);
   }
   return moduleRef;
 }
  • scanModulesForDependencies方法会找到容器中每个 module 上的一些元数据,把对应的元数据分别添加到刚才添加到容器中的 module 上面,这些元数据就是根据上面提到的 Module  装饰器的参数生成的;
  • instanceLoader.createInstancesOfDependencies()
private async createInstances(modules: Map<string, Module>) {
     await Promise.all(
       [...modules.values()].map(async moduleRef => {
         await this.createInstancesOfProviders(moduleRef);
         await this.createInstancesOfInjectables(moduleRef);
         await this.createInstancesOfControllers(moduleRef);
 
         const { name } = moduleRef.metatype;
         this.isModuleWhitelisted(name) &&
           this.logger.log(MODULE_INIT_MESSAGE`${name}`);
       }),
     );
  }

遍历 modules 然后生成 provider、Injectable、controller 的实例。生成实例的顺序上也是有讲究的,controller 是最后生成的。在生成实例的过程中,nest 还会先去找到构造器中的依赖项:

const dependencies = isNil(inject) 
  ? this.reflectConstructorParams(wrapper.metatype as Type<any>) 
  : inject;
reflectConstructorParams(type: Type): any[] {
     const paramtypes = Reflect.getMetadata(PARAMTYPES_METADATA, type) || [];
     const selfParams = this.reflectSelfParams(type);
 
     selfParams.forEach(({ index, param }) => (paramtypes[index] = param));
     return paramtypes;
 }
  • 上面代码中的的常量 PARAMTYPES_METADATA 就是 ts 中内置的;metadataKey design:paramtypes,获取到构造参数类型信息;然后就可以先实例化依赖项;
async instantiateClass(instances, wrapper, targetMetatype, contextId = constants_2.STATIC_CONTEXT, inquirer) {
         const { metatype, inject } = wrapper;
         const inquirerId = this.getInquirerId(inquirer);
         const instanceHost = targetMetatype.getInstanceByContextId(contextId, inquirerId);
         const isInContext = wrapper.isStatic(contextId, inquirer) ||
             wrapper.isInRequestScope(contextId, inquirer) ||
             wrapper.isLazyTransient(contextId, inquirer) ||
             wrapper.isExplicitlyRequested(contextId, inquirer);
         if (shared_utils_1.isNil(inject) && isInContext) {
             instanceHost.instance = wrapper.forwardRef
                 ? Object.assign(instanceHost.instance, new metatype(...instances))
                 : new metatype(...instances);
         }
         else if (isInContext) {
             const factoryReturnValue = targetMetatype.metatype(...instances);
             instanceHost.instance = await factoryReturnValue;
         }
         instanceHost.isResolved = true;
         return instanceHost.instance;
 }
  • 依赖项全部实例化后再调用 instantiateClass 方法,依赖项作为第一个参数 instances 传入。这里的 new metatype(...instances) 把依赖项的实例作为参数全部传入。

执行流程图

NestFactory.create 方法的执行逻辑大概如下

总结

  1. 元数据反射是实现依赖注入的基础;
  2. 总结依赖注入的过程,nest 主要做了三件事情
    1. 知道哪些类需要哪些对象
    2. 创建对象
    3. 并提供所有这些对象

参考

  • nestjs官方文档 (https://docs.nestjs.com)
  • 深入理解Typescript——Reflect Metadata (https://jkchao.github.io/typescript-book-chinese/tips/metadata.html#%E5%9F%BA%E7%A1%80)
  • Dependency injection in Angular (https://angular.io/guide/dependency-injection)
  • 装饰器 (https://www.typescriptlang.org/docs/handbook/decorators.html)
  • 从 JavaScript 到 TypeScript 4 - 装饰器和反射 (https://segmentfault.com/a/1190000011520817)
  • 反射的本质——元数据 (https://developer.aliyun.com/article/382120)
  • 《大话设计模式》——程杰


往期推荐


给力!快速了解Rust 模块使用方式
45 个 Git 经典操作场景,专治不会合代码
第一次拿全年年终奖的前端女程序员的2021

最后


  • 欢迎加我微信,拉你进技术群,长期交流学习...

  • 欢迎关注「前端Q」,认真学前端,做个专业的技术人...

点个在看支持我吧
浏览 23
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报