基础
Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它,你只需要:
npm i reflect-metadata --save
。
- 在
tsconfig.json
里配置 emitDecoratorMetadata
选项。
Reflect Metadata 的 API 可以用于类或者类的属性上,如:
1 2 3 4 5 6 7
| function metadata( metadataKey: any, metadataValue: any ): { (target: Function): void; (target: Object, propertyKey: string | symbol): void; };
|
Reflect.metadata
当作 Decorator
使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如:
1 2 3 4 5 6 7 8 9 10
| @Reflect.metadata('inClass', 'A') class Test { @Reflect.metadata('inMethod', 'B') public hello(): string { return 'hello world'; } }
console.log(Reflect.getMetadata('inClass', Test)); console.log(Reflect.getMetadata('inMethod', new Test(), 'hello'));
|
它具有诸多使用场景。
获取类型信息
譬如在 vue-property-decorator
6.1 及其以下版本中,通过使用 Reflect.getMetadata
API,Prop
Decorator 能获取属性类型传至 Vue,简要代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
| function Prop(): PropertyDecorator { return (target, key: string) => { const type = Reflect.getMetadata('design:type', target, key); console.log(`${key} type: ${type.name}`); }; }
class SomeClass { @Prop() public Aprop!: string; }
|
运行代码可在控制台看到 Aprop type: string
。除能获取属性类型外,通过 Reflect.getMetadata("design:paramtypes", target, key)
和 Reflect.getMetadata("design:returntype", target, key)
可以分别获取函数参数类型和返回值类型。
除能获取类型信息外,常用于自定义 metadataKey
,并在合适的时机获取它的值,示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function classDecorator(): ClassDecorator { return target => { Reflect.defineMetadata('classMetaData', 'a', target); }; }
function methodDecorator(): MethodDecorator { return (target, key, descriptor) => { Reflect.defineMetadata('methodMetaData', 'b', target, key); }; }
@classDecorator() class SomeClass { @methodDecorator() someMethod() {} }
Reflect.getMetadata('classMetaData', SomeClass); Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod');
|
例子
控制反转和依赖注入
在 Angular 2+ 的版本中,控制反转与依赖注入便是基于此实现,现在,我们来实现一个简单版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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); const args = providers.map((provider: Constructor) => new provider()); return new target(...args); };
Factory(TestService).testMethod();
|
Controller 与 Get 的实现
如果你在使用 TypeScript 开发 Node 应用,相信你对 Controller
、Get
、POST
这些 Decorator,并不陌生:
1 2 3 4 5 6 7 8 9 10
| @Controller('/test') class SomeClass { @Get('/a') someGetMethod() { return 'hello world'; }
@Post('/b') somePostMethod() {} }
|
这些 Decorator 也是基于 Reflect Metadata
实现,这次,我们将 metadataKey
定义在 descriptor
的 value
上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function mapRoute(instance: Object) { const prototype = Object.getPrototypeOf(instance);
const methodsNames = Object.getOwnPropertyNames(prototype) .filter(item => !isConstructor(item) && isFunction(prototype[item])); return methodsNames.map(methodName => { const fn = prototype[methodName];
const route = Reflect.getMetadata(PATH_METADATA, fn); const method = Reflect.getMetadata(METHOD_METADATA, fn); return { route, method, fn, methodName } }) };
|
因此,我们可以得到一些有用的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Reflect.getMetadata(PATH_METADATA, SomeClass);
mapRoute(new SomeClass());
|
最后,只需把 route
相关信息绑在 express
或者 koa
上就 ok 了。