likes
comments
collection
share

【Nest.js】一文搞懂 reflect-metadata ~

作者站长头像
站长
· 阅读数 26

Nest.js 中一个重要的概念叫做 注解,基于 TypeScript 强大的 Decorator, 同时配上 reflect-metadata 能实现注解(Annotation)功能。但 TypeScript 5.0 后移除了 Annotation 的部分, 只保留了 Decorate 的特性。metadata独立了出来,本文就来一次性搞懂 reflect-metadata

关于装饰器,前面已经写了三篇详细的文章:

接下来会逐步讲到 Nest.js 的核心实现,那 reflect-metadata 就是一个绕不过去的知识点。

1、先说 Reflect

Reflect Metadata 作为 Reflect API 的扩展和延伸,那就必须先知道 Reflect 是什么。

这里引用权威的 MDN 文档,已经足够清晰。

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handler (en-US) 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。

与大多数全局对象不同 Reflect 并非一个构造函数,所以不能通过 new 运算符对其进行调用,或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是静态的(就像 Math 对象)。

Reflect 对象提供了以下静态方法,这些方法与 proxy handler 方法的命名相同。

1.1 Reflect APIs

1.2 Reflect 应用

1.2.1 检测一个对象是否存在特定属性

const duck = {
  name: 'Maurice',
  color: 'white',
  greeting: function() {
    console.log(`Quaaaack! My name is ${this.name}`);
  }
}

Reflect.has(duck, 'color'); // true
Reflect.has(duck, 'haircut'); // false

1.2.2 返回这个对象自身的属性

Reflect.ownKeys(duck);
// [ "name", "color", "greeting" ]

1.2.3 为这个对象添加一个新的属性

Reflect.set(duck, 'eyes', 'black');
// returns "true" if successful
// "duck" now contains the property "eyes: 'black'"

1.3 有 Object 为什么要还要 Reflect?

在回答这个问题之前,我们先来了解一下 "元编程"(Metaprogramming)的概念。维基百科是这说的:

元编程(Metaprogramming)是一种编程技术,在这种技术中,计算机程序能够将其他程序视为自己的数据。这意味着一个程序可以被设计用来读取、生成、分析或转换其他程序,甚至在运行时修改自身。

反射(Reflect) 是元编程的一个分支,反射有三个子特征:

  • 代码能够自我检查。
  • 代码能够自我修改。
  • 可以通过封装、陷阱、拦截来代替原来的对象。

回到问题本身,Reflect 的这些功能ES5中的Object也有呀?为什么还要搞出个冗余的内置对象?看了一些文章,主要有以下几点理由:

  1. 统一命名空间:在ES6之前, Javascript一直没有统一的命名空间来管理对其他 object 的操作,ES6 之后都可以通过调用 Reflect 的静态方法来处理对象。

  2. 更直观的异常处理:在Object里面我们需要用到try...catch去捕捉错误 而Reflect.defineProperty() 则会直接return true or false

    // Object实现
    try {
      Object.defineProperty(car, name, desc);
      // property defined successfully
    } catch (e) {
      // possible failure (and might accidentally catch the wrong exception)
    }
    
    // Reflect实现
    if (Reflect.defineProperty(car, name, desc)) {
      // success
    } else {
      // failure
    }
    
  3. apply的可靠性func:在 ES5 中,使用可变数量的参数作为数组调用函数arr并将this值绑定到的最可靠方法是obj

    Function.prototype.apply.call(func, obj, arr);
    // or
    func.apply(obj, arr);
    

    这是不太可靠的,因为func可能是一个定义了自己的apply,在 ES6 中,我们有更可靠、更优雅的方法来解决这个问题:

    Reflect.apply(func, obj, arr);
    
  4. 代理陷阱转发:当我们使用 Proxy 对象来包装现有对象时,我们通常会拦截一个操作。我们尝试做一些特别的事情,然后一旦完成,我们想继续做下面的事情。让我们通过以下示例来理解这一点:

    const employee = {
      firstName: 'Tapas',
      lastName: 'Adhikary'
    };
    
    let logHandler = {
      get: function(target, fieldName) {        
          console.log("Log: ", target[fieldName]);
          return Reflect.get(target, fieldName);
      }
    };
    
    let func = () => {
      let p = new Proxy(employee, logHandler);
      p.firstName;
      p.lastName;
    };
    
    func(); 
    // Log:  Tapas
    // Log:  Adhikary
    

    Reflect 允许你实现代理陷阱的默认转发行为。例如,你可以拦截操作,然后尝试将拦截到的操作应用到封装对象。但是,这并不容易,因为[[Set]][[Get]]等内部方法无法直接调用。有了 Reflect,你就可以访问内部方法,并轻松地将截获的操作应用到代理中的封装对象。

