Skip to content
快速预览

使用序列化

✍️ w 🕒 2023-06-25 02:25:23(a year ago) 🔗 A.前端知识整理 前端工具库

序列化是将对象转换为可供传输或存储使用的格式的过程,序列化经常用于将对象转换为诸如 JSON、XML 或二进制格式之类的格式。在序列化过程中,对象的状态被转换为可以被传输或存储的格式,如字符串或字节序列。序列化的最终目的是将对象在网络中传输或存储到磁盘中。这个过程编码(Encode)

反序列化是将序列化后的数据重新转换为原始对象的过程。反序列化通常会分析序列化数据并尝试还原它所表示的对象状态。反序列化是序列化的逆过程。这个过程解码(Decode)

举个例子:两个服务之间要传输一个数据对象,对象不能直接在网络传输,需提前转成可传输的二进制,且要求可逆,就需要将对象转换成二进制流,通过网络传输到对方服务,再转换成对象,供服务方法调用。这个编码和解码的过程称之为序列化和反序列化

使用序列化和反序列化可以将对象转换为不同的格式进行存储和传输,以在不同语言、平台、系统或环境之间共享

几种常见的序列化和反序列化协议

在各种编程语言中,有许多序列化和反序列化协议可供选择。以下是几种常见的序列化和反序列化协议:

  1. JSON(JavaScript Object Notation) :一种轻量级的数据交换格式,易于人类阅读和编写,并且易于机器解析和生成。JSON主要用于前后端之间的数据传输。

  2. XML(Extensible Markup Language) :一种标记语言,可以描述复杂的数据结构,并且可以通过DTD、XSD和XPath等技术进行有效验证。XML主要用于Web服务中的数据交换,也可以用于一些传统企业应用中。

  3. Protocol Buffers :是谷歌开源的一种轻量级数据交换格式,以二进制形式压缩数据。Protocol Buffers的优点在于其尺寸小、性能高和跨平台支持程度高,适用于高性能的分布式系统。

  4. MessagePack :一种二进制序列化格式,可以将结构化数据打包为更小、更快、更简单的二进制格式。MessagePack的优点在于其尺寸小、性能高和跨平台支持程度高,适用于高性能系统。

  5. Thrift :是一种可伸缩的分布式系统框架,提供RPC(远程过程调用)框架、序列化和反序列化支持等。Thrift支持多种语言,可以处理高并发场景。

前端 JSON 序列化和反序列

在 JavaScript 中,可以使用内置的JSON对象来进行序列化和反序列化操作。JSON对象提供了两个核心方法:JSON.stringify()和JSON.parse()。

序列化 JSON.stringify()方法将任意JavaScript对象序列化为一个JSON字符串。

反序列化 JSON.parse()方法将一个JSON字符串解析为一个JavaScript对象。

其他语言中也有类似的

  • Java:使用Jackson、Gson等第三方库的fromJson方法或JSON库自带的JSONObject.parseObject方法实现JSON字符串转对象。

  • Python:使用内置的json库的loads方法实现JSON字符串转对象。

  • PHP:使用内置的json_decode函数实现JSON字符串转对象。

前端也有第三方库 class-transformerclass-transformer 的实现原理是基于 TypeScript 的装饰器和反射机制。它可以将 JavaScript 对象序列化为 JSON 字符串,也可以将 JSON 字符串反序列化为 TypeScript 类的实例对象。

实现一个用js 序列化和反序列化

当有一组数据格式如下,我们想让 personLs 中数组对象 反序列化为他所在映射 class 对象,让他具备class 所给予的行为不在是一个单独 反序列化的样子

js
// ----------测试数据------------
const api = {
    newName: 'api',
    personLs: [{ name: 'api', info: [{ newInfoName: 'api', age: 15 }] }],
    newEXTEND: 'api',
    newFORMATTER: 'api',
}

例如 定义了一个 class Person 赋予了他play 的动作如果是简单的反序列化使用JSON,是达不到这种效果的

js
class Person  {
    name = 'json'

    info = []

		// 行为可以玩
		play(){
			console.log('我的动作是玩')
		}
}

class Info{

}

我想将这类数据格式进行序列化到类中,让其可以具备类的本身特有的属性和方法,例如将数据注入,让字段知道属于那个要映射的类

js
// 指定字段映射对应类

class Info {
    @SerializeUtils.jsonField('newInfoName')
    infoName = 'infoName'
}

class Person extends FormBase {
    name = 'json'

    @SerializeUtils.jsonType(Info)
    info = []
}

