Reflect Metadata

基础

Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它,你只需要:

  • npm i reflect-metadata —save
  • tsconfig.json 里配置 emitDecoratorMetadata 选项。
    Reflect Metadata 的 API 可以用于类或者类的属性上,如:
  1. function metadata(
  2. metadataKey: any,
  3. metadataValue: any
  4. ): {
  5. (target: Function): void;
  6. (target: Object, propertyKey: string | symbol): void;
  7. };

Reflect.metadata 当作 Decorator 使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如:

  1. @Reflect.metadata('inClass', 'A')
  2. class Test {
  3. @Reflect.metadata('inMethod', 'B')
  4. public hello(): string {
  5. return 'hello world';
  6. }
  7. }
  8. console.log(Reflect.getMetadata('inClass', Test)); // 'A'
  9. console.log(Reflect.getMetadata('inMethod', new Test(), 'hello')); // 'B'

它具有诸多使用场景。

获取类型信息

譬如在 vue-property-decorator 6.1 及其以下版本中,通过使用 Reflect.getMetadata API,Prop Decorator 能获取属性类型传至 Vue,简要代码如下:

  1. function Prop(): PropertyDecorator {
  2. return (target, key: string) => {
  3. const type = Reflect.getMetadata('design:type', target, key);
  4. console.log(`${key} type: ${type.name}`);
  5. // other...
  6. };
  7. }
  8. class SomeClass {
  9. @Prop()
  10. public Aprop!: string;
  11. }

运行代码可在控制台看到 Aprop type: string。除能获取属性类型外,通过 Reflect.getMetadata("design:paramtypes", target, key)Reflect.getMetadata("design:returntype", target, key) 可以分别获取函数参数类型和返回值类型。

自定义 metadataKey

除能获取类型信息外,常用于自定义 metadataKey,并在合适的时机获取它的值,示例如下:

  1. function classDecorator(): ClassDecorator {
  2. return target => {
  3. // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
  4. Reflect.defineMetadata('classMetaData', 'a', target);
  5. };
  6. }
  7. function methodDecorator(): MethodDecorator {
  8. return (target, key, descriptor) => {
  9. // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
  10. Reflect.defineMetadata('methodMetaData', 'b', target, key);
  11. };
  12. }
  13. @classDecorator()
  14. class SomeClass {
  15. @methodDecorator()
  16. someMethod() {}
  17. }
  18. Reflect.getMetadata('classMetaData', SomeClass); // 'a'
  19. Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'

例子

控制反转和依赖注入

在 Angular 2+ 的版本中,控制反转与依赖注入便是基于此实现,现在,我们来实现一个简单版:

  1. type Constructor<T = any> = new (...args: any[]) => T;
  2. const Injectable = (): ClassDecorator => target => {};
  3. class OtherService {
  4. a = 1;
  5. }
  6. @Injectable()
  7. class TestService {
  8. constructor(public readonly otherService: OtherService) {}
  9. testMethod() {
  10. console.log(this.otherService.a);
  11. }
  12. }
  13. const Factory = <T>(target: Constructor<T>): T => {
  14. // 获取所有注入的服务
  15. const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
  16. const args = providers.map((provider: Constructor) => new provider());
  17. return new target(...args);
  18. };
  19. Factory(TestService).testMethod(); // 1

Controller 与 Get 的实现

如果你在使用 TypeScript 开发 Node 应用,相信你对 ControllerGetPOST 这些 Decorator,并不陌生:

  1. @Controller('/test')
  2. class SomeClass {
  3. @Get('/a')
  4. someGetMethod() {
  5. return 'hello world';
  6. }
  7. @Post('/b')
  8. somePostMethod() {}
  9. }

这些 Decorator 也是基于 Reflect Metadata 实现,这次,我们将 metadataKey 定义在 descriptorvalue 上:

  1. const METHOD_METADATA = 'method'
  2. const PATH_METADATA = 'path'
  3. const Controller = (path: string): ClassDecorator => {
  4. return target => {
  5. Reflect.defineMetadata(PATH_METADATA, path, target);
  6. }
  7. }
  8. const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
  9. return (target, key, descriptor) => {
  10. Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
  11. Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
  12. }
  13. }
  14. const Get = createMappingDecorator('GET');
  15. const Post = createMappingDecorator('POST');

接着,创建一个函数,映射出 route

  1. function mapRoute(instance: Object) {
  2. const prototype = Object.getPrototypeOf(instance);
  3. // 筛选出类的 methodName
  4. const methodsNames = Object.getOwnPropertyNames(prototype)
  5. .filter(item => !isConstructor(item) && isFunction(prototype[item]))
  6. return methodsNames.map(methodName => {
  7. const fn = prototype[methodName];
  8. // 取出定义的 metadata
  9. const route = Reflect.getMetadata(PATH_METADATA, fn);
  10. const method = Reflect.getMetadata(METHOD_METADATA, fn)
  11. return {
  12. route,
  13. method,
  14. fn,
  15. methodName
  16. }
  17. })
  18. };

因此,我们可以得到一些有用的信息:

  1. Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'
  2. mapRoute(new SomeClass());
  3. /**
  4. * [{
  5. * route: '/a',
  6. * method: 'GET',
  7. * fn: someGetMethod() { ... },
  8. * methodName: 'someGetMethod'
  9. * },{
  10. * route: '/b',
  11. * method: 'POST',
  12. * fn: somePostMethod() { ... },
  13. * methodName: 'somePostMethod'
  14. * }]
  15. *
  16. */

最后,只需把 route 相关信息绑在 express 或者 koa 上就 ok 了。

原文: https://jkchao.github.io/typescript-book-chinese/tips/metadata.html