搞定 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 {@PropertyDecoratorgreeting = '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.valuedescriptor.value = function (...args: any[]) {console.log('pre: MethodDecorator 开始打印日志')originFn.apply(this, args)console.log('post: MethodDecorator 打印日志结束')}
}class ExampleClass1 {@MethodDecoratorlog() {console.log('我是 ExampleClass 的 log 方法')}
}new ExampleClass1().log()

控制台执行结果:

image.png

OK,没啥问题。

4.访问器装饰器

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

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

class Example {innerValue = 123get 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.setdescriptor.set = function(val: number) {console.log(`set value: ${val}`)return originSetter.call(this, val)}
}class ExampleClass3 {private _value = 123@AccessorDecoratorset 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.valuedescriptor.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 {@Validategreet(@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://xiahunao.cn/news/3014904.html

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

相关文章

包管理工具npm、cnpm、yarn、NVM

[包]英文单词是package,代表了一组特定功能的源码集合 包管理工具&#xff1a; 管理[包]的应用软件,可以对[包]进行下载安装,更新,删除,上传等操作借助包管理工具,可以快速开发项目,提升开发效率 包管理工具是一个通用的概念,很多编程语言都有包管理工具,所以掌握好包管理工具非…

Error: Maximum response size reached

错误原因复现 请求下载的文件是4g的&#xff0c;postman报错Error: Maximum response size reached 解决办法 Postman设置请求时长和数据大小 Settings&#xff0c;打开设置面板 postman有默认请求时间&#xff0c;正常的postman请求后端少量数据&#xff0c;返回特别快。但…

Mac idea gradle解决异常: SSL peer shut down incorrectly

系统&#xff1a;mac 软件&#xff1a;idea 解决异常: SSL peer shut down incorrectly 查看有没有安装 gradle -v安装 根据项目gradle提示安装版本 brew install gradle7idea的配置 在settings搜索gradle&#xff0c;配置Local installation&#xff0c;选择自己的安装目录…

用标准的GNU/Linux命令替换Alpine上的精简版命令

Alpine Linux 是一个基于 musl libc 和 busybox 的轻量级Linux发行版&#xff0c;busybox 实现了很多常用类Unix命令的精简版&#xff0c;特点是体积很小&#xff0c;舍弃了很多不常用参数&#xff0c;我们简单对比一下标准Linux自带的 date 命令 和 Alpine下默认的 date 命令便…

Vue-组件中的data

一个组件的data选项必须是一个函数。保证每个组件实例&#xff0c;维护独立的一份数据对象。如下图&#xff1a; 组件一旦封装好了&#xff0c;可以使用多次&#xff0c;比如数字框组件使用了三次&#xff1a; 每次创建新的组件实例&#xff0c;都会重新执行一次data函数&#…

EMAIL-PHP功能齐全的发送邮件类可以发送HTML和附件

EMAIL-PHP功能齐全的发送邮件类可以发送HTML和附件 <?php class Email { //---设置全局变量 var $mailTo ""; // 收件人 var $mailCC ""; // 抄送 var $mailBCC ""; // 秘密抄送 var $mailFrom ""; // 发件人 var $mailSubje…

2023年腾讯暑期实习 笔试原题与答案解析

试卷名称&#xff1a;腾讯2023实习生招聘-技术研究 考试时间&#xff1a;(北京时间)2023-03-26 20:00:00 -- 22:00:00 考试时长&#xff1a;120分钟 第一题 // package Tencent;import java.util.HashSet; import java.util.Scanner;public class Main {public static void …

Omnity 进展月报 | 2024.4.1-4.30

Omnity 大事摘要 1、Octopus 官宣升级为 Omnity。 2、Omnity 4月28号正式上线&#xff0c;实现BTC 和 ICP 之间跨链转账 Runes 资产。 3、为庆祝上线&#xff0c;以符文 HOPE•YOU•GET•RICH 为资产&#xff0c;发红包快速触达大量用户&#xff0c;体验跨链服务。 4、Omni…

【完美解决】使用git时候出现error setting certificate verify locations: CAfile:问题

1、出现场景&#xff1a; 在使用idea的时候&#xff0c;进行git下的push&#xff0c;出现下面的错误&#xff1a; 2、原因分析&#xff1a; 可能因为重装过系统&#xff0c;或者是安装git的位置发生了变化等情况出现。 3、解决方案&#xff1a; 找到git的安装路径&#xf…

Linux基础配置(镜像挂载,FQDN)

CentOS基础配置&#xff1a; 以下是appsrv的基础配置脚本&#xff0c;CentOS系统只需要把appsrv改成需要的主机名即可&#xff08;因为Linux基础配置都差不多&#xff0c;写脚本是最省时间的做法&#xff0c;IP地址的配置一般用nmtui图形化界面工具&#xff09; #!/bin/bash …

什么软件能在桌面上做笔记 好用的桌面笔记软件

一直认为&#xff0c;好记性不如烂笔头。在繁忙的工作和生活中&#xff0c;做笔记能够帮助我抓住那些一闪而过的灵感&#xff0c;也能让我随时回溯重要信息。特别是在电脑桌面上做笔记&#xff0c;更是方便快捷&#xff0c;一目了然。 想象一下&#xff0c;你正在工作中&#…

【web前端2024】简单几步制作web3d《萌宠星球》智体节点模板!

使用dtns.network德塔世界&#xff08;开源的智体世界引擎&#xff09;&#xff0c;策划和设计《乔布斯超大型的开源3D纪念馆》的系列教程。dtns.network是一款主要由JavaScript编写的智体世界引擎&#xff08;内嵌了three.js编辑器的定制版-支持以第一视角游览3D场馆&#xff…

Colibri for Mac v2.2.0 原生无损音频播放器 激活版

Colibri支持所有流行的无损和有损音频格式的完美清晰的比特完美播放&#xff0c;仅使用微小的计算能力&#xff0c;并提供干净和直观的用户体验。 Colibri在播放音乐时使用极少的计算能力。该应用程序使用最先进的Swift 3编程语言构建&#xff0c;BASS音频引擎作为机器代码捆绑…

走进CDO的世界:探索其主要职能职责

首席数据官&#xff08;CDO&#xff09;是统筹管理数据资产、系统开展内外部数据开放共享和价值开发的首要负责人&#xff0c;是推动以数据为核心要素的创新转型、合法合规开辟价值增长新空间的关键领导角色。CDO 制度是包括以CDO为首的数据人才队伍的岗位设置、职能职责体系、…

苹果自研大语言模型“Ajax“ 助力iOS 18升级;Stack Overflow与OpenAI建立API合作伙伴关系

&#x1f989; AI新闻 &#x1f680; 苹果自研大语言模型"Ajax" 助力iOS 18升级 摘要&#xff1a;苹果公司预计通过自研大语言模型Ajax来为iOS 18和Siri带来重大升级&#xff0c;但不计划推出类似ChatGPT的AI聊天机器人。Ajax模型基于Google的Jax框架&#xff0c;并…

220V转18V500mA非隔离恒压WT5113

220V转18V500mA非隔离恒压WT5113 亲爱的朋友们&#xff0c;你们是否在为如何提高电源方案而烦恼呢&#xff1f;今天我给大家带来了一款芯片&#xff0c;WT5113宽输出范围非隔离交直流转换芯片&#xff0c;它可是电源方案中的得力助手哦&#xff01; 这款芯片拥有220V降12V、2…

力扣刷题:四数相加Ⅱ

题目详情&#xff1a; 解法一&#xff1a;暴力枚举 对于这道题&#xff0c;我们的第一思路就是暴力枚举&#xff0c;我们可以写一个四层的for循环进行暴力匹配&#xff0c;只要相加的结果等于0就进行统计。但是我们会发现&#xff0c;我们的事件复杂度为O(N^4)事件复杂度非常大…

电度表抄表是什么?什么叫电度表抄表?

一、电度表抄表的概念和作用 电度表抄表是电力系统中一个基本但非常重要的阶段。它指的是对安装在用户处电度表开展载入&#xff0c;记录下来电力消耗的值&#xff0c;便于测算电费的一个过程。此项工作不仅有利于供电公司精确扣除电费&#xff0c;都是监控和管理电力工程应用…

【前端--Vue】组件之间的多种通信方式,一文彻底搞懂组件通信!

本篇将重点讲解vue中的多种组件通信方式&#xff0c;包括【父传子】【子传父】【兄弟组件通信】【依赖注入】等等&#xff0c;并提供具体案例来让小伙伴们加深理解、彻底掌握&#xff01;喜欢的小伙伴们点赞收藏&#xff0c;持续关注哦~&#x1f495; &#x1f49f; 上一篇文章…

浅谈智能电气火灾监控系统的设计及应用

摘要&#xff1a;致电气火灾的原因是多方面的&#xff0c;主要成因包括漏电、绝缘层老化、短路、电火花密集、接地发生故障、电气设备自然、接触不良和电流超负荷等。文章分析电气火灾的成因&#xff0c;并探索电气火灾监控系统的设计方案与注意事项。 关键词&#xff1a;电气…