• ts的装饰器


    ts的装饰器

    ts的装饰器(Decorators)Spring注解(Annotation) 看起来很像。他们的设计理念都来自装饰器模式.

    装饰器模式
    在不改变对象自身结构的前提下,向对象添加新的功能

    首先通过下面2种方式打开装饰器支持.

    1. 命令行开启
    tsc --target ES5 --experimentalDecorators
    
    • 1
    1. 配置tsconfig.json文件

    执行tsc --init命令生成tsconfig.json配置文件后将experimentalDecorators设置为true

    {
      "compilerOptions": {
        "target": "ES5",
         /* Enable experimental support for legacy experimental decorators. */
        "experimentalDecorators": true 
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    1.装饰器

    装饰器的本质是函数,通过@expression的方式使用。

    装饰器分为5类:

    装饰器种类
    Class Decorators
    Method Decorators
    Accessor Decorators
    Property Decorators
    Parameter Decorators

    虽然很想马不停蹄的介绍接下来几个装饰器,但是在此之前不得不先介绍一下装饰器工厂

    1.1 装饰器工厂

    装饰器工厂同样本质上也是1function.装饰器工厂返回的表达式,供装饰器在运行时调用.

    function color(value: string) { // 这是一个装饰器工厂
        return function (target) { //  这是装饰器
            // do something with "target" and "value"...
        }
    }
    
    @color('blue')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    看上去是不是和注解一模一样?在装饰器工厂变量target依据装饰器type的不同可能是

    1. the constructor function of the class for a static member
    2. the prototype of the class for an instance member.

    装饰器工厂这样的写法用到了柯里化编程.

    Currying是一种编程技术,它可以将一个固定数量参数的函数转换为一个链式调用的函数,允许其参数分阶段提供。例如,add(x,y,z) 会允许它像 add(x)(y)(z)add(x)(y,z) 等方式进行调用

    // A curried function
    let add = (x: number) => (y: number) => (z: number) => z + x + y;
    
    // Simple usage
    console.log(add(1)(4)(2));// Prints "7"
    
    // partially applied
    let add123 = add(123);
    // fully apply the function
    console.log(add123(456)(1000));// Prints "1579"
    
    // 非箭头函数形式
    function currying(x: number) {
        console.log('curry X');
        return function (y: number) {
            console.log('curry Y');
            return function (z: number) {
                console.log('curry Z');
                return x + z + y
            }
        }
    }
    console.log(currying(1)(2)(3));// Prints "6"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    最后的展开形式看起来是不是跟装饰器工厂一模一样?

    好了,终于到了介绍装饰器的环节😋

    2. Class Decorators

    类装饰器表达式会在运行时当作function被调用,类的constructor作为其唯一的参数

    如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明.这也是需要extends的原因.

    function decorator<T extends { new (...args: any[]): {} }>(constructor: T) {
      return class extends constructor {
        // 在这里可以添加新的属性或方法
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    下面用一个详细的例子来说明吧,选自Class Decorators

    function reportableClassDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
        console.log(constructor); // Prints "[class BugReport]"
        return class extends constructor {
            reportedUser = "J4ck";
            title = "Needs DARK mode"
        };
    }
    	
    @reportableClassDecorator
    class BugReport {
        type = "report";
        title: string;
    	// reportedUser!: string			
        constructor(t: string) {
            this.title = t;
        }
    }
    
    const bug = new BugReport("Needs dark mode");
    //只有在class definition中声明的属性才能直接调用
    console.log(bug.type); // Prints "report"
    
    // Note that the decorator _does not_ change the TypeScript type
    // and so the new property `reportingURL` is not known
    // to the type system:
    //Property 'reportedUser' does not exist on type 'BugReport'.
    console.log(bug.reportingURL);
    
    /* decorator不会改变类型,因此为了能够调用有2种方法 */
    /* 1.为该类添加该属性  */
    /* 2.该类推导为any类型突破 _类型限制_  */
    console.log((bug as any).reportedUser); // Prints "J4ck"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    3. Method Decorators

    TypeScript中,方法装饰器是一种特殊类型的声明,它能够被附加到类的方法上。方法装饰器表达式会在运行时当作函数被调用,需要传入以下3个参数

    参数类型是什么
    targetany对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
    propertyKeystring方法名
    descriptorPropertyDescriptor成员的属性描述符

    我觉得这个例子非常好,还能顺便聊一聊装饰器的执行顺序

    1. @expression(arg: type)先从外层到内层(如果是函数工厂的话)
    2. @expression时,是从内层到外层
    function fn(str: string) {
        console.log("求值装饰器:", str);
        return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
            console.log("应用装饰器:", str);
        };
    }
    
    function decorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("应用装饰器: decorator");
    }
    
    class T {
        @fn("first")
        @decorator
        @fn("second")
        method() { }
    }
    /*
    求值装饰器: first
    求值装饰器: second
    应用装饰器: second
    应用装饰器: decorator
    应用装饰器: first
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    4. Property Decorators

    属性装饰器能够被附加到类的property .属性装饰器表达式会在运行时当作函数被调用,需要传入以下2个参数:

    参数类型是什么
    targetany对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
    propertyKeystring属性名

    请注意,属性装饰器不能直接修改属性的值,修改的是原型中的值

    import "reflect-metadata"; // npm install reflect-metadata
    
    const formatMetadataKey = Symbol("format");
    
    function format(formatString: string) {
        return Reflect.metadata(formatMetadataKey, formatString);
    }
    function getFormat(target: any, propertyKey: string) {
        return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
    }
    
    class Greeter {
        @format("Hello, %s")
        greeting: string;
        constructor(message: string) {
            this.greeting = message;
        }
        greet() {
            let formatString = getFormat(this, "greeting");
            return formatString.replace("%s", this.greeting);
        }
    }
    
    let greet = new Greeter('J4ck')
    console.log(greet.greet());	//	Prints: Hello, J4ck
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    5. Parameter Decorators

    参数装饰器能够被附加到类的property .参数装饰器表达式会在运行时当作函数被调用,需要传入以下3个参数:

    参数类型是什么
    targetany对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
    propertyKeystring方法名
    parameterIndexnumber参数在函数参数列表中的索引

    参数装饰器属性装饰器很像,因为装饰器在类声明后就已经确定并调用了,而属性需要实例化后才能确定,参数则是需要在function调用时才能确定.

    function logParameter(target: any, propertyKey: string, parameterIndex: number) {
        console.log(`Parameter ${parameterIndex} of method ${propertyKey} of class ${target.constructor.name} was accessed.`);
    }
    
    class Greeter {
        greeting: string;
        constructor(message: string) {
            this.greeting = message;
        }
    
        setGreeting(@logParameter greeting: string) {
            this.greeting = greeting
        }
    }
    // Prints "Parameter 0 of method setGreeting of class Greeter was accessed."
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    同样是1个很好的例子,我稍微将其改了1下,来自于一起读透TS装饰器#小试牛刀:方法参数类型校验.不过这个小例子使用了Metadata,如果不熟悉的可以到第6章了解1下然后自行学习.

    type Validator = (value: unknown) => boolean;
    
    const isString = applyValidator((x) => typeof x === "string");
    const isNumber = applyValidator((x) => typeof x === "number");
    
    class Person {
        @validate
        saySomething(@isString a: any, @isNumber b: any) {
            console.log(`a: ${a} || b:  ${b}`);
        }
    }
    
    function applyValidator(validator: Validator) {
        return function (target: any, key: string, idx: number) {
            let validators: Validator[] = [];
            // validatorMap 用于收集不同方法参数校验器
            let validatorMap: Map<string, Validator[]> = Reflect.getMetadata(key, target)
            if (validatorMap) {
                if (validatorMap.has(key)) {
                    //validatorMap && key 都存在
                    validators = (validatorMap.get(key) as Validator[]);
                }
            } else {
                // validatorMap不存在_new_1个
                validatorMap = new Map<string, Validator[]>();
            }
            /*
            1. validatorMap不存在,刚new所以key不存在
            2. validatorMap存在,但该key不存在
            将新的检验器加入到数组中,数组第几项就对应第几个参数的校验器
            出于简化目的,假设每一个参数最多只能有一个校验器
             */
            validators[idx] = validator;
            validatorMap.set(key, validators);
            //将validatorMap通过defineMetadata重新赋值回去
            Reflect.defineMetadata(key, validatorMap, target);
        };
    }
    
    function validate(target: any, key: string, descriptor: PropertyDescriptor) {
        const origin = descriptor.value;
        descriptor.value = function (...args: unknown[]) {
            // 获取validatorMap元数据
            let validatorMap: Map<string, Validator[]> = Reflect.getMetadata(key, target)
            //   如果该方法不需要校验,则直接运行原方法
            if (!validatorMap.has(key)) {
                return origin.apply(this, args);
            }
            let validators = validatorMap.get(key);
            //先对方法的每一个参数进行校验,遇到不符合规则的情况,直接报错
            if (validators) {
                validators.forEach((validator, idx) => {
                    if (!validate) {
                        return;
                    }
                    if (!validator(args[idx])) {
                        throw new TypeError(`Type validate failed for parameterIndex\[${idx}\]:${args[idx]}`);
                    }
                });
            }
            // 所有校验通过后再运行原方法
            return origin.apply(this, args);
        };
    }
    
    
    // Prints  "a: str || b:  12"
    new Person().saySomething("str", 12); 
    
    // Prints	"Type validate failed for parameterIndex[0]:12"
    new Person().saySomething(12, 12);
    
    // Prints	"Type validate failed for parameterIndex[1]:other str"
    new Person().saySomething("str", "other str");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    6. Metadata

    Metadata

    1. 命令行下载
    npm install reflect-metadata
    
    • 1
    1. 开启Metadata支持

    命令行开启

    tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
    
    • 1

    配置tsconfig.json开启

    {
      "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Metadata的项目地址👉reflect-metadata


    TypeScript Reference Decorators

    TypeScript手册指南装饰器

    一起读透TS装饰器

    柯里化 | 深入理解 TypeScript

  • 相关阅读:
    在 Rocky 中使用 FreeRDP 远程连接 Windows 机器
    HDLbits exercises 2 (MODULES节选题)
    网 络 编 程
    分销模式为用户解决哪些痛点?
    记录一次网站首页被刷量的经历,对网站页面静态化处理的思考和尝试
    数字孪生技术在智慧城市应用的推进建议
    move_base代码解析(一)MoveBase::executeCb
    快速入手node.js
    Kafka集群环境的部署
    常用、热门、免费的API接口应有尽有...
  • 原文地址:https://blog.csdn.net/qq_40710190/article/details/133434158