• Nest 的实现原理?理解了 reflect metadata 就懂了


    Nest 是 Node.js 的服务端框架,它最出名的就是 IOC(inverse of control) 机制了,也就是不需要手动创建实例,框架会自动扫描需要加载的类,并创建他们的实例放到容器里,实例化时还会根据该类的构造器参数自动注入依赖。

    它一般是这样用的:

    比如入口 Module 里引入某个模块的 Module:

    1. import { Module } from '@nestjs/common';
    2. import { CatsModule } from './cats/cats.module';
    3. @Module({
    4.   imports: [CatsModule],
    5. })
    6. export class AppModule {}

    然后这个模块的 Module 里会声明 Controller 和 Service:

    1. import { Module } from '@nestjs/common';
    2. import { CatsController } from './cats.controller';
    3. import { CatsService } from './cats.service';
    4. @Module({
    5.   controllers: [CatsController],
    6.   providers: [CatsService],
    7. })
    8. export class CatsModule {}

    Controller 里就是声明 url 对应的处理逻辑:

    1. import { BodyControllerGetParamPost } from '@nestjs/common';
    2. import { CatsService } from './cats.service';
    3. import { CreateCatDto } from './dto/create-cat.dto';
    4. @Controller('cats')
    5. export class CatsController {
    6.   constructor(private readonly catsService: CatsService) {}
    7.   @Post()
    8.   async create(@Body() createCatDto: CreateCatDto) {
    9.     this.catsService.create(createCatDto);
    10.   }
    11.   @Get()
    12.   async findAll(): Promise<Cat[]> {
    13.     return this.catsService.findAll();
    14.   }
    15. }

    这个 CatsController 的构造器声明了对 CatsService 的依赖:

    然后 CatsService 里就可以去操作数据库进行增删改查了。这里简单实现一下:

    1. import { Injectable } from '@nestjs/common';
    2. import { Cat } from './interfaces/cat.interface';
    3. @Injectable()
    4. export class CatsService {
    5.   private readonly catsCat[] = [];
    6.   create(cat: Cat) {
    7.     this.cats.push(cat);
    8.   }
    9.   findAll(): Cat[] {
    10.     return this.cats;
    11.   }
    12. }

    之后在入口处调用 create 把整个 nest 应用跑起来:

    1. import { NestFactory } from '@nestjs/core';
    2. import { AppModule } from './app.module';
    3. async function bootstrap() {
    4.   const app = await NestFactory.create(AppModule);
    5.   await app.listen(3000);
    6. }
    7. bootstrap();

    然后浏览器访问下我们写的那个 controller 对应的 url,打个断点:

    你会发现 controller 的实例已经创建了,而且 service 也给注入了。这就是依赖注入的含义。

    这种机制就叫做 IOC(控制反转),也叫依赖注入,好处是显而易见的,就是只需要声明依赖关系,不需要自己创建对象,框架会扫描声明然后自动创建并注入依赖。

    Java 里最流行的 Spring 框架就是 IOC 的实现,而 Nest 也是这样一个实现了 IOC 机制的 Node.js 的后端框架。

    不知道大家有没有感觉很神奇,只是通过装饰器声明了一下,然后启动 Nest 应用,这时候对象就给创建好了,依赖也给注入了。

    那它是怎么实现的呢?

    大家如果就这样去思考它的实现原理,还真不一定能想出来,因为缺少了一些前置知识。也就是实现 Nest 最核心的一些 api:Reflect 的 metadata 的 api。

    Reflect Metadata

    有的同学会说,Reflect 的 api 我很熟呀,就是操作对象的属性、方法、构造器的一些 api:

    比如 Reflect.get 是获取对象属性值

    Reflect.set 是设置对象属性值

    Reflect.has 是判断对象属性是否存在

    Reflect.apply 是调用某个方法,传入对象和参数

    Reflect.construct 是用构造器创建对象实例,传入构造器参数

    这些 api 在 MDN 文档里可以查到,因为它们都已经是 es 标准了,也被很多浏览器实现了。

    但是实现 Nest 用到的 api 还没有进入标准,还在草案阶段,也就是 metadata 的 api:

    它有这些 api:

    1. Reflect.defineMetadata(metadataKey, metadataValue, target);
    2. Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
    3. let result = Reflect.getMetadata(metadataKey, target);
    4. let result = Reflect.getMetadata(metadataKey, target, propertyKey);

    Reflect.defineMetadata 和 Reflect.getMetadata 分别用于设置和获取某个类的元数据,如果最后传入了属性名,还可以单独为某个属性设置元数据。

    那元数据存在哪呢?

    存在类或者对象上呀,如果给类或者类的静态属性添加元数据,那就保存在类上,如果给实例属性添加元数据,那就保存在对象上,用类似 [[metadata]] 的 key 来存的。

    这有啥用呢?

    看上面的 api 确实看不出啥来,但它也支持装饰器的方式使用:

    1. @Reflect.metadata(metadataKey, metadataValue)
    2. class C {
    3.   @Reflect.metadata(metadataKey, metadataValue)
    4.   method() {
    5.   }
    6. }

    Reflect.metadata 装饰器当然也可以再封装一层:

    1. function Type(type) {
    2.     return Reflect.metadata("design:type"type);
    3. }
    4. function ParamTypes(...types) {
    5.     return Reflect.metadata("design:paramtypes", types);
    6. }
    7. function ReturnType(type) {
    8.     return Reflect.metadata("design:returntype"type);
    9. }
    10. @ParamTypes(StringNumber)
    11. class Guang {
    12.   constructor(text, i) {
    13.   }
    14.   @Type(String)
    15.   get name() { return "text"; }
    16.   @Type(Function)
    17.   @ParamTypes(NumberNumber)
    18.   @ReturnType(Number)
    19.   add(x, y) {
    20.     return x + y;
    21.   }
    22. }

    然后我们就可以通过 Reflect metadata 的 api 或者这个类和对象的元数据了:

    1. let obj = new Guang("a"1);
    2. let paramTypes = Reflect.getMetadata("design:paramtypes", obj, "add"); 
    3. // [Number, Number]

    这里我们用 Reflect.getMetadata 的 api 取出了 add 方法的参数的类型。

    看到这里,大家是否明白 nest 的原理了呢?

    我们再看下 nest 的源码:

    上面就是 @Module 装饰器的实现,里面就调用了 Reflect.defineMetadata 来给这个类添加了一些元数据。

    所以我们这样用的时候:

    1. import { Module } from '@nestjs/common';
    2. import { CatsController } from './cats.controller';
    3. import { CatsService } from './cats.service';
    4. @Module({
    5.   controllers: [CatsController],
    6.   providers: [CatsService],
    7. })
    8. export class CatsModule {}

    其实就是给 CatsModule 添加了 controllers 的元数据和 providers 的元数据。

    后面创建 IOC 容器的时候就会取出这些元数据来处理:

    而且 @Controller 和 @Injectable 的装饰器也是这样实现的:

    看到这里,大家是否想明白 nest 的实现原理了呢?

    其实就是通过装饰器给 class 或者对象添加元数据,然后初始化的时候取出这些元数据,进行依赖的分析,然后创建对应的实例对象就可以了。

    所以说,nest 实现的核心就是 Reflect metadata 的 api。

    当然,现在 metadata 的 api 还在草案阶段,需要使用 reflect-metadata 这个 polyfill 包才行。

    其实还有一个疑问,依赖的扫描可以通过 metadata 数据,但是创建的对象需要知道构造器的参数,现在并没有添加这部分 metadata 数据呀:

    比如这个 CatsController 依赖了 CatsService,但是并没有添加 metadata 呀:

    1. import { BodyControllerGetParamPost } from '@nestjs/common';
    2. import { CatsService } from './cats.service';
    3. import { CreateCatDto } from './dto/create-cat.dto';
    4. @Controller('cats')
    5. export class CatsController {
    6.   constructor(private readonly catsService: CatsService) {}
    7.   @Post()
    8.   async create(@Body() createCatDto: CreateCatDto) {
    9.     this.catsService.create(createCatDto);
    10.   }
    11.   @Get()
    12.   async findAll(): Promise<Cat[]> {
    13.     return this.catsService.findAll();
    14.   }
    15. }

    这就不得不提到 TypeScript 的优势了,TypeScript 支持编译时自动添加一些 metadata 数据:

    比如这段代码:

    1. import "reflect-metadata";
    2.  
    3. class Guang {
    4.   @Reflect.metadata("名字""光光")
    5.   public say(anumber): string {
    6.     return '加油鸭';
    7.   }
    8. }

    按理说我们只添加了一个元数据,生成的代码也确实是这样的:

    但是呢,ts 有一个编译选项叫做 emitDecoratorMetadata,开启它就会自动添加一些元数据。

    开启之后再试一下:

    你会看到多了三个元数据:

    design:type 是 Function,很明显,这个是描述装饰目标的元数据,这里装饰的是函数

    design:paramtypes 是 [Number],很容易理解,就是参数的类型

    design:returntype 是 String,也很容易理解,就是返回值的类型

    所以说,只要开启了这个编译选项,ts 生成的代码会自动添加一些元数据。

    然后创建对象的时候就可以通过 design:paramtypes 来拿到构造器参数的类型了,那不就知道怎么注入依赖了么?

    所以,nest 源码里你会看到这样的代码:

    就是获取构造器的参数类型的。这个常量就是我们上面说的那个:

    这也是为什么 nest 会用 ts 来写,因为它很依赖这个 emitDecoratorMetadata 的编译选项。

    你用 cli 生成的代码模版里也都默认开启了这个编译选项:

    这就是 nest 的核心实现原理:通过装饰器给 class 或者对象添加 metadata,并且开启 ts 的 emitDecoratorMetadata 来自动添加类型相关的 metadata,然后运行的时候通过这些元数据来实现依赖的扫描,对象的创建等等功能。

    总结

    Nest 是 Node.js 的后端框架,他的核心就是 IOC 容器,也就是自动扫描依赖,创建实例对象并且自动依赖注入。

    要搞懂它的实现原理,需要先学习 Reflect metadata 的 api:

    这个是给类或者对象添加 metadata 的。可以通过 Reflect.metadata 给类或者对象添加元数据,之后用到这个类或者对象的时候,可以通过 Reflect.getMetadata 把它们取出来。

    Nest 的 Controller、Module、Service 等等所有的装饰器都是通过 Reflect.meatdata 给类或对象添加元数据的,然后初始化的时候取出来做依赖的扫描,实例化后放到 IOC 容器里。

    实例化对象还需要构造器参数的类型,这个开启 ts 的 emitDecoratorMetadata 的编译选项之后, ts 就会自动添加一些元数据,也就是 design:type、design:paramtypes、design:returntype 这三个,分别代表被装饰的目标的类型、参数的类型、返回值的类型。

    当然,reflect metadata 的 api 还在草案阶段,需要引入 refelect metadata 的包做 polyfill。

    nest 的一系列装饰器就是给 class 和对象添加 metadata 的,然后依赖扫描和依赖注入的时候就把 metadata 取出来做一些处理。

    理解了 metadata,nest 的实现原理就很容易搞懂了。

  • 相关阅读:
    python web框架django面试题收藏
    html网页制作期末大作业成品:基于HTML+CSS+JavaScript简洁汽车网站(7页)
    硼离子超标的解决方法,除硼离子树脂技术
    数据智慧:C#中编程实现自定义计算的Excel数据透视表
    时间复杂度和空间复杂度
    Hadoop第五章:序列化
    基于6G的联邦学习部署和应用
    【无标题】
    纹波类型及纹波抑制措施
    R语言使用epiDisplay包的pyramid函数可视化金字塔图、为金字塔图中的不同性别分组指定不同的颜色
  • 原文地址:https://blog.csdn.net/hebiwen95/article/details/126038140