Nest 依赖注入核心: Metadata 和 Reflector

元数据(Metadata)

Nest 依赖注入核心 api: Reflect 的 metadata (草案阶段):

typescript 复制代码
// 定义元数据,没有指定属性键,因此应用于整个目标对象
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 分别用于设置和获取某个类的元数据,如果最后传入了 propertyKey 属性名(可选),还可以单独为某个属性设置元数据。

如果给类或者类的静态属性添加元数据,那就保存在类上。

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

要使用这个特性,首先安装 reflect-metadata 库:

bash 复制代码
npm install reflect-metadata

tsconfig.json 文件这样设置:

json 复制代码
{
  "compilerOptions": {
    "target": "es2016", // 指定 ECMAScript 目标版本:这里是 ES2016,也就是 ES7。这意味着 TypeScript 会将代码编译成符合 ES2016 标准的 JavaScript。
    "module": "commonjs", // 指定生成哪种模块系统代码。这里是 CommonJS,适用于 Node.js 环境或者早期的 JavaScript 模块加载方案。
    "experimentalDecorators": true, // 启用实验性的装饰器特性。装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、访问符、属性或参数上。装饰器使用 `@expression` 形式,`expression` 求值后必须为一个函数,它会在运行时被调用。
    "emitDecoratorMetadata": true, // 启用后,编译器会为装饰器提供源代码中类型的元数据支持。这通常用于反射机制的实现,比如在 Angular 的依赖注入中。
    "esModuleInterop": true // 启用模块的 ES6 互操作性,允许默认导入从没有使用默认导出的模块。
  }
}

再安装 typescript:

bash 复制代码
npm install typescript -g

安装插件:

运行代码:

代码中这样使用:

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

这样使用:

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

typescript 复制代码
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

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

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

所以说,nest 实现的核心就是 Reflect metadata,但是目前这个 api 还处于草案阶段,需要使用 reflect-metadata 这个 polyfill 包才行。

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

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

typescript 复制代码
import { Injectable, Controller } from '@nestjs/common';
import { CatsService } from './cats.service';

@Injectable()
class CatsService {
  // CatsService 的实现...
}

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

  // CatsController 的实现...
}

这其实是 TS 支持编译时自动添加一些 metadata 数据(tsconfig.json 开启 **emitDecoratorMetadata **选项)。

比如下面代码:

typescript 复制代码
@Controller('cats')
class CatsController {
  constructor(private catsService: CatsService) {}

  // CatsController 的实现...
}

编译后的 JavaScript 代码可能会看起来像这样:

typescript 复制代码
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    // 简化的装饰器应用逻辑
};
var __metadata = (this && this.__metadata) || function (k, v) {
    // 简化的元数据应用逻辑
};
require("reflect-metadata");
var decorators_1 = require("./decorators");
var cats_service_1 = require("./cats.service");

let CatsController = class CatsController {
    constructor(catsService) {
        this.catsService = catsService;
    }
};
CatsController = __decorate([
    decorators_1.Controller('cats'),
    __metadata("design:paramtypes", [cats_service_1.CatsService])
], CatsController);

关键点在于 __metadata("design:paramtypes", [cats_service_1.CatsService]) 这一行,它使用了 __metadata 函数来定义了 CatsController 构造函数的参数类型。

创建对象的时候可以通过 design:paramtypes 拿到构造器参数的类型了,这样就能正确的注入依赖了。

如果装饰的是一个方法。

编译后会看到多了三个元数据:

  • design:type 是 Function:描述装饰目标的元数据是函数
  • design:paramtypes 是 [Number]:参数的类型
  • design:returntype 是 String:返回值的类型

这就是 nest 的核心实现原理:

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

Nest 的装饰器都是依赖 reflect-metadata 实现的。

而且还提供了一个 @SetMetadata 的装饰器让我们可以给 class、method 添加一些 metadata。这个装饰器的底层实现自然是 Reflect.defineMetadata。

@SetMetadata 搭配 reflector

新建个项目:

typescript 复制代码
nest new metadata-and-reflector -p npm

创建 guard 和 interceptor:

bash 复制代码
nest g interceptor aaa --flat --no-spec
nest g guard aaa --flat --no-spec

使用 guard 和 interceptor,并用 @SetMetadata 加个 metadata:

通过 reflector.get 取出 handler 上的 metadata,guard 使用:

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

刷新下页面,就可以看到已经拿到了 metadata:

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

除了能拿到 handler 上的装饰器,也可以拿到 class 上的:

获取:

typescript 复制代码
console.log('Interceptor', this.reflector.get('roles', context.getClass()));

reflector 还有 3 个方法:

get 的实现就是 Reflect.getMetadata。

getAll 是返回一个 metadata 的数组。

getAllAndMerge,会把它们合并为一个对象或者数组。

getAllAndOverride 会返回第一个非空的 metadata。


相关推荐
码农派大星。6 分钟前
Spring Boot 配置文件
java·spring boot·后端
GIS程序媛—椰子27 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00134 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端36 分钟前
Content Security Policy (CSP)
前端·javascript·面试
木舟100940 分钟前
ffmpeg重复回听音频流,时长叠加问题
前端
杜杜的man1 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*1 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu1 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s1 小时前
Golang--协程和管道
开发语言·后端·golang
王大锤43911 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang