TypeScript 中的元数据以及 reflect-metadata 实现原理分析
本文主要介绍 TypeScript 常搭配使用的reflect-metadata是什么;如何使用reflect-metadata来操作元数据;解读reflect-metadata的实现原理以及规范。
reflect-metadata
是一个 JavaScript 库,用于在运行时访问和操作装饰器的元数据。它提供了一组 API,可以读取和写入装饰器相关的元数据信息。元数据是关于代码中实体(例如类、方法、属性等)的描述性信息。它可以包含有关实体的类型、特性、配置选项、附加信息等。元数据可以在运行时被访问和使用,以便进行进一步的处理、验证、配置等操作。元数据与装饰器密切相关,因为装饰器可以用来添加或读取元数据。装饰器本身是一种特殊类型的声明,可以附加到类、方法、属性或参数上,以修改它们的行为或添加额外的元数据。(装饰器相关的内容详见:)
什么是元数据?
元数据(Metadata)是一种描述性信息,用于描述和解释代码中的数据或结构。定义一个数组来存放数据,那么数组的length属性就是数组的元数据。定义一个类来表达特殊的数据结构,该类的类型就是类的元数据。通常我们可以通过设计时给对应的类、属性、方法、参数等设置元数据,用来标记注解或解释代码。元数据的定义、访问和修改通常使用 reflect-metadata 来实现。 TypeScript 中的装饰器经常用来定义元数据, TypeScript 在编译的时候会执行装饰器函数代码并将元数据附加到对应的目标上【类、属性、方法、函数参数等等】。
元数据通常用于以下场景上:
- 装饰器(decorator),在 TypeScript 中可以使用元数据来辅助修改和扩展类、方法、属性等行为。
- 依赖注入(DI)。元数据用于标记类的构造函数参数或属性,以便依赖注入容器在运行时自动解析和注入依赖项。
- ORM 对象关系映射(object relational mapping)。使用元数据来映射数据库表和类之间的关系,以及字段和属性之间的映射关系。
- 序列化和反序列化。在处理数据的存储和传输时,元数据可以用于制定数据对象的序列化和反序列化规则。
总的来说,元数据是一种描述性信息,可以提供关于代码结构、类型、注解、依赖关系等更多的信息,从而使代码可以更加灵活和可扩展。
在 TypeScript 中通常借助 Reflect-metadata 来解决提供元数据的处理能力。
reflect-metadata 有啥用处?
relect-metadata 是用于对元数据进行定义、修改、查询的一组API,其基于Reflect 对象进行扩展提供一系列API 用于元编程。经典用于解决控制反转比如 DI 依赖注入,这种方式在大型项目和框架开发中使用的比较多,比如:VSCODE编辑器、国产IDE Opensumi、Next.js 框架等等。
github源码库上解释的目标
- 为(组合设计模式、依赖注入设计模式、运行时类型断言、反射/镜像、测试)提供统一的添加处理元数据的能力
- 降低开发生成元数据的装饰器的开发难度
- 扩展元数据的应用范围,不限于在对象上使用,扩展支持其他支持Proxy场景的使用。
Opensumi 中依赖注入框架定义在 opensumi/di 这个库下,感兴趣的可以研究一下。
Next 框架解决依赖注入的库,封装在 vercel/tsyringe 下。感兴趣的翻翻。
如何使用 reflect-metadata
reflect-metadata
对元数据的操作包含几个部分【定义元数据、删除、读取、检查判断(检查分两个方面其一是检查元数据是自己的还是祖上的原型链上的;其二检查一下是否存在对应的元数据)】。定义和读取是比较重要的(先让自行车能骑起来),其他API我们放到实现原理以及规范里面进行介绍。
import "reflect-metadata";
// 定义两个symbol 类型的 metadataKey
const ParamsTypeMetaKey = Symbol("design:paramtypes");
const ReturnTypeMetaKey = Symbol("design:returntype");
// Design-time type annotations, 注意区别使用的字符串作为 metadataKey
function Type(type) {
return Reflect.metadata("design:type", type);
}
function ParamTypes(...types) {
return Reflect.metadata(ParamsTypeMetaKey, types);
}
function ReturnType(type) {
return Reflect.metadata(ReturnTypeMetaKey, type);
}
// 定义一个数据解析的方式元数据
function ParseMethod(type) {
return Reflect.metadata("data:parse", type);
}
class P {
}
export class Point {
private x: number;
private y: number;
constructor(x, y) {
this.x = x;
this.y = y;
}
@ReturnType("[number, number]")
getCoord() {
return [this.x, this.y];
}
// 采用装饰器的方式进行元数据定义,另一种方式是显示定义(稍后会说)
@ParamTypes(Number)
@ReturnType(P)
@ParseMethod("JSON")
moveX(x: number) {
this.x = x;
return this;
}
}
const p = new Point(1, 1);
// 通过metadataKey 从 target 上读取对应属性名的元数据。这里的属性是一个函数
const des = Reflect.getMetadata(ReturnTypeMetaKey, p, "getCoord");
console.log("type is ", des); // type is [number, number]
const moveXReturnType = Reflect.getMetadata(ReturnTypeMetaKey, p, "moveX");
console.log("moveXReturnType is: ", moveXReturnType); // moveXReturnType is: ƒ P() {}
const moveXParamsType = Reflect.getMetadata(