一、为什么会关注这个
在nestjs中,有大量的装饰器的使用,对于controller中函数可以使用@Query
,@Param
,@Body
等函数装饰来获取相应的请求相关数据
typescript
@Get()
findAll(@Query('a') a) {
return this.testService.findAll() + a;
}
@Get('/test')
findTest(@Body() b, @Query('a') a, @Ip() ip: string) {
console.log(b, a, ip);
return this.testService.findAll() + a + b + ip;
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.testService.findOne(+id);
$}$
通过装饰器,我们可以获取自己想要的数据,并且不必在乎参数的顺序。 对于装饰器,装饰类,方法,属性我们见到的比较多,对于参数的装饰,一时不知道实现的原理,所以进行了一些探究
二、装饰器的基本介绍
TypeScript 学习笔记 - 装饰器 这篇文章对于装饰器的介绍比较全面,对于各种装饰器及其特性都进行了介绍并举了例子
三、对于参数装饰器注入参数的实现
前置知识:
Reflect.defineMetadata和Reflect.getMetadata
官方文档:github.com/rbuckton/re...
简单来说,我们可以利用Reflect.defineMetadata在类或者对象上设置一个元数据,利用Reflect.getMetadata可以获取出这个数据
实现
1. 定义参数装饰器,利用Reflect.defineMetadata挂载数据
typescript
function params1() {
return (target: any, propertyKey: any, paramsIndex: any) => {
Reflect.defineMetadata("params1", paramsIndex, target[propertyKey]);
};
}
function params2() {
return (target: any, propertyKey: any, paramsIndex: any) => {
Reflect.defineMetadata("params2", paramsIndex, target[propertyKey]);
};
}
2.定义运行函数
typescript
const run = (instance: any, funcName: string, params1: any, params2: any) => {
const params1Index = Reflect.getMetadata('params1', instance[funcName]);
const params2Index = Reflect.getMetadata('params2', instance[funcName]);
const params: any = [];
if (params1Index >= 0) {
params[params1Index] = params1;
}
if (params2Index >= 0) {
params[params2Index] = params2;
}
instance[funcName](...params);
}
3. 定义类
typescript
class T {
@addParams('hello', 'world')
a(@params1() a: any) {
console.log(a);
}
@addParams('hello1', 'world1')
b(@params2() b: any) {
console.log(b);
}
@addParams('hello2', 'world2')
c(@params1() a: any, @params2() b: any) {
console.log(a, b);
}
@addParams('hello3', 'world3')
d(@params2() b: any, @params1() a: any) {
console.log(b, a);
}
}
4. 实例化对象
typescript
const instance = new T()
5. 利用类装饰器或执行方法执行
typescript
run(instance, 'a', 'hello', 'world')
打印结果:
扩展,利用方法装饰器实现
前置知识:方法装饰器和参数装饰器的顺序
方法装饰器求值先于参数装饰器,参数装饰器调用先于方法装饰器,是一个洋葱模型
这里用上面文章中的一个例子
typescript
function f(key: string): any {
console.log("evaluate: ", key);
return function () {
console.log("call: ", key);
};
}
class C {
@f("Method Outer")
@f("Method Inner")
method(
@f("Parameter Foo Outer") @f("Parameter Foo Inner") foo,
@f("Parameter Bar") bar
) {}
}
输出为:
sql
evaluate: Method Outer
evaluate: Method Inner
evaluate: Parameter Foo Outer
evaluate: Parameter Foo Inner
evaluate: Parameter Bar
call: Parameter Bar
call: Parameter Foo Inner
call: Parameter Foo Outer
call: Method Inner
call: Method Outer
定义方法装饰器
typescript
function addParams(params1Val: string, params2Val: string){
return (target: any, propertyKey: string,descriptor: PropertyDescriptor) => {
const original = descriptor.value;
const indexA = Reflect.getMetadata('params1', target[propertyKey])
const indexB = Reflect.getMetadata('params2', target[propertyKey])
descriptor.value = function(){
const args = []
args[indexA] = params1Val
args[indexB] = params2Val
const res = original.call(this, ...args);
return res
}
}
}
这里利用了上述方法和函数装饰器的顺序
执行,直接实例执行
typescript
instance.a()
instance.b()
instance.c()
instance.d()
这不是实际使用的方法,因为不能动态的传递参数进入,这里只是介绍方法装饰器和参数装饰器的顺序以及一种设计方法
优化
在nestjs中,参数装饰器数据是是设置在
Reflect.defineMetadata("params1", paramsIndex, target.constructor, propertyKey);
为什么要这么做?
1. defineMetadata第四个参数传不传的问题
在defineMetadata的实现中,
从上述代码可以看出,metadata存储在一个WeakMap中,WeakMap的key是传进去的第三个参数,而我们具体的值,是存储在WeakMap,key为第三个参数的对应的value中,而这个value是一个Map,Map的key为第四个参数,不传则为undefined
所以我们上述的实现,直接将第三个参数传为target[propertyKey]是不正确的,因为这将在WeakMap中,重新设置一个key,而我们希望的是将一个类上的所有metadata都在key为这个类的一个Map中
2. 第三个参数的传值
对于装饰器,传进去的target为类的原型对象,在实际调用的时候,是类的实例,实例和对象其实不是相同的key,按理说应该找不到value,但实际找得到,因为reflect-metadata做了兼容
可以看到,如果找不到,会以原型对象为key继续去找,所以即使我们第三个参数传入target,在使用时传入的获取值的key是实例instance,依旧可以获得,但如果我们手动Reflect.defineMetadata('params1', value, instance)这样就实例在该实例中找到值,就不会去原型对象上面找了
所以我们可以看到,在nestjs中,定义元数据时第三个参数传的是,target.constructor,在使用时是instance.constructor,因为类的原型对象的constructor指向的就是类本身,instance.constructor也就等于 Object.getPrototypeOf(instance).constructor,所以他们指向的是WeakMap中的同一份空间,所以相对比较好的实现是
typescript
function params1() {
return (target: any, propertyKey: any, paramsIndex: any) => {
Reflect.defineMetadata("params1", paramsIndex, target, propertyKey);
};
}
function params2() {
return (target: any, propertyKey: any, paramsIndex: any) => {
Reflect.defineMetadata("params2", paramsIndex, target, propertyKey]);
};
}
执行函数
typescript
const run = (instance: any, funcName: string) => {
const params1Index = Reflect.getMetadata('params1', instance, funcName);
const params2Index = Reflect.getMetadata('params2', instance, funcName);
const params: any = [];
if (params1Index >= 0) {
params[params1Index] = 'hello';
}
if (params2Index >= 0) {
params[params2Index] = 'world';
}
instance[funcName](...params);
}
区别仅仅在于第三个和第四个参数的不同,传了第四个参数,这样能够精确的知道到底是类的还是属性或方法的元数据,不然多个函数或属性相同的元数据会使得命名冲突等问题
3. 存储结构
四、reflect-metadata的defiendMeta和getMeta原理
1. defiendMeta
typescript
function defineMetadata(metadataKey, metadataValue, target, propertyKey) {
// 不是对象报类型错误
if (!IsObject(target))
throw new TypeError();
// 转换一下propertyKey
if (!IsUndefined(propertyKey))
propertyKey = ToPropertyKey(propertyKey);
return OrdinaryDefineOwnMetadata(metadataKey, metadataValue, target, propertyKey);
}
// 定义自己的metadata
function OrdinaryDefineOwnMetadata(MetadataKey, MetadataValue, O, P) {
// 根据O和P,获取到target中propertyKey的metadataMap,此处可参考数据存储图
var metadataMap = GetOrCreateMetadataMap(O, P, /*Create*/ true);
// 将值塞进去
metadataMap.set(MetadataKey, MetadataValue);
}
var Metadata = new _WeakMap();
function GetOrCreateMetadataMap(O, P, Create) {
// 获取target的Metadata
var targetMetadata = Metadata.get(O);
// 没有就创建
if (IsUndefined(targetMetadata)) {
if (!Create)
return undefined;
targetMetadata = new _Map();
Metadata.set(O, targetMetadata);
}
// 获取propertyKey的MetadataMap
var metadataMap = targetMetadata.get(P);
// 没有就创建
if (IsUndefined(metadataMap)) {
if (!Create)
return undefined;
metadataMap = new _Map();
targetMetadata.set(P, metadataMap);
}
return metadataMap;
}
2. getMetadata
typescript
function getMetadata(metadataKey, target, propertyKey) {
// target不是对象,报类型错误
if (!IsObject(target))
throw new TypeError();
// 和defined一样的方法转换propertyKey
if (!IsUndefined(propertyKey))
propertyKey = ToPropertyKey(propertyKey);
return OrdinaryGetMetadata(metadataKey, target, propertyKey);
}
function OrdinaryGetMetadata(MetadataKey, O, P) {
// 先判断以target为key是否能找到值
var hasOwn = OrdinaryHasOwnMetadata(MetadataKey, O, P);
// 找到了
if (hasOwn)
// 返回对应的值
return OrdinaryGetOwnMetadata(MetadataKey, O, P);
// 自己target-->propertyKey-->metadataKey这一路找不到值
var parent = OrdinaryGetPrototypeOf(O);
// 原型对象不是空
if (!IsNull(parent))
// 递归继续往上找
return OrdinaryGetMetadata(MetadataKey, parent, P);
return undefined;
}
// 获取自己的metadata
function OrdinaryHasOwnMetadata(MetadataKey, O, P) {
// 根据O和P,获取到target中propertyKey的metadataMap,此处可参考数据存储图
var metadataMap = GetOrCreateMetadataMap(O, P, /*Create*/ false);
// 自己的propertyKey的metadata为空,就返回找不到
if (IsUndefined(metadataMap))
return false;
// 如果自己的metadata不为空,但是找不到我想找的metadataKey,也返回空,否则就是找到了
return ToBoolean(metadataMap.has(MetadataKey));
}
function GetOrCreateMetadataMap(O, P, Create) {
// 获取target的Metadata
var targetMetadata = Metadata.get(O);
// 没有就创建
if (IsUndefined(targetMetadata)) {
if (!Create)
return undefined;
targetMetadata = new _Map();
Metadata.set(O, targetMetadata);
}
// 获取propertyKey的MetadataMap
var metadataMap = targetMetadata.get(P);
// 没有就创建
if (IsUndefined(metadataMap)) {
if (!Create)
return undefined;
metadataMap = new _Map();
targetMetadata.set(P, metadataMap);
}
return metadataMap;
}
function OrdinaryGetOwnMetadata(MetadataKey, O, P) {
// 再找一遍target的propertyKey的metadataMap
var metadataMap = GetOrCreateMetadataMap(O, P, /*Create*/ false);
if (IsUndefined(metadataMap))
return undefined;
// 在对应的metadata里找对应的metadataKey
return metadataMap.get(MetadataKey);
}
// 找到target的原型对象
function OrdinaryGetPrototypeOf(O) {
var proto = Object.getPrototypeOf(O);
if (typeof O !== "function" || O === functionPrototype)
return proto;
// TypeScript doesn't set __proto__ in ES5, as it's non-standard.
// Try to determine the superclass constructor. Compatible implementations
// must either set __proto__ on a subclass constructor to the superclass constructor,
// or ensure each class has a valid `constructor` property on its prototype that
// points back to the constructor.
// If this is not the same as Function.[[Prototype]], then this is definately inherited.
// This is the case when in ES6 or when using __proto__ in a compatible browser.
if (proto !== functionPrototype)
return proto;
// If the super prototype is Object.prototype, null, or undefined, then we cannot determine the heritage.
var prototype = O.prototype;
var prototypeProto = prototype && Object.getPrototypeOf(prototype);
if (prototypeProto == null || prototypeProto === Object.prototype)
return proto;
// If the constructor was not a function, then we cannot determine the heritage.
var constructor = prototypeProto.constructor;
if (typeof constructor !== "function")
return proto;
// If we have some kind of self-reference, then we cannot determine the heritage.
if (constructor === O)
return proto;
// we have a pretty good guess at the heritage.
return constructor;
}
五、总结
这里介绍了如何利用装饰器向类中的函数传参,并且不必关注参数的顺序,以及reflect-metadata的实现原理。在实际中,比如nestjs中实现则更为复杂,但整体的实现原理是Reflect.defineMetadata存储参数位置,Reflect.getMetadata获取参数位置,并为其赋值,传到对应实例的执行函数中去执行