处理JSON循环引用序列化与反序列化问题的终极方案
重要声明:此博借鉴了阿里巴巴 Fastjson 的思想
『科普』:
- 对于web前端,JSON序列化可以说是在 与服务端通讯(ajax+json) ,和使用 localStorage(读 + 写) 时。
- 对于服务端,我相信绝大多数人遇到问题是在于输出JSON序列化数据。
循环引用对象序列化?这似乎是一个老生常谈的问题,但是99.9%的人所谓的『解决』,都是在『逃避』这个问题,不信你搜搜『循环引用 JSON』试试?
后端相关文章一定告诉你要『禁用循环引用检测』,『截断对象』和『配置序列化规则( 各种Filter )』等降( tao )魔( bi )大法
前端相关文章最多就是告诉你可以设置一下序列化深度。
于是导致了数据丢失,花大量时间在序列化和反序列化逻辑上做判断和处理,出现许多难以维护的bug,浪费社会资源。
说到底,JSON不就是数据交换吗?它不支持表示循环引用对象,那就对它的语法进行扩展,让它能够表示不就好了?迎刃而解。
如何表示循环引用/重复引用对象?阿里的Fastjson已经告诉了我们答案:
- 创建一个对象表示引用对象,它仅有一个 key="$ref",value=对象引用路径
- 对象引用路径使用 "$" 表示根对象的引用,使用 "[数字]" 表示数组元素,使用 ".property" 表示对象字段
- 形如{ "$ref":"$.key1.array[3].key" }
Fastjson 是 Java 的库,服务于后端,我在这里用 TypeScript 手写一下它的实现以便前端能够享受这个人类宝贵的精神财富。
首先写个序列化和反序列化方法:( serializeCircular 和 parseCircular )
The important declaration: This mind comes form Fastjson Lib (Alibaba).
basic things:
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.
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 Lib for back-end, so I implement it by TypeScript for front-end.
At the first, make serialization and parse function: ( serializeCircular and parseCircular )
const _parseCircular = (root: any, parent: any, objkey: string | number) => { const obj = parent[objkey]; if (null === obj || typeof obj !== "object") { // } else if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { _parseCircular(root, obj, i); } } else if (!!obj["$ref"]) { let paths = (obj["$ref"] as string).split(/\.|\[|\]/).filter(s => !!s); paths.shift(); parent[objkey] = paths.reduce((a, b) => a[b], root); } else { Object.keys(obj).forEach(key => { _parseCircular(root, obj, key); }); } }; const _serializeCircular = (parent: any, base: string, objkey: string | number, obj_key_map: Map<string, any>, result: any) => { const obj = parent[objkey]; if (null === obj || typeof obj !== "object") { result[objkey] = obj; } else if (obj_key_map.has(obj)) { result[objkey] = { $ref: obj_key_map.get(obj) }; } else { const endFix = Array.isArray(parent) ? `[${objkey}]` : `.${objkey}`; let objrefstr = `${base}${endFix}`; obj_key_map.set(obj, objrefstr); if (Array.isArray(obj)) { result = result[objkey] = []; for (let i = 0; i < obj.length; i++) { _serializeCircular(obj, objrefstr, i, obj_key_map, result); } } else { result = result[objkey] = {}; Object.keys(obj).forEach(key => { _serializeCircular(obj, objrefstr, key, obj_key_map, result); }); } } }; const serializeCircular = (root: any) => { const map = new Map(); map.set(root, "$"); if (Array.isArray(root)) { let result = [] as any[]; for (let i = 0; i < root.length; i++) { _serializeCircular(root, "$", i, map, result); } return result; } else if (null !== root && typeof root === "object") { let result = {}; Object.keys(root).forEach(key => { _serializeCircular(root, "$", key, map, result); }); return result; } else { return root; } }; const parseCircular = (root: any): any => { if (Array.isArray(root)) { for (let i = 0; i < root.length; i++) { _parseCircular(root, root, i); } } else if (null !== root && typeof root === "object") { Object.keys(root).forEach(key => { _parseCircular(root, root, key); }); } return root; };
然后你可以仅仅只是用在某些特定的地方 ,如 RPC 和 localStorage,或者直接替换掉原本的 JSON.stringify 和 JSON.parse
Then you can just use it at some special places. such as RPC and localStorage, Or just replace the original JSON.stringify and JSON.parse.
let stringifyInited = false; if (!stringifyInited && (stringifyInited = true)) { const oriStringify = JSON.stringify; const oriParse = JSON.parse; const serialize = serializeCircular; const parse = parseCircular; JSON.stringify = function (this: any) { const args = Array.from(arguments) as any; args[0] = serialize(args[0]); return oriStringify.apply(this, args); } as any; JSON.parse = function (this: any) { const args = Array.from(arguments) as any; let res = oriParse.apply(this, args); return parse(res); } as any; }
测试:
Test:
// 测试 test (() => { const abc = {} as any; abc.a = {}; abc.b = {}; abc.a.b = abc.b; abc.b.a = abc.a; const abcSerialization = JSON.stringify(abc); console.log(abcSerialization); const abcParse = JSON.parse(abcSerialization); console.log(abcParse.a === abcParse.b.a); })(); // {"a":{"b":{"a":{"$ref":"$.a"}}},"b":{"$ref":"$.a.b"}} // true
最后,需要 Javascript 版本的朋友们,使用 tsc 命令将 TypeScript 编译成 JavaScript 即可。
At the end, my friends, please use "tsc" command to compile TypeScript to JavaScript if need a JavaScript version.
# 安装 typescript / install typescript npm install -g typescript # 编译 ts 文件 / compile ts file tsc typescript.ts
感谢阅读 Thanks for reading