搞定 TS 装饰器,让你写 Node 接口更轻松

前言

亲爱的小伙伴,你好!我是 嘟老板。你是否用过 TypeScript 呢?对 装饰器 了解多少呢?有没有实践应用过呢?今天我们就来聊聊 装饰器 的那点事儿,看看它有哪些神奇的地方。

什么是装饰器

咱们先来看一段代码:

@Controller('/user')
export class UserController {
  @Get('/queryList')
  queryList() {
    // 查询列表逻辑....
  }
}

这段代码见于 Node 编写的服务端,其中 @Controller 注解定义了一个控制器,告诉框架 UserController 是一个可访问的服务端点。@Get 注解定义了一个 Get 方法访问的接口,可以通过 ${domainUrl}/user/queryList 访问 queryList 函数。

@Controller@Get 就是本文的主人公 - 装饰器

所以怎么定义 装饰器 呢?
装饰器ECMAScript 即将推出的功能,允许我们以 可重用 的方式定义类和类成员。其本质上还是 函数,只不过是以一种特殊的方式使用而已。

这里有两个注意点:

  1. 装饰器 是应用到 类(class)类成员 上的,比如 方法属性参数访问器。相应的,装饰器 分为五类:类装饰器属性装饰器方法装饰器参数装饰器访问器装饰器,下文详细介绍。
  2. 以可重用的方式,什么意思?对于前端来说通常都会有 utils,用于维护常用的工具集函数,避免重复造轮子。装饰器 对于类也起到类似的作用,将通用性逻辑抽离为装饰器函数,在需要的场景下使用。

装饰器 的使用比较简单, @ 加上 装饰器函数 即可,如上面代码中的 @Controller()

装饰器怎么用

需要启用 experimentalDecorators 才能使用装饰器特性。
可以通过 命令行tsconfig 启用。

  • 命令行:
tsc --target ES5 --experimentalDecorators
  • tsconfig.json:
{
  compilerOptions: {
    target: "ES5",
    experimentalDecorators: true
  }
}

创建装饰器函数

什么是装饰器函数?
@Controller 中的 Controller 就是装饰器函数,会在程序运行时执行。

那怎么创建装饰器函数呢?

  • 直接创建
  • 装饰器工厂创建

1.直接创建

装饰器函数说到底也只是和函数而已,只不过函数的参数是固定的,即目标代码的信息。

比如 @Controller,咱先上一段代码,不要在意其合理性哈:

type TController = {new (...args: any[]): any}

function Controller<T extends TController>(BaseClass: T) {
  return class extends BaseClass {
    log() {
      console.log('打印日志...')
    }
  }
}

@Controller
class UserController {}

(new UserController() as any).log()

执行以上代码,控制台打印如下: image.png

OK,没啥问题。

2.装饰器工厂创建(Decorator Factory)

直接创建的装饰器,无法自定义处理逻辑,难以实现类似自定义参数的需求,如 @Get('/queryList')。这就需要另一种创建方式 - 装饰器工厂

装饰器工厂函数的返回值一个 装饰器函数

上代码:

function Get(interfaceName: string): any {
  return function(target: any) {
    console.log(interfaceName)
  }

}

@Controller
class UserController {
  @Get('/queryList')
  queryList() {}
}

new UserController()

控制台执行后,打印如下: image.png

OK,没啥问题。

分类

1.类装饰器

简言之,在 上使用的装饰器就是类装饰器。

作用

可以监听、修改、替换声明的类。适用于继承现有类并添加适当的属性和方法。

类型声明
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
参数

target:现有类的构造函数。

返回值

若类装饰器返回一个值,则会替换原有类构造器的声明。

示例
type TBaseClass = {
  new(...args: any[]): any
}

function ClassDecorator<T extends TBaseClass>(target: T): T {
  return class extends target {
    log() {
      console.log('我是类装饰器')
    }
  }
}

class BaseClass {
  log() {
    console.log('我是 BaseClass')
  }
}

@ClassDecorator
class ExampleClass extends BaseClass {}

new ExampleClass().log()

控制台执行结果:

image.png

OK,没啥问题。

2.属性装饰器

简言之,在 类属性 上使用的装饰器就是 属性装饰器

作用

可以用来收集属性信息,为类添加额外的方法和属性。

类型声明
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
参数
  • target:
    若目标方法是 静态方法,则是类的构造器。
    若目标方法是 实例方法,则是类的原型链。
  • propertyKey: 目标属性的名称。
