目录
- 前言
- 背景
- 需求
- 设计
- Decorator
- Transform
- Object and Array
- Mock
- 使用
- 安装
- 属性装饰器
- @mapperProperty(apiField, type)
- @deepMapperProperty (apiField, Class)
- @filterMapperProperty(apiField, filterFunc)
- 方法
- deserialize(Clazz, json)
- deserializeArr(Clazz, list)
- mock(Clazz, option)
- 使用示例
- 后记
前言
实现了个能满足题目要求的小插件,type-json-mapper,对如何实现不感兴趣的小伙伴可以直接跳到 使用 。文中代码只是示例代码,只为讲清原理,源码已经开源 github.com/LuciferHuan…,欢迎 star 😉
背景
一个前端项目稳定运行一段时间以后。
突然有一天,后端同学找到你,告诉你原先的 Student.name 要改成 Student.fullName,你一遍遍去查代码,查找 Student.name → 修改 → 自测,确保修改不会有问题。
终于,你成功把 Student.name 都改成了 Student.fullName。
然而,没过几天,某个一直正常的功能突然不能使用了,你开始调试,发现原先接口一直返回整数类型的 age 字段突然变成字符串类型了,你找到后端,后端同学来了一句 “前端不做检验吗?”
卑微~~~~~~
下次会改什么字段,下次哪个字段类型又会出问题,想想都孩怕,难道没有一劳永逸的办法能解决这个问题吗?
当然有,有点 oop 编程语言基础的,马上就会想到,这不就是加个 adapter 的事吗,很多语言都内置 adaapter,but,找了一圈发现没有能实现类似功能的插件(难道我姿势不对???)
算了,不找了(懒了),干脆自己造个轮子
需求
最核心的问题就是要达到:接口字段的修改不能影响项目中实际使用的字段,无论是字段名的修改还是类型的修改
这里考虑使用装饰器附带额外信息,主要是接口字段信息,与需要转换的类型
既然可以转换类型了,考虑把字段 “翻译” 功能加上
既然能转换了,能就再加个 Mock 吧,摆脱开发过程中对后端接口的依赖
设计
语言:typescript 构建工具:rollup 自动化测试:jest 代码规范:eslint + prettier 提交规范:commitlint
Decorator
首先,我们需要一个对象
是这个对象 {}
class Lesson {
public name: string;
public teacher: string;
public datetime: string;
public applicants: number;
public compulsory: boolean;
constructor() {
this.name = "";
this.teacher = "";
this.datetime = "";
this.compulsory = false;
}
}
上面的代码,就是我们构造出的 Lesson 类,它的属性字段就是我们会在项目中实际使用的字段
现在我们需要把这个类的属性字段与接口返回的字段对应上,这时候就需要用到 装饰器 了,随便取个名字,我这里是用 mapperProperty
,接收两个参数,第一个是接口返回的字段名,第二个是期望最终得到的类型(不是接口字段本身的类型)
class Lesson {
@mapperProperty("ClassName", "string")
public name: string;
@mapperProperty("TeacherName", "string")
public teacher: string;
@mapperProperty("DateTime", "datetime")
public datetime: string;
@mapperProperty("ApplicantNumber", "int")
public applicants: number;
@mapperProperty("Compulsory", "boolean")
public compulsory: boolean;
constructor() {
this.name = "";
this.teacher = "";
this.datetime = "";
this.date = "";
this.time = "";
this.compulsory = false;
}
}
如上面的代码,我们给每个属性字段都加上了装饰器,并告知了接口中对应的字段名称,以及我们希望得到的类型。 例如代码中的 applicants 字段,对应了接口中的 ApplicantNumber 字段,无论接口返回的是字符串还是数值类型,我们都希望最终得到的是 int 类型(指代整数)的数据
接下来要把接口字段名称与我们期望得到的类型先缓存起来
这里我们借助 Reflect Metadata
实现缓存
示例代码如下
function mapperProperty(apiField, type) {
Reflect.metadata("key", {
apiField, // 接口字段名
type, // 期望类型
});
}
Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据;我们使用 reflect-metadata
来模拟该功能
Transform
有了接口字段名与期望的类型,接下来的转换就简单了
第一步,先读取上一步缓存的元数据信息
const instance = new Lesson();
const meta = Reflect.getMetadata("key", instance, "applicants");
console.log(meta);
这里的 key 即元数据的键,上面的代码是读取 Lesson 类中 applicants 字段的元数据,meta 打印的结果如下
{
apiField: 'ApplicantNumber',
type: 'int'
}
第二步,转换
function deserialize(clazz, json) {
const instance = new clazz();
const meta = Reflect.getMetadata("key", instance, "applicants");
const { apiField, type } = meta;
const ori = json[apiField]; // json 为接口返回的数据
let value;
switch (type) {
case "int":
value = parseInt(ori, 10);
break;
// 其它类型转换
}
// 后续处理
}
到这基本就实现了最核心的能力,只要愿意可以扩展更多类型,欢迎一起来完善
Object and Array
对象与数组的转换与基本类型的转换大差不差,这里我将对象、数组的装饰器命名为 deepMapperProperty
,只需将第二个参数的类型,改为接收一个类即可
示例代码如下
function deepMapperProperty(apiField, clazz) {
Reflect.metadata("key", {
apiField, // 接口字段名
clazz, // 子级
});
}
取值方式同上,不再赘述了,只需改一下转换的代码
转换对象的示例代码如下,递归调用一下即可
const { clazz } = meta;
if (clazz) {
value = deserialize(clazz, value);
}
数组则直接使用 map 遍历
function deserializeArr(clazz, list) {
return list.map((ele) => deserialize(clazz, ele));
}
Mock
模拟数据部分,是直接返回的前端项目中使用的字段,而非修改接口字段的返回值
实现模拟数据拢共分三步:
与转换同样的步骤,要先读取字段的期望类型,这里只需要类型即可
遍历读取类中各个字段的元数据,得到各个字段的期望类型
根据期望类型使用不同的随机函数,生成相应类型的数据,这里我封装了三种类型的随机函数
- 获取随机整数
- 获取随机字符串
- 获取随机小数
针对对象与数组特殊处理
- 对象:这个简单,老规矩,递归解决
- 数组:数组需要先随机生成一下数组长度,再使用 map 遍历,递归调用一下 mock 函数
使用
安装
npm i type-json-mapper
属性装饰器
内置三种类属性装饰器:
@mapperProperty(apiField, type)
基本数据类型使用该装饰器
接收两个参数:
- apiField:接口字段名
- type:字段转换类型(可选值:string | int | flot | boolean | date | time | datetime)
@deepMapperProperty (apiField, Class)
对象/数组使用该装饰器
接收两个参数:
- apiField:接口字段名
- Class:类
@filterMapperProperty(apiField, filterFunc)
自定义过滤器(翻译)使用该装饰器
接收两个参数:
- apiField:接口字段名
- filterFunc:自定义过滤器函数
const filterFunc = (value) => {
return "translated text";
};
方法
deserialize(Clazz, json)
反序列化 json 对象
- Clazz:类
- json:接口返回的对象数据
deserializeArr(Clazz, list)
反序列化数组
- Clazz:类
- list:接口返回的数组数据
mock(Clazz, option)
生成模拟数据
- Clazz:类
- option:mock 配置
mock 配置
名称 | 类型 | 描述 | 默认值 |
fieldLength | Object | 字段长度 | - |
arrayFields | string[] | 数组类型字段 | - |
fieldLength
数据类型 | length 含义 |
string | 字符串长度 |
int | 最大整数 |
float | 字符长度(保留两位小数) |
例:
class Student {
@mapperProperty("StudentID", "string")
public id: string;
@mapperProperty("StudentName", "string")
public name: string;
@mapperProperty("StudentAge", "int")
public age: number;
@mapperProperty("Grade", "float")
public grade: number;
constructor() {
this.id = "";
this.name = "";
this.age = 0;
this.grade = 0;
}
}
mock(Student, { fieldLength: { age: 20, grade: 4, name: 6 } });
/**
* age: 20 表示随机生成的 age 字段的范围在 1 ~ 20 之间
* grade: 4 表述随机生成的 grade 字段是两位整数加两位小数的形式,共4个数字字符(如:23.33)
* name: 6 表述将随机生成长度为 6 的随机字符串
*/
使用示例
这里预先造了几个类,并给类属性加上了装饰器
import {
mapperProperty,
deepMapperProperty,
filterMapperProperty,
} from "type-json-mapper";
class Lesson {
@mapperProperty("ClassName", "string")
public name: string;
@mapperProperty("Teacher", "string")
public teacher: string;
@mapperProperty("DateTime", "datetime")
public datetime: string;
@mapperProperty("Date", "date")
public date: string;
@mapperProperty("Time", "time")
public time: string;
@mapperProperty("Compulsory", "boolean")
public compulsory: boolean;
constructor() {
this.name = "";
this.teacher = "";
this.datetime = "";
this.date = "";
this.time = "";
this.compulsory = false;
}
}
class Address {
@mapperProperty("province", "string")
public province: string;
@mapperProperty("city", "string")
public city: string;
@mapperProperty("full_address", "string")
public fullAddress: string;
constructor() {
this.province = "";
this.city = "";
this.fullAddress = "";
}
}
// 状态映射关系
const stateMap = { "1": "读书中", "2": "辍学", "3": "毕业" };
class Student {
@mapperProperty("StudentID", "string")
public id: string;
@mapperProperty("StudentName", "string")
public name: string;
@mapperProperty("StudentAge", "int")
public age: number;
@mapperProperty("StudentSex", "string")
public sex: string;
@mapperProperty("Grade", "float")
public grade: number;
@deepMapperProperty("Address", Address)
public address?: Address;
@deepMapperProperty("Lessons", Lesson)
public lessons?: Lesson[];
@filterMapperProperty("State", (val: number) => stateMap[`${val}`])
public status: string;
@filterMapperProperty("Position", (val: number) => stateMap[`${val}`])
public position: string;
public extra: string;
constructor() {
this.id = "";
this.name = "";
this.age = 0;
this.sex = "";
this.grade = 0;
this.address = undefined;
this.lessons = undefined;
this.status = "";
this.position = "";
this.extra = "";
}
}
以下是接口返回的数据:
const json = [
{
StudentID: "123456",
StudentName: "李子明",
StudentAge: "10",
StudentSex: 1,
Grade: "98.6",
Address: {
province: "广东",
city: "深圳",
full_address: "xxx小学三年二班",
},
Lessons: [
{
ClassName: "中国上下五千年",
Teacher: "建国老师",
DateTime: 1609430399000,
Date: 1609430399000,
Time: 1609430399000,
Compulsory: 1,
},
{
ClassName: "古筝的魅力",
Teacher: "美丽老师",
DateTime: "",
},
],
State: 1,
Position: 123,
extra: "额外信息",
},
{
StudentID: "888888",
StudentName: "丁仪",
StudentAge: "18",
StudentSex: 2,
Grade: null,
Address: {
province: "浙江",
city: "杭州",
full_address: "xxx中学高三二班",
},
Lessons: [],
State: 2,
},
];
开始转换,因接口返回的是数组,这里使用 deserializeArr
import { deserializeArr } from "type-json-mapper";
try {
const [first, second] = deserializeArr(Student, json);
console.log(first);
console.log(second);
} catch (err) {
console.error(err);
}
输出结果如下
// first
{
id: "123456",
name: "李子明",
age: 10,
sex: "1",
grade: 98.6,
address: { province: "广东", city: "深圳", fullAddress: "xxx小学三 年二班" },
lessons: [
{
name: "中国上下五千年",
teacher: "建国老师",
datetime: "2020-12-31 23:59:59",
date: "2020-12-31",
time: "23:59:59",
compulsory: true,
},
{
name: "古筝的魅力",
teacher: "美丽老师",
datetime: "",
date: undefined,
time: undefined,
compulsory: undefined,
},
],
status: "读书中",
position: 123,
extra: "额外信息",
};
// second
{
id: "888888",
name: "丁仪",
age: 18,
sex: "2",
grade: null,
address: { province: "浙江", city: "杭州", fullAddress: "xxx中学高三二班" },
lessons: [],
status: "辍学",
position: undefined,
extra: undefined,
};
如果后端接口还没开发完成,我们还可以直接 mock
import { mock } from "type-json-mapper";
const res = mock(Student, {
fieldLength: { age: 20, grade: 4, name: 6 },
arrayFields: ["lessons"],
});
console.log(res);
输出结果如下
{
id: 'QGBLBA', name: 'KTFH6d',
age: 4,
sex: 'IINfTm',
grade: 76.15,
address: { province: 'qvbCte', city: 'DbHfFZ', fullAddress: 'BQ4uIL' },
lessons: [
{
name: 'JDtNMx',
teacher: 'AeI6hB',
datetime: '2023-2-18 15:00:07',
date: '2023-2-18',
time: '15:00:07',
compulsory: true
},
{
name: 'BIggA8',
teacher: '8byaId',
datetime: '2023-2-18 15:00:07',
date: '2023-2-18',
time: '15:00:07',
compulsory: false
},
{
name: 'pVda1n',
teacher: 'BPCmwa',
datetime: '2023-2-18 15:00:07',
date: '2023-2-18',
time: '15:00:07',
compulsory: false
}
],
status: '',
position: '',
extra: ''
}
后记
虽然要多维护一套类,看似麻烦(确实很麻烦😂),但是加强了代码的健壮性,摆脱对接口的依赖;最重要的是堵住了后端的嘴(bushi)