从reflect metadata理解Nest实现原理

目录
  • 正文
  • 入口Module 引入模块
  • CatsService操作数据库
  • Reflect Metadata
  • 那元数据存在哪呢?
  • nest 的源码:
  • 总结

正文

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

它一般是这样用的:

入口Module 引入模块

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

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

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

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

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

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();
  }
}

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

然后 CatsService 里就可以去操作数据库进行增删改查了。

CatsService操作数据库

这里简单实现一下:

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];
  create(cat: Cat) {
    this.cats.push(cat);
  }
  findAll(): Cat[] {
    return this.cats;
  }
}

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

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
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:

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 来存的。

这有啥用呢?

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

@Reflect.metadata(metadataKey, metadataValue)
class C {
  @Reflect.metadata(metadataKey, metadataValue)
  method() {
  }
}

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

function Type(type) {
    return Reflect.metadata("design:type", type);
}
function ParamTypes(...types) {
    return Reflect.metadata("design:paramtypes", types);
}
function ReturnType(type) {
    return Reflect.metadata("design:returntype", type);
}
@ParamTypes(String, Number)
class Guang {
  constructor(text, i) {
  }
  @Type(String)
  get name() { return "text"; }
  @Type(Function)
  @ParamTypes(Number, Number)
  @ReturnType(Number)
  add(x, y) {
    return x + y;
  }
}

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

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

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

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

nest 的源码:

上面就是 @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 容器的时候就会取出这些元数据来处理:

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

看到这里,大家是否想明白 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 '加油鸭';
  }
}

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

但是呢,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 的实现原理就很容易搞懂了。