返回值

无返回值,若存在将被忽略。

示例
import 'reflect-metadata'

function transformPropertyToEvent(propertyKey: string) {
  const firstLetterCapitalizedProp = propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1);
  return `on${firstLetterCapitalizedProp}Change`
}

function PropertyDecorator(target: any, propertyKey: string) {
    const eventKey = transformPropertyToEvent(propertyKey)
    target[eventKey] = function(fn: (pre: string, next: string) => void) {
      let pre = this[propertyKey]
      Reflect.defineProperty(this, propertyKey, {
        set(val) {
          fn(pre, val)
          pre = val
        }
      })
    }
}

class ExampleClass2 {

  @PropertyDecorator
  greeting = 'hello'

}

const ecInstance = new ExampleClass2()

// @ts-ignore
ecInstance.onGreetingChange((pre: string, next: string) => {
  console.log(`pre: ${pre}; next: ${next}`)
})

ecInstance.greeting = 'hi'

控制台执行结果:

image.png

OK,没啥问题。

注:
上述示例代码中引入了 reflect-metadata 库,这将添加一个 polyfill,用于支持使用 TS 实验性的元数据 API
目前装饰器和装饰器元数据已经达到 stage3 阶段。

3.方法装饰器

简言之,在 类方法 上使用的装饰器就是 方法装饰器

作用

可以修改或替换类方法原本的实现,添加一些通用逻辑等。

类型声明
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
参数
  • target:
    若目标方法是 静态方法,则是类的构造器。
    若目标方法是 实例方法,则是类的原型链。
  • propertyKey: 目标方法的名称。
  • descriptor: 目标方法的描述器。
返回值

若方法装饰器返回一个值,则会替换该方法的描述器

示例

相比 属性装饰器方法装饰器 多了一个 descriptor 参数,可以通过该参数实现对于原方法的修改。

function MethodDecorator(target: any, propertyKey: string | symbol, descriptor: any) {
  const originFn = descriptor.value

  descriptor.value = function (...args: any[]) {
    console.log('pre: MethodDecorator 开始打印日志')
    originFn.apply(this, args)
    console.log('post: MethodDecorator 打印日志结束')
  }
}

class ExampleClass1 {
  @MethodDecorator
  log() {
    console.log('我是 ExampleClass 的 log 方法')
  }
}

new ExampleClass1().log()

控制台执行结果:

image.png

OK,没啥问题。

4.访问器装饰器

简言之,在 类访问器 上使用的装饰器就是 访问器装饰器

什么是类访问器?
我们在定义类属性时,可能会用到类似以下的方式:

class Example {
  innerValue = 123

  get value() {
      console.log(`get value: ${this.innerValue}`)
      return this.innerValue
  }

  set value(val) {
      console.log(`set value: ${val}`)
      this.innerValue = val
  }
}

这里的 getset 就是 value 属性的访问器,可以分别为 value 属性的 获取设置 添加自定义逻辑。

作用

访问器装饰器方法装饰器 类似,可以修改或替换访问器原本的实现,添加一些通用逻辑等。

类型声明
declare type AccessorDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor<T>) => PropertyDescriptor<T> | void;
参数
  • target:
    若目标方法是 静态方法,则是类的构造器。
    若目标方法是 实例方法,则是类的原型链。
  • propertyKey: 目标方法的名称。
  • descriptor: 目标方法的描述器。

访问器装饰器 的类型与 方法装饰器 的类型相似,不同之处在于 描述器(descriptor) 参数的 key

  • 访问器装饰器 descriptor key:
    • get
    • set
    • enumerable
    • configurable
    • writable
  • 方法装饰器 descriptor key:
    • value
    • enumerable
    • configurable
    • writable
返回值

若方法装饰器返回一个值,则会替换该方法的描述器

示例
function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: any) {
  const originSetter = descriptor.set

  descriptor.set = function(val: number) {
    console.log(`set value: ${val}`)
    return originSetter.call(this, val)
  }
}

class ExampleClass3 {
  private _value = 123

  @AccessorDecorator
  set value(val) {
    this._value = val
  }

  get value() {
    return this._value
  }
}

const ec = new ExampleClass3()
ec.value = 234

控制台执行结果:

image.png

OK,没啥问题。

