聊聊 nestjs 中的依赖注入

百里.png

这是第 129 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客:聊聊 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 Constructor<T = any> = 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 = <T>(target: Constructor<T>): 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 create<T extends INestApplication = INestApplication>(
    module: any,
    serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
    options?: NestApplicationOptions,
  ): Promise<T> {
    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<T>(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<any>) {
     // 1. 把一些内建module添加到IoC容器中
     await this.registerCoreModule(); 
     // 2. 把传入的module添加到IoC容器中
     await this.scanForModules(module); 
     // 3. 扫描当前IoC容器中所有module的依赖
     await this.scanModulesForDependencies(); 
     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<DynamicModule>,
   scope: Type<any>[],
 ): Promise<Module | undefined> {
   // 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);
   }
   // 生成token
   const { type, dynamicMetadata, token } = await this.moduleCompiler.compile(
     metatype,
   ); 
   if (this.modules.has(token)) {
     return this.modules.get(token);
   }
  // 实例化内建Module类
   const moduleRef = new Module(type, this); 
   moduleRef.token = token;
	 // 添加在modules上
   this.modules.set(token, moduleRef);

   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<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就是 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. 并提供所有这些对象

参考

推荐阅读

开源作品

  • 政采云前端小报

开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交流群)

  • 商品选择 sku 插件

开源地址 github.com/zcy-inc/sku…

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 60 余个前端小伙伴,平均年龄 27 岁,近 4 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 [email protected]

Guess you like

Origin juejin.im/post/7049504750257045540