2、Reflect Metadata

Reflect Metadata 是一个广泛使用的第三方 npm package,扩展了 Reflect API,TypeScript 使用这个包设计装饰器 decorator。

这个包原本是为了提供Reflect API 的“元数据扩展” ECMAScript 提案的兼容方案。作者 Ron Buckton 也是Typescript的核心开发者,他于 2015 年提交了将 metadata 纳入 Typescript 官方的提案(ES7),但目前为止他手上还有太多其他工作没有完成, 所以并没有提上议程。

什么是“元数据”?

元数据 metadata,简而言之,就是实际数据的额外信息(译注:通常被称为数据的数据)。例如,如果一个变量表示一个数组,那么,数组的长度就是一个元数据。类似的,数组中的每个元素都是实际数据,而这些元素的数据类型则是它们各自的元数据。可以这样认为,元数据并不是程序真正关心的数据,但是却能帮助程序更快更好地实现自己的目标。

2.1 为什么会有 Reflect Metadata

如果你需要设计一个函数,用来输出其它函数的信息,那么,你会怎么设计?

// return information of a function
function funcInfo( func ) {
    return `Function "${ func.name }" accepts "${ func.length }" arguments.`;
}

// define sample functions
var add = ( a, b ) => a + b;
var sayHello = () => 'Hello World!';

// print function information
console.log( 'add info ->', funcInfo( add ) ); 
// add info -> Function "add" accepts "2" arguments.
console.log( 'sayHello info ->', funcInfo( sayHello ) ); 
// sayHello info -> Function "sayHello" accepts "0" arguments.

上面的例子,funcInfo函数接受一个函数作为参数,返回包含了这个参数函数的函数名参数个数的字符串。这些信息原本就包含在函数当中,虽然我们在大部分时间都不会使用这些信息。因此,func.namefunc.length就可以看作是元数据。

在上面关于 Reflect 的介绍中,我们知道Reflect API 可以检查对象、添加元数据以改变其行为。例如,Reflect.has函数就像in运算符,用于检查属性是否存在于对象或对象的原型链。Reflect.setPropertyOf函数则可以给对象增加自定义属性,从而改变其行为。Reflect提供了很多类似的方法,用于实现反射机制

metadata 提案 目的是通过新的方法扩展Reflect的能力。这些方法可以增加 JavaScript 元编程的能力。来看下面的例子:

// 在目标对象上定义 metadata
Reflect.defineMetadata(
  metadataKey,
  metadataValue,
  target
);

// 在目标对象的属性上定义 metadata
Reflect.defineMetadata(
  metadataKey,
  metadataValue,
  target,
  propertyKey
);

Reflect.defineMetadata函数允许给target对象或target对象的属性propertyKey添加新的元数据值metadataValue。这个值可以是任意 JavaScript 值。target对象则需要继承Object。你可以添加任意多的元数据值,这些值使用metadataKey进行区分。

// 获取与目标相关的元数据
let result = Reflect.getMetadata(
  metadataKey,
  target
);

// 获取与目标属性相关的元数据
let result = Reflect.getMetadata(
  metadataKey,
  target,
  propertyKey
);

Reflect.getMetadata函数可以获取target对象或其属性上面名为metadataKey的元数据。

metadata 提案为所有普通对象暴露内部槽[[Metadata]]。这个槽可以是null,表示target对象不包含任何元数据;否则,它是一个Map对象,包含target或其属性的不同元数据。

Reflect.defineMetadata调用内部方法[[DefineMetadata]]Reflect.getMetadata则使用[[GetMetadata]]获取。

2.2 在 TypeScript 中使用 Reflect Metadata 用法

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

  • npm i reflect-metadata --save
  • 在 tsconfig.json 里配置 emitDecoratorMetadata 选项。

2.2.1 在对象中使用

import "reflect-metadata";

