從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原理的資料請關註WalkonNet其它相關文章!

推薦閱讀: