Typescript使用裝飾器實現接口字段映射與Mock實例
前言
實現瞭個能滿足題目要求的小插件,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的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 使用微信小程序制作核酸檢測點查詢工具
- C# PropertyInfo類案例詳解
- Spring中如何使用@Value註解實現給Bean屬性賦值
- 在react中使用mockjs的方法你知道嗎
- vue打印功能實現的兩種方法總結