一個 json 轉換工具
- 2020 年 5 月 31 日
- 筆記
- javascript, js、框架, js學習筆記, typescript
在前後端的數據協議(主要指http
和websocket
)的問題上,如果前期溝通好了,那麼數據協議上問題會很好解決,前後端商議一種都可以接受的格式即可。但是如果接入的是老系統、第三方系統,或者由於某些奇怪的需求(如為了節省流量,json 數據使用單字母作為key
值,或者對某一段數據進行了加密),這些情況下就無法商議,需要在前端做數據轉換,如果不轉換,那麼奔放的數據格式可讀性差,也會造成項目難以維護。
這也正是我在項目種遇到的問題,網上也找了一些方案,要麼過於複雜,要麼有些功能不能很好的支援,於是有了這個工具[class-converter](//www.npmjs.com/package/class-converter)。
下面我們用例子來說明下:
面對如下的Server
返回的一個用戶user
數據:
{ "i": 1234, "n": "name", "a": "1a2b3c4d5e6f7a8b" }
或者這個樣的:
{ "user_id": 1234, "user_name": "name", "u_avatar": "1a2b3c4d5e6f7a8b" }
數據里的 avatar
欄位在使用時,可能需要拼接成一個 url
,例如 //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
來標記使用哪一個值作為逆過程的原始值,具體用法可以參考文檔。