reflect-metadata的研究

9,574 阅读8分钟

本文是基于 reflect-metadata库对ts的元编程模式的DI实现详细描述, 力求能将这个过程以及中间的难点讲清楚.

reflect-matadata

目的

  1. 有很多设计模式, 比如组合, 依赖注入, 运行时类型断言, 反射/镜像, 测试等希望可以在保持原有class的一致性的前提下为class添加元数据.
  2. 一致性是很多工具和库使用元数据的原因
  3. 元数据产生的装饰器可以通过改变装饰器来进行组合
  4. 元数据不仅仅只能在对象上使用, 也应该被代理Proxy通过相应的traps所使用,
  5. 定义一个新的元数据装饰器不应该过于复杂
  6. 元数据应该保持与其他语言以及ECMAScript的运行特性的一致性

源码

首先看这个库的index.d.ts这是一个依赖中更新最即时的API文档

export {}
declare global {
    namespace Reflect{}
}

表示该库没有任何的导出, 再往下看是对global和命名空间的声明, 于是我们在项目中直接使用 import 'reflect-metadata'引入后便可在项目中访问到

decorate

第一个方法就是decorate,有好几个重载, 一个一个来看

对类的装饰

/**
  * 应用于一个装饰器的集合给目标对象
  * @param decorators  装饰器的数组
  * @param target 目标对象
  * @returns 返回应用提供的装饰器后的值
  * 注意: 装饰器应用是与array的位置方向相反, 为从右往左
  */
function decorate(decorators: ClassDecorator[], target: Function): Function;

那么这个ClassDecorator[]究竟是个什么数组? 长什么样子?

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

该定义为: 这是一个Function, 只有一个参数Target, 也就是类的构造函数constructor, 返回值的类型与target的函数类型一致或为空, 也就是说, 如果是一个类的话, 续需返回这个类或者空. 注意, 该类型的定义不在reflect-matadata中, 而是在lib.es5.d.ts中, 也就表明该为es5的原生实现

来个例子
const classDecorator:ClassDecorator = target => {
  target.prototype.sayName = () => console.log('override');
  // return target 这里可以return也可以不return, 因为target是一个对象引用
}
export class TestClassDecrator {
  constructor(public name = '') {
  }
  sayName() {
    console.log(this.name)
  }
}
Reflect.decorate([classDecorator], TestClassDecrator) // 对其进行装饰
const t = new TestClassDecrator('nihao')
t.sayName() // 'override'

注意: 在classDecorator中传入的target, 只能修改其prototype的方法, 不能修改其属性, 因为其属性是read-only

对属性或方法的装饰

/**
  * 应用一个集合去装饰目标对象的方法
  * @param 装饰器的集合
  * @param 目标对象
  * @param 要装饰的属性名称 key
  * @param 该属性的描述 descriptor
  */
    function decorate(decorators: (PropertyDecorator | MethodDecorator)[], target: Object, propertyKey: string | symbol, attributes?: PropertyDescriptor): PropertyDescriptor;

顺便提一嘴, descriptor分为两种, 一种是数据描述符, 一种是存取描述符

// 数据描述符
{
    value:&emsp;'aaa',
    configurable: true,
    writable: true,
    enumerable: true
}
// 存取描述符
{
    get(){return 1},
    set() {console.log('set')},
    configurable: true,
    enumerable: true
}

至于每个字段表示什么意思, 就不再展开了, 可参考mdn

回到方法和属性装饰器, 举例子

属性装饰器
const propertyDecorator:PropertyDecorator = (target, propertyKey) => {
  const origin = target[propertyKey]
  target[propertyKey] = () => {
    origin.call(target)
    console.log('added override')
  }
}

class PropertyAndMethodExample {
    static staticProperty() {
        console.log('im static property')
    }
    method() {
        console.log('im one instance method')
    }
}

Reflect.decorate([propertyDecorator], PropertyAndMethodExample, 'staticProperty')
// test property decorator
PropertyAndMethodExample.staticProperty() // im static property \n added override
方法装饰器
const methodDecorator:MethodDecorator = (target, propertyKey, descriptor) => {
  // 将其描述改为不可编辑
  descriptor.configurable = false
  descriptor.writable = false
  return descriptor
}
// 获取原descriptor
let descriptor = Object.getOwnPropertyDescriptor(PropertyAndMethodExample.prototype, 'method')
// 获取修改后的descriptor
descriptor = Reflect.decorate([methodDecorator], PropertyAndMethodExample, 'method', descriptor)
// 将修改后的descriptor添加到对应的方法上
Object.defineProperty(PropertyAndMethodExample.prototype, 'method', descriptor)
// test method decorator
const example = new PropertyAndMethodExample
example.method = () => console.log('override') // 报错 Cannot assign to read only property 'method' of object '#<PropertyAndMethodExample>'

