手写TS装饰器之@JsonProperty 优雅实现前后端数据自由转换
"前端后端,真是分久必合、合久必分呢"
在前后端联调接口的过程中,开发人员经常会遇到如下情况:
-
字段命名不一致。例如同样表示 结束时间,后端接口返回
collect_end_time
,而前端习惯命名endTime
-
字段命名风格不一致。参考:
collect_end_time
和endTime
,后端习惯使用 下划线 风格,而前端更倾向于 驼峰 风格 -
字段值存在转换关系。例如
collect_end_time
和endTime
,后端要求存ms
单位的时间戳,而前端喜欢使用s
单位下的时间进行计算
所以在这种情况下,获取数据时经常要将后端接口返回的 json 遍历成前端所需要 js obj,而发送数据时,前端要经常将 js obj 遍历成后端所接受的 json 格式。可见,每次从接口获取数据以及发送数据时,都要将数据遍历一番才能得到各端青睐的数据格式。
前端开发人员的预期是,1、可以通过 fromJson
方法将后端返回的数据转成前端使用的数据格式;2、通过 toJson
方法将前端的格式转成后端接口所需要的数据格式。因此我们基于装饰器功能,实现 @JsonProperty
以及 JsonConverter
基类。
定义接口 IJsonConverter
IJsonConverter
接口应该包括两个功能
-
toJson
将 js obj 转成 json -
fromJson
将 json 转成 js obj。
interface IJsonConverter<O, J> {
/**
* 将js obj转成json格式
*/
toJson(obj: O): J;
/**
* 将json转成js obj格式
*/
fromJson(json: J): O;
}
实现类属性装饰器 @JsonProperty
@JsonProperty
属性装饰器的定位在于收集json
/js obj
数据结构中的参数命名以及转换关系。(不熟悉装饰器的小伙伴可参考 Decorators)。
收集参数命名与转换关系的逻辑涉及到一个重要的方法 Reflect.getMetadata
与 Reflect.defineMetadata
,具体用法参考API说明。
import 'reflect-metadata';
const keyForJsonProperty = Symbol.for('$JsonProperty$');
export function JsonProperty<O, J>(jsonName: string, converter?: IJsonConverter<O, J>) {
return function (target: Object, propertyName: string) {
const value = {
...Reflect.getMetadata(keyForJsonProperty, target),
[propertyName]: {
name: jsonName,
fn: converter?.toJson,
},
[jsonName]: {
name: propertyName,
fn: converter?.fromJson,
},
};
Reflect.defineMetadata(keyForJsonProperty, value, target);
};
NOTE:
-
jsonName
是 json 中的字段命名(如例子中的collect_end_time
),propertyName
是 js obj 中的字段命名 (如例子中的endTime
) -
target
这里指向的是类的prototype
属性 -
@JsonProperty
收集的映射关系就是:1、propertyName
=>jsonName
以及转换关系,2、jsonName
=>propertyName
以及转换关系 -
解构
Reflect.getMetadata(keyForJsonProperty, target)
的原因是@JsonProperty
会用于不同的属性上,因此不能覆盖已经收集的映射关系 -
需要注意的一点是,
jsonName
应保证在当前类的范围内是唯一的,即同一类中不同属性对应的jsonName
不能重复
实现 JsonConverter
基类
JsonConverter
基类实现 IJsonConverter
接口,
export class JsonConverter<O = unknown, J = unknown> implements IJsonConverter<O, J> {
public toJson(obj: O): J {
const collectedMetaGroup = Reflect.getMetadata(keyForJsonProperty, this);
if (Array.isArray(obj)) {
return obj.map(o => this.convert<O, J>(o, collectedMetaGroup)) as unknown as J;
}
return this.convert<O, J>(obj, collectedMetaGroup) as J;
}
public fromJson(json: J): O {
const collectedMetaGroup = Reflect.getMetadata(keyForJsonProperty, this);
if (Array.isArray(json)) {
return json.map(j => this.convert<J, O>(j, collectedMetaGroup)) as unknown as O;
}
return this.convert<J, O>(json, collectedMetaGroup) as O;
}
private convert<X, Y>(value: X, collectedMetaGroup: Record<string, unknown>): Y {
return Reflect.ownKeys(value).reduce((acc, key) => {
const collectedMeta = collectedMetaGroup?.[key];
if (collectedMeta === undefined || collectedMeta === null) {
// 未使用JsonProperty收集依赖,说明toJson/formJson字段命名一直
acc[key] = value[key] as unknown as Y;
return acc;
}
const { name, fn } = collectedMeta;
acc[name] = (fn?.(value[key]) ?? value[key]) as unknown as Y;
return acc;
}, {} as unknown as Record<string | symbol, Y>) as unknown as Y;
}
}
测试子类
定义 TestClass
子类实现 ITestClass
,并继承 JsonConverter
基类
interface ITestClass {
createdTime: string;
updatedTime: string;
endTime: number;
creator: string;
results: IResultData[];
user: IUserInfo;
}
export class TestClass extends JsonConverter<ITestClass> implements ITestClass {
@JsonProperty('created_time')
createdTime: string;
@JsonProperty('updated_time')
updatedTime: string;
@JsonProperty<number, number>('collect_end_time', {
toJson: time => time * 1000,
fromJson: time => ~~(time / 1000),
})
endTime: number;
creator: string; // 前后端的字段命名一致,且无须转换关系
@JsonProperty<IResultData>('result_data', {
toJson: ResultData.prototype.toJson.bind(ResultData.prototype),
fromJson: ResultData.prototype.fromJson.bind(ResultData.prototype),
})
results: IResultData[];
@JsonProperty<IUserInfo>('user_info', {
toJson: user => UserInfo.prototype.toJson(user),
fromJson: user => UserInfo.prototype.fromJson(user),
})
user: IUserInfo;
}
以其中 collect_end_time
与 endTime
为例:
-
后端接口返回字段
collect_end_time
,且为下划线风格,而前端使用endTime
承接并使用驼峰命名; -
从
collect_end_time
到endTime
,要使用s
做单位,故fromJson
:time => ~~(time / 1000)
-
从
endTime
到collect_end_time
,要使用ms
做单位,故toJson
:time => time * 1000
其中,
interface IUserInfo {
name: string;
nick: string;
}
export class UserInfo extends JsonConverter<IUserInfo> implements IUserInfo {
@JsonProperty('real_name')
name: string;
nick: string;
}
interface IResultData {
id: string;
content: string;
author: string;
submittedTime: string;
}
export class ResultData extends JsonConverter<IResultData> implements IResultData {
id: string;
@JsonProperty('data')
content: string;
author: string;
@JsonProperty('created_time')
submittedTime: string;
}
测试 TestClass
describe('测试TestClass', () => {
const tc = new TestClass();
it('fromJson', () => {
const obj = tc.fromJson({
created_time: '2022-11-07T09:01:39.056Z',
updated_time: '2022-11-08T09:01:39.056Z',
collect_end_time: 1667811721198,
creator: 'RyanOnCloud',
result_data: [{
id: '1',
data: '123',
author: 'RyanOnCloud1',
created_time: '2022-11-01T09:01:39.056Z',
}, {
id: '2',
data: '234',
author: 'RyanOnCloud2',
created_time: '2022-11-02T09:01:39.056Z',
}],
user_info: {
real_name: 'RyanOnCloud',
nick: '大哈哈',
},
});
expect(obj).toEqual({
createdTime: '2022-11-07T09:01:39.056Z',
updatedTime: '2022-11-08T09:01:39.056Z',
endTime: 1667811721,
creator: 'RyanOnCloud',
results: [{
id: '1',
content: '123',
author: 'RyanOnCloud1',
submittedTime: '2022-11-01T09:01:39.056Z',
}, {
id: '2',
content: '234',
author: 'RyanOnCloud2',
submittedTime: '2022-11-02T09:01:39.056Z',
}],
user: {
name: 'RyanOnCloud',
nick: '大哈哈',
},
});
});
it('toJson', () => {
const json = tc.toJson({
createdTime: '2022-11-07T09:01:39.056Z',
updatedTime: '2022-11-08T09:01:39.056Z',
endTime: 1667811721,
creator: 'RyanOnCloud',
results: [{
id: '1',
content: '123',
author: 'RyanOnCloud1',
submittedTime: '2022-11-01T09:01:39.056Z',
}, {
id: '2',
content: '234',
author: 'RyanOnCloud2',
submittedTime: '2022-11-02T09:01:39.056Z',
}],
user: {
name: 'RyanOnCloud',
nick: '大哈哈',
},
});
expect(json).toEqual({
created_time: '2022-11-07T09:01:39.056Z',
updated_time: '2022-11-08T09:01:39.056Z',
collect_end_time: 1667811721000,
creator: 'RyanOnCloud',
result_data: [{
id: '1',
data: '123',
author: 'RyanOnCloud1',
created_time: '2022-11-01T09:01:39.056Z',
}, {
id: '2',
data: '234',
author: 'RyanOnCloud2',
created_time: '2022-11-02T09:01:39.056Z',
}],
user_info: {
real_name: 'RyanOnCloud',
nick: '大哈哈',
},
});
});
});

参考
转载自:https://juejin.cn/post/7165480530794774558