likes
comments
collection
share

TypeScript 用装饰器实现依赖注入

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

最近在学 nestjs,发现里面有很多装饰器,实现依赖注入的效果

作为前端开发,基本不会用到装饰器,所以刚开始学习时,对装饰器的作用就不了解,决定学习一下它

装饰器

装饰器是 ts 的一个特性,可以在不改变原有代码的情况下,给类、方法、属性等添加一些额外的功能。

装饰器的本质是一个函数,它运行的阶段是在类定义的时候,而不是类实例化的时候

我们来看一下怎么编写一个装饰器

首先需要在 tsconfig.json 中开启 experimentalDecorators 选项

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

我们定义一个 log 函数,这个函数接受三个参数:

  • target:被装饰方法所属的类
  • key:被装饰方法的名称
  • descriptor:被装饰方法的描述符
    {
      value: [Function: sayHello],    // 函数本身
      writable: true,                 // 是否可写
      enumerable: false,              // 是否可枚举
      configurable: true              // 是否可配置
    }
    

我们定义 log 函数后,在需要使用装饰器的上面,使用 @ 操作符,就可以了

如下所示:

function log(target: Function, key: string, descriptor: PropertyDescriptor) {
  // 拿到 sayHello 函数
  const fn = descriptor.value;
  // 重写 sayHello 函数
  descriptor.value = function (...args: any[]) {
    console.log("log");
    // 调用原来的 sayHello 函数
    return fn.apply(this, args);
  };
}

class A {
  @log
  static sayHello(a: number, b: number) {
    return a + b;
  }
}
const a = A.sayHello(1, 2);
console.log(a);
// 🔽
// log
// 3

装饰器类型

装饰器分为五种类型:

  • 类装饰器
  • 属性装饰器
  • 方法装饰器
  • 参数装饰器
  • 访问器装饰器

不同类型的装饰器,接收的类型不一样

类装饰器

类装饰器只接收一个参数 targettarget 是被装饰的类本身

function d1(target: Function) {
  console.log(target);
}
@d1
class B {}

属性装饰器

属性装饰器分文两种:

  • 静态属性装饰器
  • 实例属性装饰器

他们都是接收两个参数:

  • target,如果装饰的是静态属性,那么 target 就是类本身,如果装饰的是实例属性,那么 target 就是类的实例
  • key,被装饰的属性名称

他们的执行顺序是先执行实例属性的装饰器,在执行静态属性的装饰器

function d2(target: Function | Object, key: string) {
  console.log("d2", target, key);
}

class B {
  @d2
  a: number = 1;
  @d2
  static b: number = 2;
}

方法装饰器

方式装饰器也是分为两种

  • 静态方法装饰器
  • 实例方法装饰器

他们都是接收三个参数:

  • target,如果装饰的是静态方法,那么 target 就是类本身,如果装饰的是实例方法,那么 target 就是类的实例
  • key,被装饰的方法名称
  • descriptor,被装饰的方法的描述符
    {
      value: [Function: sayHello],    // 函数本身
      writable: true,                 // 是否可写
      enumerable: false,              // 是否可枚举
      configurable: true              // 是否可配置
    }
    

它们的执行顺序也是先执行实例方法的装饰器,再执行静态方法的装饰器

function d3(target: Function | Object, key: string, descriptor: PropertyDescriptor) {
  console.log("d3", target, key);
}

class B {
  @d3
  getA() {}
  @d3
  static getB() {}
}

参数装饰器

参数装饰器只有一种,毕竟参数是没有静态和实例之分的

它也是接收三个参数:

  • target:类的实例
  • key:参数所在方法的名称
  • index:参数的索引

它们的执行顺序是先执行最后的参数装饰器,再执行前面的参数装饰器

function d4(target: Function | Object, key: string, index: number) {
  console.log("d4", target, key, index);
}

class B {
  getA(@d4 param1: number, @d4 param2: number) {}
}

访问器装饰器

访问器装饰器也分为两种:

  • 静态访问器装饰器
  • 实例访问器装饰器

