Mercer-Lee的空间

vuePress-theme-reco Mercer-Lee的空间    2018 - 2024
Mercer-Lee的空间 Mercer-Lee的空间

Choose mode

  • dark
  • auto
  • light
TimeLine
分类
  • 数据结构和算法
  • 后端
  • 运维
  • 前端
  • 工具
  • 语言
标签
我的GitHub (opens new window)
author-avatar

Mercer-Lee的空间

27

文章

29

标签

TimeLine
分类
  • 数据结构和算法
  • 后端
  • 运维
  • 前端
  • 工具
  • 语言
标签
我的GitHub (opens new window)
  • 用reflect-metadata和装饰器实现依赖注入和控制反转

    • 问题的由来
      • 基础知识
        • 类的装饰
        • 方法的装饰
        • reflect-metadata
      • 实现
        • 控制反转
        • @Controller 和 @Get的实现

    用reflect-metadata和装饰器实现依赖注入和控制反转

    vuePress-theme-reco Mercer-Lee的空间    2018 - 2024

    用reflect-metadata和装饰器实现依赖注入和控制反转


    Mercer-Lee的空间 2021-01-30 Typescript NestJS MidwayJS Javascript

    # 问题的由来

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

    @Injectable()
    class CatService {
        constructor(public readonly animalService: AnimalService) {}
    
        run(){
            this.animalService.run()
        }
    }
    

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

    @Controller('cats')
    export class CatsController {
        constructor(public readonly catService: CatService) {}
    
        @Get('/run')
        run(){
            this.catService.run()
        }
    }
    

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

    # 基础知识

    # 类的装饰

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

    @testable
    class MyTestableClass {
      // ...
    }
    
    function testable(target) {
      target.isTestable = true;
    }
    
    MyTestableClass.isTestable // true
    

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

    # 方法的装饰

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

    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直接安装

    npm i reflect-metadata --save
    

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

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

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

    @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 存储数据,比如:

    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'
    

    # 实现

    # 控制反转

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

    @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》的一段代码:

    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 的 映射:

    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 基础不够牢固的同学可能无法理解,知识本身就是连贯性的,一层一层深入,不过难免有时候某些内容不够牢固导致会卡壳,这时候就需要回去补下对应的知识才能走下去。共勉!!!