Typescript使用裝飾器實(shí)現(xiàn)接口字段映射與Mock實(shí)例
前言
實(shí)現(xiàn)了個(gè)能滿足題目要求的小插件,type-json-mapper,對(duì)如何實(shí)現(xiàn)不感興趣的小伙伴可以直接跳到 使用 。文中代碼只是示例代碼,只為講清原理,源碼已經(jīng)開源 github.com/LuciferHuan…,歡迎 star ??
背景
一個(gè)前端項(xiàng)目穩(wěn)定運(yùn)行一段時(shí)間以后。
突然有一天,后端同學(xué)找到你,告訴你原先的 Student.name 要改成 Student.fullName,你一遍遍去查代碼,查找 Student.name → 修改 → 自測(cè),確保修改不會(huì)有問題。
終于,你成功把 Student.name 都改成了 Student.fullName。
然而,沒過幾天,某個(gè)一直正常的功能突然不能使用了,你開始調(diào)試,發(fā)現(xiàn)原先接口一直返回整數(shù)類型的 age 字段突然變成字符串類型了,你找到后端,后端同學(xué)來了一句 “前端不做檢驗(yàn)嗎?”
卑微~~~~~~
下次會(huì)改什么字段,下次哪個(gè)字段類型又會(huì)出問題,想想都孩怕,難道沒有一勞永逸的辦法能解決這個(gè)問題嗎?
當(dāng)然有,有點(diǎn) oop 編程語言基礎(chǔ)的,馬上就會(huì)想到,這不就是加個(gè) adapter 的事嗎,很多語言都內(nèi)置 adaapter,but,找了一圈發(fā)現(xiàn)沒有能實(shí)現(xiàn)類似功能的插件(難道我姿勢(shì)不對(duì)???)
算了,不找了(懶了),干脆自己造個(gè)輪子
需求
最核心的問題就是要達(dá)到:接口字段的修改不能影響項(xiàng)目中實(shí)際使用的字段,無論是字段名的修改還是類型的修改
這里考慮使用裝飾器附帶額外信息,主要是接口字段信息,與需要轉(zhuǎn)換的類型
既然可以轉(zhuǎn)換類型了,考慮把字段 “翻譯” 功能加上
既然能轉(zhuǎn)換了,能就再加個(gè) Mock 吧,擺脫開發(fā)過程中對(duì)后端接口的依賴
設(shè)計(jì)
語言:typescript 構(gòu)建工具:rollup 自動(dòng)化測(cè)試:jest 代碼規(guī)范:eslint + prettier 提交規(guī)范:commitlint
Decorator
首先,我們需要一個(gè)對(duì)象
是這個(gè)對(duì)象 {}
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;
  }
}
上面的代碼,就是我們構(gòu)造出的 Lesson 類,它的屬性字段就是我們會(huì)在項(xiàng)目中實(shí)際使用的字段
現(xiàn)在我們需要把這個(gè)類的屬性字段與接口返回的字段對(duì)應(yīng)上,這時(shí)候就需要用到 裝飾器 了,隨便取個(gè)名字,我這里是用 mapperProperty,接收兩個(gè)參數(shù),第一個(gè)是接口返回的字段名,第二個(gè)是期望最終得到的類型(不是接口字段本身的類型)
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;
  }
}
如上面的代碼,我們給每個(gè)屬性字段都加上了裝飾器,并告知了接口中對(duì)應(yīng)的字段名稱,以及我們希望得到的類型。 例如代碼中的 applicants 字段,對(duì)應(yīng)了接口中的 ApplicantNumber 字段,無論接口返回的是字符串還是數(shù)值類型,我們都希望最終得到的是 int 類型(指代整數(shù))的數(shù)據(jù)
接下來要把接口字段名稱與我們期望得到的類型先緩存起來
這里我們借助 Reflect Metadata 實(shí)現(xiàn)緩存
示例代碼如下
function mapperProperty(apiField, type) {
  Reflect.metadata("key", {
    apiField, // 接口字段名
    type, // 期望類型
  });
}
Reflect Metadata 是 ES7 的一個(gè)提案,它主要用來在聲明的時(shí)候添加和讀取元數(shù)據(jù);我們使用 reflect-metadata 來模擬該功能
Transform
有了接口字段名與期望的類型,接下來的轉(zhuǎn)換就簡(jiǎn)單了
第一步,先讀取上一步緩存的元數(shù)據(jù)信息
const instance = new Lesson();
const meta = Reflect.getMetadata("key", instance, "applicants");
console.log(meta);
這里的 key 即元數(shù)據(jù)的鍵,上面的代碼是讀取 Lesson 類中 applicants 字段的元數(shù)據(jù),meta 打印的結(jié)果如下
{
    apiField: 'ApplicantNumber',
    type: 'int'
}
第二步,轉(zhuǎn)換
function deserialize(clazz, json) {
  const instance = new clazz();
  const meta = Reflect.getMetadata("key", instance, "applicants");
  const { apiField, type } = meta;
  const ori = json[apiField]; // json 為接口返回的數(shù)據(jù)
  let value;
  switch (type) {
    case "int":
      value = parseInt(ori, 10);
      break;
    // 其它類型轉(zhuǎn)換
  }
  // 后續(xù)處理
}
到這基本就實(shí)現(xiàn)了最核心的能力,只要愿意可以擴(kuò)展更多類型,歡迎一起來完善
Object and Array
對(duì)象與數(shù)組的轉(zhuǎn)換與基本類型的轉(zhuǎn)換大差不差,這里我將對(duì)象、數(shù)組的裝飾器命名為 deepMapperProperty,只需將第二個(gè)參數(shù)的類型,改為接收一個(gè)類即可
示例代碼如下
function deepMapperProperty(apiField, clazz) {
  Reflect.metadata("key", {
    apiField, // 接口字段名
    clazz, // 子級(jí)
  });
}
取值方式同上,不再贅述了,只需改一下轉(zhuǎn)換的代碼
轉(zhuǎn)換對(duì)象的示例代碼如下,遞歸調(diào)用一下即可
const { clazz } = meta;
if (clazz) {
  value = deserialize(clazz, value);
}
數(shù)組則直接使用 map 遍歷
function deserializeArr(clazz, list) {
  return list.map((ele) => deserialize(clazz, ele));
}
Mock
模擬數(shù)據(jù)部分,是直接返回的前端項(xiàng)目中使用的字段,而非修改接口字段的返回值
實(shí)現(xiàn)模擬數(shù)據(jù)攏共分三步:
與轉(zhuǎn)換同樣的步驟,要先讀取字段的期望類型,這里只需要類型即可
遍歷讀取類中各個(gè)字段的元數(shù)據(jù),得到各個(gè)字段的期望類型
根據(jù)期望類型使用不同的隨機(jī)函數(shù),生成相應(yīng)類型的數(shù)據(jù),這里我封裝了三種類型的隨機(jī)函數(shù)
- 獲取隨機(jī)整數(shù)
 - 獲取隨機(jī)字符串
 - 獲取隨機(jī)小數(shù)
 
