搞定 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 装饰器 相关的知识点。如有疑问,欢迎评论区留言。

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


往期推荐

相关推荐
PleaSure乐事几秒前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 分钟前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo2 分钟前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v5 分钟前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫6 分钟前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.11 分钟前
Chrome调试工具(查看CSS属性)
前端·chrome
栈老师不回家1 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙1 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds2 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js