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

news2024/12/26 21:21:39

前言

亲爱的小伙伴,你好!我是 嘟老板。你是否用过 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.coloradmin.cn/o/1648502.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring中的Bean相关理解

在Spring框架中&#xff0c;Bean是一个由Spring IoC容器实例化、配置和管理的对象。Bean是一个被Spring框架管理并且被应用程序各个部分所使用的对象。Spring IoC容器负责Bean的创建、初始化、依赖注入以及销毁等生命周期管理。 注&#xff1a;喜欢的朋友可以关注公众号“JAVA学…

学习软考----数据库系统工程师25

关系规范化 1NF&#xff08;第一范式&#xff09; 2NF&#xff08;第二范式&#xff09; 3NF&#xff08;第三范式&#xff09; BCNF&#xff08;巴克斯范式&#xff09; 4NF&#xff08;第四范式&#xff09; 总结

茶多酚复合纳米纤维膜

茶多酚复合纳米纤维膜是一种结合了茶多酚与纳米纤维技术的创新材料。茶多酚作为茶叶中多酚类物质的总称&#xff0c;具有抗氧化、抗辐射、抗*等多种药理作用&#xff0c;是一种非常有益的天然物质。而纳米纤维膜则因其超细纤维结构、高比表面积和高孔隙率等特性&#xff0c;在过…

深度解读:Agent AI智能体如何重塑我们的现实和未来|TodayAI

​​​​​​​ 一、 引言 在当今时代&#xff0c;人工智能&#xff08;AI&#xff09;技术的快速发展正不断改变着我们的生活与工作方式。尤其是Agent AI智能体&#xff0c;作为AI技术中的一种重要形式&#xff0c;它们通过模拟人类智能行为来执行各种复杂任务&#xff0c;从…

基于51单片机的电子钟秒表LCD1602仿真设计( proteus仿真+程序+设计报告+原理图+讲解视频)

