TS装饰器

TS装饰器,第1张

通过本文你可以知道什么
装饰器的发展历程JS和TS装饰器有何不同Angular中的装饰器到底是什么装饰器的定义,语法,作用reflect-meta是什么及如何使用
前言

我们平常开发中或多或少的听说或使用过装饰器,也切身感受到了它带给我们的便利。但是应该很少去系统的了解过装饰器。不清楚装饰器到底擅长干什么,怎么干。
由于目前js和ts中的装饰器有很多不同,本期只聚焦于ts的装饰器进行探讨。
本文预计阅读时间——20分钟

装饰器的演变
2015-3-24 stage 1阶段,也是目前广为使用的用法,也基本等同于TS开启了experimentalDecorators的用法。 2018-09 进入到stage2阶段,用法和stage1很大不同 2021-12 针对stage2提案进行了一次修改。 2022-03 正式进入stage3。去掉了metadata部分,使用方式没有发生太大变化。

冷知识:ts只会对Stage-3以上的提案提供支持,而TS引入装饰器实在2015年3月,差不多stage-1的时间段,这是因为在 NG-Conf上,angular团队宣布与TS团队进行合作。

JS装饰器和TS装饰器

js原生目前不支持装饰器,装饰器提案在stage-3阶段,只能通过babel体验装饰器这个新特性。TS目前实现的装饰器是基于JS装饰器stage-1的语法,所以在JS装饰器正式发布后,会和TS装饰器语法产生差异,之后看TS团队如何处理了,但预计也不是近期的事情了。

定义

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。装饰器使用@expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息作为参数传入。
定义来自:https://www.tslang.cn/docs/handbook/decorators.html

配置

由于装饰器目前还是实验中的特定,在js中处于stage-3阶段。在ts中已经作为一项实验性予以支持。开启装饰器需要在tsconfig.json文件中启用 experimentalDecorators 编译器选项。

装饰器于2022年三月底刚进入了stage-3阶段,详情见https://github.com/tc39/proposal-decorators/pull/454

Angular中的装饰器

我们在使用angular中经常会看到此类代码


每个指令,组件,module都会有对应的@expression进行标注,完全吻合装饰器的写法。但其实这种@Component类似的写法不能称作装饰器,更贴切的叫法为注解(Annotation)。它们是用于给编译器做数据描述,最终在build阶段会完全被抹去。
注解并不产生任何行为,仅仅添加附加内容。

装饰器使用

类装饰器

类装饰器是我们最常使用到的,它的通常作用是,为该类扩展功能

类装饰器有且只有一个参数,参数为类的构造函数constructor如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明

如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中不会为你做这些。—— 官方文档

设想有这样一个场景。
目前有一个Tank类,有一个Plane类,有一个Animal类。这三个类都需要一个公共的方法来获取他们所在的位置。我们第一可能想到使用继承来实现。

class BaseClass {
    getPosition() {
        return {
            x: 100,
            y: 200,
            z: 300,
        }
    }
}
class Tank extends BaseClass{}
class Plane extends BaseClass {}
class Animal extends BaseClass {}

这样三个类都可以调用getPosition方法来获取各自的位置了。到目前为止看起来没什么问题。

现在又有了一个新的诉求,Tank 类和Plane类需要一个新的方法addPetrol来给坦克和飞机加油。而动物不需要加油。此时这种写法好像不能继续进行下去了。而js目前没有直接语法提供多继承的功能,我们的继承方式好像行不通了。这时候装饰器可以很完美的实现这样的功能。此时就可以请我们的装饰器闪亮登场了~


装饰器功能之——能力扩展
我们把getPositionaddPertrol都抽象成一个单独的功能,它们得作用是给宿主扩展对应的功能。

const getPositionDecorator: ClassDecorator = (constructor: Function) => {
    constructor.prototype.getPosition = () => {
        return [100, 200]
    }
}

const addPetrolDecorator: ClassDecorator = (constructor: Function) => {
    constructor.prototype.addPetrol = () => {
        // do something
        console.log(`${constructor.name}进行加油`);
    }
}

@addPetrolDecorator
@getPositionDecorator
class Tank {}
@addPetrolDecorator
@getPositionDecorator
class Plane {}

@getPositionDecorator
class Animal {}

这样的话,加入日后我们有其他的猫猫狗狗,都可以对他进行能力扩展,让其具有加油的能力。

多个装饰器叠加的时候,执行顺序为离被装饰对象越近的装饰器越先执行。

装饰器功能之——重载构造函数
在类装饰器中如果返回一个值,它会使用提供的构造函数来替换类的声明。

function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override";
    }
}

@classDecorator
class Greeter {
    property = "property";
    hello: string;
    constructor(m: string) {
        this.hello = m;
    }
}

这个一个官方的例子,暂时没有想到业务中的适用场景。

方法装饰器

方法装饰器也是非常常用的,(敲黑板)这道题去年没考,今年肯定考~

方法装饰器接受三个参数:

对于静态方法,第一个参数为类的构造函数。对于实例方法,为类的原型对象。第二个参数为方法名。第三个参数为方法描述符。方法装饰器可以有返回值,返回值会作为方法的属性描述符

装饰器功能之——能力增强
我们带代码编写时候,经常会做一些错误catch。

class MusicSystem {
    getMusicById(name: string): Promise<{name: string, singer: string}> {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (Math.round(Math.random())) {
                    resolve({name: '凤凰传奇', singer: '玲花|曾毅'});
                } else {
                    reject()
                }
            }, 1000);
        })
    }

    async play(name: string) {
        // ... do something
        try {
            const music = await this.getMusicById(name);
            console.log(`在曲库中找到了名为${music.name}的音乐,由${music.singer}进行演唱,敬请欣赏。`);
        } catch (error) {
            throw new Error(`未找到名为${name}的音乐,播放失败`);
        }
    }
}

const musicSystem = new MusicSystem();
musicSystem.play('凤凰传奇');

有一个音乐系统,可以进行音乐播放。在播放时候,如果未找到对应的歌,会throw对应的错误。我们正常会想象到用如上方式实现。现在我们需要为音乐播放器增加一个删除歌曲的功能,并且在失败时候也需要throw出对应的异常。继续撸代码

class MusicSystem {
    ...
    async deleteByName(name: string) {
        // ... do something
        try {
            const music = await this.getMusicById(name);
            // ... do something
            console.log(`${music.name}音乐删除成功!`);
        } catch (error) {
            throw new Error(`未找到名为${name}的音乐,删除失败`);
        }
    }
}

easy,很快啊,就写出来了。但是我们发现,我们的代码结构,有很多相同的地方。作为一个程序员,是绝对不能容忍这样的事情发生!这时候,使用装饰器,也许是一种很好的解决方式。使用装饰器对每个方法进行增加,使它们自动获取catch错误的能力~

const ErrorDecorator: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    const sourceMethod = descriptor.value;
    descriptor.value = async function (...args: any) {
        try {
            await sourceMethod.apply(this, args);
        } catch (error) {
            console.error('捕获到了错误');
            // do something
        }
    }
}
class MusicSystem {
    getMusicById(name: string): Promise<{name: string, singer: string}> {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (Math.round(Math.random())) {
                    resolve({name: '凤凰传奇', singer: '玲花|曾毅'});
                } else {
                    reject()
                }
            }, 1000);
        })
    }
    
    @ErrorDecorator
    async play(name: string) {
        const music = await this.getMusicById(name);
        // ... do something
        console.log(`在曲库中找到了名为${music.name}的音乐,由${music.singer}进行演唱,敬请欣赏。`);

    }

    @ErrorDecorator
    async deleteByName(name: string) {
        const music = await this.getMusicById(name);
        // ... do something
        console.log(`${music.name}音乐删除成功!`);
    }
}

const musicSystem = new MusicSystem();
musicSystem.play('凤凰传奇');
musicSystem.deleteByName('凤凰传奇');

我们定义了一个错误捕获装饰器,名为ErrorDecorator该装饰器可以将宿主中throw出的错误捕获到。这样,我们不管以后扩展多少个功能,只要需要捕获错误,就可以使用该装饰器。业务中例如错误埋点上报等也是很适用的。

细心的同学可以发现了,我们在decorator中无法捕获到实际的错误,比如精准报错哪首歌没找到。很遗憾,目前装饰器的原生能力,是无法获取到我们调用时候传入的具体参数的。因为装饰器实在编译阶段执行的。但是,我们可以通过其他方式实现这样的功能,这就是大名鼎鼎的 metadata 。我们会在文章的末尾提到它。

装饰器功能之——descriptor修改
通过修改descriptor,我们可以实现对方法进行重新描述。比如设置方法禁止修改,禁止删除等。

const DescriptorDecorator: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) : object => {
    return {
        value: () => {
            console.log('eat方法被替换')
        },
        writable: true,
        enumerable: true,
        configurable: true,
    };
}

class Pig {
    name = 'peiqi';
    @DescriptorDecorator
    eat() {

    }
}

同样的,也可以直接对descriptor进行修改。

descriptor.value = () => {console.log('eat方法被替换')};
descriptor.writable = true;
descriptor.enumerable = true;
descriptor.configurable = true;

方法装饰器的使用方式很多,大多数的使用方式是对descriptor的value属性进行替换,拦截等实现功能。

【下边的三个装饰器类型,相对来说使用比较少,有兴趣的小伙伴可以卷】

属性装饰器

属性装饰器接受两个参数

对于静态属性,第一个参数为类的构造函数。对于实例属性,参数为类的原型对象第二个参数为属性名称

返回值将被忽略

网上有很多教程在使用属性装饰器时候,使用defineProperty对属性设置getter和setter,这是非常错误的用法!!官方文档已经明确说明了不能使用属性装饰器类监听和修改属性。https://www.tslang.cn/docs/handbook/decorators.html

装饰器功能之——初始化属性

const initCarPropertyDec  = <T>(property: T) => {
    return (target: object, propertyKey: string | symbol) => {
        target[propertyKey] = property;
    }
}

