TS - 装饰器与注解
Typescript装饰器模式,可以有效的提高开发效率,就像Java中使用注解一样,装饰器让TypeScript的世界更友好。 我们使用的许多库都基于这一强大特性构建, 例如Angular和Nestjs。 在这文章中我将介绍装饰器和它的许多细节。 我希望在读完这篇文章后,你可以掌握何时和如何使用这一强大的特性
你可能在前端项目中很少见过注解形式,这是有一定原因的,一方面可能就是见得少,另一个读完文章你就会明白
文章首发公众号,扫码关注获取更多优质内容
概念
装饰器本身就是一种特殊的函数,被用于类的各个属性(类本身、类属性、类方法、类访问器、类方法的参数),装饰器就像高阶函数一样,对目标做了一层中间操作,可以很简洁的无痛修改一些功能和做一些有趣的功能
一个小例子:
// 日志打印(只做代码演示,运行时机有出入) function logger(key: string): any { return function () { console.log("call: ", key); }; } class HTTP { @logger("get") static get(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中两个独立的部分。 然而,如果你想实现像是反射这样的能力,你总是同时需要它们。有了reflect-metadata的帮助, 我们可以获取编译期的类型。
借助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) // 声明式定义元数据 class TestMeta { @Reflect.metadata(metadataKey, metadataValue) name: string; getUser(@Reflect.metadata(metadataKey, metadataValue) name: string): number { return 0; } } // 命令式定义 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方法 type Consturctor = { new (...args: any[]): any }; function toString<T extends Consturctor>(target: T): T { return class extends target { public toString() { return JSON.stringify(this); } }; } @toString class Car { 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"; // 这里需要借助一个反射库 type Constructor = { new (...args: any): any } // 用来管理所有可注入的service const services: Map<Constructor, Constructor> = new Map(); // 注入装饰器 function Inject<T extends Constructor>(target: T) { services.set(target, target); } // 获取注入的service装饰器 function Service(target: Record<string|symbol, any>, key: string | symbol) { const service = services?.get(Reflect.getMetadata("design:type", target, key)); service && (target[key] = new service()); } // 常量装饰器 function constant(value: any) { return function (target: object, key: string) { Object.defineProperty(target, key, { enumerable: false, configurable: false, get() { return value; }, set() { return value; }, }); }; } // 用户相关Service // 让当前service变成可注入 @Inject class UserService { // 模拟获取用户信息 public getUser(...args: any) { return new Promise((resolve, reject) => { setTimeout(() => { resolve("user") }, 1000) }) } } // 用户页面 class UserPage extends React.Component{ @Service // 注入UserService public service: UserService; // 让BASE_URL变成常量 @constant(process.env.BASE_URL || 'https://www.baidu.com') private _BASE_URL: string; public getUser() { console.log(this.service) return this.service?.getUser({ BASE_URL: this._BASE_URL }) } render() { return <> <button onClick={this.getUser}>获取用户信息</button> </> } }
上面点击获取用户信息按钮就会去请求用户的信息,打印结果如下
方法装饰器
类型:
Type MethodDecorator = (target: Record<string|symbol, any>, prop: string | symbol, descriptor: PropertyDescriptor) => PropertyDescriptor | void
参数:
- @target:对于静态成员是类构造器,对实例成员是原型链
- @prop:属性名称
- @descriptor:属性描述器
- @return:属性描述器或不返回
用途:
方法装饰器可以高阶目标方法,做一些参数转换,数据收集等等,如日志收集
// 接着上面的介绍,做一个日志收集logger // 日志收集 function logger(target: object, key: string, descriptor: PropertyDescriptor) { const origin = descriptor.value; descriptor.value = async function (...args: any[]) { try { const start = +new Date(); const rs = await origin.call(this, ...args); const end = +new Date(); // 打印请求耗时 console.log(`@logger: ${key} api request spend`, `${end - start}ms.`); // 这里可以做一些相关收集... return rs; } catch (err) { console.log(err); } }; } @Inject class UserService { @logger // 登录Request async postLogin(username: string) { const time = Math.floor(Math.random() * 10 * 1000); const rs = await new Promise((resolve, reject) => { setTimeout(() => { if (Math.floor(Math.random() * 26) % 10) { resolve(username + ' logined success...'); } else { reject('error'); } }, time); }); return rs; } } // 用户页面 class UserPage extends React.Component{ @Service // 注入UserService public service: UserService; public async toLogin(username: string) { return await this.service?.postLogin(username); } render() { return <> <button onClick={() => this.toLogin('Mr Ming')}>登录</button> </> } }
上面点击登录后会记录请求耗时,控制台打印的结果如下:
访问器装饰器
- 访问器装饰器和方法装饰器差不多,唯一不同的就是key不同
- 方法装饰器:value、wriable、enumerable、configurable
- 访问器装饰器:get、set、enumerate、configurable
用途:
// 不变装饰器 function immutable( target: any, propertyKey: string, descriptor: PropertyDescriptor, ) { descriptor.set = function (value: any) { return value; }; } // 私有属性装饰器 function toPrivate(target: object, key: string) { let _val: any = undefined; Object.defineProperty(target, key, { enumerable: false, configurable: false, get() { return _val; }, set(val) { _val = val; }, }); } // 用户页面 class UserPage extends React.Component{ @Service // 注入UserService public service: UserService; @toPrivate private _PORT: number = 3306; get PORT() { return this._PORT; } @immutable set PORT(port: number) { this._PORT = port; } render() { return <> <button onClick={() => (this.PORT = 2)}>改变PORT</button> </> } }
现在给PORT赋值,将不会改变_PORT的值,如下图:
参数装饰器
类型:
type ParamerDecorator = (target: Record<string|symbol, any>, prop: string | symbol, paramIdx: number) => void
参数:
- @target:对于实例属性是类的原型链,对于静态属性是类的构造器
- prop:属性名
- paramIdx:参数位置
用途:
参数装饰器单独使用很有限,一般结合其他装饰器一起使用;下面介绍一个验证器案例,用户可以自定义验证器也可以使用原始类型来验证参数是否正确:
// 参数装饰器 type ParamerDecorator = ( target: Record<string | symbol, any>, prop: string | symbol, paramIdx: number, ) => void; // 自定义验证器 type Validater = (...args: any) => boolean; const validatorStorage: 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'); // 源验证器或自定义参数验证器 function validator( target: object, prop: string, descriptor: PropertyDescriptor, ) { // 反射获取参数源类型 const typeMetaDatas: 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)) { throw new TypeError(errorMsg); } // 没有自定义验证器执行默认验证器 else if (!validate) { if (metaValidate !== arg?.constructor) { throw new TypeError(errorMsg); } } }); } // 执行源函数 return origin.call(this, ...args); }; } class LoginPage { // 省略前面代码... @validator public async toLogin(@isString username: string) { // 期望username为string return await this.service?.postLogin(username); } render() { return <> <button onClick={() => (this.toLogin(xxx)}>登录</button> </> } }
现在当点击登录时,调用toLogin方法,就会验证username的参数是否合法(当然前提是参数不能为空的,ts也是无法知道参数是否必填,只是来验证参数的类型罢了),打印如下:
当将isString代码改为 typeDecoratorFactory((str: object) => str?.name === ‘Mr Ming’);
当删除自定义验证器,再次执行会验证默认的类型验证器:
执行顺序
上面我们针对每个类型的装饰器分别做了介绍并作了相关的例子实验,现在我们可以同时使用不同的装饰器,测试一下不同类型装饰器的执行顺序是如何的:
// 记录 function trace(key: string): any { console.log('evaluate: \t', key); return function () { console.log('call: \t\t', key); }; } // 类装饰器 @trace('Class Decorator') class People { protected _name: string; @trace('Static Property _instance') public static _instance?: People; @trace('Instance Property grade') public grade: number; constructor(@trace('Constructor Parameter') name: string) { this._name = name; this.grade = 0; this.age = 0; } @trace('属性访问器') public get name() { return this._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') public age: number; @trace('Static Method getInstance') public static getInstance( @trace('Static Method Param name') name: string, ): People { if (!this._instance) { this._instance = new People(name); } return this._instance; } }
上面的打印结果如下:
从上面的结果可以得出结论:
装饰器访问顺序
- 实例属性:按定义从上往下 => 属性/方法(参数 -> 方法名)/属性访问器
- 静态属性:按定义从上往下 => 属性/方法(参数 -> 方法名)
- 构造器参数
- 类装饰器
当改变实例或者静态属性的定义顺序,响应的执行顺序也会按着从上往下定义的顺序执行,有兴趣的小伙伴可以动手试试
至此,有关typescript的装饰器的基本使用就介绍完了,相信看到这里的小伙伴也会对其有相关的了解,希望读完后自己动手实践
进阶与感悟
你是不是已经通过以上讲解对装饰器的各种想法已经蠢蠢欲动了,确实通过注解
形式可以很方便无痛做一些通用的功能,其基本的核心原理还是在于元数据的存取、然后通过拦截的形式修改或者附加一些额外功能
因此,使用装饰器我们需要准备一个容器来存放元数据;如果在一个大型项目中使用,元数据过多就会导致容器过大,资源加载的时间也就会变长,尤其是在前端页面这种加载时间是衡量用户体验的一种很关键指标。我们知道前端工程打包通常都会进行treeshaking来优化掉没有使用的代码来减小资源体积,而使用装饰器则必须将资源存放在容器中,这就导致treeshaking的指标降低,很明显二者是相悖的,因此解决这个矛盾就成了最关键的问题
那么如何解决这个矛盾呢?一方面我想用,另一方面我还想要性能,这世上哪有这么好的事情,当然要进行取舍
大家知道前端资源是动态加载的,尤其性能优化都会对首屏或者当前用不到的资源懒加载、并进行分包处理,但通用的逻辑还是会在第一时间加载。按照这种思路是不是容器也可以这样处理?或者说有个全局容器
、局部容器
将必要的通用的逻辑提取到全局的容器,而其他页面非全局通用的局部逻辑提取到局部容器,这样只有在局部容器的服务页面加载时再加载便会提高全局的速度;当然,加载逻辑程序设计往往需要深思熟虑才行,这里不过多介绍,大家自行发挥
实践
说了这么多,我们来个真实案例。简单概述就类似于Java中的实体类
,通过注解、反射实现ORM(类型校验、字段转换等等)。
假设有一个简单的表单页面,提交相关字段给后端,我们都会这么做:
function FormPage() { const [formData, setFormData] = useState({ name: '', email: '', }); function onSubmit() { console.log(formData); fetch(formData); } return () => <Form> <input type="text" value={formData.name}/> <input type="text" value={formData.email}/> <Button onClick={onSubmit}>提交</Button> </Form> }
以上是一个很简单的例子,当他的字段变得非常复杂且接口的字段名可能不一致或因为什么历史原因导致的,那么对于数据提交或者或者回显也会变得非常麻烦。这时候我们使用装饰器
模式可以很方便的解决这些问题:
- 假设还是以上页面
function FormPage() { // 参考下方 const [formData, setFormData] = useState(createEntity(User)); function onSubmit() { 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> }
- 定义用户表单的实体类:
@Entity() // 标识实体类 class UserEntity { @Transform({ from: 'u_name' }) name: string; @ToField('u_email') email: string; @ToJson<UserEntity, 'age'>((_, v) => v >> 0) age: number; @FieldType(Gender) gender: Gender; }
- 项目中使用装饰器的基建:
/** * 创建实体 * @param target 目标实体类 * @injectTarget Class * @returns 包装后的实体 */ export function Entity() { return function (target: Constructor): InstanceType<typeof target> { return class extends target { constructor(args: any = {}) { super(args); this.initFields(args); } private get isEntity() { return true; } protected initFields(args = {}) { const isInitialize = args.__initialize__; if (isInitialize) this.copyDataToEntity(args); else this.transformDataToEntity(args); } private copyDataToEntity(args: IDict = {}) { /** 省略 **/ } private transformDataToEntity(args: IDict = {}) { /** 省略 **/ } protected toJSON() { /** 省略 **/ } // 省略... }; }; } /** * 创建实体 * @param Entity 目标实体类 * @param initValue? 初始化值 * @param __initialize__? 是否初始化(初始化不执行实体内部逻辑,直接赋值) * @returns 实体 */ export function createEntity<T extends Constructor>( Entity: T, initValue: Partial<InstanceType<T>> = {}, __initialize__ = true ): InstanceType<T> { return new Entity({ ...initValue, __initialize__ }); }
由于代码篇幅过长,以上只简单的列举了一小部分使用,感兴趣的话可以 在线体验前端注解使用
文章首发公众号,扫码关注获取更多优质内容
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)