如何在 TypeScript 中使用装饰器

作者选择了 COVID-19 救援基金作为 Write for Donations计划的一部分接受捐款。

介绍

TypeScript是使用JavaScript的运行时间与编译时间类型检查器的JavaScript(https://www.digitalocean.com/community/tutorial_series/how-to-code-in-javascript)语言的扩展,这种组合允许开发人员使用完整的JavaScript生态系统和语言功能,同时还添加可选的静态类型检查,列表,类和界面。

Decorators 是装饰类成员或类本身的一种方式,具有额外的功能性. 当您将装饰程序应用于类或类成员时,您实际上正在调用一个函数,该函数将接收被装饰的东西的细节,然后装饰程序的实现将能够动态地转换代码,添加额外的功能性,并减少锅板代码。

<$>[注] 目前,有一个 阶段 2 提议添加装饰者到 ECMAScript 标准. 由于它还不是 JavaScript 功能,TypeScript 提供自己的装饰者的实现,在实验旗帜下。

本教程将向您展示如何为班级和班级成员创建自己的TypeScript装饰器,以及如何使用它们。它将引导您通过不同的代码样本,您可以在自己的TypeScript环境中或在TypeScript播放场(https://www.typescriptlang.org/play?ts=4.2.2#),一个在线环境,允许您直接在浏览器中写TypeScript。

前提条件

要遵循本教程,您将需要:

  • 联合国 您可以执行 TypeScript 程序的环境与示例一起执行 。 为了在本地机器上安装这个,你需要以下设备.
  • 都安装了节点npm(或yarn),以便运行一个处理TypeScript相关包的开发环境. 这个教程用Node.js版本14.3.0和npm版本6.14.5进行了测试. 要安装在 macOS 或 Ubuntu 18.04 上,请遵循 [如何在 macOS (https://andsky.com/tech/tutorials/how-to-install-node-js-and-create-a-local-development-environment-on-macos) 上安装节点并创建本地开发环境 或 ** 使用 [如何在 Ubuntu 18.04 (https://andsky.com/tech/tutorials/how-to-install-node-js-on-ubuntu-18-04 上安装节点.js 的PPA** 部分。 如果您正在使用 Linux (WSL) (https://docs.microsoft.com/en-us/windows/wsl/install-win10) 的 Windows 子系统,此功能也会有效 。
  • 此外,您需要安装在您的机器上的脚本编译器。 要做到这一点,请参考官方TypeScript网站.
  • 如果您不想在本地机器上创建类型脚本环境, 您可以使用官方 [TypeScript Playground] (https://www.typescriptlang.org/play) 来跟踪 。
  • 您需要足够的JavaScript知识,特别是ES6+语法,例如解构,休息操作员,以及进出口. 如果您需要更多有关这些主题的信息,请读取我们的[JavaScript series How To Code (https://www.digitalocean.com/community/tutorial_series/how-to-code-in-javascript).
  • 此教程会引用文本编辑器中支持TypeScript并显示行内错误的方面. 这对使用TypeScript是不必要的,但确实更多地利用了TypeScript的特性. 为了获得这些好处,您可以使用像[Visual Studio Code (https://code.visualstudio.com/)这样的文本编辑器,该编辑器对出框的TypeScript有完全的支持. 您可以在 [TypeScript Playground] (https://www.typescriptlang.org/play ) 中尝试这些好处. (英语)

本教程中的所有示例都是使用TypeScript版本 4.2.2创建的。

启用TypeScript中的装饰支持

目前,装饰器仍然是TypeScript的实验功能,因此必须首先启用它. 在本节中,您将看到如何在TypeScript中启用装饰器,取决于您如何使用TypeScript。

TypeScript 编译器 CLI

要在使用TypeScript 编译器 CLI(‘tsc’)时启用装饰者支持,唯一需要的额外步骤是通过一个额外的旗帜:

1tsc --experimentalDecorators

主持人:JSON

在具有 tsconfig.json 文件的项目中工作时,要启用实验装饰,您必须在 compilerOptions' 对象中添加 实验装饰' 属性:

1{
2  "compilerOptions": {
3    "experimentalDecorators": true
4  }
5}

在TypeScript Playground中,默认情况下启用了装饰器。

使用 Decorator Syntax

在本节中,您将应用TypeScript类中的装饰。

在TypeScript中,您可以使用特殊语法@expression创建装饰品,其中expression是一个函数,在运行时会自动调用有关装饰者的目标的详细信息。

装饰师的目标取决于你在哪里添加它们. 目前,装饰师可以添加到类的下列组件:

  • 类声明本身
  • 属性
  • 配件
  • 方法
  • 参数

例如,假设你有一个名为密封的装饰器,该装饰器在一类中呼叫 Object.seal

1@sealed
2class Person {}

注意在突出代码中,您在您的密封装饰仪的目标之前添加了装饰仪,在这种情况下,是类声明。

同样适用于所有其他类型的装饰师:

 1@classDecorator
 2class Person {
 3  @propertyDecorator
 4  public name: string;
 5
 6  @accessorDecorator
 7  get fullName() {
 8    // ...
 9  }
10
11  @methodDecorator
12  printName(@parameterDecorator prefix: string) {
13    // ...
14  }
15}

要添加多个装饰器,您可以将它们添加在一起,一个接一个:

1@decoratorA
2@decoratorB
3class Person {}

在TypeScript中创建类装修器

在本节中,您将通过创建 TypeScript 中的类装饰程序的步骤进行。

对于一个名为@decoratorA的装饰器,你会告诉TypeScript它应该调用函数decoratorAdecoratorA函数将被调用以详细说明您在代码中如何使用装饰器。

要创建自己的装饰器,你必须创建一个与装饰器相同的名称的函数,也就是说,要创建你在上一节看到的密封类装饰器,你必须创建一个密封函数,该函数接收了一组特定的参数。

1@sealed
2class Person {}
3
4function sealed(target: Function) {
5  Object.seal(target);
6  Object.seal(target.prototype);
7}

传递给装饰师的参数(s)将取决于装饰师将使用的位置。

密封装饰器将仅用于类声明,因此您的函数将收到一个单个参数,即目标,该参数将是功能类型。

密封函数中,您正在调用Object.seal(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal)对目标,即类型构建器,以及其原型。

重要的是要记住,在使用装饰器时,目前无法扩展目标的TypeScript类型,这意味着,例如,您无法使用装饰器将新字段添加到类中,从而使其安全打字。

如果您返回了密封类装饰器中的值,则该值将成为该类的新构造函数。

你已经创建了你的第一个装饰器,并使用它与一类. 在下一节,你将学习如何创建装饰工厂。

创建装饰工厂

有时你需要在应用时向装饰师传递额外的选项,为此你必须使用装饰工厂. 在本节中,你将学习如何创建这些工厂并使用它们。

装饰工厂是返回另一个函数的函数,它们得到这个名称,因为它们不是装饰者的实现本身,相反,它们返回负责装饰者的实现的另一个函数,并作为包装函数。

假设你有一个名为decoratorA的类装饰器,你想添加一个选项,当调用装饰器时可以设置,例如布尔旗。

1const decoratorA = (someBooleanFlag: boolean) => {
2  return (target: Function) => {
3  }
4}

在这里,你的decoratorA函数返回了另一个函数,随着装饰器的实现,注意装饰器工厂如何获得布尔旗作为其唯一参数:

1const decoratorA = (someBooleanFlag: boolean) => {
2  return (target: Function) => {
3  }
4}

您可以在使用装饰器时传递此参数的值. 请参见下面的示例中突出显示的代码:

1const decoratorA = (someBooleanFlag: boolean) => {
2  return (target: Function) => {
3  }
4}
5
6@decoratorA(true)
7class Person {}

在这里,当你使用decoratorA装饰器时,装饰工厂将被称为someBooleanFlag参数设置为true。然后装饰器实现本身将运行。

请注意,您需要通过装饰工厂预期的所有参数,如果您只是在没有通过任何参数的情况下应用装饰器,如下示例:

1const decoratorA = (someBooleanFlag: boolean) => {
2  return (target: Function) => {
3  }
4}
5
6@decoratorA
7class Person {}

TypeScript 编译器会给你两个错误,这可能取决于装饰器的类型。

1[secondary_label Output]
2Unable to resolve signature of class decorator when called as an expression.
3  Type '(target: Function) => void' is not assignable to type 'typeof Person'.
4    Type '(target: Function) => void' provides no match for the signature 'new (): Person'. (1238)
5Argument of type 'typeof Person' is not assignable to parameter of type 'boolean'. (2345)

你刚刚创建了一个装饰工厂,能够根据这些参数接收参数并改变他们的行为。

房地产设计师

类属性是您可以使用装饰品的另一个地方,在本节中您将看看如何创建它们。

任何房地产装饰师都会获得以下参数:

  • 对于静态属性,该类的构造函数. 对于所有其他属性,该类的原型。
  • 成员的名称。

目前,没有办法获得属性描述器作为参数,这是因为属性描述器在TypeScript中被初始化的方式。

这里有一个装饰函数,将会员的名称打印到控制台:

1const printMemberName = (target: any, memberName: string) => {
2  console.log(memberName);
3};
4
5class Person {
6  @printMemberName
7  name: string = "Jon";
8}

当您运行上述 TypeScript 代码时,您将在控制台中看到以下内容:

1[secondary_label Output]
2name

您可以使用物业装饰器来对所装饰的物业进行排列。这可以通过使用 Object.defineProperty 以及对该物业的新设置器和排列器来完成。让我们来看看如何创建一个名为 allowlist 的装饰器,这只允许一个物业设置为静态允许列表中的值:

 1const allowlist = ["Jon", "Jane"];
 2
 3const allowlistOnly = (target: any, memberName: string) => {
 4  let currentValue: any = target[memberName];
 5
 6  Object.defineProperty(target, memberName, {
 7    set: (newValue: any) => {
 8      if (!allowlist.includes(newValue)) {
 9        return;
10      }
11      currentValue = newValue;
12    },
13    get: () => currentValue
14  });
15};

首先,您正在创建一个静态允许列表,在代码的顶部:

1const allowlist = ["Jon", "Jane"];

然后您正在创建房地产装饰程序的实现:

 1const allowlistOnly = (target: any, memberName: string) => {
 2  let currentValue: any = target[memberName];
 3
 4  Object.defineProperty(target, memberName, {
 5    set: (newValue: any) => {
 6      if (!allowlist.includes(newValue)) {
 7        return;
 8      }
 9      currentValue = newValue;
10    },
11    get: () => currentValue
12  });
13};

注意如何使用任何作为目标类型:

1const allowlistOnly = (target: any, memberName: string) => {

对于物业装饰家来说,目标参数的类型可以是类的构造者或类的原型,在这种情况下更容易使用任何

在您的装饰器实现的第一行中,您将装饰的资产的当前值存储在当前值变量中:

1let currentValue: any = target[memberName];

对于静态属性,这将设置为默认值,如果有,对于非静态属性,这将始终是未定义的,这是因为在运行时,在编译的JavaScript代码中,装饰程序在实例属性设置为默认值之前运行。

然后,您使用Object.defineProperty来取代该属性:

1Object.defineProperty(target, memberName, {
2    set: (newValue: any) => {
3      if (!allowlist.includes(newValue)) {
4        return;
5      }
6      currentValue = newValue;
7    },
8    get: () => currentValue
9  });

Object.defineProperty调用有一个getter和一个settergetter返回存储在currentValue变量中的值。

让我们使用你刚刚写的装饰程序,创建以下类:

1class Person {
2  @allowlistOnly
3  name: string = "Jon";
4}

您现在将创建一个新的类的实例,并测试设置并获得名称实例属性:

 1const allowlist = ["Jon", "Jane"];
 2
 3const allowlistOnly = (target: any, memberName: string) => {
 4  let currentValue: any = target[memberName];
 5
 6  Object.defineProperty(target, memberName, {
 7    set: (newValue: any) => {
 8      if (!allowlist.includes(newValue)) {
 9        return;
10      }
11      currentValue = newValue;
12    },
13    get: () => currentValue
14  });
15};
16
17class Person {
18  @allowlistOnly
19  name: string = "Jon";
20}
21
22const person = new Person();
23console.log(person.name);
24
25person.name = "Peter";
26console.log(person.name);
27
28person.name = "Jane";
29console.log(person.name);

运行代码时,您应该看到以下输出:

1[secondary_label Output]
2Jon
3Jon
4Jane

该值从未设置为彼得,因为彼得不在允许列表中。

如果你想让你的代码更可重复使用,允许在应用装饰器时设置允许列表? 这是装饰工厂的绝佳用例。 让我们把你的AllowlistOnly装饰器变成装饰工厂:

 1const allowlistOnly = (allowlist: string[]) => {
 2  return (target: any, memberName: string) => {
 3    let currentValue: any = target[memberName];
 4
 5    Object.defineProperty(target, memberName, {
 6      set: (newValue: any) => {
 7        if (!allowlist.includes(newValue)) {
 8          return;
 9        }
10        currentValue = newValue;
11      },
12      get: () => currentValue
13    });
14  };
15}

在这里,你将以前的实现包装成另一个功能,一个装饰工厂.装饰工厂收到一个单一的参数,称为Allowlist,这是一个系列的字符串。

现在要使用您的装饰器,您必须通过允许列表,如下所示的代码:

1class Person {
2  @allowlistOnly(["Claire", "Oliver"])
3  name: string = "Claire";
4}

尝试运行类似于你之前写的代码,但有新的更改:

 1const allowlistOnly = (allowlist: string[]) => {
 2  return (target: any, memberName: string) => {
 3    let currentValue: any = target[memberName];
 4
 5    Object.defineProperty(target, memberName, {
 6      set: (newValue: any) => {
 7        if (!allowlist.includes(newValue)) {
 8          return;
 9        }
10        currentValue = newValue;
11      },
12      get: () => currentValue
13    });
14  };
15}
16
17class Person {
18  @allowlistOnly(["Claire", "Oliver"])
19  name: string = "Claire";
20}
21
22const person = new Person();
23console.log(person.name);
24person.name = "Peter";
25console.log(person.name);
26person.name = "Oliver";
27console.log(person.name);

代码应该给你以下输出:

1[secondary_label Output]
2Claire
3Claire
4Oliver

顯示它按預期運作,「person.name」永遠不會設定為「Peter」,因為「Peter」不在所提供的許可清單中。

现在你已经创建了你的第一个房产装饰器,使用一个正常的装饰器功能和装饰工厂,是时候看看如何为类配件创建装饰器了。

创建配件装饰

在本节中,您将看看如何装饰类配件。

就像房地产装饰品一样,配件中使用的装饰品收到以下参数:

对于静态属性,该类的构造函数;对于所有其他属性,该类的原型。

但与物业装饰器不同,它还收到第三个参数,附件成员的物业描述器。

鉴于 Property Descriptors 包含特定成员的 setter 和 getter,配件装饰器只能应用于单个成员的 setter 或 getter,而不是两者。

如果您从配件装饰器返回一个值,该值将成为配件的新的属性描述器,无论是子成员还是 setter成员。

以下是可以用来更改 getter/setter 配件的可编号旗的装饰器的例子:

1const enumerable = (value: boolean) => {
2  return (target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
3    propertyDescriptor.enumerable = value;
4  }
5}

注意在示例中,你是如何使用装饰工厂的。这允许你在调用装饰师时指定可编号的旗帜。

1class Person {
2  firstName: string = "Jon"
3  lastName: string = "Doe"
4
5  @enumerable(true)
6  get fullName () {
7    return `${this.firstName} ${this.lastName}`;
8  }
9}

配件装饰器与财产装饰器类似。唯一的区别是,它们通过属性描述器获得了第三个参数。现在你创建了第一个配件装饰器,下一节将向你展示如何创建方法装饰器。

创建装饰方法

在本节中,您将看看如何使用方法装饰器。

方法装饰师的实施方式与您创建配件装饰师的方式非常相似.传递给装饰师的实现参数与传递给配件装饰师的参数相同。

让我们重复使用您之前创建的相同的可编号装饰器,但这次使用以下类的getFullName方法:

 1const enumerable = (value: boolean) => {
 2  return (target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
 3    propertyDescriptor.enumerable = value;
 4  }
 5}
 6
 7class Person {
 8  firstName: string = "Jon"
 9  lastName: string = "Doe"
10
11  @enumerable(true)
12  getFullName () {
13    return `${this.firstName} ${this.lastName}`;
14  }
15}

如果您从方法装饰程序返回一个值,则该值将成为该方法的新属性描述器。

让我们创建一个被贬值的装饰器,在使用该方法时将传递的消息打印到控制台,记录一个消息说该方法被贬值:

 1const deprecated = (deprecationReason: string) => {
 2  return (target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
 3    return {
 4      get() {
 5        const wrapperFn = (...args: any[]) => {
 6          console.warn(`Method ${memberName} is deprecated with reason: ${deprecationReason}`);
 7          propertyDescriptor.value.apply(this, args)
 8        }
 9
10        Object.defineProperty(this, memberName, {
11            value: wrapperFn,
12            configurable: true,
13            writable: true
14        });
15        return wrapperFn;
16      }
17    }
18  }
19}

在这里,您正在使用装饰工厂创建一个装饰师工厂,该装饰师工厂收到一个单一的类型字符串的参数,这就是贬值的原因,如下所示:

1const deprecated = (deprecationReason: string) => {
2  return (target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
3    // ...
4  }
5}

在将贬值消息登录到控制台时,将使用贬值原因。在实现您的贬值装饰器时,您将返回一个值。

您正在利用这一点,为您的装饰类方法添加一个ghetter,这样您就可以更改该方法的实现。

但是为什么不只使用Object.defineProperty,而不是返回方法的新属性装饰程序? 这是必要的,因为您需要访问this的值,而对于非静态类方法,它与类实例有关。

在您的情况下,getter 本身有this 值与非静态方法的类实例绑定,并且与静态方法的类构造器绑定。

在您的getter内,您正在本地创建一个包装函数,称为wrapperFn,该函数使用console.warn登录到控制台的消息,通过从装饰工厂接收的deprecationReason,然后调用原始方法,使用propertyDescriptor.value.apply(This, args)这样原始方法被称为其This值正确绑定到类实例,如果它是一个非静态的方法。

然后,您使用defineProperty来重写类中的方法值,这就像一个 memoization 机制,因为多次调用相同的方法不再叫您的getter,而是直接叫wrapperFn

让我们使用你的退缩装饰器:

 1const deprecated = (deprecationReason: string) => {
 2  return (target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
 3    return {
 4      get() {
 5        const wrapperFn = (...args: any[]) => {
 6          console.warn(`Method ${memberName} is deprecated with reason: ${deprecationReason}`);
 7          propertyDescriptor.value.apply(this, args)
 8        }
 9
10        Object.defineProperty(this, memberName, {
11            value: wrapperFn,
12            configurable: true,
13            writable: true
14        });
15        return wrapperFn;
16      }
17    }
18  }
19}
20
21class TestClass {
22  static staticMember = true;
23
24  instanceMember: string = "hello"
25
26  @deprecated("Use another static method")
27  static deprecatedMethodStatic() {
28    console.log('inside deprecated static method - staticMember =', this.staticMember);
29  }
30
31  @deprecated("Use another instance method")
32  deprecatedMethod () {
33    console.log('inside deprecated instance method - instanceMember =', this.instanceMember);
34  }
35}
36
37TestClass.deprecatedMethodStatic();
38
39const instance = new TestClass();
40instance.deprecatedMethod();

在这里,您创建了一个具有两个属性的TestClass:一个静态和一个非静态,您还创建了两种方法:一个静态和一个非静态。

然后,你将你的退缩装饰器应用于这两种方法。当你运行代码时,以下内容将出现在控制台中:

1[secondary_label Output]
2(warning) Method deprecatedMethodStatic is deprecated with reason: Use another static method
3inside deprecated static method - staticMember = true
4(warning)) Method deprecatedMethod is deprecated with reason: Use another instance method
5inside deprecated instance method - instanceMember = hello

这表明,这两种方法都与您的包装函数正确包装,该函数将向控制台记录一个消息,其中包含了折扣原因。

您现在使用 TypeScript 创建了第一个方法装饰程序,下一节将向您展示如何创建由参数装饰程序 TypeScript 支持的最后一个装饰程序类型。

创建参数装饰

参数装饰器可用于类方法的参数. 在本节中,您将学习如何创建一个。

与参数使用的装饰函数接收以下参数:

  1. 对于静态属性,该类的构造函数. 对于所有其他属性,该类的原型。
  2. 成员的名称.
  3. 方法的参数列表中的参数索引。

无法更改与参数本身有关的任何东西,所以这样的装饰器只对观察参数本身的使用有用(除非你使用一些更先进的东西,如 reflect-metadata)。

以下是打印装饰仪的示例,打印了装饰的参数的索引,以及方法名称:

1function print(target: Object, propertyKey: string, parameterIndex: number) {
2  console.log(`Decorating param ${parameterIndex} from ${propertyKey}`);
3}

然后你可以使用你的参数装饰器,如下:

1class TestClass {
2  testMethod(param0: any, @print param1: any) {}
3}

运行上述代码应在控制台中显示下列内容:

1[secondary_label Output]
2Decorating param 1 from testMethod

您现在创建并执行了参数装饰,并打印了返回装饰参数索引的结果。

结论

在本教程中,您已经实现了所有支持TypeScript的装饰程序,使用它们与类,并了解了它们之间的差异. 您现在可以开始写自己的装饰程序来减少您的代码库中的锅炉板代码,或使用装饰程序与库,如 Mobx,更有信心。

有关TypeScript的更多教程,请参阅我们的 How To Code in TypeScript 系列页面

Published At
Categories with 技术
comments powered by Disqus