注意:
构造器装饰器 不能同时装饰单个成员的 getset 访问器。而应该将所有装饰器都添加到该成员声明的第一个访问器上。
因为装饰器是应用于 属性描述符,而 描述符 中涵盖了 getset,不是单独声明。

5.参数装饰器

简言之,在 函数参数 前使用的装饰器就是 参数装饰器。经常用于类的 构造函数类方法 中。

作用

通常用于收集参数信息,供其他装饰器使用。

类型声明
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) => void;
参数
  • target:
    若目标方法是 静态方法,则是类的构造器。
    若目标方法是 实例方法,则是类的原型链。
  • propertyKey: 属性名称(参数所在的 方法名,而不是参数名称)。
  • parameterIndex: 参数在方法中的位置下标。
返回值

无返回值,若存在将被忽略。

示例

我们来实现一个 参数必填 的验证装饰器。

import "reflect-metadata";

const requiredMetadataKey = Symbol('required')

function Required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
  const requiredParameters: number[] = Reflect.getMetadata(requiredMetadataKey, target, propertyKey) || []
  requiredParameters.push(parameterIndex)
  Reflect.defineMetadata(requiredMetadataKey, requiredParameters, target, propertyKey)
}

function Validate(target: Object, propertyKey: string | symbol, descriptor: any) {
  const originFn = descriptor.value

  descriptor.value = function (...args: any[]) {
    const requiredParameters: number[] = Reflect.getMetadata(requiredMetadataKey, target, propertyKey)
    if (requiredParameters) {
      for (let parameterIndex of requiredParameters) {
        if ([undefined, null, ''].includes(args[parameterIndex])) {
          throw new Error(`方法 ${String(propertyKey)} 缺少必填参数`)
        }
      }
    }
    return originFn.apply(this, args)
  }
}

class ExampleClass4 {
  @Validate
  greet(@Required name: string) {
    return `Hello, ${name}`
  }
}

const ec = new ExampleClass4()
ec.greet('')

控制台执行结果: image.png OK,没啥问题。

执行规则

1.应用时机

装饰器只会在 解释执行 应用一次。

例如:

function T(target: any) {
  console.log('装饰器执行')
  return target
}

@T
class EvaExampleClass1 {}

const eec = new EvaExampleClass1()

image.png

装饰器 T 中的 console 只会打印一次,不会因为 new 操作而再次打印。

2.执行顺序

不同类型的装饰器,有明确的执行顺序。

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

其中,
方法/访问器/属性装饰器 的执行顺序,按照其在类中的定义顺序而定。
同一方法中的不同参数的装饰器,按相反的顺序执行,最后一个参数的装饰器最先执行。

上代码验证一下:

function decorator(key: string): any {
  console.log('装饰器应用: ', key);
  return function () {
    console.info('装饰器执行: ', key);
  };
}

@decorator('类装饰器')
class EvaExampleClass2 {
  @decorator('静态属性')
  static prop?: number;

  @decorator('静态方法')
  static method(@decorator('静态方法参数:foo') foo: string, @decorator('静态方法参数:bar') bar: string) {}

  constructor(@decorator('构造器参数') foo: string) {}

  @decorator('实例方法')
  method(@decorator('实例方法参数') foo: string) {}

  @decorator('实例属性')
  prop?: number;
}

执行结果:

image.png

3.组合装饰器

对同一目标同时使用多个装饰器,叫做 组合装饰器。比如同一个类方法添加多个 方法装饰器

调用顺序如下:

  1. 应用外层装饰器
  2. 应用内层装饰器
  3. 调用内层装饰器
  4. 调用外层装饰器

例如:

function decorator(key: string): any {
  console.log('应用: ', key);
  return function () {
    console.info('执行: ', key);
  };
}

class EvaExampleClass3 {
  @decorator('外层装饰器')
  @decorator('内层装饰器')
  method() {}
}

执行结果:

image.png

什么时候使用装饰器

结合以上介绍,简单列举一下 装饰器 的可能应用场景:

  1. 通用 Before/After 钩子
  2. 监听属性变更方法调用
  3. 转换方法参数
  4. 给类添加额外的方法属性
  5. 运行时类型检查
  6. 自动编码/解码
  7. 依赖注入

若小伙伴在实际应用中有更多合适的场景,可评论区留言讨论。

结语

好啦,今天的内容就到这里。本文从一个极简的 User 服务类切入,重点讲述 TS 装饰器 相关的知识点。如有疑问,欢迎评论区留言。

感谢阅读,愿 你我共同进步,谢谢!!!