class A extends FormBase {
    @SerializeUtils.jsonField('newName')
    name = '123'

    @SerializeUtils.jsonType(Person)
    personLs = []

    // 输出时候key 不变
    @SerializeUtils.jsonField('newFORMATTER', false)
    FORMATTER = '2'
    _zz = '转换后将不输出'
}


const a = new A().init(api)

这样数据重新注入到实际初始化对应的类中,让对象可以更灵活的具备 class 中定义的一些属性和行为,要想做到这样,整体设计思路如下

  • 要将字段和类形成映射关系 例如 @SerializeUtils.jsonType(Person) personLs = [] { 当前构造函数:{当前构造函数字段:映射的构造函数 } } , 因为如果仅仅以字段为 映射的话,可能有不同构造函数中 同名字段问题,因此要具体到 那个函数下 那个字段 和谁形成映射

  • 字段重新映射 这种情况经常发生在,某一端修改了代码字段,但另一端完全不想修改 例如 @SerializeUtils.jsonField('newFORMATTER', false) FORMATTER = '2'这种数据结构设计 为 { 当前构造函数:{当前构造函数字段:{ 映射的重新命名,isOutput :Boolean} } } , 当前构造函数 下的 当前构造函数字段 要对应重新命名后的字段名

js
/**
    @class 进行装饰器 和序列化的工具类,可以基于此类进继承扩展
 */
export default class SerializeUtils {
    // 字段映射的装饰器的存储{ 当前构造函数:{当前构造函数字段:映射的构造函数 } }
    static jsonTypeWeakMap = new WeakMap()

    // 字段重新映射{ 当前构造函数:{当前构造函数字段:映射的构造函数:{isOutput :Boolean} } }
    static jsonFieldWeakMap = new WeakMap()

    /**
     * 描述 字段类型映射的装饰器
     * @date 2021-08-18
     * @param {Object} cls 定义类s
     * @returns {Function}
     *
     * @example
     * 当数据api格式{personLs:[{name:11},{name:11}]},将数组中的对象提取出成类
     * class A {
     *   @jsonType(映射类)
     *   personLs = []
     * }
     */
    static jsonType(cls) {
        return (target, name, descriptor) => {
            // 存储格式{ 当前构造函数:{当前构造函数字段:映射的构造函数 } }
            if (!SerializeUtils.jsonTypeWeakMap.has(target.constructor)) SerializeUtils.jsonTypeWeakMap.set(target.constructor, {})
            const obj = SerializeUtils.jsonTypeWeakMap.get(target.constructor)
            obj[name] = cls
            return descriptor
        }
    }

    /**
     * 描述  字段类型重命名的装饰器
     * @date 2021-08-19
     * @param {string} rename -- 新对象对应key
     * @param {Boolean} isOutput  输出字段是否重置翻译
     * @returns {any}
     */
    static jsonField(rename, isOutput = true) {
        return (target, name, descriptor) => {
            // 检差映射名字是否已经在当前对象存在
            if (Reflect.has(target, rename)) throw new Error('Field already exists')
            // 存储格式{ 当前构造函数:{当前构造函数字段:映射的构造函数:{isOutput:Boolean} } }
            if (!SerializeUtils.jsonFieldWeakMap.has(target.constructor)) SerializeUtils.jsonFieldWeakMap.set(target.constructor, {})
            const obj = SerializeUtils.jsonFieldWeakMap.get(target.constructor)
            obj[name] = { rename, isOutput }
            return descriptor
        }
    }

    /**
     * 描述
     * @date 2021-08-20
     * @param {Object} obj 转换对象
     * @param {String} oldKey 转换对象当前key
     * @returns {Object} {key:string,isOutput:Boolean}返回对应映射key
     */
    static oldKeyToNewKey(obj, oldKey) {
        let Cls, fieldsObj
        Cls = obj.constructor
        fieldsObj = SerializeUtils.jsonFieldWeakMap.get(Cls)
        return { newKey: fieldsObj?.[oldKey]?.rename || oldKey, isOutput: fieldsObj?.[oldKey]?.isOutput ?? true }
    }

    /**
     * 描述 对象合并
     * @date 2021-08-18
     * @param {Object} target 合并的目标对象
     * @param {any} copy 被合并的对象
     * @param { Object } [deep=true] 是否深递归
     * @returns {any}
     *
     * @example
     * const a = {name:12,age:456}
     * const b = {name:1299,age:45633,zz:456}
     * SerializeUtils.objExtend(a,b)
     * a:{name:1299,age:45633}
     * b:{name:1299,age:45633,zz:456}
     */
    static objExtend(target, copy, deep = true) {
        let targetVal, copyVal, Cls, fieldsObj, rsArr, arrItem, shallowCopy, oldKey

        for (let key in target) {
            oldKey = key
            let { newKey, isOutput } = SerializeUtils.oldKeyToNewKey(target, key)
            targetVal = target[key]
            copyVal = copy[newKey]
            if (!Reflect.has(copy, newKey)) continue

            // 浅copy
            if (!deep) {
                if (typeof copyVal === 'object' && copyVal !== null) {
                    shallowCopy = Array.isArray(copyVal) ? [...copyVal] : { ...copyVal }
                } else {
                    shallowCopy = copyVal
                }
                target[key] = shallowCopy
                continue
            }

            // 深copy
            if (typeof copyVal === 'object' && copyVal !== null) {
                if (Array.isArray(copyVal)) {
                    rsArr = []
                    // 处理映射类字段 当使用jsonType映射的字段实例化自己的对应类
                    Cls = target.constructor
                    fieldsObj = SerializeUtils.jsonTypeWeakMap.get(Cls)
                    // fieldsObj && fieldsObj[key]
                    if (fieldsObj?.[key]) arrItem = new fieldsObj[key]()
                    copyVal.forEach((it, itKey) => {
                        if (!fieldsObj?.[key]) arrItem = targetVal[itKey]
                        typeof it !== 'object' || (it = SerializeUtils.objExtend(arrItem, it, deep))
                        rsArr.push(it)
                    })
                    target[key] = rsArr
                } else {
                    target[key] = SerializeUtils.objExtend(targetVal, copyVal, deep)
                }
            } else {
                target[key] = copyVal
            }
        }
        return target
    }

    /**
     * 描述 去除特殊标记字段
     * @date 2021-08-19
     * @param {Object} obj 去除对象
     * @param {string} startSymbol 特殊符号标记对象
     * @returns {any}
     *
     * @example
     * const a = {name:12,_age:456}
     * SerializeUtils.formatter(a,'_')
     * a:{name:1299}
     *
     * class A{
     *    @SerializeUtils.jsonField('newnew')
     *    obj1 = {ls:[1,3]}
     * }
     * const a = new A()
     * a:{newnew:[1,3]}
     */
    static formatter(obj, startSymbol) {
        if (obj === null) return null
        let clone = {}
        for (let key in obj) {
            if (key.startsWith(startSymbol)) continue
            let { newKey, isOutput } = SerializeUtils.oldKeyToNewKey(obj, key)
            if (!isOutput) newKey = key
            clone[newKey] = typeof obj[key] === 'object' ? SerializeUtils.formatter(obj[key], startSymbol) : obj[key]
        }
        if (Array.isArray(obj)) {
            clone.length = obj.length
            return Array.from(clone)
        }
        return clone
    }
}
  • 使用的时候定义基本类
js
import SerializeUtils from './SerializeUtils'

export default class FormBase {
    init(data) {
        return SerializeUtils.objExtend(this, data)
    }

    shallowInit(data) {
        return SerializeUtils.objExtend(this, data, false)
    }

    formateObj() {
        return SerializeUtils.formatter(this, '_')
    }
}

代码参考地址

class-transformer

社区中已经 存在了类似上面我们构想的库,把普通对象转换成已有的ES6类实例。举个例子,如果你从后端、一些api或者一个json文件加载了一个json,在经过 JSON.parse 之后,你拥有了一个普通的js对象,而不是已有类的实例,例如下面的案例

json
{
  "id": 3,
  "firstName": "Luke",
  "lastName": "Dacascos",
  "age": 12
}

我们有一个 User 类

ts
export class User {
    id: number;
    firstName: string;
    lastName: string;
    age: number;

    getName() {
        return this.firstName + " " + this.lastName;
    }

    isAdult() {
        return this.age > 36 && this.age < 60;
    }
}

实际如果是通过 JSON 方法直接将 转换对象,这个 对象并不能使用上我们 在类上定义的行为,但是如果 一个字段赋值到我们的类也是一个重复的工程 ,这时候就可以使用使用 class-transformer。 该库的目的是帮助您映射普通的 js对象 指向您所拥有的 类的实例

安装

Node.js 上使用:

shell
npm install class-transformer --save
shell
npm install reflect-metadata --save

并且确保在一个全局的地方引入它,例如 app.ts