// check if `Reflect.defineMetadata` is a `function`
console.log("check ->", typeof Reflect.defineMetadata); // check -> function

// define a sample target
var target = { name: "Ross" };

// add metadata to `target` and `target.name`
Reflect.defineMetadata("version", 1, target);
Reflect.defineMetadata("info", { props: 1 }, target);
Reflect.defineMetadata("is", "string", target, "name");

// see the target
console.log("target ->", target); // target -> { name: 'Ross' }

// extract metadata
console.log("target(info) ->", Reflect.getMetadata("info", target)); 
// target(info) -> { props: 1 }
console.log("target.name(is) ->", Reflect.getMetadata("is", target, "name")); 
// target.name(is) -> string

// when metadata is missing
console.log("target(missing) ->", Reflect.getMetadata("missing", target)); 
// target(missing) -> undefined

require( 'reflect-metadata' );将导入 reflect-metadata 包导入,这给Reflect对象在运行时增加了诸如defineMetadata这样的函数,我们可以通过检查Reflect.defineMetadata的类型得知是否正常。因此,本质上说,这个包其实是一个兼容方案 polyfill

然后,我们定义了一个对象target,然后在这个对象上添加了versioninfois元数据。其中,versioninfo直接添加到target对象,is添加到name属性。元数据值可以是任意 JavaScript 值。

Reflect.getMetadata返回关联到target或其属性的元数据。如果没有找到,则返回undefined。利用这些方法,我们可以将任意元数据关联到任意对象或属性。

从日志可以看出来,向target或其属性注册元数据并不会改变这个对象。事实上,元数据应该被保存在[[Metadata]]内部槽,而兼容方案中是使用的是WeakMap来保存target的元数据。

使用hasMetadata检查元数据是否存在,使用getMetadataKeys获取注册到target或其属性上面的所有元数据的键的集合。如果要删除元数据,则可以使用deleteMetadata方法。

// check for presence of a metadata key (returns a boolean)
let result = Reflect.hasMetadata(key, target);
let result = Reflect.hasMetadata(key, target, property);
// get all metadata keys (returns an Array)
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, property);
// delete metadata with a key (returns a boolean)
let result = Reflect.deleteMetadata(key, target);
let result = Reflect.deleteMetadata(key, target, property);
import "reflect-metadata";

// define a sample target
var target = { name: "Ross", age: 21 };

// add metadata to `target` and `target.name`
Reflect.defineMetadata("version", 1, target);
Reflect.defineMetadata("is", "string", target, "name");

// check if metadata exists
console.log("has: target(version) ->", Reflect.hasMetadata("version", target)); // has: target(version) -> true
console.log(
  "has: target.name(is) ->",
  Reflect.hasMetadata("is", target, "name")
); // has: target.name(is) -> true
console.log("has: target.age(is) ->", Reflect.hasMetadata("is", target, "age")); // has: target.age(is) -> false

// get metadata keys
console.log("keys: target ->", Reflect.getMetadataKeys(target)); // keys: target -> [ 'version' ]
console.log("keys: target.name ->", Reflect.getMetadataKeys(target, "name")); // keys: target.name -> [ 'is' ]
console.log("keys: target.age ->", Reflect.getMetadataKeys(target, "age")); // keys: target.age -> []

// delete metedata key
console.log(
  "delete: target(version) ->",
  Reflect.deleteMetadata("version", target)
); // delete: target(version) -> true
console.log(
  "delete: target.name(is) ->",
  Reflect.deleteMetadata("is", target, "name")
); // delete: target.name(is) -> true
console.log(
  "delete: target.age(is) ->",
  Reflect.deleteMetadata("is", target, "age")
); // delete: target.age(is) -> false

默认情况下,getMetadatahasMetadatagetMetadataKeys会检查target的原型链,以便查找指定元数据键关联的值。因此,我们还有getOwnMetadatahasOwnMetadatagetOwnMetadataKeys这样的函数,只查找target对象,而不去查询原型链。

// get metadata value of an own metadata key
let result = Reflect.getOwnMetadata(key, target);
let result = Reflect.getOwnMetadata(key, target, property);
// check for presence of an own metadata key
let result = Reflect.hasOwnMetadata(key, target);
let result = Reflect.hasOwnMetadata(key, target, property);
// get all own metadata keys
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, property);
import "reflect-metadata";

