nestjs学习11:Metadata 和 Reflector(装饰器的基础)

0 阅读5分钟

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

那它是怎么实现的呢?

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

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

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

image.png

Reflect.set 是设置对象属性值:

image.png

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

image.png

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

image.png

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

image.png

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

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

它有这些 api:

Reflect.defineMetadata(metadataKey, metadataValue, target);

Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);


let result = Reflect.getMetadata(metadataKey, target);

let result = Reflect.getMetadata(metadataKey, target, propertyKey);

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

那元数据存在哪呢?

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

这有啥用呢?

我们再看下 nest 的@Module源码:

image.png

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

所以我们这样用的时候:

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

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

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

比如,key 为 importsproviders时的元数据,存储在CatsModule这个类上:

image.png

image.png

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

image.png

@Injectable 装饰的类,我们定义了一个元数据为{ INJECTABLE_WATERMARK: true},这表示这个类是可以被注入的,nestjs就会把这个类实例化放到容器中。

image.png

同理,@Controller也一样,只不过它的元数据要更多,因为它要处理路由。

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

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

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

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

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

import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

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

比如这段代码:

import "reflect-metadata";
 
class Guang {
  @Reflect.metadata("名字", "光光")
  public say(a: number): string {
    return '加油鸭';
  }
}

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

image.png

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

开启之后再试一下:

image.png

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

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

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

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

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

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

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

image.png

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

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

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

Nest 的装饰器都是依赖 reflect-metadata 实现的,而且还提供了一个 @SetMetadata 的装饰器让我们可以给 class、method 添加一些 metadata:

image.png

这个装饰器的底层实现自然是 Reflect.defineMetadata:

image.png

Nest 为什么暴露这样一个底层的 metadata api 出来呢?

因为这个 metadata 是可以在代码里取出来用的:

image.png

在 guard 和 interceptor 就就可以这样取出来:

image.png

通过 ExecutationContext 取到目标 handler,然后注入 reflector,通过 reflector.get 取出 handler 上的 metadata。

interceptor 里也是这样,这里换种属性注入方式:

image.png

拿到 metadata 有什么用呢?

可以判断权限呀,比如这个路由需要 admin 角色,那可以取出 request 的 user 对象,看看它有没有这个角色,有才放行。

reflector 还有 3 个方法:

image.png

这里就不举例了。