往期推荐

  • express 基础入门
  • 一文带你了解多数企业系统都在用的 RBAC 权限管理策略
  • 项目实战 | 如何恰当的处理 Vue 路由权限
  • 项目实战 | 如何正确使用 watch/computed/ref

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/600685.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

密码学《图解密码技术》 记录学习 第十三章

目录 第十三章 13.1 本章学习的内容 13.2 PGP 简介 13.2.1 什么是 PGP 13.2.2 关于 OpenPGP 13.2.3关于GNU Privacy Guard 13.2.4 PGP 的功能 公钥密码 数字签名 单向散列函数 证书 压缩 文本数据 大文件的拆分和拼合 13.3 生成密钥对 13.4 加密与解密 13.4.1 加密 生成…

Qt | QComboBox(组合框)

01、上节回顾 Qt 基础教程合集02、QComBox 一、QComboBox 类(下拉列表、组合框) 1、QComboBox 类是 QWidget 类的直接子类,该类实现了一个组合框 2、QComboBox 类中的属性 ①、count:const int 访问函数:int count() const; 获取组合框中的项目数量,默认情况下,对于空…

js 图片渐变

1. 点击图片&#xff0c;使其渐变为另一张图片 通过定义keyframes来创建一个淡入淡出的动画效果。当图片被点击时&#xff0c;先添加淡出动画使图片透明度从0渐变到1&#xff0c;然后在1秒后切换图片源并添加淡入动画使新图片透明度从0渐变到1&#xff0c;实现图片渐变效果。 …

光伏SRM供应商管理解决方案

供应商管理是光伏企业中重要的一环&#xff0c;通过SRM管理供应商&#xff0c;可以提高产品质量&#xff0c;降低采购成本&#xff0c;并集成供应链&#xff0c;提高核心竞争力。 一、搭建管理系统 分为供应商和商户&#xff0c;供应商需要完善基本信息、类别、等级、产品概要…

2005-2021年全国各地级市生态环境注意力/环保注意力数据(根据政府报告文本词频统计)

2005-2021年全国各地级市生态环境注意力/环保注意力数据&#xff08;根据政府报告文本词频统计&#xff09; 2005-2021年全国各地级市生态环境注意力/环保注意力数据&#xff08;根据政府报告文本词频统计&#xff09; 1、时间&#xff1a;2005-2021年 2、范围&#xff1a;2…

记一些内存取证题

生活若循规蹈矩&#xff0c;我们便随心而动 1.Suspicion 给了俩文件 python2 vol.py -f mem.vmem imageinfo 查看可疑进程 python2 vol.py -f mem.vmem --profileWinXPSP2x86 pslist 发现可疑进程TrueCrypt.exe 把这个进程提取出来。memdump -p 进程号 -D 目录 python2 vol…

Ypay源支付6.9无授权聚合免签系统可运营源码

Ypay源支付6.9无授权聚合免签系统可运营源码 效果图说明安装说明后台 部分源码领取源码下期更新预报 效果图 YPay是一款专为个人站长设计的聚合免签系统&#xff0c;YPay基于高性能的ThinkPHP 6.1.2 Layui PearAdmin架构&#xff0c;提供了实时监控和管理的功能&#xff0c;让…

GhostNetV2 Enhance Cheap Operation with Long-Range Attention 论文学习

论文地址&#xff1a;https://arxiv.org/abs/2211.12905 代码地址&#xff1a;https://github.com/huawei-noah/Efficient-AI-Backbones/tree/master/ghostnetv2_pytorch 解决了什么问题&#xff1f; 在计算机视觉领域&#xff0c;深度神经网络在诸多任务上扮演着重要角色。为…

Linux —— 信号(3)

Linux —— 信号&#xff08;3&#xff09; Core dump为什么core默认是被关闭的阻塞信号信号其他相关常见概念信号递达信号未决信号阻塞两者的区别信号的结构 信号集操作函数一个简单使用例子sigpending的使用例子 我们今天接着来了解信号&#xff1a; Core dump 大家不知道有…

大模型爱好者的福音,有了它个人电脑也可以运行大模型了

GPT4ALL是一款可以运行在个人电脑上的大模型系统&#xff0c;不需要GPU即可运行&#xff0c;目前支持mac&#xff0c;linux和windows系统。 什么是GPT4ALL&#xff1f; 不论学习任何东西&#xff0c;首先要明白它是个什么东西。 Open-source large language models that run …

