linqinghao/blog

[译]Typescript中的装饰器与元数据反射(part4)

Opened this issue · 0 comments

这篇文章会深入讲解Typescript如何实现装饰器以及诸如反射或依赖注入这些令人激动的javascript特性。

这个系列包含了一下几个部分:
第一部分:方法装饰器
第二部分:属性装饰器 & 类装饰器
第三部分:参数装饰器 & 装饰器工厂
第四部分:类型序列化 & 元数据反射API

我假设你已经阅读了前面的文章并且已经准备好了。

在此系列前面的文章中,我们学习了什么是装饰器并且知道如何在Typescript中实现它。我们了解了类、方法、属性和参数装饰器如何工作,如何创建装饰器工厂,以及如何使用装饰器工厂实现可配置的装饰器。

在这篇文章中我们将学习:

  • 为什么在Javascript中我们需要反射?
  • 元数据反射API
  • 基本类型序列化
  • 复杂类型序列化

1. 为什么在Javascript中我们需要反射?

反射这个名词是用于描述能够检查同一系统(或自身)中的其他代码的代码。

反射对于许多用例(复合/依赖注入、运行时类型断言、测试)是有用的。

我们的JavaScript应用程序越来越大,我们开始需要一些工具(比如控制容器的反转)和诸如(运行时类型断言)之类的特性来管理这种日益增长的复杂性。

问题是,由于JavaScript没有反射,所以一些工具和特性无法实现,或者至少它们不能像C#或者Java这样的编程语言那样强大。

一个强大的反射API应该允许我们在运行时检查一个未知的对象,并找出它的所有内容。

我们应该能找到这样的东西:

  • 实体的名称。

  • 实体的类型。

  • 哪些接口是由实体实现的。

  • 实体的属性的名称和类型。

  • 实体的构造器参数的名称和类型。

Javascript中我们可以使用像Object.getOwnPropertyDescriptor()或者Object.keys()这样的方法取得实体的信息,但是我们需要反射实现更加强大的开发工具。

不过,由于Typescript开始支持一些反射特性,事情正在发生改变。让我们看下这些特性吧。

2. 元数据反射API

原生的Javascrtpt支持元数据反射API还处在开发的早期阶段中。有人提议将Decorator特性添加到ES7中,并提供一个用于Decorator元数据的ES7反射API的原型对象,该对象能够访问。

Typescript团队中的某些人一直在为ES7反射API的原型开发一个Polyfill,而Typescript编译器现在能够编译一些序列化的设计时类型元数据

我们可以通过使用 reflect-metadata包来使用元数据反射API的Polyfill。

npm install reflect-metadata;

我们必须将它与Typescript 1.5,并且将编译器中的emitDecoratorMetadata标志设为true才能使用。同时,我们也需要包含reflect-metadata.d.ts的引用,以及加载Reflect.js文件。

然后,我们需要实现我们自己的装饰器,并使用其中一个可用的reflect元数据设计的键。目前只有三中选择:

  • 类型元数据使用的键"design:type"
  • 参数类型元数据使用的键"design:paramtypes"
  • 返回类型元数据使用的键"design:returntype"

让我们看一组例子。

A) 使用reflect元数据API获得类型元数据

先定义下面的属性装饰器:

    function logType(target : any, key : string) {
      var t = Reflect.getMetadata("design:type", target, key);
      console.log(`${key} type: ${t.name}`);
    }

然后,我们可以将其应用于类的一个属性,以获得其类型:

class Demo{ 
      @logType // apply property decorator
      public attr1 : string;
    }

上面的例子将在控制台打印:

attr1 type: String

B) 使用reflect元数据API获得参数类型元数据

先定义一下的参数装饰器:

function logParamTypes(target : any, key : string) {
      var types = Reflect.getMetadata("design:paramtypes", target, key);
      var s = types.map(a => a.name).join();
      console.log(`${key} param types: ${s}`);
    }  