基于51单片机的电子钟秒表LCD1602仿真设计( proteus仿真程序设计报告原理图讲解视频&#xff09; 这里写目录标题 1. 主要功能&#xff1a;2. 讲解视频&#xff1a;3. 仿真4. 程序代码5. 设计报告6. 原理图7. 设计资料内容清单&&下载链接 仿真图proteus7.8及以上 程序…

pynq7020系列的资源有多少

pynq系列的资源有多少 对比 查找表107&#xff0c;273 39.14 140&#xff0c;537 51.28查找表随机存储器17&#xff0c;457 12.12 19&#xff0c;524 13.56触发器67&#xff0c;278 12.27 81&#xff0c;453 14.95 Block RAMs ( 36 KB ) 264.5 29.00 457 50.11 Table 1: Zynq-…

英语口语情景对话视频软件分享!

在当今全球化的时代&#xff0c;英语已成为一种通用的国际语言。为了提高英语口语能力&#xff0c;越来越多的人选择使用英语口语情景对话视频软件。本文将为您推荐几款备受欢迎的英语口语情景对话视频软件&#xff0c;帮助您轻松提高英语口语水平。 AI外语陪练 AI外语陪练软件…

计量校准使用测量不确定度具备什么意义?有哪些作用?

一些企业在收到校准报告/证书时&#xff0c;会看到证书之中有“测量不确定度”一栏&#xff0c;因此会有很多人咨询&#xff0c;什么是不确定度&#xff0c;它的作用是什么&#xff1f; 计量校准报告的不确定度 如果以书面形式理解的话&#xff0c;相信很多朋友都不明白不确定…

适合小白使用的编译器(c语言和Java编译器专属篇)

本节课主要讲如何安装适合编程小白的编译器 废话不多说&#xff0c;我们现在开始 c/c篇 首先&#xff0c;进入edge浏览器&#xff0c;在搜索框输入visual studio &#xff0c;找到带我画圈的图标&#xff0c;点击downloads 找到community版&#xff08;社区版&#xff09;的下…

什么是web3D?应用场景有哪些?如何实现web3D展示?

Web3D是一种将3D技术与网络技术完美结合的全新领域&#xff0c;它可以实现将数字化的3D模型直接在网络浏览器上运行&#xff0c;从而实现在线交互式的浏览和操作。 Web3D通过将多媒体技术、3D技术、信息网络技术、计算机技术等多种技术融合在一起&#xff0c;实现了它在网络上…

【JAVA |基础】运算符、程序逻辑控制以及方法的使用

目录 一、前言 二、操作符 1.算术运算符 2.赋值运算符 3.比较运算符 4.逻辑运算符 5.条件&#xff08;三目、三元&#xff09;运算符 6.位运算符(都是基于二进制来计算) 三、 程序逻辑控制 1.顺序结构 2.分支结构 if语句 Switch语句 3.循环结构 while语句 for循环…

【C++ 】二叉搜索树

文章目录 1. 二叉搜索树的概念2. 二叉搜索树的代码实现2.1 Find ( ) 查找的实现2.2 Insert () 插入的实现2.3 InOrder ( ) 中序遍历的实现2.4 Erase ( ) 删除的实现 3. 二叉搜索树的应用 1. 二叉搜索树的概念 &#x1f427;① 二叉搜索树(BST&#xff0c;Binary Search Tree)&a…

SQL注入基础-3

一、宽字节注入 1、宽字节&#xff1a;字符大小为两个及以上的字节&#xff0c;如GBK&#xff0c;GB2312编码 2、数据库使用GBK编码时&#xff0c;会将两个字符合并为一个汉字(宽字节)。特殊值字符如单引号都会被转义【--->\】&#xff0c;如sqli-lads第32关&#xff0c;输…

如果你作 为Java程序员曾经遭遇过NullPointerException,请举起手

如果你作 为Java程序员曾经遭遇过NullPointerException&#xff0c;请举起手 1.让Optional发光发热&#xff1a;去除代码中对null的检查&#xff0c; 采用防御式检查减少NullPointerException java8实战 书籍 第225页 免费下载链接&#xff1a; https://pan.quark.cn/s/5cf68…

HTML5+CSS3+JS小实例:旋转渐变光标

实例:旋转渐变光标 技术栈:HTML+CSS+JS 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale…

冲突:故事型游戏的燃料

在故事型游戏中&#xff0c;冲突是推动情节发展的关键因素。没有冲突&#xff0c;故事就会变得平淡无奇&#xff0c;缺乏吸引力。在这篇博客中&#xff0c;我将探讨冲突在故事型游戏中的重要性&#xff0c;以及如何利用冲突为游戏增色添彩。 首先&#xff0c;让我们来了解一下冲…

1688快速获取整店铺列表 采集接口php Python

在电子商务的浪潮中&#xff0c;1688平台作为中国领先的批发交易平台&#xff0c;为广大商家提供了一个展示和销售商品的广阔舞台&#xff1b;然而&#xff0c;要在众多店铺中脱颖而出&#xff0c;快速获取商品列表并进行有效营销是关键。 竞争对手分析 价格比较&#xff1a;…

市面上好用的AI工具有哪些?

市面上的AI工具数不胜数&#xff0c;选择合适自己的AI工具则需要考虑自己的需求&#xff0c;看是否能满足的使用需求。那么市面上又有哪些好用的AI工具呢&#xff1f; 泰迪智能科技拥有简单易用的大数据挖掘建模平台&#xff0c;能够让数据创造更大的价值。 功能板块&…

Spring Data JPA自定义Id生成策略、复合主键配置、Auditing使用

前言 在Spring Data JPA系列的第一篇文章 SpringBoot集成JPA及基本使用-CSDN博客 中讲解了实体类的Id生成策略可以通过GeneratedValue注解进行配置&#xff0c;该注解的strategy为GenerationType类型&#xff0c;GenerationType为枚举类&#xff0c;支持四种Id的生成策略&…

什么牌子的骨传导耳机质量好?五大宝藏热门机型测评对比!

我作为一名音乐发烧友&#xff0c;对各类耳机产品都有深入的了解&#xff0c;最近也经常被人问及骨传导耳机哪个牌子好。通过交流&#xff0c;我发现很多人在选择骨传导耳机的时候&#xff0c;都有出现踩坑的情况&#xff0c;这也难怪&#xff0c;随着骨传导耳机热度逐渐增加&a…