迴圈引用對象序列化?這似乎是一個老生常談的問題,但是99.9%的人所謂的『解決』,都是在『逃避』這個問題。 ...
重要聲明:此博借鑒了阿裡巴巴 Fastjson 的一點思想
The important declaration: This mind comes form Fastjson Lib (Alibaba).
說點『科(fei)普(hua)』先:
- 對於web前端,JSON序列化可以說是在 與服務端通訊(ajax+json) ,和使用 localStorage(讀 + 寫) 時。
- 對於服務端,我相信絕大多數人遇到問題是在於輸出JSON序列化數據。
『科(fei)普(hua)』完畢。
Let's talk about some basic things first:
- For the web front-end, JSON is usually used on conmunication with server part, and localStorage.
- For the back-end, I think where the most people have problem is serialization.
Basic things done.
迴圈引用對象序列化?這似乎是一個老生常談的問題,但是99.9%的人所謂的『解決』,都是在『逃避』這個問題,不信你搜搜『迴圈引用 JSON』試試?
後端相關文章一定告訴你要『禁用迴圈引用檢測』,『截斷對象』和『配置序列化規則( 各種Filter )』等降( tao )魔( bi )大法
前端相關文章最多就是告訴你可以設置一下序列化深度。
於是導致了數據丟失,花大量時間在序列化和反序列化邏輯上做判斷和處理,出現許多難以維護的bug,浪費社會資源。
說到底,JSON不就是數據交換嗎?它不支持表示迴圈引用對象,那就對它的語法進行擴展,讓它能夠表示不就好了?迎刃而解。
如何表示迴圈引用/重覆引用對象?阿裡的Fastjson已經告訴了我們答案:
- 創建一個對象表示引用對象,它僅有一個 key="$ref",value=對象引用路徑
- 對象引用路徑使用 "$" 表示根對象的引用,使用 "[數字]" 表示數組元素,使用 ".property" 表示對象欄位
- 形如{ "$ref":"$.key1.array[3].key" }
Fastjson 是 Java 的庫,服務於後端,我在這裡用 TypeScript 手寫一下它的實現以便前端能夠享受這個人類寶貴的精神財富。
首先寫個序列化和反序列化方法:( serializeCircular 和 parseCircular )
Serialization Circular? It seems to be a very familiar issue? But what 99.9% of people call "handle" is "escape".Or you can try searching "Circular JSON".
The information about back-end would tell you to "disable circular checking", "cut object" and "create filters" to "h( e )a( s )n( c )d( a )l( p )e" it.
The information about front-end would tell you to change/set the deep/level in the settings of serialization.
And all above would cause losing data, and you will take huge time at the further logics of serialization, fixing bugs. It's a sheer waste.
But all in all, JSON is just for data-exchanging. It doesn't support circular , why not expand the role to make it supports?
How to express circular or repeatition? The answer has already been in the Fastjson lib, which built by Alibaba:
- Create an object to express the circular, which has only one property named "$ref" and the value of it is a path-expression, which express the position of the circular from the root.
- It uses "$" to express the reference of the root, "[index:number]" to express the item of an array, and ".key" to express the key of an common object.
- Just like { "$ref":"$.key1.array[3].key" }
But Fastjson is a Java Lab for back-end, so I implement it by TypeScript for front-end.
At the first, make serialization and parse function: ( serializeCircular and parseCircular )
1 const _parseCircular = (root: any, parent: any, objkey: string | number) => {
2 const obj = parent[objkey];
3 if (null === obj || typeof obj !== "object") {
4 //
5 } else if (Array.isArray(obj)) {
6 for (let i = 0; i < obj.length; i++) {
7 _parseCircular(root, obj, i);
8 }
9 } else if (!!obj["$ref"]) {
10 let paths = (obj["$ref"] as string).split(/\.|\[|\]/).filter(s => !!s);
11 paths.shift();
12 parent[objkey] = paths.reduce((a, b) => a[b], root);
13 } else {
14 Object.keys(obj).forEach(key => {
15 _parseCircular(root, obj, key);
16 });
17 }
18 };
19 const _serializeCircular = (parent: any, base: string, objkey: string | number, obj_key_map: Map<string, any>, result: any) => {
20 const obj = parent[objkey];
21 if (null === obj || typeof obj !== "object") {
22 result[objkey] = obj;
23 } else if (obj_key_map.has(obj)) {
24 result[objkey] = { $ref: obj_key_map.get(obj) };
25 } else {
26 const endFix = Array.isArray(parent) ? `[${objkey}]` : `.${objkey}`;
27 let objrefstr = `${base}${endFix}`;
28 obj_key_map.set(obj, objrefstr);
29 if (Array.isArray(obj)) {
30 result = result[objkey] = [];
31 for (let i = 0; i < obj.length; i++) {
32 _serializeCircular(obj, objrefstr, i, obj_key_map, result);
33 }
34 } else {
35 result = result[objkey] = {};
36 Object.keys(obj).forEach(key => {
37 _serializeCircular(obj, objrefstr, key, obj_key_map, result);
38 });
39 }
40 }
41 };
42 const serializeCircular = (root: any) => {
43 const map = new Map();
44 map.set(root, "$");
45 if (Array.isArray(root)) {
46 let result = [];
47 for (let i = 0; i < root.length; i++) {
48 _serializeCircular(root, "$", i, map, result);
49 }
50 return result;
51 } else if (null !== root && typeof root === "object") {
52 let result = {};
53 Object.keys(root).forEach(key => {
54 _serializeCircular(root, "$", key, map, result);
55 });
56 return result;
57 } else {
58 return root;
59 }
60 };
61 const parseCircular = (root: any): any => {
62 if (Array.isArray(root)) {
63 for (let i = 0; i < root.length; i++) {
64 _parseCircular(root, root, i);
65 }
66 } else if (null !== root && typeof root === "object") {
67 Object.keys(root).forEach(key => {
68 _parseCircular(root, root, key);
69 });
70 }
71 return root;
72 };
然後你可以僅僅只是用在某些特定的地方 ( 推薦 ),如 RPC 和 localStorage
或者直接替換掉原本的 JSON.stringify 和 JSON.parse ( 也推薦 ) ^_^
Then you can just use it at some special places. ( recommend ) such as RPC and localStorage
Or just replace the original JSON.stringify and JSON.parse.( recommend too ) ^_^
1 let stringifyInited = false;
2 if (!stringifyInited && (stringifyInited = true)) {
3 const oriStringify = JSON.stringify;
4 const oriParse = JSON.parse;
5 const serialize = serializeCircular;
6 const parse = parseCircular;
7 JSON.stringify = function (this: any) {
8 const args = Array.from(arguments) as any;
9 args[0] = serialize(args[0]);
10 return oriStringify.apply(this, args);
11 } as any;
12 JSON.parse = function (this: any) {
13 const args = Array.from(arguments) as any;
14 let res = oriParse.apply(this, args);
15 return parse(res);
16 } as any;
17 }
測試:
Test:
1 // 測試 test
2
3 (() => {
4 let abc = {} as any;
5 abc.a = {};
6 abc.b = {};
7 abc.a.b = abc.b;
8 abc.b.a = abc.a;
9 let abcSerialization = JSON.stringify(abc);
10 console.log(abcSerialization);
11 let abcParse = JSON.parse(abcSerialization);
12 console.log(abcParse.a===abcParse.b.a);
13 })();
14
15 // {"a":{"b":{"a":{"$ref":"$.a"}}},"b":{"$ref":"$.a.b"}}
16 // true
最後,需要 Javascript 版本的朋友們,只需把類型限定標記 ( : ??? 和 as ??? ) 去掉。
感謝閱讀
At the end, my friends, if need a JavaScript version, just remove the limitation marks ( : ??? 和 as ??? )
Thanks for reading