TS - 装饰器与注解

Typescript 装饰器模式,可以有效的提高开发效率,就像 Java 中使用注解一样,装饰器让 TypeScript 的世界更友好。 我们使用的许多库都基于这一强大特性构建, 例如Angular[1]和Nestjs[2]。 在这文章中我将介绍饰器和它的许多细节。 我希望在读完这篇文章后,你可以掌握何时和如何使用这一强大的特性

你可能在前端项目中很少见过注解形式,这是有一定原因的,一方面可能就是见得少,另一个读完文章你就会明白

概念

装饰器本身就是一种特殊的函数,被用于类的各个属性(类本身、类属性、类方法、类访问器、类方法的参数),装饰器就像高阶函数一样,对目标做了一层中间操作,可以很简洁的无痛修改一些功能和做一些有趣的功能

一个小例子:

// 日志打印(只做代码演示,运行时机有出入)
function logger(key: string): any {
  return function () {
    console.log("call: \t\t", key);
  };
}

class HTTP {
  @logger("get")
  staticget(url?: string) {
    return url;
  }
}
HTTP.get();  // 打印 call: get

上面简单的演示了调用get方法时打印 logger 的功能,只需要在指定的属性前方加上@logger即可,对原有的业务功能 0 侵入,这就是装饰器的强大,如果以传统的方式必然会在 get 内部写一些逻辑

💡 与 Java 注解的异同点:

  • • 共同点:

    • • 都是作为 AOP 编程范式的横切点

    • • 都是给目标的注入一些额外的元数据,方便扩展其他功能

    • • 都可以通过容器进行元数据自由存取

  • • 不同点:

  • •意义不同:装饰器只是一个函数去动态拦截原来的逻辑,本身不会附加任何元数据,而注解本身就是标记然后通过反射拿到相关数据

    • • 运行时机:Typescript 的装饰器函数只在运行时运行,而 Java 的注解在编译时会生成对应的元数据信息,运行时阶段通过反射获取对应目标

    • • 类型:Typescript 的装饰器在编译后类型信息会被抹去,运行时无法获取到对应的类型信息,除非使用reflect-matadata(本质也是存放数据而已);而 Java 的类型会存放在字节码中,因此 Java 的注解是强类型的、静态性强

环境配置

ts 中的装饰器是 ES 提案的一种实验性实现,使用它需要进行一些配置,在tsconfig.json中修改配置:

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

装饰器类别

ts 的装饰器只能用于类中,所以就有类、类方法、类属性、类访问器属性、类方法参数装饰器,接下来我们就一个个介绍

在介绍装饰器前,先介绍个工具库,在装饰器中反射往往发挥着很大作用,reflect-metadata库通常在装饰器中都会使用,接下来介绍下其基本作用

Reflect-Metadata

严格地说,元数据和装饰器是 EcmaScript 中两个独立的部分。 然而,如果你想实现像是反射[3]这样的能力,你总是同时需要它们。有了reflect-metadata[4]的帮助, 我们可以获取编译期的类型。
借助reflect-metadata,运行时默认可以拿到的类型有三种:

  • • design:type:属性类型

  • • design:paramtypes:方法的参数类型

  • • design:returntypes:方法返回值的类型

function 
GetPropertyMetaType() {
  return function(target: object, key: string) {
    return Reflect.getMetadata("design:type", target, key);
  }
}

function GetReturnMetaType() {
  return function(target: object, key: string, descriptor: PropertyDescriptor) {
    return Reflect.getMetadata("design:returntypes`", target, key);
  }
}

function GetParamMetaType() {
  return function(target: object, key: string, paramIdx: number) {return 
Reflect.getMetadata("design:paramtypes`", target, key);
  }
}

class TestMeta {
  @GetPropertyMetaType()
  name: string;

  @GetReturnMetaType()
  getUser(@GetParamMetaType() name: string): number {
    return 0;
  }
}

这三种方式拿到的结果都是构造函数(例如 String 和 Number)。规则是:

  • • number -> Number

  • • string -> String

  • • boolean -> Boolean

  • • void/null/never -> undefined

  • • Array/Tuple -> Array

  • • Class -> Construtor

  • • Enum -> 如果是纯数字枚举为 Number,否则为 Object

  • • Function -> function

  • • 其余都是 Object