js
import "reflect-metadata";

由于使用ES6的新特性,所以如果你正在使用旧版本的 nodejs,你可能还需要安装 es6-shim es6-shim:

shell
npm install es6-shim --save

并且确保在一个全局的地方引入它,例如 app.ts

js
import "es6-shim";

Browser 浏览器 上使用:

shell
npm install class-transformer --save
shell
npm install reflect-metadata --save

在你的index.html中添加 reflect-metadata 的标签

html
<html>
   <head>
       <!-- ... -->
       <script src="node_modules/reflect-metadata/Reflect.js"></script>
   </head>
   <!-- ... -->
</html>

如果你是工程化的项目也可以直接导入

如果你正在使用 system.js,你可能需要添加 map and package 配置

json
{
    "map": {
        "class-transformer": "node_modules/class-transformer"
    },
    "packages": {
        "class-transformer": { "main": "index.js", "defaultExtension": "js" }
    }
}

弊端

装饰器 语法 ???!!!

关于 reflect-metadata

想做到通过修饰符通过声明性语法添加了在定义类时增加类及其成员的能力,将注释附加到类的静态属性,像 C # (这样的语言。NET) ,Java 支持向类型添加元数据的属性或注释,以及用于读取元数据的反射 API

reflect-metadata 是一个 TypeScript 库,可以在运行时向 TypeScript 类、属性、方法和参数添加和读取元数据,简化了元数据解析的流程,是一个方便实用的工具。Reflect metadata 主要用于在代码声明时添加/读取某个对象的元数据(metadata)

Reflect Metadata 是一个广泛使用的第三方npm package,同时该 package 的作者Ron Buckton也是Typescript的核心开发者(core contributor) 他于 2015 年提交了将 metadata 纳入 Typescript 官方的提案(ES7),但目前为止他手上还有太多其他工作没有完成, 所以并没有提上议程。感兴趣的可以去以下链接查看具体信息:

元数据

当我们在开发程序的时候,通常需要在代码中定义很多类、属性、方法和参数等元素。这些元素的信息可以被认为是数据的一部分。而元数据,就是描述这些元素的信息的数据。比如,对于一个类,它的元数据可以包含类的名称、继承关系、成员方法和属性等信息。而在程序运行时,我们可以使用元数据来读取和处理这些元素,以实现一些功能

在某种程度上,可以认为属性就是元数据,因为它们都是用来描述数据的。但是,在编程中,元数据更多地是用于描述代码中的元素,包括类、属性、方法和参数等。属性是具体的数据值,而元数据是描述这些值的信息。比如,对于一个类,它的属性是类中存储的具体数据,而元数据则是描述这个类的名称、继承关系、成员方法和属性等信息的数据。在 reflect-metadata 中,我们可以将元数据添加到类的属性、方法和参数等位置,以便于在运行时处理这些元素。

使用方法:

shell
npm i reflect-metadata --save

如果你是 ts 开发你需要 在 tsconfig.json 里配置 emitDecoratorMetadata 选项为 true, 并在 入口文件位置导入 import 'reflect-metadata'

一个案例

ts
import 'reflect-metadata'

@Reflect.metadata("role", "admin")
class Post {
  @Reflect.metadata("role", "admin")
  name = "";
}

const metadataClass = Reflect.getMetadata("role", Post);
const metadataObj = Reflect.getMetadata("role", new Post(), "name");

console.log(metadataClass); // admin
console.log(metadataObj); // admin


@Reflect.metadata("role", "admin")
class Post1 {
  @Reflect.metadata("role", "admin")
  name2 = "";
}

const metadataClass1 = Reflect.getMetadata("role", Post1);
const metadataObj1 = Reflect.getMetadata("role", new Post1(), "name");

console.log(metadataClass); // admin
console.log(metadataObj); // admin

在 reflect-metadata 源码位置 可以找到 var targetMetadata = Metadata.get(O); 将 Metadata 打印出来 可以看出做了Map 映射让数据直接形成关系

使用 class-transformer 案例

https://juejin.cn/post/7125066863150628900 ??

参考

掌握 JS 高级编程基础 - Reflect Metadata

ECMAScript 双月报告:装饰器提案进入 Stage 3

Reflect Metadata

REFLECT-METADATA 包以及 ECMASCRIPT 提案

TypeScript中的装饰器和metadata reflection API反射:从新手到专家

JavaScript Reflect 和 reflect-metadata

【译】reflect-metadata及相关ECMAScript提案简介

Released under the MIT License.