3W 1.5KVDC 3KVDC 隔离宽范围输入,单、双输出 DC/DC 电源模块——TP2L-3W 系列

TP2L-3W系列是一款高性能、超小型的电源模块&#xff0c;宽范围2:1,4:1输入&#xff0c;输出有稳压和连续短路保护功能&#xff0c;隔离电压为1.5KVDC、3KVDC工作温度范围为–40℃到85℃。特别适合对输出电压的精度有严格要求的地方&#xff0c;外部遥控功能对您的设计又多一项…

【极速前进】20240423-20240428:Phi-3、fDPO、TextSquare多模态合成数据、遵循准则而不是偏好标签、混合LoRA专家

一、Phi-3技术报告 论文地址&#xff1a;https://arxiv.org/pdf/2404.14219 ​ 发布了phi-3-mini&#xff0c;一个在3.3T token上训练的3.8B模型。在学术基准和内部测试中的效果都优于Mixtral 8*7B和GPT-3.5。此外&#xff0c;还发布了7B和14B模型phi-3-small和phi-3-medium。…

Transformer详解:从放弃到入门(三)

上篇文章中我们了解了多头注意力和位置编码&#xff0c;本文我们继续了解Transformer中剩下的其他组件。 层归一化 层归一化想要解决一个问题&#xff0c;这个问题在Batch Normalization的论文中有详细的描述&#xff0c;即深层网络中内部结点在训练过程中分布的变化问题。  …

风吸式杀虫灯解析

TH-FD2S风吸式杀虫灯是一种创新且环保的害虫控制设备&#xff0c;它结合了太阳能和风力的双重优势&#xff0c;为农业生产、园林绿化以及居民生活等提供了高效且安全的害虫防治方案。 首先&#xff0c;风吸式杀虫灯的工作原理是利用害虫的趋光性&#xff0c;通过特定的光源吸引…

AI视频教程下载:用ChatGPT做SEO的终极教程

ChatGPT是由OpenAI开发的一款尖端人工智能&#xff0c;它已经彻底改变了我们进行搜索引擎优化&#xff08;SEO&#xff09;的方式。其先进的语言处理能力使其成为增强网站内容、提高搜索引擎排名和显著提升在线可见性的宝贵工具。 这个全面的课程旨在为你提供使用ChatGPT进行SE…

突破AI迷雾:英特尔携手星环科技打造向量数据库革新方案,直降大模型幻觉

去年爆火的大模型&#xff0c;正在从百模大战走向千行百业落地应用。不过行业数据规模有限&#xff0c;企业数据隐私安全的要求等等因素&#xff0c;都让行业大模型的准确率面临挑战。近期发布的《CSDN AI 开发者生态报告》数据显示&#xff0c;“缺乏数据/数据质量问题”在大模…

ILI9341显示驱动芯片的使用

ILI9341是一种常见的TFT LCD显示驱动芯片&#xff0c;它在众多的应用中都有广泛的使用。这种芯片的一个显著特点是它支持16位RGB565颜色&#xff0c;这意味着它可以显示多达65536种不同的颜色。这使得ILI9341能够提供鲜艳、生动的色彩效果&#xff0c;对于需要表现丰富色彩的应…

解决Python中的 `ModuleNotFoundError: No module named ‘fcmeans‘` 错误

ModuleNotFoundError: No module named fcmeans 解决Python中的 ModuleNotFoundError: No module named fcmeans 错误如何解决这个错误fcmeans 库简介应用实例 解决Python中的 ModuleNotFoundError: No module named fcmeans 错误 在进行数据科学或机器学习项目时&#xff0c;…

【QA】Java常见运算符

前言 本文主要讲述Java常见的运算符 运算符的概念 两个基本概念&#xff1a; 运算符&#xff1a;对字面量或者变量进行操作的符号 表达式&#xff1a;用运算符把字面量或者变量连接起来符合java语法的式子就可以称为表达式 示例&#xff1a; int a 10; int b 20; int …

上证50etf期权到底该怎么玩?

今天期权懂带你了解上证50etf期权到底该怎么玩&#xff1f;ETF期权是一种股票市场上的金融衍生品&#xff0c;它是在交易所上市交易的期权合约&#xff0c;其标的资产是某个特定的交易所交易基金&#xff08;ETF&#xff09;&#xff0c;如上证50指数ETF或沪深300指数ETF等。 上…
最新文章