除此之外还可以自定义一些其他的附加信息:

@Reflect.metadata(metadataKey, metadataValue) // 声明式定义元数据
classTestMeta {
  @Reflect.metadata(metadataKey, metadataValue)
  name: string;

  getUser(@Reflect.metadata(metadataKey, metadataValue) name: string): number {
    return0;
  }
}

// 命令式定义
Reflect.defineMetadata(metadataKey, metadataValue, TestMeta.prototype, "method");

// 获取元数据
let metadataValue = Reflect.getMetadata(metadataKey, ins, "method");

类装饰器

类型:

type 
ClassDecorator = <Func 
extends 
Function>(target: Func) => 
Func | void;

参数:

  • • @target:类的构造器

  • • @return:如果有值将会替代原有的类构造器的声明;或不返回值也可以修改原有的类

用途:

类装饰器可以继承现有类添加或修改一些属性或方法

// 扩展一个toString方法
typeConsturctor = { new (...args: any[]): any };

function toString<T extendsConsturctor>(target: T): T {
  return class extends target {
    public toString() {
       returnJSON.stringify(this);
    }
  };
}

@toString
classCar {
constructor(public prize: number, public name: string) {}
}

// ts不会智能的推导出toString方法
console.log(new 
Car(1000, "BMW").toString()); // {"prize":1000,"name":"BMW"}

属性装饰器

类型:

type 
PropertyDecorator = (target: Record<string|symbol, any>, prop: string | symbol) => 
any | void

参数:

  • • @target:对于实例属性成员是类的原型链,对于静态属性是类的构造器

  • • @prop:当前属性名称

  • • @return:返回的值将被忽略

用途:

属性装饰器可以收集信息,反射赋值,给类添加方法等等,下面介绍一个完整的例子

import 
"reflect-metadata"; // 这里需要借助一个反射库

typeConstructor = { new (...args: any): any }

// 用来管理所有可注入的service
constservices: Map<Constructor, Constructor> = newMap();

// 注入装饰器
functionInject<T extendsConstructor>(target: T) {
  services.set(target, target);
}
// 获取注入的service装饰器
functionService(target: Record<string|symbol, any>, key: string | symbol) {
const service = services?.get(Reflect.getMetadata("design:type", target, key));
  service && (target[key] = newservice());
}

// 常量装饰器
functionconstant(value: any) {
returnfunction (target: object, key: string) {
Object.defineProperty(target, key, {
enumerable: false,
configurable: false,
get() {
return value;
      },
set() {
return value;
      },
    });
  };
}

// 用户相关Service
// 让当前service变成可注入
@Inject
classUserService {
// 模拟获取用户信息
publicgetUser(...args: any) {
returnnewPromise((resolve, reject) => {
setTimeout(() => {
resolve("user")
      }, 1000)
    })
  }
}

// 用户页面
classUserPageextendsReact.Component{
@Service// 注入UserService
publicservice: UserService;

// 让BASE_URL变成常量
@constant(process.env.BASE_URL || 'https://www.baidu.com')
private_BASE_URL: string;

publicgetUser() {
console.log(this.service)
returnthis.service?.getUser({ BASE_URL: this._BASE_URL })
  }

render() {
return<>
      
<button 
onClick={this.getUser}>获取用户信息</button>
    
</>
  }
}

上面点击获取用户信息按钮就会去请求用户的信息,打印结果如下

f5c1f6511c1ed84c272f63714598223c.png

方法装饰器

类型:

Type 
MethodDecorator = (target: Record<string|symbol, any>,prop: string | symbol, descriptor: PropertyDescriptor) => 
PropertyDescriptor |  void

参数:

  • • @target:对于静态成员是类构造器,对实例成员是原型链

  • • @prop:属性名称

  • • @descriptor:属性描述器

  • • @return:属性描述器或不返回

用途:

方法装饰器可以高阶目标方法,做一些参数转换,数据收集等等,如日志收集

// 接着上面的介绍,做一个日志收集logger

// 日志收集
functionlogger(target: object, key: string, descriptor: PropertyDescriptor) {
const origin = descriptor.value;
  descriptor.value = asyncfunction (...args: any[]) {
try {
const start = +newDate();
const rs = await origin.call(this, ...args);
const end = +newDate();

// 打印请求耗时
console.log(`@logger: ${key} api request spend`, `${end - start}ms.`);

// 这里可以做一些相关收集...
return rs;
    } catch (err) {
console.log(err);
    }
  };
}