針對(duì)對(duì)象與數(shù)組特殊處理
- 對(duì)象:這個(gè)簡(jiǎn)單,老規(guī)矩,遞歸解決
 - 數(shù)組:數(shù)組需要先隨機(jī)生成一下數(shù)組長度,再使用 map 遍歷,遞歸調(diào)用一下 mock 函數(shù)
 
使用
安裝
npm i type-json-mapper
屬性裝飾器
內(nèi)置三種類屬性裝飾器:
@mapperProperty(apiField, type)
基本數(shù)據(jù)類型使用該裝飾器
接收兩個(gè)參數(shù):
apiField:接口字段名
type:字段轉(zhuǎn)換類型(可選值:string | int | flot | boolean | date | time | datetime)
@deepMapperProperty (apiField, Class)
對(duì)象/數(shù)組使用該裝飾器
接收兩個(gè)參數(shù):
apiField:接口字段名
Class:類
@filterMapperProperty(apiField, filterFunc)
自定義過濾器(翻譯)使用該裝飾器
接收兩個(gè)參數(shù):
apiField:接口字段名
filterFunc:自定義過濾器函數(shù)
const filterFunc = (value) => {
  return "translated text";
};
方法
deserialize(Clazz, json)
反序列化 json 對(duì)象
Clazz:類
json:接口返回的對(duì)象數(shù)據(jù)
deserializeArr(Clazz, list)
反序列化數(shù)組
Clazz:類
list:接口返回的數(shù)組數(shù)據(jù)
mock(Clazz, option)
生成模擬數(shù)據(jù)
Clazz:類
option:mock 配置
mock 配置
| 名稱 | 類型 | 描述 | 默認(rèn)值 | 
|---|---|---|---|
| fieldLength | Object | 字段長度 | - | 
| arrayFields | string[] | 數(shù)組類型字段 | - | 
fieldLength
| 數(shù)據(jù)類型 | length 含義 | 
|---|---|
| string | 字符串長度 | 
| int | 最大整數(shù) | 
| float | 字符長度(保留兩位小數(shù)) | 
例:
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 表示隨機(jī)生成的 age 字段的范圍在 1 ~ 20 之間
 * grade: 4 表述隨機(jī)生成的 grade 字段是兩位整數(shù)加兩位小數(shù)的形式,共4個(gè)數(shù)字字符(如:23.33)
 * name: 6 表述將隨機(jī)生成長度為 6 的隨機(jī)字符串
 */
