TypeScript 中的 Decorator & 元數據反射:從小白到專家(部分 IV)

redhorse 8年前發布 | 32K 次閱讀 JavaScript開發 TypeScript

深入探尋 TypeScript 的裝飾器實現,發現它們是如何為 JavaScript 添加令人興奮的特性,比如反射和依賴注入。

這個系列包含4篇文章:

  • 部分 I:方法裝飾器
  • 部分 II:屬性注解與類裝飾器
  • 部分 III:參數裝飾器與裝飾器工廠
  • 部分 IV:類型的序列化與元數據反射 API

我會假設你已經讀過了這個系列的前幾篇文章。

在前面的文章中我們已經知道了什么是裝飾器和 TypeScript 是怎么實現裝飾器的。我們知道了如何在類、方法、屬性和參數上使用裝飾器,如何創建一個裝飾器工廠,如何使用一個裝飾器工廠,如何實現一個可配置的裝飾器工廠。

在本篇文章中,我們將會了解到:

  1. 我們為什么需要 JavaScript 中的反射
  2. 元數據反射 API
  3. 基本類型序列
  4. 復雜類型序列

讓我們從學習為什么需要 Javascript 中的反射開始。

1. 我們為什么需要 JavaScript 中的反射

反射這個詞用來描述那些可以檢查同一個系統中其它代碼(或自己)的代碼。

反射在一些用例下非常有用(組合/依賴注入,運行時類型檢查,測試)。

我們的 Javascript 應用變得越來越大,我們開始需要一些工具(比如控制反轉容器)和功能(運行時類型檢測)來管理不斷增長的復雜度。問題在于如果 Javascript 沒有反射,一些工具和功能就無法實現,或者至少它們不能實現得像它們在 C# 或者 Java 中的那么強大。

一個強大的反射 API 可以讓我們在運行時檢測一個未知的對象并且得到它的所有信息。我們要能通過反射得到以下的信息:

  • 這個實例的名字
  • 這個實例的類型
  • 這個實例實現了哪個接口
  • 這個實例的屬性的名字和類型
  • 這個實例構造函數的參數名和類型

在 JavaScript 中我們可以通過 Object.getOwnPropertyDescriptor()Object.keys() 函數獲取一些實例的信息,但是我們還需要反射來實現更加強大的開發工具。

然而事情有所轉機,因為 TypeScript 已經開始支持一些反射的功能。讓我們看一下這些功能:

2. 元數據反射 API

原生 Javascript 對元數據反射的支持處于早期的開發階段。這里是線上的 裝飾器與元數據裝飾器需要的 ES7 反射 API 原型的提案

Typescript 團隊的一些人已經開始實現 ES7 反射 API 原型的兼容版本 ,Typescript 的編譯器已經可以 將一些設計時類型元數據序列化給裝飾器

我們可以引入 reflect-metadata 庫來使用元數據反射 API:

npm install reflect-metadata

我們必須隨 TypeScript 1.5+ 一起使用這個庫并且將編譯參數 emitDecoratorMetadata 設為 true。我們也必須包含對 reflect-metadata.d.ts 的引用并加載 Reflect.js 文件。

隨后我們可以實現我們自己的裝飾器并且使用一個可用的 元數據設計鍵 。到目前為止,只有三個可用的鍵:

  • 類型元數據 使用元數據鍵 "design:type"
  • 參數類型元數據 使用元數據鍵 "design:paramtypes"
  • 返回值類型元數據 使用元數據鍵 "design:returntype"

讓我們來看一組例子:

A) 使用元數據反射 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) 使用元數據反射 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) 使用元數據反射 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
  • 如果是一個多元組,序列化為 Array
  • 如果是一個類,序列化為 class constructor
  • 如果是一個枚舉,序列化為 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 的功能中,但我們已經迫不及待了因為我們計劃用它為我們的 JavaScript IoC 容器: InversifyJS 增加一些碉堡的特性。

5. 結論

在本系列中,我們深入淺出的學習了4種可用的裝飾器、如何創建一個裝飾器工廠和如何使用裝飾器工廠實現一個可配置的裝飾器。

我們也學會了如何使用元數據反射 API。

來自: http://qianduan.guru/2016/04/13/decorators-metadata-reflection-in-typescript-part-iv/

本文譯自: Decorators & metadata reflection in TypeScript: From Novice to Expert (Part IV)

 本文由用戶 redhorse 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!