Skip to content

问题的由来

无论是NestJS或者MidwayJS,它们都用到了依赖注入和控制反转来实现OOP编程,而他们几乎都是通过装饰器和元数据来实现的。说到这里很多人很懵,我在这里举个NestJS中常见的日常开发例子:

typescript
@Injectable()
class CatService {
    constructor(public readonly animalService: AnimalService) {}

    run(){
        this.animalService.run()
    }
}

有没有想过这个animalService是通过什么原理自动实例成AnimalService的呢?还有控制器:

typescript
@Controller('cats')
export class CatsController {
    constructor(public readonly catService: CatService) {}

    @Get('/run')
    run(){
        this.catService.run()
    }
}

你能大概猜到他们是怎么直接通过装饰器@Controller 和 @Get 就把CatsController的run方法应用在了 “/casts/run” 路由上吗?

基础知识

类的装饰

我们这里直接饮用阮一峰的一段代码:

javascript
@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  target.isTestable = true;
}

MyTestableClass.isTestable // true

我们可以看出可以通过装饰器函数的函数 target 拿到类本身。

方法的装饰

首先我们先来温习下装饰器的一些基础知识,首先是方法装饰器,这里再引用一段代码:

javascript
class Math {
  @log
  add(a, b) {
    return a + b;
  }
}

function log(target, name, descriptor) {
  var oldValue = descriptor.value;

  descriptor.value = function() {
    console.log(`Calling ${name} with`, arguments);
    return oldValue.apply(this, arguments);
  };

  return descriptor;
}

const math = new Math();

// passed parameters should get logged now
math.add(2, 4);

如上代码,方法的装饰器函数可以直接重定义所装饰的方法的内容,也就是说他可以直接用第三个参数 descriptor 拿到要装饰方法本身。

reflect-metadata

元数据,本身就是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。Typescript1.5+ 就已经支持了,我们可以通过npm直接安装

bash
npm i reflect-metadata --save

然后在 tsconfig.json 里配置 emitDecoratorMetadata 选项:

json
{
  "compilerOptions": {
        "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}

我们可以先看下 reflect-metadata 的基本用法:

typescript
@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'

可以看到 Reflect.metadata当作装饰器用的时候,可以修饰类的时候添加元数据,修饰方法的时候也可以往方法上添加元数据。最关键的是它可以通过 Reflect.getMetadata("design:paramtypes", target, key)获取函数参数类型。

自定义 key

reflect-metadata 除了可以获取类型的信息还能直接自定义 key 存储数据,比如:

typescript
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'

实现

控制反转

我们找到上面的代码增加控制反转工厂函数:

typescript
@Injectable()
class CatService {
    constructor(public readonly animalService: AnimalService) {}

    run(){
        this.animalService.run()
    }
}

const factory = <T>(target: Constructor<T>): T => {
  const providers = Reflect.getMetadata('design:paramtypes', target); // [AnimalService]
  const args = providers.map((provider: Constructor) => new provider()); // 全部实例化
  return new target(...args);
};

factory(CatService).run();

通过 Reflect.getMetadata('design:paramtypes', target)

@Controller 和 @Get的实现

我们这里直接借用下《深入浅出 Typescript》的一段代码:

typescript
const METHOD_METADATA = 'method'
const PATH_METADATA = 'path'

const Controller = (path: string): ClassDecorator => {
  return target => {
     // @Controller 传进来的参数存储值,看不懂可以往上去看“自定义 key”的描述
    Reflect.defineMetadata(PATH_METADATA, path, target);
  }
}

const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
  return (target, key, descriptor) => {
    // 分别存储请求的方法和路径,记住 descriptor.value,你现在可以上去看文章前面的“方法装饰”
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
  }
}

const Get = createMappingDecorator('GET');

接着实现 Route 的 映射:

javascript
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 => {
      // fn 就是前面的 descriptor.value
    const fn = prototype[methodName];

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

这时候我们把 mapRoute遍历然后把相关挂载在 express 或者 koa 上就可以了,其实《深入浅出 Typescript》这本书的 Reflect Metadata 章节里面有讲过这个的实现,但是很多 Typescript 基础不够牢固的同学可能无法理解,知识本身就是连贯性的,一层一层深入,不过难免有时候某些内容不够牢固导致会卡壳,这时候就需要回去补下对应的知识才能走下去。共勉!!!