然后,我们可以将它应用到类的一个方法中,以获取关于其参数类型的信息:

class Foo {}
    interface IFoo {}

    class Demo{ 
      @logParameters // apply parameter decorator
      doSomething(
        param1 : string,
        param2 : number,
        param3 : Foo,
        param4 : { test : string },
        param5 : IFoo,
        param6 : Function,
        param7 : (a : number) => void,
      ) : number { 
          return 1
      }
    }

上面的例子将在控制台打印:

doSomething param types: String, Number, Foo, Object, Object, Function, Function

C) 使用reflect元数据API获得返回值类型元数据

我们还可以使用“design:returntype”元数据键获得关于方法返回类型的信息:

Reflect.getMetadata("design:returntype", target, key);

##3. 基本类型的序列化

我们看下上面"design:paramtypes"的例子。这里需要注意,接口IFoo和对象字面量{ test: string }被序列化为Object。这是因为Typescript只支持基本类型的序列化。基本类型序列化规则是:

  • number序列化为Number
  • string序列化为String
  • boolean序列化为Boolean
  • any序列化为Object
  • void序列化为undefined
  • Array序列化为Array
  • 如果是Tuple,序列化为Array
  • 如果是class,序列化为类的构造函数
  • 如果是Enum,序列化为Number
  • 如果至少有一个调用签名,则序列化为Function
  • 其他则序列化为Object(包括接口)

接口和对象字面量可能在未来序列化为更复杂的类型但是现在是行不通的。

4. 复杂类型的序列化

Typescript团队正在提议,允许我们为复杂类型生成元数据。

该提议描述了一些复杂类型如何被序列化。上面基本类型的序列化规则保持一致,但是复杂类型的序列化将会采用不同的序列化规则。在这个提议中,基本类型被用来描述所有可能的类型:

/** 
  * Basic shape for a type.
  */
interface _Type {
  /** 
    * Describes the specific shape of the type.
    * @remarks 
    * One of: "typeparameter", "typereference", "interface", "tuple", "union", 
    * or "function".
    */
  kind: string; 
}

我们还可以找到用于描述每种可能类型的类。比如,我们可以找到类被用来序列化继承的接口interface foo<bar> { /* ... */}

/**
  * Describes a generic interface.
  */
interface InterfaceType extends _Type {
  kind: string; // "interface"

  /**
    * Generic type parameters for the type. May be undefined.
    */
  typeParameters?: TypeParameter[];

  /**
    * Implemented interfaces.
    */
  implements?: Type[];

  /**
    * Members for the type. May be undefined. 
    * @remarks Contains property, accessor, and method declarations.
    */
  members?: { [key: string | symbol | number]: Type; };

  /**
    * Call signatures for the type. May be undefined.
    */
  call?: Signature[];

  /**
    * Construct signatures for the type. May be undefined.
    */
  construct?: Signature[];

  /**
    * Index signatures for the type. May be undefined.
    */
  index?: Signature[];
}

正如我们上面所看到的,将会有一个属性来标示实现的接口。

/**
 * Implemented interfaces.
 */
implements?: Type[];

如果一个实体在运行时实现了某个接口,可以使用这些信息进行验证,这对IoC容器非常有用。

我们不知道复杂类型的序列化什么时候会被添加到Typescript中,但是我们不能等下去,因为我们计划使用它为我们出色的IoC容器添加一些很酷的特性:
InversifyJS

结论

我们现在深入理解了4种可用类型的装饰器,了解如何使用装饰器工厂,以及如何使用装饰器工厂来实现可配置装饰器。

并且,我们了解了元数据反射API是如何工作的。

我们将继续更新这个博客,并在未来撰写更多关于元数据反射API的文章。如果你不想错过的话,别忘了订阅!

请随时通过@OweR_ReLoaDeD@WolkSoftwareLtd与我们讨论这篇文章。