以上就是从reflect metadata理解Nest实现原理的详细内容,更多关于reflect metadata Nest原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • Nest.js散列与加密实例详解

    0x0 前言 先要知道,什么是散列算法呢?产生一些数据片段(例如消息或会话项)的散列值的算法.例如,md5就是一种散列算法.软件开发中的散列函数或散列算法,又称哈希函数,英语:Hash Function,是一种从任何一种数据中创建小的数字"指纹"的方法.所有散列函数都有如下一个基本特性:如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的. 加 密算法通常分为对称性加密算法和非对称性加密算法,对于对称性加密算法,信息接收双方都需事先知道密匙和加解密算法且其密

  • nestjs返回给前端数据格式的封装实现

    一般开发过程中不不会根据httpcode来判断接口请求成功与失败的,而是会根据请求返回的数据,里面加上code字段 一.返回的数据格式对比 1.直接返回的数据格式 { "id": 1, "uuid": "cbbe7abc-b95e-48a0-8d24-b1ac93c45328", "name": "哈士奇1", "age": 12, "color": null, &q

  • NestJS+Redis实现缓存步骤详解

    NestJS的缓存模块天生支持Redis等缓存机制.以下通过一个示例,说明如何在NestJS中操作Redis.步骤如下: 先安装运行Redis服务,步骤参见链接 新建nestjs项目: nest new [项目名称] 安装cache相关依赖 npm install cache-manager npm install -D @types/cache-manager npm install cache-manager-redis-store --save 注册Redis Store 打开src->a

  • Nest框架中集成使用Swagger示例说明

    目录 Swagger Nest 中使用 Swagger 安装 初始化 Swagger 接口中使用 Swagger Swagger 是一个能够生成.描述.调用和可视化的 RESTful 风格的 Web 服务.主要是对 REST API 接口自动生成接口说明文档.生成的接口文档也是直接在线测试.即在可视化的Web 界面上直接输入参数对应的值就可以在线测试接口. Nest 框架也提供了一个专门用于支持 Swagger 规范的模块. Nest 中使用 Swagger 安装 首先需要安装所需要的 swag

  • SpringBoot异常: nested exception is java.lang.NoClassDefFoundError: javax/servlet/ServletContext解决方案

    今天在使用SpringBoot创建了一个项目出现如下异常 org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'documentationPluginsBootstrapper': Resolution of declared constructors on bean Class [springfox.documentation.spring.web.plugins.Docu

  • NestJs使用Mongoose对MongoDB操作的方法

    最近开始学习NestJs这个框架.学习成本比其他框架相对高了很多. 其注册配置相对复杂一开始学起来有点摸不着哪跟哪:但这也是相比其他框架更规范严谨的体现.保证了大型项目的稳定健壮的使用! 在学习node基础框架Express和Koa的时候.对数据库的操作一直都是以编写SQL语句实现.需要什么编写什么.这样显得非常死板也不灵活.后来了解到NestJs是OOP思想编程(NestJs可以使用TypeScript也是OOP思想)才知道原来对数据库的操作可以用对象形式体现.在数据库中每一个表(Schema

  • Nest.js参数校验和自定义返回数据格式详解

    0x0 参数校验 参数校验大部分业务是使用 Nest.js 中的管道 方法实现,具体可以查阅文档 .不过编写过程中遇到一些问题,虽然文档讲得比较晦涩. 在做个查询接口,里面包含一些参数,做成 dto 结构数据: import { ApiProperty } from '@nestjs/swagger' export class QueryUserDto { @ApiProperty({ required: false, description: '页码' }) readonly currentP

  • 从reflect metadata理解Nest实现原理

    目录 正文 入口Module 引入模块 CatsService操作数据库 Reflect Metadata 那元数据存在哪呢? nest 的源码: 总结 正文 Nest 是 Node.js 的服务端框架,它最出名的就是 IOC(inverse of control) 机制了,也就是不需要手动创建实例,框架会自动扫描需要加载的类,并创建他们的实例放到容器里,实例化时还会根据该类的构造器参数自动注入依赖. 它一般是这样用的: 入口Module 引入模块 比如入口 Module 里引入某个模块的 Mo

  • 深入理解 TypeScript Reflect Metadata

    在定义类或者类方法的时候,可以设置一些元数据,我们可以获取到在类与类方法上添加的元数据,用的方法就是 Reflect Metadata.元数据指的是描述东西时用的数据. 在 TypeScript 里使用 Reflect Metadata 需要做下面这样的配置: { "compilerOptions": { "experimentalDecorators": true , "emitDecoratorMetadata": true }, } 然后在

  • JavaScript Reflect Metadata实现详解

    引言 在 ES6 的规范当中,就已经存在 Reflect API 了.简单来说这个 API 的作用就是可以实现对变量操作的函数化,也就是反射.具体的关于这个 API 的内容,可以查看这个教程 然而我们在这里讲到的,却是 Reflect 里面还没有的一个规范,那么就是 Reflect Metadata. Metadata 想必对于其他语言的 Coder 来说,比如说 Java 或者 C#,Metadata 是很熟悉的.最简单的莫过于通过反射来获取类属性上面的批注(在 JS 当中,也就是所谓的装饰器

  • 深入理解ThreadLocal工作原理及使用示例

    简介:本文已一个简要的代码示例介绍ThreadLocal类的基本使用方式,在此基础上结合图片阐述它的内部工作原理. 早在JDK1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地编写出优美的多线程程序. 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本. 从线程的角

  • 深入理解 CAS 算法原理已经在jdk中的运用

    1.什么是CAS? CAS:Compare and Swap,即比较再交换. jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁.JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁. 2.CAS算法理解 对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B.当且仅当预期值A和内存值V相同时,将内存值V修改为B,

  • 深入理解React State 原理

    目录 问题:setState 到底是同步还是异步的? 类组件state setState原理揭秘 函数组件state 问题:setState 到底是同步还是异步的? 如果对 React 底层有一定了解,可以回答出 batchUpdate 批量更新概念,以及批量更新被打破的条件. 答案:有时是同步,有时是异步. 在 合成事件 和 生命周期函数 里是 异步 的在 原生事件 和 setTimeout.promise里是 同步 的 造成setState的异步并不是由内部的异步代码引起的,在本身的执行过程

  • go reflect要不要传指针原理详解

    目录 正文 什么时候传递指针? 1. 通过传递指针修改变量的值 传值无法修改变量本身 传指针可以修改变量 2. 通过传递指针修改结构体的字段 3. 结构体:获取指针接收值方法 4. 变量本身包含指向数据的指针 通过值反射对象修改 chan.map 和 slice slice 反射对象扩容的影响 slice 容量够的话是不是就可以正常追加元素了? map 也不能通过值反射对象来修改其元素. chan 没有追加 结构体字段包含指针的情况 5. interface 类型处理 interface 底层类

  • 深入理解Spring事务原理

    一.事务的基本原理 Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的.对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:  1.获取连接 Connection con = DriverManager.getConnection()  2.开启事务con.setAutoCommit(true/false);  3.执行CRUD  4.提交事务/回滚事务 con.commit() / con.rollback();  5.关闭连接

  • 理解Javascript_11_constructor实现原理

    constructor是什么 简单的理解,constructor指的就是对象的构造函数.请看如下示例: 复制代码 代码如下: function Foo(){}; var foo = new Foo(); alert(foo.constructor);//Foo alert(Foo.constructor);//Function alert(Object.constructor);//Function alert(Function.constructor);//Function 对于foo.con

  • 理解Javascript_07_理解instanceof实现原理

    那么instanceof的这种行为到底是如何实现的呢,现在让我们揭开instanceof背后的迷雾. instanceof原理 照惯例,我们先来看一段代码: 复制代码 代码如下: function Cat(){} Cat.prototype = {} function Dog(){} Dog.prototype ={} var dog1 = new Dog(); alert(dog1 instanceof Dog);//true alert(dog1 instanceof Object);//t

随机推荐