myfreax

Typescript修饰器指南

JavaScript是一种惊人的编程语言,允许您在几乎任何平台上构建应用程序。但是TypeScript 在类型上已经做出了很大的贡献,例如装饰器

8 min read
By myfreax
Typescript修饰器指南

JavaScript是一种惊人的编程语言,允许您在几乎任何平台上构建应用程序。 虽然它也有着自己的缺点,但是TypeScript 在类型上已经做出了很大的贡献,涵盖了JavaScript中固有的一些差距。 它不仅为动态语言添加了类型的安全性,而且还具有一些很酷的功能,尚未存在于JavaScript中,例如装饰器。

什么装饰器?

虽然定义方式可能因不同的编程语言而异,修饰器是一种在编程中的模式,您可以在其中包装以改变其行为的代码。

在JavaScript中,此功能目前在两个阶段。 它尚未在浏览器或Node.js中使用,但您可以使用Babel等编译器来测试它。这不是一个全新的东西。在JavaScript之前,几种编程语言,如Python,Java和C#,采用了此模式。

即使JavaScript已经提出了此功能,TypeScript的装饰器功能以几种重要方式不同。 由于TypeScript是一种强类型的语言,您可以访问与数据类型关联的一些附加信息,以进行一些很酷的动作,例如运行时类型 - 断言和依赖项注入。

入门

首先创建空白Node.js 项目。

$ mkdir typescript-decorators
$ cd typescript decorators
$ npm init -y

接下来,将TypeScript安装为开发依赖项。

$ npm install -D typescript @types/node

@types/node包含Node.js的类型定义。 我们需要这个包来访问Node.js标准库类型定义。

package.json文件中添加NPM脚本以编译您的TypeScript 代码。

{
  // ...
  "scripts": {
    "build": "tsc"
  }
}

TypeScript 标记已将此功能标记为实验。 尽管如此,它足以在生产中使用。 事实上,开源社区一直在使用它已有很长一段时间。

要使用该功能,您需要对tsconfig.json文件进行一些调整。

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

创建一个简单的TypeScript 文件以测试它。

console.log("Hello, world!");


$ npm run build
$ node index.js
Hello, world!

而不是一遍又一遍地重复此命令,可以使用名为ts-node的包来简化编译和执行过程。 它是一个社区包,使您可以直接运行TypeScript代码而无需首先编译它。

让我们将其安装为开发依赖性。

$ npm install -D ts-node

接下来,将start脚本添加到package.json文件。

{
  "scripts": {
    "build": "tsc",
    "start": "ts-node index.ts"
  }
}

只需执行行npm start就可以运行代码。

$ npm start
Hello, world!

以下是一个装饰器参考示例, 您可以使用以下命令将其克隆到计算机上。

$ git clone https://github.com/rahmanfadhil/typescript-decorators.git

装饰器的类型

在TypeScript中,装饰器是可以附加到类及其成员函数,例如方法和属性。 让我们来看看一些例子。

class类装饰器

当您将函数附加到作为装饰器的类class时,您将接收到类的构造函数作为第一个参数。

const classDecorator = (target: Function) => {
  // do something with your class
}

@classDecorator
class Rocket {}

如果要覆盖类中的属性,则可以返回的新类中扩展其构造函数并设置属性。

const addFuelToRocket = (target: Function) => {
  return class extends target {
    fuel = 100
  }
}

@addFuelToRocket
class Rocket {}

现在,您的Rocket类将具有fuel属性,其中默认值为100

const rocket = new Rocket()
console.log((rocket).fuel) // 100

方法装饰器

附加装饰器的另一个好地方是类的方法。 在这里,您可以在函数中获得三个参数:targetpropertyKeydescriptor

const myDecorator = (target: Object, propertyKey: string, descriptor: PropertyDescriptor) =>  {
  // do something with your method
}

class Rocket {
  @myDecorator
  launch() {
    console.log("Launching rocket in 3... 2... 1... 🚀")
  }
}

第一个参数包含此方法所在的类,在这种情况下是Rocket类。 第二个参数包含方法名称字符串,最后一个参数是属性描述符,这是一个定义属性行为的一组信息。 这可用于观察,修改或替换方法定义。

如果要扩展方法的功能,则该方法装饰器可能非常有用,我们将稍后介绍。

属性装饰器

就像方法装饰器一样,您将获得targetpropertyKey参数。 唯一的区别是您没有得到属性描述符。

const propertyDecorator = (target: Object, propertyKey: string) => {
  // do something with your property
}

