• 一个 json 转换工具


          在前后端的数据协议(主要指httpwebsocket)的问题上,如果前期沟通好了,那么数据协议上问题会很好解决,前后端商议一种都可以接受的格式即可。但是如果接入的是老系统、第三方系统,或者由于某些奇怪的需求(如为了节省流量,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: "zqczqc@xxx.com",
        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来标记使用哪一个值作为逆过程的原始值,具体用法可以参考文档。

  • 相关阅读:
    后台返回null iOS
    iOS代码规范
    没落的公司 该何去何从
    定位框一闪而过 iOS Swift
    根据appid跳到App Store某个APP的详情页
    Analyze 静态分析内存泄漏代码优化 instrument之Xcode
    bugly手动上传符号表和自动上传符号表
    __null_unspecified属性关键字解决内存泄漏
    栅栏函数dispatch_barrier_sync/async
    cell左滑加删除置顶功能 iOS
  • 原文地址:https://www.cnblogs.com/zquancai/p/13019689.html
Copyright © 2020-2023  润新知