他们接收的阐述跟方法装饰器一致,都是接收三个参数

  • target,如果装饰的是静态方法,那么 target 就是类本身,如果装饰的是实例方法,那么 target 就是类的实例
  • key,被装饰的方法名称
  • descriptor,被装饰的方法的描述符
    {
      value: [Function: sayHello],    // 函数本身
      writable: true,                 // 是否可写
      enumerable: false,              // 是否可枚举
      configurable: true              // 是否可配置
    }
    

它们的执行顺序也是先执行实例访问器的装饰器,再执行静态访问器的装饰器

function d5(target: Function | Object, key: string, descriptor: PropertyDescriptor) {
  console.log("d5", target, key);
}

class B {
  @d5
  get c() {
    return 1;
  }
  @d5
  static get c() {
    return 1;
  }
}

装饰器的执行顺序

实例装饰器 => 静态装饰器 => 类装饰器

具体的过程如下:

  1. 参数装饰器在方法装饰器之前执行
  2. 参数装饰器从后往前执行
  3. 实例装饰器在静态装饰器之前执行
  4. 写在前面的装饰器先执行
  5. 类装饰器总是最后执行

装饰器工厂函数

装饰器工厂函数的意思是,装饰器可以接收一个参数,并返回一个函数,返回的函数就是遵循装饰器类型

通过传入一个参数就可以实现对同一个装饰器处理不同的逻辑

代码如下:

function log(type: string) {
  return function (target: Function, key: string, descriptor: PropertyDescriptor) {
    const fn = descriptor.value;
    descriptor.value = function (...args: any[]) {
      console.log("log", type);
      return fn.apply(this, args);
    };
  };
}
class A {
  @log("type1")
  static sayHello(a: number, b: number) {
    return a + b;
  }
  @log("type2")
  static sayHello2(a: number, b: number) {
    return a + b;
  }
}
const a = A.sayHello(1, 2);
console.log(a);
const b = A.sayHello2(11, 22);
console.log(b);

元数据

我们先来看一个需求

如果我们一个类中要有多个方法要使用 log("type1") 装饰器,那么我们就需要在每个方法上都写一遍,这样就会很麻烦

有没有什么方法可以让我们只写一遍呢?

就是在类装饰器上传入一个参数,因为 target 是类本身,所以我们在它的原型上,也就是 prototype 上挂一个属性 type

当方法装饰器执行时,去读取 prototype 上的 type 属性就可以了

function l(type: string) {
  return function (target: Function) {
    // 将 type 挂载到原型上
    target.prototype.type = type;
  };
}
function log(type?: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const fn = descriptor.value;
    descriptor.value = function (...args: any[]) {
      let _type = type;
      if (!_type) {
        // 静态方法的 target 是类本身,类本质是一个函数,通过判断 target 是否是函数,来判断是静态方法还是实例方法
        // 如果是静态方法,那么要取到 type,就要通过 prototype 来取
        // 否则就是实例方法,直接取 type 就可以了
        if (typeof target === "function") {
          _type = target.prototype.type;
        } else {
          _type = target.type;
        }
      }

      console.log("log", _type);
      return fn.apply(this, args);
    };
  };
}

@l("type1")
class A {
  @log()
  static sayHello(a: number, b: number) {
    return a + b;
  }
  @log("type222")
  static sayHello2(a: number, b: number) {
    return a + b;
  }
}
const a = A.sayHello(1, 2);
console.log(a);
const b = A.sayHello2(11, 22);
console.log(b);

这种方法你在原型上定义了一个属性 type

但是就会出现一个问题,如果有一个人不知道,和你定义了同样一个属性名,这时你的 type 就有可能会被覆盖

这种危险的行为是不允许的,这时就出现了元数据,我们用元数据来解决这个问题

什么是元数据呢?

元数据是用来描述数据的数据,在我们的对象中:类,对象都是数据,它们描述了某种数据

那如何描述类和对象呢?

这个就是元数据

举个例子,我们用类或者对象来描述一个人,那么我们会用元数据描述这个类或者对象应该有哪些属性

