ts 装饰器学习总结2

第2次学习装饰器记录内含实际案例

使用装饰器

tsc --target ES5 --experimentalDecorators

tsconfig.json

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

1.类装饰器

类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)。

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

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

基本用法


function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
        //继承装饰的class 这里定义的方法及属性会添加到下去
        name = '自定义的值'
        //自定义添加的方法
        test() {
            return '自定义添加的方法和' + this.name 
        }
        
    }
}

@classDecorator
class Test {
}

console.log(new Test().test());// 自定义添加的方法和自定义的值

使用示例

import axios, { AxiosRequestConfig } from "axios";
// 类装饰器
const requestDecorator = (baseURL) => <T extends { new(...args: any[]): {} }>(constructor: T) => {


    return class extends constructor {
		//post 请求封装
        post(url, data = {}, config?: AxiosRequestConfig) {
            return axios({
                method: 'POST',
                url: url,
                data: data,
                baseURL: baseURL,
                ...config,
            })
        }
        //get 请求封装
        get(url, data = {}, config?: AxiosRequestConfig) {
            return axios({
                method: 'GET',
                url: url,
                params: data,
                baseURL: baseURL,
                ...config,
            })

        }

        //继承装饰的class 这里定义的方法及属性会添加到下去
    }
}


@requestDecorator(' https://www.baidu.com/')
class ClassTest {
    [x: string]: any; 
    async getData() { 
        let res = await this.get('/s', {
            wd: 'axios'
        })
        console.log(res.statusText)//OK
        return res
    }
 


}

new ClassTest().getData()

2.方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
3.成员的属性描述符。

如果方法装饰器返回一个值,它会被用作方法的属性描述符。

注意  如果代码输出目标版本小于ES5返回值会被忽略。

基本用法

const methodsDecorator = (name: string) => (target: any, methodsName: string, desc: PropertyDescriptor) => {
    const _value = desc.value; 
    //函数覆盖
    desc.value = function () {
            //在这里你可以定义你想要的操作 
            _value.apply(this, arguments) 
    } 
} 


class Test {
    @methodsDecorator()
    test(num) {
      
    }
}


console.log(new Test().test(123))

错误捕获示例

const error = 
(params = `函数name出错了\n参数数组:parms\n错误信息:error\n`) =>
 (_, methodsName: any, desc: PropertyDescriptor) => {
    const newValue = function (...parms: any[]) { // 覆盖封装函数并添加 try catch 
        try {
            return desc.value.apply(this, parms) // 借用封装的函数并返回结果,记得写 return
        } catch (error) { 
            const errorText = params
                .replace('name', methodsName)
                .replace('parms', JSON.stringify(parms))
                .replace('error', JSON.stringify( error instanceof Error ? { message: error.message, name: error.name } : error ))
            console.log(errorText) 
           // 函数test出错了 
		//参数数组:["参数1","参数2"] 
       //错误信息:{"message":"错误捕获示例","name":"Error"} 
        }
    }
    return {
        value: newValue
    }
}



class Test {  
    @error() 
    test() { 
        throw new Error('错误捕获示例')

     }
}


new Test().test('参数1','参数2')

3.访问器装饰器

访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。

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

访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
3.成员的属性描述符。

如果访问器装饰器返回一个值,它会被用作方法的属性描述符。


function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }
	//设置成不可配置
    @configurable(false)
    get x() { return this._x; }

    @configurable(false)
    get y() { return this._y; }
}


let res = new Point(1,2)

res.x = 12 // 不允许你改这个属性


4.属性装饰器

属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。

属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:

1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。

1.返回值被忽略。
2.因此,属性描述符只能用来监视类中是否声明了某个名字的属性。

基本用法

//属性装饰器
const attributeDecorator = () => (target: any, key: string) => {
    //在这里你可以做一套非常复杂的计算 出默认属性并设置下去
    target[key] = '默认属性' 
}


class Point {  
    @attributeDecorator()
    private x: number; 
}

console.log(new Point().x)

5.参数装饰器

参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
3.参数在函数参数列表中的索引。

参数装饰器的返回值会被忽略。
参数装饰器必须要和方法装饰器配合使用才有意义

效果

class Test {
    @testMethods()
    test(@func(Number) num, @func(String) str) {

        console.log(typeof num) //参数转化成 number
        console.log(typeof str) //参数转化成 string
    }
   
}

new Test().test('123', 22222)

装饰器定义