使用示例
這里預(yù)先造了幾個(gè)類,并給類屬性加上了裝飾器
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 = "";
  }
}
// 狀態(tài)映射關(guān)系
const stateMap = { "1": "讀書中", "2": "輟學(xué)", "3": "畢業(yè)" };
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 = "";
  }
}
以下是接口返回的數(shù)據(jù):
const json = [
  {
    StudentID: "123456",
    StudentName: "李子明",
    StudentAge: "10",
    StudentSex: 1,
    Grade: "98.6",
    Address: {
      province: "廣東",
      city: "深圳",
      full_address: "xxx小學(xué)三年二班",
    },
    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中學(xué)高三二班",
    },
    Lessons: [],
    State: 2,
  },
];
開始轉(zhuǎn)換,因接口返回的是數(shù)組,這里使用 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);
}
輸出結(jié)果如下
// first
{
id: "123456",
name: "李子明",
age: 10,
sex: "1",
grade: 98.6,
address: { province: "廣東", city: "深圳", fullAddress: "xxx小學(xué)三 年二班" },
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中學(xué)高三二班" },
lessons: [],
status: "輟學(xué)",
position: undefined,
extra: undefined,
};
如果后端接口還沒開發(fā)完成,我們還可以直接 mock
import { mock } from "type-json-mapper";
const res = mock(Student, {
  fieldLength: { age: 20, grade: 4, name: 6 },
  arrayFields: ["lessons"],
});
console.log(res);
輸出結(jié)果如下
{
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: ''
}
后記
雖然要多維護(hù)一套類,看似麻煩(確實(shí)很麻煩??),但是加強(qiáng)了代碼的健壯性,擺脫對(duì)接口的依賴;最重要的是堵住了后端的嘴(bushi)
以上就是Typescript使用裝飾器實(shí)現(xiàn)接口字段映射與Mock實(shí)例的詳細(xì)內(nèi)容,更多關(guān)于Typescript字段映射Mock的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
 rollup?cli開發(fā)全面系統(tǒng)性rollup源碼分析
這篇文章主要為大家介紹了rollup?cli開發(fā)全網(wǎng)系統(tǒng)性rollup源碼分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
 TypeScript?5.0?正式發(fā)布及使用指南詳解
這篇文章主要為大家介紹了TypeScript?5.0?正式發(fā)布及使用指南,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
 使用typeScript 進(jìn)行扁平化數(shù)據(jù)轉(zhuǎn)樹實(shí)現(xiàn)demo
這篇文章主要介紹了使用typeScript 進(jìn)行扁平化數(shù)據(jù)轉(zhuǎn)樹實(shí)現(xiàn)demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
 TS中Array.reduce提示沒有與此調(diào)用匹配的重載解析
這篇文章主要為大家介紹了TS中Array.reduce提示沒有與此調(diào)用匹配的重載解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
 數(shù)據(jù)結(jié)構(gòu)TypeScript之鄰接表實(shí)現(xiàn)示例詳解
這篇文章主要為大家介紹了數(shù)據(jù)結(jié)構(gòu)TypeScript之鄰接表實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
 自動(dòng)生成typescript類型聲明工具實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了自動(dòng)生成typescript類型聲明工具實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04