// define a sample target with a custom prototype
var target = { name: "Ross" };
var proto = { age: 21 };
Reflect.setPrototypeOf(target, proto);

// add metadata to `proto`
Reflect.defineMetadata("version", 1, proto);
Reflect.defineMetadata("is", "number", proto, "age");

// get metadata
console.log(
  "getMetadata: target(version) ->",
  Reflect.getMetadata("version", target)
); // getMetadata: target(version) -> 1
console.log(
  "getMetadata: target.age(is) ->",
  Reflect.getMetadata("is", target, "age")
); // getMetadata: target.age(is) -> number
console.log(
  "getOwnMetadata: target(version) ->",
  Reflect.getOwnMetadata("version", target)
); // getOwnMetadata: target(version) -> undefined
console.log(
  "getOwnMetadata: target.age(is) ->",
  Reflect.getOwnMetadata("is", target, "age")
); // getOwnMetadata: target.age(is) -> undefined

// check if metadata exists
console.log(
  "hasMetadata: target(version) ->",
  Reflect.hasMetadata("version", target)
); // hasMetadata: target(version) -> true
console.log(
  "hasMetadata: target.age(is) ->",
  Reflect.hasMetadata("is", target, "age")
); // hasMetadata: target.age(is) -> true
console.log(
  "hasOwnMetadata: target(version) ->",
  Reflect.hasOwnMetadata("version", target)
); // hasOwnMetadata: target(version) -> false
console.log(
  "hasOwnMetadata: target.age(is) ->",
  Reflect.hasOwnMetadata("is", target, "age")
); // hasOwnMetadata: target.age(is) -> false

// check for metadata keys
console.log("getMetadataKeys: target ->", Reflect.getMetadataKeys(target)); // getMetadataKeys: target -> [ 'version' ]
console.log(
  "getMetadataKeys: target.age ->",
  Reflect.getMetadataKeys(target, "age")
); // getMetadataKeys: target.age -> [ 'is' ]
console.log(
  "getOwnMetadataKeys: target ->",
  Reflect.getOwnMetadataKeys(target)
); // getOwnMetadataKeys: target -> []
console.log(
  "getOwnMetadataKeys: target.age ->",
  Reflect.getOwnMetadataKeys(target, "age")
); // getOwnMetadataKeys: target.age -> []

上面的例子,prototarget的原型,因此,所有在proto定义的元数据值都可以在target访问。然而,带有Own的函数不会检查proto的元数据。

Reflect Metadata 的 API 可以用作给 Object 及其属性添加元数据:

const car = {
  brand: 'BMW',
  model: 'X6 2012',
  price: 99999,
  getMaxSpeed() {
    console.log(`Max speed is 200km/h`);
  }
}

// 添加一个叫`desc`的元数据到car这个对象
Reflect.defineMetadata('desc', 'This is a good car', car);
// 添加一个叫`desc`的元数据到car的price上(这里的price可以是其他值或car不存在的属性)
Reflect.defineMetadata('desc', 'This is so cheap', car, 'price');
// 添加一个叫`desc`的元数据到car的model上(这里的model可以是其他值或car不存在的属性)
Reflect.defineMetadata('note', 'This model is too old', car, 'model');

// 检查metadata是否存在
console.log(Reflect.hasMetadata('desc', car)); // 输出: true
console.log(Reflect.hasMetadata('desc', car, 'price')); // 输出: true
console.log(Reflect.hasMetadata('note', car, 'model')); // 输出: true
console.log(Reflect.hasMetadata('desc', car, 'brand')); // 输出: false

// 获取元数据
console.log(Reflect.getMetadata('desc', car)); // 输出: 'This is a good car'
console.log(Reflect.getMetadata('desc', car, 'price')); // 输出: 'This is so cheap'
console.log(Reflect.getMetadata('note', car, 'model')); // 输出: 'This model is too old'
console.log(Reflect.getMetadata('desc', car, 'brand')); // 输出: undefined

// 获取对象上所有元数据的keys
console.log(Reflect.getMetadataKeys(car)) // 输出: ['desc']
console.log(Reflect.getMetadataKeys(car, 'price')) // 输出: ['desc']
console.log(Reflect.getMetadataKeys(car, 'model')) // 输出: ['note']
console.log(Reflect.getMetadataKeys(car, 'brand')) // 输出: []