class Car {
    @initCarPropertyDec('奔驰')
    name!: string;
}

console.log(new Car().name)

属性装饰器还有一个更为常用的功能,配合reflect-metadata来向属性中添加元数据。并在恰当的时候消费它。

例如angular中,经常会对属性加上此类装饰器。它们就是向对应属性添加元数据。我们更贴切的把其称作为注解。

参数装饰器

参数装饰器接受三个参数

对于静态方法,第一个参数为类的构造函数。对于实例方法,为类的原型对象。第二个参数为参数所在的方法名称。第三个参数为参数在参数列表中的索引。

参数装饰器的返回值会被忽略。

参数装饰器一般用来做参数校验,在ts中使用场景很少

import 'reflect-metadata'

const validate: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    const method = descriptor.value;
    descriptor.value = function (...args: Array<any>) {
        const paramIndexArr = Reflect.getMetadata('required', target, propertyKey);
        paramIndexArr.forEach((index: number) => {
            if(args[index] === undefined) {
                throw new Error(`${index}参数未必传项!`)
            }
        })
        method.apply(this, args);
    }
}

const required: ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => {
    const paramIndexArr = Reflect.getMetadata('required', target, propertyKey) || [];
    paramIndexArr.push(parameterIndex);
    Reflect.defineMetadata('required', paramIndexArr, target, propertyKey);
}

class SSO {
    @validate
    login(@required username: string, @required password: string) {

    }
}

以上demo有一个单点登录类,其中login方法必须传入usernamepassword。我们使用参数装饰器,当函数未传入指定类型数据时候进行报错。

参数装饰器基本是用于对参数进行验证,并自定义报错信息,在ts作用较小。

访问器装饰器

接受三个参数

对于静态成员,第一个参数为类的构造函数。对于实例方法,为类的原型对象。第二个参数为访问器名称第三个参数为成员的属性描述符

注意!ts不允许同时装饰一个成员的get和set访问器。一个成员的所有装饰器必须应用于文档顺序的第一个访问器上。因为装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。

该装饰器的使用方法和方法装饰器一致,因为getter,setter本质也是一对方法。

元数据和reflect-metadata

本文只对TS的装饰器进行讲解,元数据reflect-metadata简单进行普及即可。

元数据概念

元素据是用来描述数据的数据。

例如,一张照片,照片本身是数据。而元数据就是照片的大小,分辨率,拍摄地等描述这张照片的数据。

Reflect-metadata

reflect-metadata是ES7的一个提案,目前还没有实现。现在可以通过reflect-metadata这个库手动引入这个特性。
提案链接:https://rbuckton.github.io/reflect-metadata/
github: https://github.com/rbuckton/reflect-metadata#api

API声明:

namespace Reflect {
  // 用于装饰器
  function metadata(metadataKey: any, metadataValue: any): {
        (target: Function): void;
        (target: Object, propertyKey: string | symbol): void;
    };
  
  // 在对象或属性上面定义元数据
  function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void;
  function defineMetadata(metadataKey: any, metadataValue: any, target: Object, propertyKey: string | symbol): void;
  
  // 检查对象或属性的原型链上是否存在元数据
  function hasMetadata(metadataKey: any, target: Object): boolean;
  function hasMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): boolean;
  
  // 检查对象或属性的原型链上是否存在自己的元数据
  function hasOwnMetadata(metadataKey: any, target: Object): boolean;
  function hasOwnMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): boolean;
  
  // 获取对象或属性上的元数据键的元数据值
  function getMetadata(metadataKey: any, target: Object): any;
  function getMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): any;
  
  // 获取对象或属性上自己的元数据键的元数据值
  function getOwnMetadata(metadataKey: any, target: Object): any;
  function getOwnMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): any;
  
  // 获取对象或属性原型链上的所有元数据键
  function getMetadataKeys(target: Object): any[];
  function getMetadataKeys(target: Object, propertyKey: string | symbol): any[];
  
  // 获取对象或属性的所有自己的元数据键
  function getOwnMetadataKeys(target: Object): any[];
  function getOwnMetadataKeys(target: Object, propertyKey: string | symbol): any[];
  
  // 从对象或属性中删除元数据
  function deleteMetadata(metadataKey: any, target: Object): boolean;
  function deleteMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): boolean;
}

// 需要在 tsconfig.json 配置的开关:
{
    "experimentalDecorators": true, 
    "emitDecoratorMetadata": true,     
} 

有好奇的小伙伴可能有一个疑问,这元数据到底是存放在哪里?会是一个普通的map么?

猜对了一半,存储也是按照常会的key value对,但是是使用weak map来存储,这样既可以保存数据,又不会影响数据源本身。

总结 装饰器很擅长在不破坏原有代码结构的情况下,为其扩展功能。装饰器配合metadata可以实现很多强大的功能。

本文都是作者基于官方文档和各路大神及其自身实践整理出来的。文档中如果有错误的地方请各位指正~

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/941524.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-17
下一篇 2022-05-17

发表评论

登录后才能评论

评论列表(0条)

保存