客户管理系统开发定制通过本文你可以知道什么
- 客户管理系统开发定制装饰器的发展历程
- 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的时间段,这是因为在 -Conf上,angular团队宣布与TS团队进行合作。
JS装饰器和TS装饰器
js原生目前不支持装饰器,装饰器提案在stage-3阶段,只能通过babel体验装饰器这个新特性。TS目前实现的装饰器是基于JS装饰器stage-1的语法,所以在JS装饰器正式发布后,会和TS装饰器语法产生差异,之后看TS团队如何处理了,但预计也不是近期的事情了。
定义
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。装饰器使用@expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息作为参数传入。
定义来自:
配置
由于装饰器目前还是实验中的特定,在js中处于stage-3阶段。在ts中已经作为一项实验性予以支持。开启装饰器需要在tsconfig.json文件中启用 experimentalDecorators 编译器选项。
装饰器于2022年三月底刚进入了stage-3阶段,详情见
中的装饰器
我们在使用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 {}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
这样三个类都可以调用getPosition
方法来获取各自的位置了。到目前为止看起来没什么问题。
现在又有了一个新的诉求,Tank 类和Plane类需要一个新的方法addPetrol
来给坦克和飞机加油。而动物不需要加油。此时这种写法好像不能继续进行下去了。而js目前没有直接语法提供多继承的功能,我们的继承方式好像行不通了。这时候装饰器可以很完美的实现这样的功能。此时就可以请我们的装饰器闪亮登场了~
装饰器功能之——能力扩展
我们把getPosition
和addPertrol
都抽象成一个单独的功能,它们得作用是给宿主扩展对应的功能。
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@getPositionDecoratorclass Tank {}@addPetrolDecorator@getPositionDecoratorclass Plane {}@getPositionDecoratorclass Animal {}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
这样的话,加入日后我们有其他的猫猫狗狗,都可以对他进行能力扩展,让其具有加油
的能力。
多个装饰器叠加的时候,执行顺序为离被装饰对象越近的装饰器越先执行。
装饰器功能之——重载构造函数
在类装饰器中如果返回一个值,它会使用提供的构造函数来替换类的声明。
function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) { return class extends constructor { newProperty = "new property"; hello = "override"; }}@classDecoratorclass Greeter { property = "property"; hello: string; constructor(m: string) { this.hello = m; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
这个一个官方的例子,暂时没有想到业务中的适用场景。
方法装饰器
方法装饰器也是非常常用的,(敲黑板)这道题去年没考,今年肯定考~
方法装饰器接受三个参数:
- 对于静态方法,第一个参数为类的构造函数。对于实例方法,为类的原型对象。
- 第二个参数为方法名。
- 第三个参数为方法描述符。
- 方法装饰器可以有返回值,返回值会作为方法的属性描述符
装饰器功能之——能力增强
我们带代码编写时候,经常会做一些错误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('凤凰传奇');
- 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
有一个音乐系统,可以进行音乐播放。在播放时候,如果未找到对应的歌,会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}的音乐,删除失败`); } }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
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('凤凰传奇');
- 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
我们定义了一个错误捕获装饰器,名为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() { }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
同样的,也可以直接对descriptor进行修改。
descriptor.value = () => {console.log('eat方法被替换')};descriptor.writable = true;descriptor.enumerable = true;descriptor.configurable = true;
- 1
- 2
- 3
- 4
方法装饰器的使用方式很多,大多数的使用方式是对descriptor的value属性进行替换,拦截等实现功能。
【下边的三个装饰器类型,相对来说使用比较少,有兴趣的小伙伴可以卷】
属性装饰器
属性装饰器接受两个参数
- 对于静态属性,第一个参数为类的构造函数。对于实例属性,参数为类的原型对象
- 第二个参数为属性名称
返回值将被忽略
网上有很多教程在使用属性装饰器时候,使用defineProperty
对属性设置getter和setter,这是非常错误的用法!!官方文档已经明确说明了不能使用属性装饰器类监听和修改属性。
装饰器功能之——初始化属性
const initCarPropertyDec = <T>(property: T) => { return (target: object, propertyKey: string | symbol) => { target[propertyKey] = property; }}class Car { @initCarPropertyDec('奔驰') name!: string;}console.log(new Car().name)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
属性装饰器还有一个更为常用的功能,配合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) { }}
- 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
以上demo有一个单点登录类,其中login
方法必须传入username
和password
。我们使用参数装饰器,当函数未传入指定类型数据时候进行报错。
参数装饰器基本是用于对参数进行验证,并自定义报错信息,在ts作用较小。
访问器装饰器
接受三个参数
- 对于静态成员,第一个参数为类的构造函数。对于实例方法,为类的原型对象。
- 第二个参数为访问器名称
- 第三个参数为成员的属性描述符
注意!ts不允许同时装饰一个成员的get和set访问器。一个成员的所有装饰器必须应用于文档顺序的第一个访问器上。因为装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。
该装饰器的使用方法和方法装饰器一致,因为getter,setter本质也是一对方法。
元数据和reflect-metadata
本文只对TS的装饰器进行讲解,元数据
和reflect-metadata
简单进行普及即可。
元数据概念
元素据是用来描述数据的数据。
例如,一张照片,照片本身是数据。而元数据就是照片的大小,分辨率,拍摄地等描述这张照片的数据。
Reflect-metadata
reflect-metadata是ES7的一个提案,目前还没有实现。现在可以通过reflect-metadata这个库手动引入这个特性。
提案链接:
github:
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, }
- 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
有好奇的小伙伴可能有一个疑问,这元数据到底是存放在哪里?会是一个普通的map么?
猜对了一半,存储也是按照常会的key value对,但是是使用weak map
来存储,这样既可以保存数据,又不会影响数据源本身。
总结
- 装饰器很擅长在不破坏原有代码结构的情况下,为其扩展功能。
- 装饰器配合
metadata
可以实现很多强大的功能。
本文都是作者基于官方文档和各路大神及其自身实践整理出来的。文档中如果有错误的地方请各位指正~