metadata

默认的元数据装饰器可以被用于类, 类成员以及参数.

注意: 如果 metadataKey 已经被定义在target或target key, 那么metadataValue将会被覆盖

/**
* @param metadataKey 元数据入口的key
* @param metadataValue 元数据入口的value
* @returns 装饰器函数
*/
function metadata(metadataKey: any, metadataValue: any): {
    (target: Function): void;
    (target: Object, propertyKey: string | symbol): void;
};

下面就使用一波

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)

defineMetadata

该方法是metadata的定义版本, 也就是非@版本, 会多传一个参数target, 表示待装饰的对象

/**
  * @param metadataKey 设置或获取时的key
  * @param metadataValue 元数据内容
  * @param target 待装饰的target
  * @param targetKey target的property
*/
function defineMetadata(metadataKey: any, metadataValue: any, target: Object, targetKey: string | symbol): void;

看看例子

class DefineMetadata {
  static staticMethod() {}
  static staticProperty = 'static'
  getName() {}
}
const type = 'type'
Reflect.defineMetadata(type, 'class', DefineMetadata)
Reflect.defineMetadata(type, 'staticMethod', DefineMetadata.staticMethod)
Reflect.defineMetadata(type, 'method', DefineMetadata.prototype.getName)
Reflect.defineMetadata(type, 'staticProperty', DefineMetadata, 'staticProperty')
const t1 = Reflect.getMetadata(type, DefineMetadata)
const t2 = Reflect.getMetadata(type, DefineMetadata.staticMethod)
const t3 = Reflect.getMetadata(type, DefineMetadata.prototype.getName)
const t4 = Reflect.getMetadata(type, DefineMetadata,'staticProperty')
console.log(t1,t2,t3,t4) // class staticMethod method staticProperty

注意t4定义和获取不一样的地方, 比如t2到t3都有两种写法, 一种就是将target传为对应的对象且必须为对象.以t2为例, 也可以写为

Reflect.defineMetadata(type, 'staticMethos', DefineMetadata, 'staticMethod')
const t2 = Reflect.getMetadata(type, DefineMetadata, 'staticMethod')

需要注意的是这两者并不能混合使用, 比如

Reflect.defineMetadata(type, 'staticMethos', DefineMetadata, 'staticMethod')
const t2 = Reflect.getMetadata(type, DefineMetadata.staticMethod)

是无法获取到对应的metadataValue的, 原因是如果未传入property, 会当property当作undefined, 此时该target的metadata entries(本质是一个Map)中就有一个 {undefined: Map()} 而传入了property, 就是在该target下的property属性下的entries set一个{property: Map()} 具体拿t2来说 方法1

Reflect.defineMetadata(type, 'staticMethod', DefineMetadata.staticMethod)

这条语句是在DefineMetadata.staticMethod 下的元数据为

方法2

Reflect.defineMetadata(type, 'staticMethos', DefineMetadata, 'staticMethod')

元数据为

明显这两者不等价, 所以无法互换. 顺带一嘴, @Reflect.metadata的行为跟设置property一致, 也就是为方法2的实现, 使用方法1获取@Reflect.metadata的数据会为undefined

hasMetadata

该方法返回布尔值, 表明该target或其原型链上有没有对应的元数据

/**
  * @param metadataKey 元数据的key.
  * @param target 定义的对象
  * @param targetKey 重载参数, 可选
  * @returns 在target或其原型链上返回true.
*/
function hasMetadata(metadataKey: any, target: Object,targetKey?:symbol|string): boolean;

举个例子

const type = 'type'
class HasMetadataClass {
  @Reflect.metadata(type, 'staticProperty')
  static staticProperty = ''
}
Reflect.defineMetadata(type, 'class', HasMetadataClass)
const t1 = Reflect.hasMetadata(type, HasMetadataClass)
const t2 = Reflect.hasMetadata(type, HasMetadataClass, 'staticProperty')
console.log(t1, t2)

其余的像实例属性/方法, 静态方法都以此类推

hasOwnMetadata

Object.prototype.hasOwnProperty类似, 是只查找对象上的元数据, 而不会继续向上查找原型链上的, 其余的跟hasMetadata一致

const type = 'type'
class Parent {
  @Reflect.metadata(type, 'getName')
  getName() {}
}
@Reflect.metadata(type, 'class')
class HasOwnMetadataClass extends Parent{
  @Reflect.metadata(type, 'static')
  static staticProperty() {}
  @Reflect.metadata(type, 'method')
  method() {}
}

