在前后端的數據協議(主要指http和websocket)的問題上,如果前期溝通好了,那么數據協議上問題會很好解決,前后端商議一種都可以接受的格式即可。但是如果接入的是老系統、第三方系統,或者由於某些奇怪的需求(如為了節省流量,json 數據使用單字母作為key值,或者對某一段數據進行了加密),這些情況下就無法商議,需要在前端做數據轉換,如果不轉換,那么奔放的數據格式可讀性差,也會造成項目難以維護。
這也正是我在項目種遇到的問題,網上也找了一些方案,要么過於復雜,要么有些功能不能很好的支持,於是有了這個工具 class-converter。歡迎提 issue 和 star~~https://github.com/zquancai/class-converter
下面我們用例子來說明下:
面對如下的Server返回的一個用戶user數據:
{
"i": 1234,
"n": "name",
"a": "1a2b3c4d5e6f7a8b"
}
或者這個樣的:
{
"user_id": 1234,
"user_name": "name",
"u_avatar": "1a2b3c4d5e6f7a8b"
}
數據里的 avatar 字段在使用時,可能需要拼接成一個 url,例如 https://xxx.cdn.com/1a2b3c4d5e6f7a8b.png。
當然可以直接這么做:
const json = {
"i": 1234,
"n": "name",
"a": "1a2b3c4d5e6f7a8b",
};
const data = {};
const keyMap = {
i: 'id',
n: 'name',
a: 'avatar',
}
Object.entries(json).forEach(([key, value]) => {
data[keyMap[key]] = value;
});
// data = { id: 1234, name: 'name', avatar: '1a2b3c4d5e6f7a8b' }
然后我們進一步就可以把這個抽象成一個方法,像下面這個樣:
const jsonConverter = (json, keyMap) => {
const data = {};
Object.entries(json).forEach(([key, value]) => {
data[keyMap[key]] = value;
});
return data;
}
如果這個數據擴展了,添加了教育信息,user 數據結構看起來這個樣:
{
"i": 1234,
"n": "name",
"a": "1a2b3c4d5e6f7a8b",
"edu": {
"u": "South China Normal University",
"ea": 1
}
}
此時的 jsonConverter 方法已經無法正確轉換 edu 字段的數據,需要做一些修改:
const json = {
"i": 1234,
"n": "name",
"a": "1a2b3c4d5e6f7a8b",
"edu": {
"u": "South China Normal University",
"ea": 1
}
};
const data = {};
const keyMap = {
i: 'id',
n: 'name',
a: 'avatar',
edu: {
key: 'education',
keyMap: {
u: 'universityName',
ea: 'attainment'
}
},
}
隨着數據復雜度的上升,keyMap 數據結構會變成一個臃腫的配置文件,此外 jsonConverter 方法會越來越復雜,以至於后面同樣難以維護。但是轉換后的數據格式,對於項目來說,數據的可讀性是很高的。所以,這個轉換必須做,但是方式可以更優雅一點。
寫這個工具的初衷也是為了更優雅的進行數據轉換。
工具用法
還是上面的例子(這里使用typescript寫法):
import { toClass, property } from 'class-converter';
// 待解析的數據
const json = {
"i": 1234,
"n": "name",
"a": "1a2b3c4d5e6f7a8b",
};
class User {
@property('i')
id: number;
@property('n')
name: string;
@property('a')
avatar: string;
}
const userIns = toClass(json, User);
你可以輕而易舉的獲得下面的數據:
// userIns 是 User 的一個實例
const userIns = {
id: 1234,
name: 'name',
avatar: '1a2b3c4d5e6f7a8b',
}
userIns instanceof User // true
Json 類既是文檔又是類似於上文說的與keyMap類似的配置文件,並且可以反向使用。
import { toPlain } from 'class-converter';
const user = toPlain(userIns, User);
// user 數據結構
{
i: 1234,
n: 'name',
a: '1a2b3c4d5e6f7a8b',
};
這是一個最簡單的例子,我們來一個復雜的數據結構:
{
"i": 10000,
"n": "name",
"user": {
"i": 20000,
"n": "name1",
"email": "zqczqc",
// {"i":1111,"n":"department"}
"d": "eyJpIjoxMTExLCJuIjoiZGVwYXJ0bWVudCJ9",
"edu": [
{
"i": 1111,
"sn": "szzx"
},
{
"i": 2222,
"sn": "scnu"
},
{
"i": 3333
}
]
}
}
這是后端返回的一個叫package的json對象,字段意義在文檔中這么解釋:
- i:package 的 id
- n:package 的名字
- user:package 的所有者,一個用戶
- i:用戶 id
- n:用戶名稱
- email:用戶email,但是只有郵箱前綴
- d:用戶的所在部門,使用了base64編碼了一個json字符串
- i:部門 id
- n:部門名稱
- edu:用戶的教育信息,數組格式
- i:學校 id
- sn:學校名稱
我們的期望是將這一段數據解析成,不看文檔也能讀懂的一個json對象,首先我們經過分析得出上面一共有4類實體對象:package、用戶信息、部門信息、教育信息。
下面是代碼實現:
import {
toClass, property, array, defaultVal,
beforeDeserialize, deserialize, optional
} from 'class-converter';
// 教育信息
class Education {
@property('i')
id: number;
// 提供一個默認值
@defaultVal('unknow')
@prperty('sn')
schoolName: string;
}
// 部門信息
class Department {
@property('i')
id: number;
@prperty('n')
name: string;
}
// 用戶信息
class User {
@property('i')
id: number;
@property('n')
name: string;
// 保留一份郵箱前綴數據
@optional()
@property()
emailPrefix: string;
@optional()
// 這里希望自動把后綴加上去
@deserialize(val => `${val}@xxx.com`)
@property()
email: string;
@beforeDeserialize(val => JSON.parse(atob(val)))
@typed(Department)
@property('d')
department: Department;
@array()
@typed(Education)
@property('edu')
educations: Education[];
}
// package
class Package {
@property('i')
id: number;
@property('n')
name: string;
@property('user', User)
owner: User;
}
數據已經定義完畢,這時只要我們執行toClass方法就可以得到我們想要的數據格式:
{
id: 10000,
name: 'name',
owner: {
id: 20000,
name: 'name1',
emailPrefix: 'zqczqc',
email: "zqczqc@xxx.com",
department: {
id: 1111,
name: 'department'
},
educations: [
{
id: 1111,
schoolName: 'szzx'
},
{
id: 2222,
schoolName: 'scnu'
},
{
id: 3333,
schoolName: 'unknow'
}
]
}
}
上面這一份數據,相比后端返回的數據格式,可讀性大大提升。這里的用法出現了@deserialize、@beforeDeserialize、@yped的裝飾器,這里對這幾個裝飾器是管道方式調用的(前一個的輸出一個的輸入),這里做一個解釋:
beforeDeserialize第一個參數可以最早拿到當前屬性值,這里可以做一些解碼操作typed這個是轉換的類型,入參是一個類,相當於自動調用toClass,並且調動時的第一個參數是beforeDeserialize的返回值或者當前屬性值(如果沒有@beforeDeserialize裝飾器)。如果使用了@array裝飾器,則會對每一項數組元素都執行這個轉換deserialize這個裝飾器是最后執行的,第一個參數是beforeDeserialize返回值,@typed返回值,或者當前屬性值(如果前面兩個裝飾器都沒設置的話)。在這個裝飾器里可以做一些數據訂正的操作
這三個裝飾器是在執行toClass時才會調用的,同樣的,當調用toPlain時也會有對應的裝飾器@serialize 、@fterSerialize,結合@typed進行一個相反的過程。下面將這兩個轉換過程的流程繪制出來。
調用 toClass的過程:
調用 toPlain的過程是調用 toClass的逆過程,但是有些許不一樣,有一個注意點就是:在調用 toClass時允許出現一對多的情況,就是一個屬性可以派生出多個屬性,所以調用調用 toPlain時需要使用 @serializeTarget來標記使用哪一個值作為逆過程的原始值,具體用法可以參考文檔。
