【Nest.js】一文搞懂 reflect-metadata ~
Nest.js 中一个重要的概念叫做
注解
,基于 TypeScript 强大的 Decorator, 同时配上 reflect-metadata 能实现注解(Annotation)
功能。但 TypeScript 5.0 后移除了Annotation
的部分, 只保留了Decorate
的特性。metadata
被独立了出来,本文就来一次性搞懂reflect-metadata
。
关于装饰器,前面已经写了三篇详细的文章:
接下来会逐步讲到 Nest.js 的核心实现,那 reflect-metadata 就是一个绕不过去的知识点。
1、先说 Reflect
Reflect Metadata 作为 Reflect API 的扩展和延伸,那就必须先知道 Reflect 是什么。
这里引用权威的 MDN 文档,已经足够清晰。
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handler (en-US) 的方法相同。Reflect
不是一个函数对象,因此它是不可构造的。
与大多数全局对象不同 Reflect
并非一个构造函数,所以不能通过 new 运算符对其进行调用,或者将 Reflect
对象作为一个函数来调用。Reflect
的所有属性和方法都是静态的(就像 Math
对象)。
Reflect
对象提供了以下静态方法,这些方法与 proxy handler 方法的命名相同。
1.1 Reflect APIs
-
Reflect.apply(target, thisArgument, argumentsList):对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和
Function.prototype.apply()
功能类似。 -
Reflect.construct(target, argumentsList[, newTarget]):对构造函数进行
new
操作,相当于执行new target(...args)
。 -
Reflect.defineProperty(target, propertyKey, attributes):和
Object.defineProperty()
类似。如果设置成功就会返回true
-
Reflect.deleteProperty(target, propertyKey):作为函数的
delete
操作符,相当于执行delete target[name]
。 -
Reflect.get(target, propertyKey[, receiver]):获取对象身上某个属性的值,类似于
target[name]
。 -
Reflect.getOwnPropertyDescriptor(target, propertyKey):类似于
Object.getOwnPropertyDescriptor()
。如果对象中存在该属性,则返回对应的属性描述符,否则返回undefined
。 -
Reflect.has(target, propertyKey):判断一个对象是否存在某个属性,和
in
运算符 的功能完全相同。 -
Reflect.ownKeys(target):返回一个包含所有自身属性(不包含继承属性)的数组。(类似于
Object.keys()
, 但不会受enumerable
影响). -
Reflect.preventExtensions(target):类似于
Object.preventExtensions()
。返回一个Boolean
。 -
Reflect.set(target, propertyKey, value[, receiver]):将值分配给属性的函数。返回一个
Boolean
,如果更新成功,则返回true
。 -
Reflect.setPrototypeOf(target, prototype):设置对象原型的函数。返回一个
Boolean
,如果更新成功,则返回true
。
1.2 Reflect 应用
1.2.1 检测一个对象是否存在特定属性
const duck = {
name: 'Maurice',
color: 'white',
greeting: function() {
console.log(`Quaaaack! My name is ${this.name}`);
}
}
Reflect.has(duck, 'color'); // true
Reflect.has(duck, 'haircut'); // false
1.2.2 返回这个对象自身的属性
Reflect.ownKeys(duck);
// [ "name", "color", "greeting" ]
1.2.3 为这个对象添加一个新的属性
Reflect.set(duck, 'eyes', 'black');
// returns "true" if successful
// "duck" now contains the property "eyes: 'black'"
1.3 有 Object 为什么要还要 Reflect?
在回答这个问题之前,我们先来了解一下 "元编程"(Metaprogramming)
的概念。维基百科是这说的:
元编程(Metaprogramming)是一种编程技术,在这种技术中,计算机程序能够将其他程序视为自己的数据。这意味着一个程序可以被设计用来读取、生成、分析或转换其他程序,甚至在运行时修改自身。
反射(Reflect)
是元编程的一个分支,反射有三个子特征:
- 代码能够自我检查。
- 代码能够自我修改。
- 可以通过封装、陷阱、拦截来代替原来的对象。
回到问题本身,Reflect 的这些功能ES5中的Object也有呀?为什么还要搞出个冗余的内置对象?看了一些文章,主要有以下几点理由:
-
统一命名空间:在ES6之前, Javascript一直没有统一的命名空间来管理对其他 object 的操作,ES6 之后都可以通过调用 Reflect 的静态方法来处理对象。
-
更直观的异常处理:在Object里面我们需要用到
try...catch
去捕捉错误 而Reflect.defineProperty()
则会直接return true or false
:// Object实现 try { Object.defineProperty(car, name, desc); // property defined successfully } catch (e) { // possible failure (and might accidentally catch the wrong exception) } // Reflect实现 if (Reflect.defineProperty(car, name, desc)) { // success } else { // failure }
-
apply的可靠性
func
:在 ES5 中,使用可变数量的参数作为数组调用函数arr
并将this
值绑定到的最可靠方法是obj
:Function.prototype.apply.call(func, obj, arr); // or func.apply(obj, arr);
这是不太可靠的,因为
func
可能是一个定义了自己的apply
,在 ES6 中,我们有更可靠、更优雅的方法来解决这个问题:Reflect.apply(func, obj, arr);
-
代理陷阱转发:当我们使用 Proxy 对象来包装现有对象时,我们通常会拦截一个操作。我们尝试做一些特别的事情,然后一旦完成,我们想继续做下面的事情。让我们通过以下示例来理解这一点:
const employee = { firstName: 'Tapas', lastName: 'Adhikary' }; let logHandler = { get: function(target, fieldName) { console.log("Log: ", target[fieldName]); return Reflect.get(target, fieldName); } }; let func = () => { let p = new Proxy(employee, logHandler); p.firstName; p.lastName; }; func(); // Log: Tapas // Log: Adhikary
Reflect 允许你实现代理陷阱的默认转发行为。例如,你可以拦截操作,然后尝试将拦截到的操作应用到封装对象。但是,这并不容易,因为
[[Set]]
、[[Get]]
等内部方法无法直接调用。有了 Reflect,你就可以访问内部方法,并轻松地将截获的操作应用到代理中的封装对象。
2、Reflect Metadata
Reflect Metadata 是一个广泛使用的第三方 npm package,扩展了 Reflect API,TypeScript 使用这个包设计装饰器 decorator。
这个包原本是为了提供
Reflect
API 的“元数据扩展” ECMAScript 提案的兼容方案。作者Ron Buckton
也是Typescript的核心开发者,他于 2015 年提交了将 metadata 纳入 Typescript 官方的提案(ES7),但目前为止他手上还有太多其他工作没有完成, 所以并没有提上议程。
什么是“元数据”?
元数据 metadata,简而言之,就是实际数据的额外信息(译注:通常被称为数据的数据)。例如,如果一个变量表示一个数组,那么,数组的长度就是一个元数据。类似的,数组中的每个元素都是实际数据,而这些元素的数据类型则是它们各自的元数据。可以这样认为,元数据并不是程序真正关心的数据,但是却能帮助程序更快更好地实现自己的目标。
2.1 为什么会有 Reflect Metadata
如果你需要设计一个函数,用来输出其它函数的信息,那么,你会怎么设计?
// return information of a function
function funcInfo( func ) {
return `Function "${ func.name }" accepts "${ func.length }" arguments.`;
}
// define sample functions
var add = ( a, b ) => a + b;
var sayHello = () => 'Hello World!';
// print function information
console.log( 'add info ->', funcInfo( add ) );
// add info -> Function "add" accepts "2" arguments.
console.log( 'sayHello info ->', funcInfo( sayHello ) );
// sayHello info -> Function "sayHello" accepts "0" arguments.
上面的例子,funcInfo
函数接受一个函数作为参数,返回包含了这个参数函数的函数名和参数个数的字符串。这些信息原本就包含在函数当中,虽然我们在大部分时间都不会使用这些信息。因此,func.name
和func.length
就可以看作是元数据。
在上面关于 Reflect 的介绍中,我们知道Reflect API
可以检查对象、添加元数据以改变其行为。例如,Reflect.has
函数就像in
运算符,用于检查属性是否存在于对象或对象的原型链。Reflect.setPropertyOf
函数则可以给对象增加自定义属性,从而改变其行为。Reflect
提供了很多类似的方法,用于实现反射机制。
metadata 提案 目的是通过新的方法扩展Reflect
的能力。这些方法可以增加 JavaScript 元编程的能力。来看下面的例子:
// 在目标对象上定义 metadata
Reflect.defineMetadata(
metadataKey,
metadataValue,
target
);
// 在目标对象的属性上定义 metadata
Reflect.defineMetadata(
metadataKey,
metadataValue,
target,
propertyKey
);
Reflect.defineMetadata
函数允许给target
对象或target
对象的属性propertyKey
添加新的元数据值metadataValue
。这个值可以是任意 JavaScript 值。target
对象则需要继承Object
。你可以添加任意多的元数据值,这些值使用metadataKey
进行区分。
// 获取与目标相关的元数据
let result = Reflect.getMetadata(
metadataKey,
target
);
// 获取与目标属性相关的元数据
let result = Reflect.getMetadata(
metadataKey,
target,
propertyKey
);
Reflect.getMetadata
函数可以获取target
对象或其属性上面名为metadataKey
的元数据。
metadata 提案为所有普通对象暴露内部槽[[Metadata]]
。这个槽可以是null
,表示target
对象不包含任何元数据;否则,它是一个Map
对象,包含target
或其属性的不同元数据。
Reflect.defineMetadata
调用内部方法[[DefineMetadata]]
,Reflect.getMetadata
则使用[[GetMetadata]]
获取。
2.2 在 TypeScript 中使用 Reflect Metadata 用法
Reflect Metadata
已经是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它,你只需要:
npm i reflect-metadata --save
。- 在
tsconfig.json
里配置emitDecoratorMetadata
选项。
2.2.1 在对象中使用
import "reflect-metadata";
// check if `Reflect.defineMetadata` is a `function`
console.log("check ->", typeof Reflect.defineMetadata); // check -> function
// define a sample target
var target = { name: "Ross" };
// add metadata to `target` and `target.name`
Reflect.defineMetadata("version", 1, target);
Reflect.defineMetadata("info", { props: 1 }, target);
Reflect.defineMetadata("is", "string", target, "name");
// see the target
console.log("target ->", target); // target -> { name: 'Ross' }
// extract metadata
console.log("target(info) ->", Reflect.getMetadata("info", target));
// target(info) -> { props: 1 }
console.log("target.name(is) ->", Reflect.getMetadata("is", target, "name"));
// target.name(is) -> string
// when metadata is missing
console.log("target(missing) ->", Reflect.getMetadata("missing", target));
// target(missing) -> undefined
require( 'reflect-metadata' );
将导入 reflect-metadata 包导入,这给Reflect
对象在运行时增加了诸如defineMetadata
这样的函数,我们可以通过检查Reflect.defineMetadata
的类型得知是否正常。因此,本质上说,这个包其实是一个兼容方案 polyfill。
然后,我们定义了一个对象target
,然后在这个对象上添加了version
、info
和is
元数据。其中,version
和info
直接添加到target
对象,is
添加到name
属性。元数据值可以是任意 JavaScript 值。
Reflect.getMetadata
返回关联到target
或其属性的元数据。如果没有找到,则返回undefined
。利用这些方法,我们可以将任意元数据关联到任意对象或属性。
从日志可以看出来,向target
或其属性注册元数据并不会改变这个对象。事实上,元数据应该被保存在[[Metadata]]
内部槽,而兼容方案中是使用的是WeakMap
来保存target
的元数据。
使用hasMetadata
检查元数据是否存在,使用getMetadataKeys
获取注册到target
或其属性上面的所有元数据的键的集合。如果要删除元数据,则可以使用deleteMetadata
方法。
// check for presence of a metadata key (returns a boolean)
let result = Reflect.hasMetadata(key, target);
let result = Reflect.hasMetadata(key, target, property);
// get all metadata keys (returns an Array)
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, property);
// delete metadata with a key (returns a boolean)
let result = Reflect.deleteMetadata(key, target);
let result = Reflect.deleteMetadata(key, target, property);
import "reflect-metadata";
// define a sample target
var target = { name: "Ross", age: 21 };
// add metadata to `target` and `target.name`
Reflect.defineMetadata("version", 1, target);
Reflect.defineMetadata("is", "string", target, "name");
// check if metadata exists
console.log("has: target(version) ->", Reflect.hasMetadata("version", target)); // has: target(version) -> true
console.log(
"has: target.name(is) ->",
Reflect.hasMetadata("is", target, "name")
); // has: target.name(is) -> true
console.log("has: target.age(is) ->", Reflect.hasMetadata("is", target, "age")); // has: target.age(is) -> false
// get metadata keys
console.log("keys: target ->", Reflect.getMetadataKeys(target)); // keys: target -> [ 'version' ]
console.log("keys: target.name ->", Reflect.getMetadataKeys(target, "name")); // keys: target.name -> [ 'is' ]
console.log("keys: target.age ->", Reflect.getMetadataKeys(target, "age")); // keys: target.age -> []
// delete metedata key
console.log(
"delete: target(version) ->",
Reflect.deleteMetadata("version", target)
); // delete: target(version) -> true
console.log(
"delete: target.name(is) ->",
Reflect.deleteMetadata("is", target, "name")
); // delete: target.name(is) -> true
console.log(
"delete: target.age(is) ->",
Reflect.deleteMetadata("is", target, "age")
); // delete: target.age(is) -> false
默认情况下,getMetadata
、hasMetadata
和getMetadataKeys
会检查target
的原型链,以便查找指定元数据键关联的值。因此,我们还有getOwnMetadata
、hasOwnMetadata
和getOwnMetadataKeys
这样的函数,只查找target
对象,而不去查询原型链。
// get metadata value of an own metadata key
let result = Reflect.getOwnMetadata(key, target);
let result = Reflect.getOwnMetadata(key, target, property);
// check for presence of an own metadata key
let result = Reflect.hasOwnMetadata(key, target);
let result = Reflect.hasOwnMetadata(key, target, property);
// get all own metadata keys
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, property);
import "reflect-metadata";
// define a sample target with a custom prototype
var target = { name: "Ross" };
var proto = { age: 21 };
Reflect.setPrototypeOf(target, proto);
// add metadata to `proto`
Reflect.defineMetadata("version", 1, proto);
Reflect.defineMetadata("is", "number", proto, "age");
// get metadata
console.log(
"getMetadata: target(version) ->",
Reflect.getMetadata("version", target)
); // getMetadata: target(version) -> 1
console.log(
"getMetadata: target.age(is) ->",
Reflect.getMetadata("is", target, "age")
); // getMetadata: target.age(is) -> number
console.log(
"getOwnMetadata: target(version) ->",
Reflect.getOwnMetadata("version", target)
); // getOwnMetadata: target(version) -> undefined
console.log(
"getOwnMetadata: target.age(is) ->",
Reflect.getOwnMetadata("is", target, "age")
); // getOwnMetadata: target.age(is) -> undefined
// check if metadata exists
console.log(
"hasMetadata: target(version) ->",
Reflect.hasMetadata("version", target)
); // hasMetadata: target(version) -> true
console.log(
"hasMetadata: target.age(is) ->",
Reflect.hasMetadata("is", target, "age")
); // hasMetadata: target.age(is) -> true
console.log(
"hasOwnMetadata: target(version) ->",
Reflect.hasOwnMetadata("version", target)
); // hasOwnMetadata: target(version) -> false
console.log(
"hasOwnMetadata: target.age(is) ->",
Reflect.hasOwnMetadata("is", target, "age")
); // hasOwnMetadata: target.age(is) -> false
// check for metadata keys
console.log("getMetadataKeys: target ->", Reflect.getMetadataKeys(target)); // getMetadataKeys: target -> [ 'version' ]
console.log(
"getMetadataKeys: target.age ->",
Reflect.getMetadataKeys(target, "age")
); // getMetadataKeys: target.age -> [ 'is' ]
console.log(
"getOwnMetadataKeys: target ->",
Reflect.getOwnMetadataKeys(target)
); // getOwnMetadataKeys: target -> []
console.log(
"getOwnMetadataKeys: target.age ->",
Reflect.getOwnMetadataKeys(target, "age")
); // getOwnMetadataKeys: target.age -> []
上面的例子,proto
是target
的原型,因此,所有在proto
定义的元数据值都可以在target
访问。然而,带有Own
的函数不会检查proto
的元数据。
Reflect Metadata 的 API 可以用作给 Object 及其属性添加元数据:
const car = {
brand: 'BMW',
model: 'X6 2012',
price: 99999,
getMaxSpeed() {
console.log(`Max speed is 200km/h`);
}
}
// 添加一个叫`desc`的元数据到car这个对象
Reflect.defineMetadata('desc', 'This is a good car', car);
// 添加一个叫`desc`的元数据到car的price上(这里的price可以是其他值或car不存在的属性)
Reflect.defineMetadata('desc', 'This is so cheap', car, 'price');
// 添加一个叫`desc`的元数据到car的model上(这里的model可以是其他值或car不存在的属性)
Reflect.defineMetadata('note', 'This model is too old', car, 'model');
// 检查metadata是否存在
console.log(Reflect.hasMetadata('desc', car)); // 输出: true
console.log(Reflect.hasMetadata('desc', car, 'price')); // 输出: true
console.log(Reflect.hasMetadata('note', car, 'model')); // 输出: true
console.log(Reflect.hasMetadata('desc', car, 'brand')); // 输出: false
// 获取元数据
console.log(Reflect.getMetadata('desc', car)); // 输出: 'This is a good car'
console.log(Reflect.getMetadata('desc', car, 'price')); // 输出: 'This is so cheap'
console.log(Reflect.getMetadata('note', car, 'model')); // 输出: 'This model is too old'
console.log(Reflect.getMetadata('desc', car, 'brand')); // 输出: undefined
// 获取对象上所有元数据的keys
console.log(Reflect.getMetadataKeys(car)) // 输出: ['desc']
console.log(Reflect.getMetadataKeys(car, 'price')) // 输出: ['desc']
console.log(Reflect.getMetadataKeys(car, 'model')) // 输出: ['note']
console.log(Reflect.getMetadataKeys(car, 'brand')) // 输出: []
2.2.2 结合装饰器使用
提案还定义了Reflect.metadata
函数,但并不是Reflect
对象的方法。这个函数式一个 装饰器工厂 decorator factory
,这意味着调用这个函数时,它返回一个修饰器函数,可以用来修饰类或类属性。
Reflect Metadata
的 API 可以用于类或者类的属性上,如:
function metadata(
metadataKey: any,
metadataValue: any
): {
(target: Function): void;
(target: Object, propertyKey: string | symbol): void;
};
Reflect.metadata
当作 Decorator
使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,先来看两个例子:
@Reflect.metadata('inClass', 'A')
class Test {
@Reflect.metadata('inMethod', 'B')
public hello(): string {
return 'hello world';
}
}
console.log(Reflect.getMetadata('inClass', Test)); // 'A'
console.log(Reflect.getMetadata('inMethod', new Test(), 'hello')); // 'B'
import "reflect-metadata";
const nameSymbol = Symbol("lorry");
// 类元数据
@Reflect.metadata("class", "class")
class MetaDataClass {
// 实例属性元数据
@Reflect.metadata(nameSymbol, "nihao")
public name = "origin";
// 实例方法元数据
@Reflect.metadata("getName", "getName")
public getName() {}
// 静态方法元数据
@Reflect.metadata("static", "static")
static staticMethod() {}
}
const value = Reflect.getMetadata("name", MetaDataClass);
const metadataInstance = new MetaDataClass();
const name = Reflect.getMetadata(nameSymbol, metadataInstance, "name");
const methodVal = Reflect.getMetadata("getName", metadataInstance, "getName");
const staticVal = Reflect.getMetadata("static", MetaDataClass, "staticMethod");
console.log(value, name, methodVal, staticVal);
// undefined nihao getName static
@Reflect.metadata(metadataKey, metadataValue)
class MyClass {
@Reflect.metadata(metadataKey, metadataValue)
methodName(){
// ...
}
}
在上面的例子中,@Reflect.metadata
方法调用修饰了类MyClass
和属性methodName
方法。简单而言,它给这些实体添加了键为metadataKey
的元数据metadataValue
。
如果你想知道它是怎么做到的,其实很简单。Reflect.metadata
返回一个修饰器函数。这个修饰器函数内部实现了Reflect.defineMetadata
,从而将元数据添加到了它所修饰的实体上面。
问题在于,reflect-metadata
实现了修饰器提案的原始版本 legacy version。TypeScript 同样实现了这个版本的修饰器实现。你可以阅读这篇文章了解如何使用原始版本的修饰器实现。
由于我们不能在原生 JavaScript 中使用这个包,因此下面的例子是 TypeScript 的。
当然,你也可以使用这个 babel 插件转译为原生 JavaScript。
import "reflect-metadata";
// define a custom decorator
function myDecorator(metaValue) {
return Reflect.metadata("returns", metaValue);
}
@Reflect.metadata("version", 1)
class Person {
fname: string;
lname: string;
constructor(fname, lname) {
this.fname = fname;
this.lname = lname;
}
@myDecorator({ returns: "string" })
getFullName() {
return this.fname + " " + this.lname;
}
}
// create an instance of Person
const person = new Person("Ross", "Geller");
// get metadata
console.log("Person(version) ->", Reflect.getMetadata("version", Person));
// Person(version) -> 1
console.log(
"person.getFullName(returns) ->",
Reflect.getMetadata("returns", person, "getFullName")
);
// person.getFullName(returns) -> { returns: 'string' }
在这个例子中,@Reflect.metadata
给Person
类增加了元数据,之后,我们可以通过Reflect.getMetadata(<key>, Person)
读取这些元数据。你也可以基于Reflect.metadata
实现自己的修饰器工厂,例如myDecorator
。
本节介绍了 reflect-metadata
包以及元数据提案。这个提案的用例是无穷无尽的。有人说,这是 JavaScript 元编程的圣杯。不过,我们就等待它变成 ECMAScript 提案的那天。
最后再来看一个例子巩固一下:
// 把metadata都定义到这个全局变量
const globalMeta = Object.create(null);
const propertyCollector = (target: Object,
propertyKey: string | symbol, descriptor?: any) => {
// 把property的名字都放进一个叫properties的array
const properties = Reflect.getMetadata('properties', globalMeta);
if (properties) {
Reflect.defineMetadata('properties', [...properties, propertyKey], globalMeta);
} else {
Reflect.defineMetadata('properties', [propertyKey], globalMeta);
}
}
const methodCollector = (target: Object,
propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
// 把methods的名字都放进一个叫methods的array
const methodss = Reflect.getMetadata('methods', globalMeta);
if (methodss) {
Reflect.defineMetadata('methods', [...methodss, propertyKey], globalMeta);
} else {
Reflect.defineMetadata('methods', [propertyKey], globalMeta);
}
}
const classCollector = (constructor: Function) => {
// 把class的名字存到globalMeta的className
Reflect.defineMetadata('className', constructor.name, globalMeta);
}
@classCollector
class Car {
@propertyCollector
private speed = 0;
@propertyCollector
private brand = 'BMW';
@propertyCollector
private model = 'X6 2012';
@methodCollector
run() {
this.speed = 100;
console.log(`The car is running at 100km/h`)
}
@methodCollector
stop() {
this.speed = 0;
console.log(`The car stopped`)
}
}
const className = Reflect.getMetadata('className', globalMeta);
const properties = Reflect.getMetadata('properties', globalMeta);
const methods = Reflect.getMetadata('methods', globalMeta);
console.log(
`Class [${className}] has ${properties.length} properties: ${properties.join(', ')}\t
Class [${className}] has ${methods.length} methods: ${methods.join(', ')}`,
);
// 输出
// Class [Car] has 3 properties: speed, brand, model
// Class [Car] has 2 methods: run, stop
2.3 获取类型信息
你可以通过Reflect.defineMetadata
获取到类或者函数的参数类型,也可以给类或者函数设置元数据再获取,具体代码如下:
function Prop(): PropertyDecorator {
return (target, key: string) => {
const type = Reflect.getMetadata('design:type', target, key);
console.log(`${key} type: ${type.name}`);
// other...
};
}
class SomeClass {
@Prop()
public Aprop!: string;
}
运行代码可在控制台看到 Aprop type: string
。譬如在 vue-property-decorator
6.1 及其以下版本中,通过使用 Reflect.getMetadata
API,Prop
Decorator 能获取属性类型传至 Vue。
TypeScript 的优势了,TypeScript 支持编译时自动添加一些 metadata 数据,如下所示:
Reflect.getMetadata("design:type", target, key)
, 获取target函数类型Reflect.getMetadata("design:paramtypes", target, key)
, 获取target函数参数类型Reflect.getMetadata("design:returntype", target, key)
, 获取target函数返回值类型
这个Reflect.getMetadata("design:paramtypes", target, key)
基本上就是Nest.js实现Ioc和DI的核心代码。
2.4 自定义 metadataKey
除能获取类型信息外,常用于自定义 metadataKey
,并在合适的时机获取它的值,示例如下:
function classDecorator(): ClassDecorator {
return target => {
// 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
Reflect.defineMetadata('classMetaData', 'a', target);
};
}
function methodDecorator(): MethodDecorator {
return (target, key, descriptor) => {
// 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
Reflect.defineMetadata('methodMetaData', 'b', target, key);
};
}
@classDecorator()
class SomeClass {
@methodDecorator()
someMethod() {}
}
Reflect.getMetadata('classMetaData', SomeClass); // 'a'
Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'
2.5 metadata 的应用
2.5.1 IoC(控制反转)和DI(依赖注入)实现原理
其实在了解完Reflect.getMetadata
,我们就大概知道IoC和DI的实现原理,我们以一个@Controller
为例, 具体步骤如下:
- 实现
@Controller
装饰器工厂,标识待注入的类 - 实现IoC容器,注册要被依赖注入的类
- 获取待注入的类其构造函数所需要的参数类型,并实例化,返回待注入的类
在 Angular 2+ 的版本中,控制反转与依赖注入便是基于此实现,现在,我们来实现一个简单版:
type Constructor<T = any> = new (...args: any[]) => T;
const Injectable = (): ClassDecorator => target => {};
class OtherService {
a = 1;
}
@Injectable()
class TestService {
constructor(public readonly otherService: OtherService) {}
testMethod() {
console.log(this.otherService.a);
}
}
const Factory = <T>(target: Constructor<T>): T => {
// 获取所有注入的服务
const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
const args = providers.map((provider: Constructor) => new provider());
return new target(...args);
};
Factory(TestService).testMethod(); // 1
2.5.2 Controller 与 Get 的实现
如果你在使用 TypeScript 开发 Node 应用,相信你对 Controller
、Get
、POST
这些 Decorator,并不陌生:
@Controller('/test')
class SomeClass {
@Get('/a')
someGetMethod() {
return 'hello world';
}
@Post('/b')
somePostMethod() {}
}
这些 Decorator 也是基于 Reflect Metadata
实现,这次,我们将 metadataKey
定义在 descriptor
的 value
上:
const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';
const Controller = (path: string): ClassDecorator => {
return target => {
Reflect.defineMetadata(PATH_METADATA, path, target);
}
}
const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
return (target, key, descriptor) => {
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
}
}
const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');
接着,创建一个函数,映射出 route
:
function mapRoute(instance: Object) {
const prototype = Object.getPrototypeOf(instance);
// 筛选出类的 methodName
const methodsNames = Object.getOwnPropertyNames(prototype)
.filter(item => !isConstructor(item) && isFunction(prototype[item]));
return methodsNames.map(methodName => {
const fn = prototype[methodName];
// 取出定义的 metadata
const route = Reflect.getMetadata(PATH_METADATA, fn);
const method = Reflect.getMetadata(METHOD_METADATA, fn);
return {
route,
method,
fn,
methodName
}
})
};
因此,我们可以得到一些有用的信息:
Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'
mapRoute(new SomeClass());
/**
* [{
* route: '/a',
* method: 'GET',
* fn: someGetMethod() { ... },
* methodName: 'someGetMethod'
* },{
* route: '/b',
* method: 'POST',
* fn: somePostMethod() { ... },
* methodName: 'somePostMethod'
* }]
*
*/
最后,只需把 route
相关信息绑在 express
或者 koa
上就 ok 了。
参考
转载自:https://juejin.cn/post/7267539993965363235