TS 装饰器 + 实践应用

3,200 阅读5分钟

装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明方法访问符属性参数上。

装饰器目前还是一项实验性特性,想要使用装饰器,需要在tsconfig.json中开启选项:

// tsconfig.json
{
    "compilerOptions": {
        "experimentalDecorators": true // 开启装饰器
     }
}

装饰器的写法为:@expression,以@开头,expression为一个函数。

function logController(target: any) {
    console.log('log')
}

@logController
class Person {
    name: string = ''
    age: number = 0
    
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
}

const p = new Person('张三', 18) // log

上面是一个简单的装饰器示例代码,logController会被作用于类Person上。 除此之外,装饰器也可以传递参数:如@logController('hello'),至于如何传递参数以及接收参数,还有上面的target是什么,这些问题咱们在下面的内容介绍。

装饰器分类

装饰器分类装饰器方法装饰器参数装饰器属性装饰器访问器装饰器。不同装饰器所接收的参数也有所不同。

类装饰器

类装饰器顾名思义作用于类上,可以用来监视、修改或替换类定义。

类装饰器的参数有且只有一个:

  • target:类的构造函数。
const addSex: ClassDecorator = (target: any) => {
  target.prototype.sex = '男'
}

@addSex
class Person {
    name: string = ''
    age: number = 0

    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
}

interface P {
  name: string
  age: number
  sex?: string
}

const p: P = new Person('张三', 18)
console.log(p.sex) // 男

上面我们通过装饰器为Person类增加了sex属性。

前面说过,装饰器也可以传递参数,要实现参数传递,只需要在装饰器内部返回一个装饰器函数即可,类似于匿名函数的形式。

const addSex = (sex: string): ClassDecorator => {
  return (target: any)  => {
    target.prototype.sex = sex
  }
}

@addSex('女')
class Person {
    name: string = ''
    age: number = 0

    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
}

interface P {
  name: string
  age: number
  sex?: string
}

const p: P = new Person('张三', 18)
console.log(p.sex) // 女

这种叫做装饰器工厂

方法装饰器

方法装饰器对方法进行修饰,它接收3个参数:

  • target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • propertyKey: 成员的名字。
  • descriptor: 成员的属性描述符。
const logResult = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
  const fn = descriptor.value
  descriptor.value = function(...rest) {
    const result = fn.apply(this, rest)
    console.log(propertyKey + ':' + result)
    return result
  }
}

class Person {
    name: string = ''
    age: number = 0

    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }

    @logResult
    getName() {
      return this.name
    }

    @logResult
    getAge() {
      return this.age
    }
}

const p = new Person('张三', 18)
p.getName() // getName:张三
p.getAge() // getAge:18

方法装饰器的参数中比较重要的是第三个参数: 属性描述符descriptor

descriptor包含configurableenumerablewritablevalue4个属性。这里的value指向我们装饰的函数,我们可以对其进行重写,实现打印内容:方法名+方法的返回结果。

参数装饰器

参数装饰器用于装饰函数参数,参数装饰器接收3个参数:

  • target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • propertyKey: 方法名。
  • paramIndex: 参数所在位置的索引。
const paramDecorator = (target: any, propertyKey: string, paramIndex: number) => {
    console.log(target, propertyKey, paramIndex)
}

class Person {
    name: string = ''
    age: number = 0

    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }

    setName(@paramDecorator name: string) {
      this.name = name
    }
}

const p = new Person('张三', 18) // Person、setName、0
p.setName('李四')

属性装饰器

属性装饰器作用于类属性上,接收两个参数:

  • target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • propertyKey: 成员的名字。
const propertyName = (target: any, propertyKey: string) => {
  console.log(target, propertyKey)
}

class Person {
    @propertyName // Person, name
    name: string = ''
    age: number = 0

    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
}

const p = new Person('张三', 18)

属性装饰器应用较少,很多文章用它来初始化属性值,实际上TS本就建议类属性在声明时进行初始化,所以用其来初始化属性值意义不大,引用用官方文档说的:属性装饰器用来监视类中是否声明了某个名字的属性。

访问器装饰器

访问器装饰器作业用于访问器上,接收3个参数:

  • target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • propertyKey: 方法名。
  • descriptor: 属性描述符。
const getSetDecorator = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
  console.log(target, propertyKey, descriptor)
}

class Person {
  private _name: string = ''
  private _age: number = 0

  constructor(name: string, age: number) {
    this._name = name
    this._age = age
  }
  
  @getSetDecorator
  get name() {
    return this._name
  }

  set name(name: string) {
    this._name = name
  }
}

const p = new Person('张三', 18)

TypeScript不允许同时装饰一个成员的getset访问器,所以上面的set访问器不能够再添加装饰器,否则会报错。

实战例子

请求loading

在平时的业务开发中,我们往往会在请求接口数据时添加loading:

import { Loading } from 'element-ui'

export default {
    methods: {
        async fetchData() {
            const loading = Loading.service()
            try {
                await api.getList()
            } finally {
                loading.close() // 关闭loading
            }
        }
    }
}

通过装饰器实现后的样子:

export default {
    methods: {
        @loading()
        async fetchData() {
            await api.getList()
        }
    }
}

装饰器实现代码:

import { Loading } from 'element-ui'

/**
 * 装饰器:loading
 */
export const loading = (text = '加载中') => {
  return (target, name, descriptor) => {
    const fn = descriptor.value
    descriptor.value = async (...rest) => {
      const loading = Loading.service({
        text
      })
      try {
        return await fn.apply(this, rest)
      } finally {
        loading.close()
      }
    }
  }
}

确认弹窗

在进行删除操作时我们一般会弹出确认弹窗,确认弹窗的代码一般也都很固定,因此我们也可以写一个装饰器来简化这个过程。

import { MessageBox } from 'element-ui'
/**
 * 装饰器:确认弹窗
 */
export const confirm = (message) => {
  return (target, name, descriptor) => {
    const fn = descriptor.value
    descriptor.value = async (...rest) => {
      await MessageBox.confirm(message)
      fn.apply(this, rest)
    }
  }
}
// 使用装饰器
export default {
    methods: {
        @confirm('确认删除吗?')
        async onDelete() {
            await api.deleteUser()
        }
    }
}

以上两个例子只是装饰器应用于实际开发中的冰山一角,对装饰器感兴趣的朋友可以多深入了解并实践,实现更复杂的应用场景。

最后

本次分享到这里就结束了,感谢您的阅读。