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

前言

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

什么是装饰器

咱们先来看一段代码:

js 复制代码
@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 启用。

  • 命令行:
shell 复制代码
tsc --target ES5 --experimentalDecorators
  • tsconfig.json:
json 复制代码
{
  compilerOptions: {
    target: "ES5",
    experimentalDecorators: true
  }
}

创建装饰器函数

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

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

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

1.直接创建

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

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

js 复制代码
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()

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

OK,没啥问题。

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

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

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

上代码:

js 复制代码
function Get(interfaceName: string): any {
  return function(target: any) {
    console.log(interfaceName)
  }

}

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

new UserController()

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

OK,没啥问题。

分类

1.类装饰器

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

作用

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

类型声明

js 复制代码
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

参数

target:现有类的构造函数。

返回值

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

示例

js 复制代码
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()

控制台执行结果:

OK,没啥问题。

2.属性装饰器

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

作用

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

类型声明

js 复制代码
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

参数

  • target :
    若目标方法是 静态方法 ,则是类的构造器。
    若目标方法是 实例方法,则是类的原型链。
  • propertyKey: 目标属性的名称。

返回值

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

示例

js 复制代码
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'

控制台执行结果:

OK,没啥问题。

注:

上述示例代码中引入了 reflect-metadata 库,这将添加一个 polyfill,用于支持使用 TS 实验性的元数据 API

目前装饰器和装饰器元数据已经达到 stage3 阶段。

3.方法装饰器

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

作用

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

类型声明

js 复制代码
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

参数

  • target :
    若目标方法是 静态方法 ,则是类的构造器。
    若目标方法是 实例方法,则是类的原型链。
  • propertyKey: 目标方法的名称。
  • descriptor : 目标方法的描述器

返回值

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

示例

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

js 复制代码
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()

控制台执行结果:

OK,没啥问题。

4.访问器装饰器

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

什么是类访问器?

我们在定义类属性时,可能会用到类似以下的方式:

js 复制代码
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 属性的 获取设置 添加自定义逻辑。

作用

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

类型声明

js 复制代码
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

返回值

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

示例

js 复制代码
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

控制台执行结果:

OK,没啥问题。

注意:
构造器装饰器 不能同时装饰单个成员的 getset 访问器。而应该将所有装饰器都添加到该成员声明的第一个访问器上。

因为装饰器是应用于 属性描述符 ,而 描述符 中涵盖了 getset,不是单独声明。

5.参数装饰器

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

作用

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

类型声明

js 复制代码
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) => void;

参数

  • target :
    若目标方法是 静态方法 ,则是类的构造器。
    若目标方法是 实例方法,则是类的原型链。
  • propertyKey : 属性名称(参数所在的 方法名,而不是参数名称)。
  • parameterIndex: 参数在方法中的位置下标。

返回值

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

示例

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

js 复制代码
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('')

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

执行规则

1.应用时机

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

例如:

js 复制代码
function T(target: any) {
  console.log('装饰器执行')
  return target
}

@T
class EvaExampleClass1 {}

const eec = new EvaExampleClass1()

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

2.执行顺序

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

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

其中,

方法/访问器/属性装饰器 的执行顺序,按照其在类中的定义顺序而定。

同一方法中的不同参数的装饰器,按相反的顺序执行,最后一个参数的装饰器最先执行。

上代码验证一下:

js 复制代码
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;
}

执行结果:

3.组合装饰器

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

调用顺序如下:

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

例如:

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

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

执行结果:

什么时候使用装饰器

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

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

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

结语

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

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


往期推荐

相关推荐
gnip2 小时前
企业级配置式表单组件封装
前端·javascript·vue.js
一只叫煤球的猫3 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
excel4 小时前
Three.js 材质(Material)详解 —— 区别、原理、场景与示例
前端
掘金安东尼4 小时前
抛弃自定义模态框:原生Dialog的实力
前端·javascript·github
hj5914_前端新手8 小时前
javascript基础- 函数中 this 指向、call、apply、bind
前端·javascript
薛定谔的算法8 小时前
低代码编辑器项目设计与实现:以JSON为核心的数据驱动架构
前端·react.js·前端框架
Hilaku8 小时前
都2025年了,我们还有必要为了兼容性,去写那么多polyfill吗?
前端·javascript·css
yangcode8 小时前
iOS 苹果内购 Storekit 2
前端
LuckySusu8 小时前
【js篇】JavaScript 原型修改 vs 重写:深入理解 constructor的指向问题
前端·javascript
LuckySusu8 小时前
【js篇】如何准确获取对象自身的属性?hasOwnProperty深度解析
前端·javascript