@Inject
classUserService {
@logger
// 登录Request
asyncpostLogin(username: string) {
const time = Math.floor(Math.random() * 10 * 1000);
const rs = awaitnewPromise((resolve, reject) => {
setTimeout(() => {
if (Math.floor(Math.random() * 26) % 10) {
resolve(username + ' logined success...');
        } else {
reject('error');
        }
      }, time);
    });
return rs;
  }
}

// 用户页面
classUserPageextendsReact.Component{
@Service// 注入UserService
publicservice: UserService;

publicasynctoLogin(username: string) {
returnawaitthis.service?.postLogin(username);
  }
  render() {
return<>
      
<button 
onClick={() => this.toLogin('Mr Ming')}>登录</button>
    
</>
  }
}

上面点击登录后会记录请求耗时,控制台打印的结果如下:

70d9575d36762a37a1a48658154a4632.jpeg

访问器装饰器

  • • 访问器装饰器和方法装饰器差不多,唯一不同的就是 key 不同

  • • 方法装饰器:value、wriable、enumerable、configurable

  • • 访问器装饰器:get、set、enumerate、configurable

用途:

// 不变装饰器
functionimmutable(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor,
) {
  descriptor.set = function (value: any) {
return value;
  };
}

// 私有属性装饰器
functiontoPrivate(target: object, key: string) {
let_val: any = undefined;
Object.defineProperty(target, key, {
enumerable: false,
configurable: false,
get() {
return _val;
    },
set(val) {
      _val = val;
    },
  });
}

// 用户页面
classUserPageextendsReact.Component{
@Service// 注入UserService
publicservice: UserService;

@toPrivate
private_PORT: number = 3306;
getPORT() {
returnthis._PORT;
  }
@immutable
setPORT(port: number) {
this._PORT = port;
  }

render() {
return<>
      
<button 
onClick={() => (this.PORT = 2)}>改变PORT</button>
    
</>
  }
}

现在给 PORT 赋值,将不会改变_PORT 的值,如下图:

49638438ec0e4029b720cf945c768bac.jpeg

参数装饰器

类型:

type 
ParamerDecorator = (target: Record<string|symbol, any>, prop: string | symbol, paramIdx: number) => 
void

参数:

  • • @target:对于实例属性是类的原型链,对于静态属性是类的构造器

  • • prop:属性名

  • • paramIdx:参数位置

用途:

参数装饰器单独使用很有限,一般结合其他装饰器一起使用;下面介绍一个验证器案例,用户可以自定义验证器也可以使用原始类型来验证参数是否正确:

// 参数装饰器
typeParamerDecorator = (
  target: Record<string | symbol, any>,
  prop: string | symbol,
  paramIdx: number,
) =>void;
// 自定义验证器
typeValidater = (...args: any) =>boolean;
constvalidatorStorage: Record<string | symbol, Validater[]> = {};
const typeDecoratorFactory = (validator: Validater): ParamerDecorator =>
(target, prop, idx) => {
const targetValidators = validatorStorage[prop] ?? [];
    targetValidators[idx] = validator;
    validatorStorage[prop] = targetValidators;
  };
const isString = typeDecoratorFactory((str: string) =>typeof str === 'string');
// 源验证器或自定义参数验证器
functionvalidator(
  target: object,
  prop: string,
  descriptor: PropertyDescriptor,
) {
// 反射获取参数源类型
consttypeMetaDatas: Function[] = Reflect.getMetadata(
'design:paramtypes',
    target,
    prop,
  );
const origin = descriptor.value;
  descriptor.value = function (...args: any[]) {
// 取到自定义验证器
const validators = validatorStorage[prop];

// 验证参数
if (args) {
      args.forEach((arg, idx) => {
// 自定义验证器
const validate = validators?.[idx];
// 源类型验证器
const metaValidate = typeMetaDatas?.[idx];
const errorMsg = `Failed for parameter: ${prop} at method of ${
          
JSON.stringify(target.constructor?.toString())?.match(
            /function (\w+)/i,
          )?.[1]
        }, expect 【${metaValidate.name?.toLowerCase()}】but 【${typeof arg}】`;
if (validate && !validate(arg)) {
thrownewTypeError(errorMsg);
        }
// 没有自定义验证器执行默认验证器
elseif (!validate) {
if (metaValidate !== arg?.constructor) {
thrownewTypeError(errorMsg);
          }
        }
      });
    }

// 执行源函数
return origin.call(this, ...args);
  };
}

