前言
实现了个能满足题目要求的小插件,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)
以上就是Typescript使用装饰器实现接口字段映射与Mock实例的详细内容,更多关于Typescript字段映射Mock的资料请关注脚本之家其它相关文章!