Typescript怎么使用装饰器实现接口字段映射与Mock

寻技术 JS脚本 2023年12月14日 72

本篇内容主要讲解“Typescript怎么使用装饰器实现接口字段映射与Mock”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Typescript怎么使用装饰器实现接口字段映射与Mock”吧!

需求

最核心的问题就是要达到:接口字段的修改不能影响项目中实际使用的字段,无论是字段名的修改还是类型的修改

这里考虑使用装饰器附带额外信息,主要是接口字段信息,与需要转换的类型

既然可以转换类型了,考虑把字段 “翻译” 功能加上

既然能转换了,能就再加个 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: ''
}

关闭

用微信“扫一扫”