classLoginPage {
// 省略前面代码...

@validator
publicasynctoLogin(@isString username: string) { // 期望username为string
returnawaitthis.service?.postLogin(username);
  }

render() {
return<>
      
<button 
onClick={() => (this.toLogin(xxx)}>登录</button>
    
</>
  }
}

现在当点击登录时,调用 toLogin 方法,就会验证 username 的参数是否合法(当然前提是参数不能为空的,ts 也是无法知道参数是否必填,只是来验证参数的类型罢了),打印如下:

5aae8462a4a009f5c2c4863165b26a04.jpeg

当将 isString 代码改为 typeDecoratorFactory((str: object) => str?.name === ‘Mr Ming’);

2799477301fe47ed7b0633865fdfb7e1.jpeg

当删除自定义验证器,再次执行会验证默认的类型验证器:

f71235ce792ab2d68906e057ed087740.jpeg

执行顺序

上面我们针对每个类型的装饰器分别做了介绍并作了相关的例子实验,现在我们可以同时使用不同的装饰器,测试一下不同类型装饰器的执行顺序是如何的:

// 记录
functiontrace(key: string): any {
console.log('evaluate: \t', key);
returnfunction () {
console.log('call: \t\t', key);
  };
}

// 类装饰器
@trace('Class Decorator')
classPeople {
protected_name: string;

@trace('Static Property _instance')
publicstatic _instance?: People;

@trace('Instance Property grade')
publicgrade: number;

constructor(@trace('Constructor Parameter') name: string) {
this._name = name;
this.grade = 0;
this.age = 0;
  }

@trace('属性访问器')
publicgetname() {
returnthis._name;
  }

@trace('Instance Method')
add(
@trace('Instance Method Param x') x: number,
@trace('Instance Method Param y') y: number,
  ): number {
return x + y;
  }

@trace('Instance Property age')
publicage: number;

@trace('Static Method getInstance')
publicstaticgetInstance(
@trace('Static Method Param name') name: string,
  ): People {
if (!this._instance) {
this._instance = newPeople(name);
    }
returnthis._instance;
  }
}

上面的打印结果如下:

eabce677b1306827d8e0f4be933524d7.jpeg

从上面的结果可以得出结论: 装饰器访问顺序

  1. 1. 实例属性:按定义从上往下 => 属性/方法(参数 -> 方法名)/属性访问器

  2. 2. 静态属性:按定义从上往下 => 属性/方法(参数 -> 方法名)

  3. 3. 构造器参数

  4. 4. 类装饰器

当改变实例或者静态属性的定义顺序,响应的执行顺序也会按着从上往下定义的顺序执行,有兴趣的小伙伴可以动手试试

至此,有关 typescript 的装饰器的基本使用就介绍完了,相信看到这里的小伙伴也会对其有相关的了解,希望读完后自己动手实践

进阶与感悟

你是不是已经通过以上讲解对装饰器的各种想法已经蠢蠢欲动了,确实通过注解形式可以很方便无痛做一些通用的功能,其基本的核心原理还是在于元数据的存取、然后通过拦截的形式修改或者附加一些额外功能

因此,使用装饰器我们需要准备一个容器来存放元数据;如果在一个大型项目中使用,元数据过多就会导致容器过大,资源加载的时间也就会变长,尤其是在前端页面这种加载时间是衡量用户体验的一种很关键指标。我们知道前端工程打包通常都会进行 treeshaking 来优化掉没有使用的代码来减小资源体积,而使用装饰器则必须将资源存放在容器中,这就导致 treeshaking 的指标降低,很明显二者是相悖的,因此解决这个矛盾就成了最关键的问题

那么如何解决这个矛盾呢?一方面我想用,另一方面我还想要性能,这世上哪有这么好的事情,当然要进行取舍

大家知道前端资源是动态加载的,尤其性能优化都会对首屏或者当前用不到的资源懒加载、并进行分包处理,但通用的逻辑还是会在第一时间加载。按照这种思路是不是容器也可以这样处理?或者说有个全局容器局部容器

将必要的通用的逻辑提取到全局的容器,而其他页面非全局通用的局部逻辑提取到局部容器,这样只有在局部容器的服务页面加载时再加载便会提高全局的速度;当然,加载逻辑程序设计往往需要深思熟虑才行,这里不过多介绍,大家自行发挥

实践

说了这么多,我们来个真实案例。简单概述就类似于 Java 中的实体类,通过注解、反射实现 ORM(类型校验、字段转换等等)。

假设有一个简单的表单页面,提交相关字段给后端,我们都会这么做:

function 
FormPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
  });