我们使用第三方库 reflect-metadata

reflect-metadata

这个库的作用是可以给类或者对象定义一些元数据,

这些数据会被附加到指定的类或者方法之上,但是又不会影响到类或者方法本身的代码

主要介绍两个方法:

  • Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey):定义元数据
    • metadataKey:元数据的名称
    • metadataValue:元数据的值
    • target:被装饰的类或者对象
    • propertyKey:被装饰的方法名称
      • 这里要注意 targetpropertyKey 要一一对应,如果是类,那么 propertyKey 就是静态属性或者方法,如果是对象,那么 propertyKey 就是实例属性或者方法
  • Reflect.getMetadata(metadataKey, target, propertyKey):获取元数据

通过 Reflect.defineMetadata 方法调用来添加元数据,通过 @Reflect.metadata 给类添加元数据

import "reflect-metadata";

class A {
  @Reflect.metadata("key", "3")
  public static method1() {}
  public method2() {}
}
let obj = new A();

Reflect.defineMetadata("key", "1", A);
Reflect.defineMetadata("key", "2", obj);
Reflect.defineMetadata("key", "3", A, "method1"); // 等价于 @Reflect.metadata("key", "3") public static method1() {}
Reflect.defineMetadata("key", "4", obj, "method2");

console.log(Reflect.getMetadata("key", A));
console.log(Reflect.getMetadata("key", obj));
console.log(Reflect.getMetadata("key", A, "method1"));
console.log(Reflect.getMetadata("key", obj, "method2"));

对上面例子的改造:

function l(type: string) {
  return function (target: any) {
    // 将 type 挂载到类上,元数据的形式,而不是直接挂在到类上
    Reflect.defineMetadata("type", type, target);
  };
}
function log(type?: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const fn = descriptor.value;
    descriptor.value = function (...args: any[]) {
      let _type = type;
      if (!_type) {
        if (typeof target === "function") {
          // 通过 Reflect.getMetadata 获取元数据
          _type = Reflect.getMetadata("type", target);
        } else {
          // target.constructor 获取实例
          _type = Reflect.getMetadata("type", target.constructor);
        }
      }

      console.log("log", _type);
      return fn.apply(this, args);
    };
  };
}
@l("type1")
class A {
  @log()
  static sayHello(a: number, b: number) {
    return a + b;
  }
  @log("type222")
  sayHello2(a: number, b: number) {
    return a + b;
  }
}
const a = A.sayHello(1, 2);
console.log(a);
const b = new A().sayHello2(11, 22);
console.log(b);

emitDecoratorMetadata

tsconfig.json 配置了 emitDecoratorMetadata 后,ts 会在编译后自动添加三个元数据:

  • design:type
    • 属性:属性标注的类型
    • 方法:Function 类型
  • design:paramtypes:方法的参数类型
    • 方法:形参标注的类型
    • 类:构造函数形参标注的类型
  • design:returntype:方法的返回值类型
    • 方法:函数返回值标志的类型

代码如下:

function log(type?: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const designType = Reflect.getMetadata("design:type", target, key);
    const designParamTypes = Reflect.getMetadata("design:paramtypes", target, key);
    const designReturnType = Reflect.getMetadata("design:returntype", target, key);
    console.log(designType, designParamTypes, designReturnType); // [Function: Function] [ [Function: String], [Function: String] ] [Function: String]
  };
}

class A {
  @log()
  method2(a: string, b: string): string {
    return a + b;
  }
}

const a = new A();
const b = a.method2("1", "2");
console.log(b);

有了这三种参数后,就可以实现依赖注入了

代码如下:

import "reflect-metadata";

function Inject(target: any, key: string) {
  target[key] = new (Reflect.getMetadata("design:type", target, key))();
}

class A {
  sayHello() {
    console.log("hello");
  }
}

class B {
  @Inject // 编译后等同于执行了 @Reflect.metadata("design:type", A)
  a!: A;

  say() {
    this.a.sayHello(); // 不需要再对class A进行实例化
  }
}

new B().say(); // hello