还有其他几个地方可以在TypeScript中连接您的装饰器,但这超出了本文的范围。 如果您很奇怪,您可以在TypeScript文档中了解更多有关它的信息。

使用TypeScript 装饰器案例

现在我们涵盖了装饰器是什么以及如何正确使用它们,让我们来看看一些具体的问题装饰器可以帮助我们解决的。

计算执行时间

假设您想要估计运行函数需要多长时间,以衡量您的应用程序性能。 您可以创建装饰器以计算函数的执行时间并在控制台上打印它。

class Rocket {
  @measure
  launch() {
    console.log("Launching in 3... 2... 1... 🚀");
  }
}

Rocket类具有内部的launch方法。 要评估launch方法的执行时间,可以附加measure装饰器。

import { performance } from "perf_hooks";

const measure = (
  target: Object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) => {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args) {
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const finish = performance.now();
    console.log(`Execution time: ${finish - start} milliseconds`);
    return result;
  };

  return descriptor;
};

正如您所看到的,measure装饰器用新的方法替换原始方法,该方法使其能够计算原始方法的执行时间并将其打印到console控制台。

要计算执行时间,我们将使用Node.js标准库中的性能hook API

实例化一个新的Rocket实例并调用launch方法。

const rocket = new Rocket();
rocket.launch();

你会得到以下结果。

Launching in 3... 2... 1... 🚀
Execution time: 1.0407989993691444 milliseconds

装饰器工厂

将装饰器配置为在某种情况下以不同的方式执行,您可以使用一个名为Decorator Factory的概念。

装饰工厂是一个返回装饰器的函数。 这使您可以通过在工厂传递一些参数来自定义装饰器的行为。

看看下面的例子。

const changeValue = (value) => (target: Object, propertyKey: string) => {
  Object.defineProperty(target, propertyKey, { value });
};

changeValue函数返回一个装饰器,可以根据从您传递到工厂Factory的值更改属性的值。

class Rocket {
  @changeValue(100)
  fuel = 50
}

const rocket = new Rocket()
console.log(rocket.fuel) // 100

现在,如果将装饰器工厂绑定到fuel属性,则该值将是100

自动守卫

让我们实现我们学会了什么来解决真实世界的问题。

class Rocket {
  fuel = 50;

  launchToMars() {
    console.log("Launching to Mars in 3... 2... 1... 🚀");
  }
}

假设您有一个Rocket类,具有launchToMars方法。 要发射火箭到火星,fuel必须高于100.

为此我们需要创建一个装饰器。

const minimumFuel = (fuel: number) => (
  target: Object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) => {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args) {
    if (this.fuel > fuel) {
      originalMethod.apply(this, args);
    } else {
      console.log("Not enough fuel!");
    }
  };

  return descriptor;
}; 

minimumFuel是工厂装饰者。 它需要fuel参数,这表示启动特定火箭所需的燃料是多少。

检查燃料条件,用新方法包装原始方法,就像在以前的用例中一样。

现在您可以将装饰器插入launchToMars方法并设置最小燃料级别。

class Rocket {
  fuel = 50;

  @minimumFuel(100)
  launchToMars() {
    console.log("Launching to Mars in 3... 2... 1... 🚀");
  }
}

现在,如果您调用launchToMars方法,它不会将火箭发射到火星,因为当前的燃油值50.

const rocket = new Rocket()
rocket.launchToMars()


Not enough fuel!

这个装饰器的很酷的事情是您可以将相同的逻辑应用于不同的方法,而无需重写整个 if-else 声明。

你想制作一个新的方法来推动火箭到月球。 要做到这一点,燃料水平必须高于25.

重复相同的代码并更改参数。

class Rocket {
  fuel = 50;

  @minimumFuel(100)
  launchToMars() {
    console.log("Launching to Mars in 3... 2... 1... 🚀");
  }

  @minimumFuel(25)
  launchToMoon() {
    console.log("Launching to Moon in 3... 2... 1... 🚀")
  }
}

现在,这枚火箭可以发射到月球。

const rocket = new Rocket()
rocket.launchToMoon()


Launching to Moon in 3... 2... 1... 🚀

这种类型的装饰器非常有用,可用于认证和授权目的,例如检查是否允许用户访问某些私有数据。

结论

在某些情况下,没有必要编写自己的装饰器。 许多TypeScript库/框架,例如typeorm Angular ,已经提供了您需要的所有装饰器。 但是,了解原理总是值得努力的,甚至可能会激励你建立自己的TypeScript框架。