functiononSubmit() {
console.log(formData);

    
fetch(formData);
  }

return() =><Form>
    
<input 
type="text" 
value={formData.name}/>
    
<input 
type="text" 
value={formData.email}/>

    
<Button 
onClick={onSubmit}>提交</Button>
  
</Form>
}

以上是一个很简单的例子,当他的字段变得非常复杂且接口的字段名可能不一致或因为什么历史原因导致的,那么对于数据提交或者或者回显也会变得非常麻烦。这时候我们使用装饰器模式可以很方便的解决这些问题:

  1. 1. 假设还是以上页面

function 
FormPage() {
// 参考下方
const [formData, setFormData] = useState(createEntity(User));

functiononSubmit() {
console.log(formData);

fetch(resolveEntity(formData)); // 转换后表达数据
  }

return() =><Form>
    
<input 
type="text" 
value={formData.name}/>
    
<input 
type="text" 
value={formData.email}/>
    
<input 
type="text" 
value={formData.age}/>

    
<Button 
onClick={onSubmit}>提交</Button>
</Form>
}
  1. 1. 定义用户表单的实体类:

@Entity() // 标识实体类
classUserEntity {
@Transform({ from: 'u_name' })
name: string;

@ToField('u_email')
email: string;

@ToJson<UserEntity, 'age'>((_, v) => v >> 0)
age: number;

@FieldType(Gender)
gender: Gender;
}
  1. 1. 项目中使用装饰器的基建:

/**
 * 创建实体
 * @param target 目标实体类
 * @injectTarget 
Class
 * @returns 包装后的实体
 */
exportfunctionEntity() {
returnfunction (target: Constructor): InstanceType<typeof target> {
returnclassextends target {
constructor(args: any = {}) {
super(args);

this.initFields(args);
      }

privategetisEntity() {
returntrue;
      }

protectedinitFields(args = {}) {
const isInitialize = args.__initialize__;

if (isInitialize) this.copyDataToEntity(args);
elsethis.transformDataToEntity(args);
      }

privatecopyDataToEntity(args: IDict = {}) { /** 省略 **/ }

privatetransformDataToEntity(args: IDict = {}) { /** 省略 **/ }

protectedtoJSON() { /** 省略 **/ }

// 省略...
    };
  };
}


/**
 * 创建实体
 * @param Entity 目标实体类
 * @param initValue? 初始化值
 * @param __initialize__? 是否初始化(初始化不执行实体内部逻辑,直接赋值)
 * @returns 实体
 */
exportfunction createEntity<T extendsConstructor>(
Entity: T,
initValue: Partial<InstanceType<T>> = {},
  __initialize__ = true
): InstanceType<T> {
returnnewEntity({ ...initValue, __initialize__ });
}

由于代码篇幅过长,以上只简单的列举了一小部分使用,感兴趣的话可以 在线体验前端注解使用[5]

参考文档

  • • typescriptlang - decorators[6]

  • • microsoft - devblogs[7]

  • • reflect-metadata[8]

引用链接

[1] Angular:https://angular.io/
[2]Nestjs:https://nestjs.com/
[3]反射:https://zh.wikipedia.org/wiki/%E5%8F%8D%E5%B0%84_%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6
[4]reflect-metadata:https://github.com/rbuckton/reflect-metadata
[5]在线体验前端注解使用:https://case.usword.cn/#/test-ioc
[6]typescriptlang - decorators:https://www.typescriptlang.org/docs/handbook/decorators.html
[7]microsoft - devblogs:https://devblogs.microsoft.com/typescript/
[8]reflect-metadata: https://rbuckton.github.io/reflect-metadata/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值