最近在开发一个DDS shapes demo的日志记录功能,是一个比较简单的需求。在同事的提醒下,发现使用装饰器(Decorator)可以比较优雅的实现此功能,在此之前对于装饰器也没有太多的了解,因此特意记录一些在学习使用中了解到的知识点。
装饰器是什么
装饰器是一个在代码运行时动态添加功能的方式,它可以用来修改类或函数的行为,在其他的语言中比较常见,ECMAScript目前处于stage3 的阶段,typescript中也有对应的实现。但目前装饰器还没有完全定案,各版差异可以参看:
它通常用于:
- 扩展已有的类或函数功能;
- 修改类或函数的属性;
- 将类或函数转化为不同的形式,例如,将类转化为单例模式。
装饰器使用 @expression 的形式使用,expression 是一个函数,会在运行时被调用,被装饰的数据会作为参数传入到这个函数中。
示例
下面是一个示例:
function addLogFunction(cls) {
cls.prototype.log = function(msg) {
console.log(`[${new Date().toISOString()}] ${msg}`);
};
return cls;
}
@addLogFunction
class MyClass {
constructor() {}
}
const myObj = new MyClass();
myObj.log('hello');
这是一个简单的日志记录功能,通过扩展类的原型增加了类的功能,可以在不侵入函数内部结构的情况扩展函数的功能。除了加到类或者函数上,他还可以加在类的属性上。
但是上面装饰器其实有一种缺点,就是除了传入要装饰的数据之外,装饰器本身的功能不能通过传参去自定义,比如我们有一个接口管理类:
export class ShapesDemoDDSManger implements DDSManager {
public participantId: number;
private domainId = 0;
private shapeDataType: TopicDataType = 'Shape';
...
// 创建Publisher
public async createPublisher(
data: PubSubDataModel,
moveSpeed: number,
rotateSpeed: number,
fillKind: number,
): Promise<void> {
data.participant = this.participantId;
const pubId = await this.server.createPublisher(data);
const publisher = new ShapesDemoPublisher();
publisher.fillKind = fillKind;
publisher.rotateSpeed = rotateSpeed;
publisher.moveSpeed = moveSpeed;
publisher.config = data;
publisher.id = pubId;
this.shapesPublishers.push(publisher);
this.onPublisherAddEmitter.fire(publisher);
}
// 暂停所有Publisher
pauseAllPub(): void {
this.publishers.forEach((publisher) => {
this.privateTogglePubState(publisher.id, false);
});
}
}
里面有很多很多的接口方法,但是每一个方法的功能并不一致,我们如果只有一个addLogFunction的装饰器方法,没法知晓这个函数究竟是干什么的。因此,想要通过传参自定义装饰器的功能,我们可以使用装饰器工厂。
export class ShapesDemoDDSManger implements DDSManager {
public participantId: number;
private domainId = 0;
private shapeDataType: TopicDataType = 'Shape';
...
// 创建Publisher
@log("创建Publisher")
public async createPublisher(
data: PubSubDataModel,
moveSpeed: number,
rotateSpeed: number,
fillKind: number,
): Promise<void> {
data.participant = this.participantId;
const pubId = await this.server.createPublisher(data);
const publisher = new ShapesDemoPublisher();
publisher.fillKind = fillKind;
publisher.rotateSpeed = rotateSpeed;
publisher.moveSpeed = moveSpeed;
publisher.config = data;
publisher.id = pubId;
this.shapesPublishers.push(publisher);
this.onPublisherAddEmitter.fire(publisher);
}
// 暂停所有Publisher
@log("暂停所有Publisher")
pauseAllPub(): void {
this.publishers.forEach((publisher) => {
this.privateTogglePubState(publisher.id, false);
});
}
}
export const log = (methodDesc: string, needArgs?: string[] | number[]) => {
return (target: any, name: string, descriptor: PropertyDescriptor) => {
const original = descriptor.value;
if (typeof original === 'function') {
descriptor.value = function (...args: any[]) {
const argsRes: string[] = [];
if (needArgs) {
needArgs.forEach((arg) => {
const res = findKeyValue(args, arg);
argsRes.push(`${arg}: ${res}`);
});
}
const log: logContent = {
time: new Date().toLocaleString(),
desc: methodDesc,
args: argsRes,
};
eventBus.emit(`${LOG_EVENT}`, log);
try {
const result = original.apply(this, args);
return result;
} catch (e) {
console.log(`Error from ${name}: ${e}`);
throw e;
}
};
}
return descriptor;
};
};
这样,饰器工厂通过 @log(methodDesc) 形式使用,装饰器工厂中的 log会返回一个装饰器函数methodDesc是用户想自定义传入的参数(也可以增加其他更多的参数)。
装饰器分类
类装饰器
类装饰器在类声明之前被声明(紧靠着类声明),它应用于类构造函数,可以用来监视,修改或替换类定义。
function decorator(target: any, name: string, descriptor: PropertyDescriptor) {
console.log(target);
}
@decorator
class Animal {
name = 'cat';
}
其中的三个参数分别是
- target:被装饰的类的构造函数,在这里就是指Animal的构造函数;
- name:类的名称,即"Animal";
- descriptor:类的描述对象。
方法/属性装饰器
方法装饰器修饰类的方法或属性,扩展其功能或修改其属性。
function readonly(target, name, descriptor){
descriptor.writable = false
return descriptor;
}
function log(target, name, descriptor){
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
console.log(`Function ${name} called with ${args}`);
const result = originalMethod.apply(this, args);
return result;
};
return descriptor
}
class Animal {
@readonly
name() { return 'PeiQi' }
@log
eat() { console.log("ice cream") }
}
其中的三个参数分别是
- target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
- name:方法/属性的名称,即"name"和"eat";
- descriptor:类的描述对象。
参数装饰器
参数装饰器用来修饰函数的参数,这个我们在theia的源码中经常会出现。
@injectable()
export class ShapesDemoFrontendContribution
extends MultiAbstractViewContribution<ShapesDemoWidget>
implements FrontendApplicationContribution, CommandContribution, MenuContribution
{
constructor(@inject(ThemeService) protected readonly themeService: ThemeService) {
super({
widgetId: ShapesDemoWidget.ID,
widgetName: ShapesDemoWidget.LABEL,
defaultWidgetOptions: {area: 'main'},
toggleCommandId: ShapesDemoCommand.id,
});
}
}
其中的三个参数分别是
- target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- name:成员的名字。
- index:参数在函数参数列表中的索引。
参数装饰器只能用来监视一个方法的参数是否被传入,返回值会被忽略。
遇到的问题
日志记录功能是有了,但是现在又发现一个新的问题,我们的shapes demo可以同时打开多个tabs,每一个tab都是相互独立的,即每一个tab的shapes demo都生成了一个dds-manager的实例化对象,我们的日志记录当然也要对每一个shapes demo进行区分,因此,在日志记录过程中我们需要拿到ShapesDemoDDSManger实例的uid:
private readonly uid = UUID.uuid4();
根据这个uid我们展示不同的日志集,但是在用装饰器时,却犯了难,因为拿不到这个uid。
为啥呢?
上文中我们有提到,装饰器函数的第一个函数通常是类的构造函数或原型对象。是因为装饰器的本意是要“装饰”类的实例,但是在编译这个类时,实例还没生成,所以只能去装饰原型;而这个uid明显就是实例化后才生成的,没法传入装饰器函数。
最后,我又只能退回去,不使用装饰器的方式来进行日志记录,在dds-manager中增加了一个私有方法,还是侵入了函数内部来记录:
export class ShapesDemoDDSManger implements DDSManager {
// 记录日志功能
private logFormatter(methodDesc: string, needArgs?: Array<logArgs>) {
const argsRes: string[] = [];
if (needArgs) {
needArgs.forEach((arg) => {
argsRes.push(`${arg.key}: ${arg.value}`);
});
}
const log: logContent = {
time: new Date().toLocaleString(),
desc: methodDesc,
args: argsRes,
};
eventBus.emit(`${LOG_EVENT}-${this.id}`, log);
}
// 调整Subscriber状态
toggleSubState(id: number, canSub: boolean): void {
// 日志记录
this.logFormatter('调整Subscriber状态', [
{key: 'topicName', value: this.getSubById(id)?.topicName || ''},
{key: 'status', value: canSub ? '恢复' : '暂停'},
]);
if (canSub) {
this.server.resumeSubscribeSubData(id);
} else {
this.server.stopSubscribeSubData(id);
}
}
}
资料参考:一文搞懂 ts 装饰器 - 掘金
更多的装饰器运用,可以参看:JS装饰器,看这一篇就够了 - 掘金