2.2.2 结合装饰器使用

提案还定义了Reflect.metadata函数,但并不是Reflect对象的方法。这个函数式一个 装饰器工厂 decorator factory,这意味着调用这个函数时,它返回一个修饰器函数,可以用来修饰类或类属性。

Reflect Metadata 的 API 可以用于类或者类的属性上,如:

function metadata(
  metadataKey: any,
  metadataValue: any
): {
  (target: Function): void;
  (target: Object, propertyKey: string | symbol): void;
};

Reflect.metadata 当作 Decorator 使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,先来看两个例子:

@Reflect.metadata('inClass', 'A')
class Test {
  @Reflect.metadata('inMethod', 'B')
  public hello(): string {
    return 'hello world';
  }
}

console.log(Reflect.getMetadata('inClass', Test)); // 'A'
console.log(Reflect.getMetadata('inMethod', new Test(), 'hello')); // 'B'
import "reflect-metadata";

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);
// undefined nihao getName static
@Reflect.metadata(metadataKey, metadataValue)
class MyClass {
  @Reflect.metadata(metadataKey, metadataValue)
  methodName(){
    // ...
  }
}

在上面的例子中,@Reflect.metadata方法调用修饰了类MyClass和属性methodName方法。简单而言,它给这些实体添加了键为metadataKey的元数据metadataValue

如果你想知道它是怎么做到的,其实很简单。Reflect.metadata返回一个修饰器函数。这个修饰器函数内部实现了Reflect.defineMetadata,从而将元数据添加到了它所修饰的实体上面。

问题在于,reflect-metadata实现了修饰器提案的原始版本 legacy version。TypeScript 同样实现了这个版本的修饰器实现。你可以阅读这篇文章了解如何使用原始版本的修饰器实现。

由于我们不能在原生 JavaScript 中使用这个包,因此下面的例子是 TypeScript 的。

当然,你也可以使用这个 babel 插件转译为原生 JavaScript。

import "reflect-metadata";

// define a custom decorator
function myDecorator(metaValue) {
  return Reflect.metadata("returns", metaValue);
}

@Reflect.metadata("version", 1)
class Person {
  fname: string;
  lname: string;

  constructor(fname, lname) {
    this.fname = fname;
    this.lname = lname;
  }

  @myDecorator({ returns: "string" })
  getFullName() {
    return this.fname + " " + this.lname;
  }
}

// create an instance of Person
const person = new Person("Ross", "Geller");

// get metadata
console.log("Person(version) ->", Reflect.getMetadata("version", Person)); 
// Person(version) -> 1
console.log(
  "person.getFullName(returns) ->",
  Reflect.getMetadata("returns", person, "getFullName")
); 
// person.getFullName(returns) -> { returns: 'string' }

在这个例子中,@Reflect.metadataPerson类增加了元数据,之后,我们可以通过Reflect.getMetadata(<key>, Person)读取这些元数据。你也可以基于Reflect.metadata实现自己的修饰器工厂,例如myDecorator

本节介绍了 reflect-metadata 包以及元数据提案。这个提案的用例是无穷无尽的。有人说,这是 JavaScript 元编程的圣杯。不过,我们就等待它变成 ECMAScript 提案的那天。

最后再来看一个例子巩固一下:

// 把metadata都定义到这个全局变量
const globalMeta = Object.create(null);

const propertyCollector = (target: Object,
  propertyKey: string | symbol, descriptor?: any) => {
    // 把property的名字都放进一个叫properties的array
    const properties = Reflect.getMetadata('properties', globalMeta);
    if (properties) {
      Reflect.defineMetadata('properties', [...properties, propertyKey], globalMeta);
    } else {
      Reflect.defineMetadata('properties', [propertyKey], globalMeta);
    }
}

const methodCollector = (target: Object,
  propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    // 把methods的名字都放进一个叫methods的array
    const methodss = Reflect.getMetadata('methods', globalMeta);
    if (methodss) {
      Reflect.defineMetadata('methods', [...methodss, propertyKey], globalMeta);
    } else {
      Reflect.defineMetadata('methods', [propertyKey], globalMeta);
    }
}

const classCollector = (constructor: Function) => {
  // 把class的名字存到globalMeta的className
  Reflect.defineMetadata('className', constructor.name, globalMeta);
}

