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。


相关推荐
Noii.2 分钟前
Spring Boot初级概念及自动配置原理
java·spring boot·后端
xiaopengbc3 分钟前
火狐(Mozilla Firefox)浏览器离线安装包下载
前端·javascript·firefox
探索java9 分钟前
Tomcat Server 组件原理
java·后端·tomcat
咕白m62516 分钟前
通过 C# 高效提取 PDF 文本的完整指南
后端·c#
用户0165238444123 分钟前
Webpack5 入门与实战,前端开发必备技能无密
前端
小高00724 分钟前
🔥🔥🔥前端性能优化实战手册:从网络到运行时,一套可复制落地的清单
前端·javascript·面试
smallyu24 分钟前
Go 语言 GMP 调度器的原理是什么
后端·go
古夕26 分钟前
my-first-ai-web_问题记录01:Next.js的App Router架构下的布局(Layout)使用
前端·javascript·react.js
杨超越luckly32 分钟前
HTML应用指南:利用POST请求获取上海黄金交易所金价数据
前端·信息可视化·金融·html·黄金价格
Jerry37 分钟前
Compose 中的基本布局
前端