一個 json 轉換工具
在前後端的資料協議(主要指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: "[email protected]", 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
來標記使用哪一個值作為逆過程的原始值,具體用法可以參考文