@classCollector
class Car {
  @propertyCollector
  private speed = 0;
  @propertyCollector
  private brand = 'BMW';
  @propertyCollector
  private model = 'X6 2012';
  @methodCollector
  run() {
    this.speed = 100;
    console.log(`The car is running at 100km/h`)
  }
  @methodCollector
  stop() {
    this.speed = 0;
    console.log(`The car stopped`)
  }
}

const className = Reflect.getMetadata('className', globalMeta);
const properties = Reflect.getMetadata('properties', globalMeta);
const methods = Reflect.getMetadata('methods', globalMeta);

console.log(
`Class [${className}] has ${properties.length} properties: ${properties.join(', ')}\t
Class [${className}] has ${methods.length} methods: ${methods.join(', ')}`,
);

// 输出
// Class [Car] has 3 properties: speed, brand, model
// Class [Car] has 2 methods: run, stop

2.3 获取类型信息

你可以通过Reflect.defineMetadata获取到类或者函数的参数类型,也可以给类或者函数设置元数据再获取,具体代码如下:

function Prop(): PropertyDecorator {
  return (target, key: string) => {
    const type = Reflect.getMetadata('design:type', target, key);
    console.log(`${key} type: ${type.name}`);
    // other...
  };
}

class SomeClass {
  @Prop()
  public Aprop!: string;
}

运行代码可在控制台看到 Aprop type: string。譬如在 vue-property-decorator 6.1 及其以下版本中,通过使用 Reflect.getMetadata API,Prop Decorator 能获取属性类型传至 Vue。

TypeScript 的优势了,TypeScript 支持编译时自动添加一些 metadata 数据,如下所示:

  • Reflect.getMetadata("design:type", target, key), 获取target函数类型
  • Reflect.getMetadata("design:paramtypes", target, key), 获取target函数参数类型
  • Reflect.getMetadata("design:returntype", target, key), 获取target函数返回值类型

这个Reflect.getMetadata("design:paramtypes", target, key)基本上就是Nest.js实现Ioc和DI的核心代码。

2.4 自定义 metadataKey

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

function classDecorator(): ClassDecorator {
  return target => {
    // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
    Reflect.defineMetadata('classMetaData', 'a', target);
  };
}

function methodDecorator(): MethodDecorator {
  return (target, key, descriptor) => {
    // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
    Reflect.defineMetadata('methodMetaData', 'b', target, key);
  };
}

@classDecorator()
class SomeClass {
  @methodDecorator()
  someMethod() {}
}

Reflect.getMetadata('classMetaData', SomeClass); // 'a'
Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'

2.5 metadata 的应用

2.5.1 IoC(控制反转)和DI(依赖注入)实现原理

其实在了解完Reflect.getMetadata,我们就大概知道IoC和DI的实现原理,我们以一个@Controller为例, 具体步骤如下:

  • 实现@Controller装饰器工厂,标识待注入的类
  • 实现IoC容器,注册要被依赖注入的类
  • 获取待注入的类其构造函数所需要的参数类型,并实例化,返回待注入的类

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

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); // [OtherService]
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
};

Factory(TestService).testMethod(); // 1

2.5.2 Controller 与 Get 的实现

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

@Controller('/test')
class SomeClass {
  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

  @Post('/b')
  somePostMethod() {}
}

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

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

function mapRoute(instance: Object) {
  const prototype = Object.getPrototypeOf(instance);

  // 筛选出类的 methodName
  const methodsNames = Object.getOwnPropertyNames(prototype)
                              .filter(item => !isConstructor(item) && isFunction(prototype[item]));
  return methodsNames.map(methodName => {
    const fn = prototype[methodName];

    // 取出定义的 metadata
    const route = Reflect.getMetadata(PATH_METADATA, fn);
    const method = Reflect.getMetadata(METHOD_METADATA, fn);
    return {
      route,
      method,
      fn,
      methodName
    }
  })
};

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

Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'

mapRoute(new SomeClass());

/**
 * [{
 *    route: '/a',
 *    method: 'GET',
 *    fn: someGetMethod() { ... },
 *    methodName: 'someGetMethod'
 *  },{
 *    route: '/b',
 *    method: 'POST',
 *    fn: somePostMethod() { ... },
 *    methodName: 'somePostMethod'
 * }]
 *
 */

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

参考