const t1 = Reflect.hasOwnMetadata(type, HasOwnMetadataClass)
const t2 = Reflect.hasOwnMetadata(type, HasOwnMetadataClass, 'staticProperty')
const t3 = Reflect.hasOwnMetadata(type, HasOwnMetadataClass.prototype, 'method')
const t4 = Reflect.hasOwnMetadata(type, HasOwnMetadataClass.prototype, 'getName')
const t5 = Reflect.hasMetadata(type, HasOwnMetadataClass.prototype, 'getName')

console.log(t1, t2, t3, t4, t5) // true true true false true

注意t4和t5的区别

getMetadata

这个属性在之前验证各个属性的时候就已经使用过了, 就是用于获取target的元数据值, 会往原型链上找

/**
* @param metadataKey 元数据key
* @param target 元数据定义的target
* @param targetKey 可选项, 是否选择target的某个key
* @returns 如果找到了元数据则返回元数据值, 否则返回undefined
*
*/
function getMetadata(metadataKey: any, target: Object, targetKey?: string | symbol ): any;

getOwnMetadata

与hasOwnMetadata和hasMetadata的区别一样, 是否往原型链上找

getMetadataKeys

类似Object.keys, 返回该target以及原型链上target的所有元数据的keys

const type = 'type'
@Reflect.metadata('parent', 'parent')
class Parent {
  getName() {}
}
@Reflect.metadata(type, 'class')
class HasOwnMetadataClass extends Parent{
  @Reflect.metadata(type, 'static')
  static staticProperty() {}
  @Reflect.metadata('bbb', 'method')
  @Reflect.metadata('aaa', 'method')
  method() {}
}

const t1 = Reflect.getMetadataKeys(HasOwnMetadataClass)
const t2 = Reflect.getMetadataKeys(HasOwnMetadataClass.prototype, 'method')
console.log(t1, t2) // ["type", "parent"] \n ["design:returntype", "design:paramtypes", "design:type", "aaa", "bbb"]

t1很好理解, 因为会向上找原型链的parent t2好像多了一些东西, design: 开头的, 先按下不表, 看看 'aaa' 和 'bbb' 的顺序是和我们添加的顺序是相反的, 还记得前面说过装饰器的顺序是从右到左的, 所以先应用的bbb, aaa再应用的design:xxx

getOwnMetadataKeys

跟getMetadataKeys 一样, 只是不向原型链中查找

deleteMetadata

用于删除元数据

/**
* @param metadataKey 元数据key
* @param target 元数据定义的对象
* @param targetKey 对象对应的key
* @returns 如果对象上有该元数据, 返回true, 否则返回false
*/
function deleteMetadata(metadataKey: any, target: Object, targetKey?:symbol|string): boolean;

因为比较简单, 就直接上例子了:

const type = 'type'
@Reflect.metadata(type, 'class')
class DeleteMetadata {
  @Reflect.metadata(type, 'static')
  static staticMethod() {}
}

const res1 = Reflect.deleteMetadata(type, DeleteMetadata)
const res2 = Reflect.deleteMetadata(type, DeleteMetadata, 'staticMethod')
const res3 = Reflect.deleteMetadata(type, DeleteMetadata)
console.log(res1, res2, res3) // true true false

design:

好了还有一个问题没有解决, 就是之前说的在getMetadataKey时出现的design:xxx的内容是怎么来的, 表示什么意思呢? design:type 表示被装饰的对象是什么类型, 比如是字符串? 数字?还是函数等 design:paramtypes 表示被装饰对象的参数类型, 是一个表示类型的数组, 如果不是函数, 则没有该key design:returntype 表示被装饰对象的返回值属性, 比如字符串,数字或函数等 来个例子

@Reflect.metadata('type', 'class')
class A {
  constructor(public name: string, public age: number) {
  }
  @Reflect.metadata(undefined, undefined)
  method():boolean {
    return true
  }
}

const t1 = Reflect.getMetadata('design:paramtypes', A)
const t2 = Reflect.getMetadata('design:returntype', A.prototype, 'method')
const t3 = Reflect.getMetadata('design:type', A.prototype, 'method')

console.log(...t1, t2, t3)

打印输出为:

但是要注意

  1. 没有装饰的target是get不到这些metadata的
  2. 必须手动指定类型, 无法进行推断, 比如method方法如果不指定返回值为boolean, 那么t2将会是 undefined
  3. 应用的顺序为 type -> paramtypes -> returntype

所有源码实现可到github