本文是基于 reflect-metadata库对ts的元编程模式的DI实现详细描述, 力求能将这个过程以及中间的难点讲清楚.
reflect-matadata
目的
- 有很多设计模式, 比如组合, 依赖注入, 运行时类型断言, 反射/镜像, 测试等希望可以在保持原有class的一致性的前提下为class添加元数据.
- 一致性是很多工具和库使用元数据的原因
- 元数据产生的装饰器可以通过改变装饰器来进行组合
- 元数据不仅仅只能在对象上使用, 也应该被代理Proxy通过相应的traps所使用,
- 定义一个新的元数据装饰器不应该过于复杂
- 元数据应该保持与其他语言以及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: '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 下的元数据为

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)
打印输出为:

但是要注意
- 没有装饰的target是get不到这些metadata的
- 必须手动指定类型, 无法进行推断, 比如method方法如果不指定返回值为boolean, 那么t2将会是
undefined
- 应用的顺序为 type -> paramtypes -> returntype
所有源码实现可到github