import "reflect-metadata";
//参数装饰器
const func = (params: any) => (target: any, method: string, index: number) => { 
    let res = Reflect.getMetadata(method, target) || []
    res.push({ name: params, index: index })
    Reflect.defineMetadata(method, res, target) 
}


//方法装饰器
const testMethods = (params?: any) => (target: any, methodsName: any, desc: any) => {
    let _value = desc.value;
    let list = Reflect.getMetadata(methodsName, target) 
    desc.value = function () {
        let param = Array.from(arguments) 
        for (const { name, index } of list) {

            if (typeof name === 'function') {
                param[index] = name(param[index])
            } else {
                param[index] = name
            }

        }
        _value.apply(this, param)

    }


}


元数据

1.元数据 主要是使用 reflect-metadata 这个库来实现的
2.主要作用是在class 中传递数据给不同的装饰器使用

npm i reflect-metadata --save

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

在全局js 中引入 reflect-metadata 会挂载 Reflect 到全局

import "reflect-metadata"; 

1.Reflect.getMetadata(“design:paramtypes”, target, key) //获取函数参数类型
2.Reflect.getMetadata(“design:returntype”, target, key) //返回值类型
3.Reflect.getMetadata(“design:type”, target, key) // 类型

示例

function Prop(): PropertyDecorator {
  return (target, key: string) => {
    const type = Reflect.getMetadata('design:type', target, key);
    console.log(`${key} type: ${type.name}`);
    // other...
  };
}

class SomeClass {
  @Prop()
  public Aprop!: string;
}

自定义

除能获取类型信息外,常用于自定义 metadataKey,并在合适的时机获取它的值,示例如下:

function classDecorator(): ClassDecorator {
  return target => {
    // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
    Reflect.defineMetadata('classMetaData', 'a', target);
  };
}

function methodDecorator(): MethodDecorator {
  return (target, key, descriptor) => {
    // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
    Reflect.defineMetadata('methodMetaData', 'b', target, key);
  };
}

@classDecorator()
class SomeClass {
  @methodDecorator()
  someMethod() {}
}

Reflect.getMetadata('classMetaData', SomeClass); // 'a'
Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'

例子:控制反转和依赖注入

type Constructor<T = any> = new (...args: any[]) => T;

const Injectable = (): ClassDecorator => target => {};

class OtherService {
  a = 1;
}

@Injectable()
class TestService {
  constructor(public readonly otherService: OtherService) {}

  testMethod() {
    console.log(this.otherService.a);
  }
}

const Factory = <T>(target: Constructor<T>): T => {
  // 获取所有注入的服务
  const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
};

Factory(TestService).testMethod(); // 1

Controller 与 Get 的实现

如果你在使用 TypeScript 开发 Node 应用,相信你对 Controller、Get、POST 这些 Decorator,并不陌生:

@Controller('/test')
class SomeClass {
  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

  @Post('/b')
  somePostMethod() {}
}

这些 Decorator 也是基于 Reflect Metadata 实现,这次,我们将 metadataKey 定义在 descriptor 的 value 上:

const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';

const Controller = (path: string): ClassDecorator => {
  return target => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
  }
}

const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
  }
}

const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');

接着,创建一个函数,映射出 route:

function mapRoute(instance: Object) {
  const prototype = Object.getPrototypeOf(instance);

  // 筛选出类的 methodName
  const methodsNames = Object.getOwnPropertyNames(prototype)
                              .filter(item => !isConstructor(item) && isFunction(prototype[item]));
  return methodsNames.map(methodName => {
    const fn = prototype[methodName];

    // 取出定义的 metadata
    const route = Reflect.getMetadata(PATH_METADATA, fn);
    const method = Reflect.getMetadata(METHOD_METADATA, fn);
    return {
      route,
      method,
      fn,
      methodName
    }
  })
};

因此,我们可以得到一些有用的信息:

Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'

mapRoute(new SomeClass());

/**
 * [{
 *    route: '/a',
 *    method: 'GET',
 *    fn: someGetMethod() { ... },
 *    methodName: 'someGetMethod'
 *  },{
 *    route: '/b',
 *    method: 'POST',
 *    fn: somePostMethod() { ... },
 *    methodName: 'somePostMethod'
 * }]
 *
 */

最后,只需把 route 相关信息绑在 express 或者 koa 上就 ok 了。

参考资料

1.ts 官网
2.深入理解 TypeScript

上一篇:js reflect


下一篇:JS中无处不在的元编程