Skip to content

装饰器

装饰器是 TypeScript 的实验性特性,目的是给装饰的目标添加上某些特定的功能,就好比是AOP切面编程,可以对类、方法、属性、参数进行切面增强。

Nestjs 完全使用装饰器语法来构建应用,学习作为前置知识之一的装饰器是必不可少的。

装饰器环境

装饰器属于 TypeScript 的实验性特性,所以需要在配置文件 tsconfig.json 中开启支持。

json
{
  "compilerOptions": {
    // ...
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

类装饰器

对类进行切面增强的装饰器,会在绑定时注入一个参数,即使这个类还未实例化,装饰器函数也会执行。

装饰器在代码解析执行时,应用一次,即时我们并没有使用 Animal 类。

类型声明:

typescript
type ClassDecorator = <TFunction extends Function>
  (target: TFunction) => TFunction | void;
  • @参数:

    1. target: 当前装饰的类构造器。
  • @返回:

    如果类装饰器返回了一个值,它将会被用来代替原有的类构造器的声明。

ts
const LoggerDecorator =  <T extends { new (...args: any[]): {} }>(target: T)=>{
  console.log('无须new实例,类装饰器也会执行')
  console.log(target) // target === 当前装饰的类 -> Animal
}

@LoggerDecorator
class Animal{}

如果类装饰器返回一个新的类,它会将返回的类来替换原有类的定义。

ts
const LoggerDecoratorNewClass = <T extends { new (...args: any[]): {} }>(
  target: T,
) => {
  return class extends IAnimal {
    name: string = "new class";
    age: number = 18;
  };
};

class IAnimal {
  name?: string;
  age?: number;
}

@LoggerDecoratorNewClass
class NewAnimal extends IAnimal {}

console.log(new NewAnimal().name);

遗憾的是,对于拓展的类,TypeScript 并无类型提示,意味着我们只能额外提供一个类用于提供类型提示,比如 IAnimal 类就是这个作用。

语法糖

装饰器本质上是一个普通的函数,装饰器的语法 @Decorator 相当于将被装饰的类传递进这个函数中,然后在函数内进行增强操作。

ts
const Decorator: ClassDecorator = (target) => {
  target.prototype.name = "class装饰器";
};

// 1.语法糖写法
@Decorator
class SyntacticSugar {
  constructor() {
    console.log((this as any).name);
  }
}

// 2.函数写法
Decorator(SyntacticSugar);

new SyntacticSugar(); // "class装饰器"

叠加使用

多个装饰器可以叠加使用,不同类型的装饰器的执行顺序是明确定义的:

  1. 实例成员:参数、方法、访问器、属性
  2. 静态成员:参数、方法、访问器、属性
  3. 构造函数:参数
  4. 类装饰器

对于属性、方法、访问器装饰器来说,静态与实例的执行顺序是根据它们的定义顺序来决定的。

同类型装饰器执行顺序

对同一个目标应用多个装饰器,它们呈现的顺序是 Koa洋葱模型的顺序。也就是先会执行外层的装饰器代码,再执行调用内从的装饰器代码。

ts
const OrderDecorator: (key: string) => ClassDecorator = (key) => {
  console.log("evaluate:  ", key);
  return (target) => {
    console.log("call:  ", key);
    return target;
  };
};

@OrderDecorator("class A")
@OrderDecorator("class B")
class User {}

image-20240104164828669

不同类型装饰器执行顺序

不同类型的装饰器(类、方法、参数)的装饰器可以对同一个类叠加使用。静态成员和实例成员的执行顺序是根据它们定义的顺序来定的。

ts
const PrintDecorator: (key: string) => any = (key) => {
  console.log("解释: ", key);
  return function () {
    console.log("执行: ", key);
  };
};

@PrintDecorator("类")
class C {
  @PrintDecorator("静态属性")
  static prop?: number;

  @PrintDecorator("静态方法")
  static method(@PrintDecorator("静态方法参数") foo: any) {}

  constructor(@PrintDecorator("构造器参数") foo: any) {}

  @PrintDecorator("实例方法")
  method(@PrintDecorator("实例方法参数") foo: any) {}

  @PrintDecorator("实例属性")
  prop?: number;
}

image-20240104231228536

装饰器工厂

装饰器工厂是一个高阶函数,让装饰器函数接收参数,进一步增强切面处理。

接收任意类型的参数,返回一个装饰器函数,就是装饰器工厂。

ts
const DecoratorFactory = (type: string): ClassDecorator => {
  return function (target) {
    target.prototype.name = `类型-- ${type}`;
  };
};

@DecoratorFactory("Animal")
class Animal {}

@DecoratorFactory("person")
class Person {}

console.log((new Animal() as any).name); // "类型-- Animal"
console.log((new Person() as any).name); // "类型-- Person"

方法装饰器

对方法进行切面增强的装饰器,可以对方法的原型修改、切面增强、替换实现等操作方式。

类型声明:

ts
type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
  • @参数:
    1. target:对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
    2. propertyKey :装饰的方法的名称。
    3. descriptor :装饰的方法的属性描述符。

方法装饰器不同于其他装饰器的地方在于,我们可以通过 descriptor 参数属性描述符,来对方法进行增强,添加一些通用的逻辑。

例如我们可以给方法新增切面打印参数和结果的效果:

ts
const LoggerDecorator: MethodDecorator = function (
  target,
  propertyKey,
  descriptor: PropertyDescriptor,
) {
  const original = descriptor.value as Function;

  descriptor.value = function (...args: any[]) {
    console.log("params: ", ...args);
    const result = original.call(this, ...args);
    console.log("result: ", result);
    return result;
  };
};
class LoggerTest {
  @LoggerDecorator
  add(x: number, y: number) {
    return x + y;
  }
}
const l = new LoggerTest();
l.add(1, 2);

image-20240104233454032

下面是用方法装饰器实现一个最小的 Get 装饰器案例。

ts
import axios from "axios";

const Get = (url: string): MethodDecorator => {
  return (
    target: Object,
    propertyKey: string | symbol,
    descriptor: PropertyDescriptor,
  ) => {
    const original = descriptor.value as Function;

    descriptor.value = async function (...args: any[]) {
      const params = Object.keys(args[0] as IParams).reduce(
        (prev, item, currentIndex, array) => {
          prev += `${currentIndex === 0 ? "?" : "&"}${item}=${args[0][item]}`;
          return prev;
        },
        "",
      );
      const response = await axios.get(url + params);
      const responseData = response.data;

      console.log("params: ", ...args);
      const result = original.call(this, ...args, responseData);
      console.log("result: ", result);
      return result;
    };
  };
};

interface IParams {
  page: number;
  size: number;
}

class Http {
  @Get("https://api.apiopen.top/api/getHaoKanVideo")
  public getList(params: IParams, data?: any) {
    console.log("🚀 ~ data", data);
    return data;
  }
}

new Http().getList({ page: 1, size: 5 });

image-20240104232246562

参数装饰器

类型声明:

ts
type ParameterDecorator = (
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) => void;
  • @参数:
  1. target :对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
  2. propertyKey :装饰的参数名称。
  3. parameterIndex :参数在方法中所处的位置的索引。
  • @返回:

    返回的结果将被舍弃忽略,也就是不需要返回。

单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。

借助方法装饰器“最小Get案例”,传递一个 key ,得到对应 keydata 数据。

ts
import axios from "axios";
import "reflect-metadata";

interface IParams {
  page: number;
  size: number;
}

const Get = (url: string): MethodDecorator => {
  return (
    target: Object,
    propertyKey: string | symbol,
    descriptor: PropertyDescriptor,
  ) => {
    const original = descriptor.value as Function;

    descriptor.value = async function (...args: any[]) {
      const params = Object.keys(args[0] as IParams).reduce(
        (prev, item, currentIndex, array) => {
          prev += `${currentIndex === 0 ? "?" : "&"}${item}=${args[0][item]}`;
          return prev;
        },
        "",
      );
      const response = await axios.get(url + params);
      const responseData = response.data;
      const key = Reflect.getMetadata("key", target, propertyKey) as string;

      return original.call(
        this,
        ...args,
        key ? responseData[key] : responseData,
      );
    };
  };
};

const Result = (key: string): ParameterDecorator => {
  return (target: Object, propertyKey: string | symbol | undefined) => {
    // 记录数据,方便方法装饰器校验处理
    Reflect.defineMetadata("key", key, target, propertyKey as string | symbol);
  };
};

class Http {
  @Get("https://api.apiopen.top/api/getHaoKanVideo")
  public getList(params: IParams, @Result("result") data?: any) {
    console.log("🚀 ~ data", data);
  }
}

new Http().getList({ page: 1, size: 5 });

image-20240105000411908

属性装饰器

类型声明:

ts
type PropertyDecorator =
  (target: Object, propertyKey: string | symbol) => void;
  • @参数:
  1. target :对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
  2. propertyKey:装饰的属性名称。
  • @返回:

    返回的结果将被忽略。

属性装饰器常用于监视、修改或替换类的属性定义。比如实现类型检查,或者实现 get/set 方法。属性装饰器一般不单独使用,主要用于配合类/方法装饰器进行组合装饰。

实现属性类型检查的例子:

ts
const MinLength = (length: number): PropertyDecorator => {
  return (target: Object, propertyKey: string | symbol) => {
    let value: string;
    let descriptor: PropertyDescriptor = {
      set(v: string) {
        if (v.length < length) {
          throw new Error(`密码长度不能低于${length}位`);
        }

        value = v;
      },
      get() {
        return value;
      },
    };

    Object.defineProperty(target, propertyKey, descriptor);
  };
};

class User {
  @MinLength(6)
  public password: string;

  constructor(password: string) {
    this.password = password;
  }
}

new User("123");

image-20240106014048953

元数据

假如你想实现反射这样的能力,那么就需要元数据 reflect-metadata

元数据可以帮助我们获取编译器的类型。

目前只有三个可用的键:

  • design:type: 属性的类型。
  • desin:paramtypes: 方法的参数的类型。
  • design:returntype: 方法的返回值的类型。

以下是使用反射编写的自动类型检查器的例子:

ts
import 'reflect-metadata';

function validate(
  target: Object,
  key: string,
  descriptor: PropertyDescriptor
) {
  const originalFn = descriptor.value;

  // 获取参数的编译期类型
  const designParamTypes = Reflect
    .getMetadata('design:paramtypes', target, key);

  descriptor.value = function (...args: any[]) {
    args.forEach((arg, index) => {

      const paramType = designParamTypes[index];

      const result = arg.constructor === paramType
        || arg instanceof paramType;

      if (!result) {
        throw new Error(
          `Failed for validating parameter: ${arg} of the index: ${index}`
        );
      }
    });

    return originalFn.call(this, ...args);
  }
}

class C {
  @validate
  sayRepeat(word: string, x: number) {
    return Array(x).fill(word).join('');
  }
}

const c = new C();
c.sayRepeat('hello', 2); // pass
c.sayRepeat('', 'lol' as any); // throw an error

image-20240106020935632

类型不对时,抛出错误,符合例子的失败的用例。

何时使用?

在阅读上面例子之后,对于何时使用装饰器相信你已经得出结论,一般不同类型的装饰器和元数据进行结合使用,以下是一些常用的使用场景:

  • 切面编程
  • 监听属性改变或者方法调用
  • 对方法的参数进行转换
  • 运行时类型检查
  • 添加额外的方法和属性
  • 。。。等等

附上案例代码

如有转载或 CV 的请标注本站原文地址