数据的序列化在前端的应用

401 阅读6分钟

概念

  • 序列化(Serialization): 将数据结构或对象转换成二进制串的过程,
  • 反序列化(Deserialization): 将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程

广义上,不一定要是二进制串,只要是将数据结构或对象状态与可以存储或传输的格式的转换的过程,都可以称为序列化和反序列化。

v2-356e2e44cb039a216f87fe422a9bd2f5_1440w

其他概念:

  • 类型转换(convert): 将A类型的源对象转换为目标B类型的对象。
  • 格式化(format): 将一个对象输出为显示的格式或者说容易让人理解的格式.以及把从显示的格式解析为对象。
  • 数据持久化 (Data Persistence): 数据持久化是指将数据保存到持久存储介质(如硬盘、数据库等)中,以便在未来的某个时间点再次使用。是数据序列化的一种表现。
  • 数据映射(Data mapping): 数据映射是将一个源中的数据字段与另一源中的数据字段进行匹配的过程。
  • 数据转换(Data Transfer): 就是将数据进行合并、清理和整合,通过转换从一种表现形式变为另一种表现形式,并能够实现不同的源数据在语义上保持一致性的过程。

在中文网上能查找到的定义虽然是明确的。但是实际上英文存在互用的情况。如果查找不到一些相关资料时,可以尝试更换以上名词。

什么情况下需要序列化?

其实序列化最终的目的是为了对象可以跨平台存储和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。

日常开发中,序列化的使用场景,可以总结如下:

  1. 数据持久化,保存在数据库中(二进制)
  2. 数据从数据库读取出来到 Java 等后端语言(反序列化)
  3. 数据从后端中通过网络传输到前端。(JSON)
  4. 数据从 JSON 变到 Javascript 对象 (JSON.parse 和 JSON.stringfy)

序列化在前端的数据传递中是否适用?

前端的序列化,更多是“对象状态”与“传输“的格式的转换的过程。那我们其实也可以借用这个概念,在与组件间的数据传输的数据映射、转换、格式化等做一个归纳。

以下是一个处理学生信息的例子,由于页面只需要学生的名字和年龄,我们通过遍历 list 做数据映射的工作:

let listTemp = [];
let list = [];
// const res = await fetchData();
const res = {code: 0, data: {list:[{sName: '张三', sAge: 19, deleteFlag: 0}]}};
if (res.code === 0 && res.data) {
  listTemp = res.data.list;
  list = listTemp.map(item => {
    return {
      name: item.sName,
      age: item.sAge,
      info: `name: ${item.sName}, age: ${item.sAge}`
  })
}
​
console.log(list); // [{ name: '张三', age: 19, info: 'name: 张三, age: 19' }]
console.log(list[0].age); // 19
console.log(list[0].info); // 'name: 张三, age: 19'

这样写虽然能实现功能,但也仅仅能实现功能。以后有其他人员的信息接口,又要另外写数据处理的过程,所以是没办法复用的。

序列化写法:

定义一个类,专门来处理数据转换的内容。

class Student {
  constructor(data) {
    this.name = data.sName;
    this.age = data.sAge;
  }
  get info() {
    return `name: ${this.name}, age: ${this.age}`
  }
}
​
let listTemp = [];
let list = [];
// const res = await fetchData();
const res = {code: 0, data: {list:[{sName: '张三', sAge: 19, deleteFlag: 0}]}};
if (res.code === 0 && res.data) {
  listTemp = res.data.list;
  list = listTemp.map(item => {
    return new Student(item);
  })
}
​
console.log(list); // [Student]
console.log(list[0].age); // 19
console.log(list[0].info); // 'name: 张三, age: 19'

可以看到,虽然打印 list,无法直接看到 list 的具体内容。但是调用内部的数据还是正常显示的。

并且因为其 info 字段是动态实现的,还可以实现动态改变数据:

// 原处理
list[0].name = '李四';
console.log(list[0].info); // 'name: 张三, age: 19'// class 序列化
list[0].name = '李四';
console.log(list[0].info); // 'name: 李四, age: 19'

代码如何复用?

假设现在除了学生,还多了老师的数据,如何把学生老师的表,一起展示在页面中?

Hooks:

function usePeopleList({fetchData, dataProcess}) {
  const list = ref([]);
  const res = await fetchData();
  if (res.code === 0 && res.data) {
    list = dataProcess(res.data.list)
  }
}

数据处理:

function fetchStudentData() {
  axios({});
}

function dataProcessStudent(list) {
  return list.map(item => new Student(item));
}

function fetchTeacherData() {
  axios({});
}

function dataProcessTeacher(list) {
  return list.map(item => new Teacher(item));
}

Vue:

<ul listItem in list>
  <li v-for="item in listItem">{{item.name}}</li>
</ul>
<script>
const { list as teacherList } = usePeopleList({ fetchTeacherData, dataProcessTeacher });
const { list as studentList } = usePeopleList({ fetchStudentData, dataProcessStudent });
const list = [...teacherList, ...studentList]
</script>

要明显区分老师,学生,只需要修改最后展示页面的数据结构及页面结构即可:

<div listItem in list>
  <h2>{{listItem.title}}</h2>
  <ul>
    <li v-for="item in listItem">{{item.name}}</li>
  </ul>
</div>
<script>
const { list as teacherList } = usePeopleList({ fetchTeacherData, dataProcessTeacher });
const { list as studentList } = usePeopleList({ fetchStudentData, dataProcessStudent });
const list = [{
  title: '老师',
  data: teacherList
}, {
  title: '学生',
  data: studentList
}]
</script>

是否有工具可以完成?

先来看看 ES 6 的装饰器写法:

class Example {
    @log
    instanceMethod() { }
}

function log(target, methodName, descriptor) {
  const oldValue = descriptor.value;

  descriptor.value = function() {
    console.log(`Calling ${name} with`, arguments);
    return oldValue.apply(this, arguments);
  };

  return descriptor;
}

上面的例子中,我们给 intanceMethod() 添加了装饰方法 log,统一在 log 中输出日志。 同样,我们可以基于 ES6 的装饰器,将数据转换工作做统一的处理。以下是使用了该方法处理的,两个常用的 npm 库:

工具:json-object-mapper

官方示例:

class SimpleRoster {
    @JsonProperty()
    private name: String;
    @JsonProperty()
    private worksOnWeekend: Boolean;
    @JsonProperty()
    private numberOfHours: Number;
    @JsonProperty({type: Date})
    private systemDate: Date;

    public isAvailableToday(): Boolean {
        if (this.systemDate.getDay() % 6 == 0 && this.worksOnWeekend == false) {
            return false;
        }
        return true;
    }

}
let json = {
    'name': 'John Doe',
    'worksOnWeekend' : false,
    'numberOfHours': 8,
    'systemDate' : 1483142400000 // Sat Dec 31, 2016
};

let testInstance: SimpleRoster = ObjectMapper.deserialize(SimpleRoster, json);
expect(testInstance.isAvailableToday()).toBeFalsy();

可以看到其使用装饰器的模式,完成了数据的转换工作。例子中,这工具自动处理了 Date 类型和 string 类型的转换。另外提供节假日的计算方法。

工具: json2typescript

官方示例比上面略为复杂,但是同样是用装饰器模式完成,各位可以到官方查看,在此不再列出。另外,此 npm 库拥有更多的日常下载量。

总结与思考:序列化与前端分层

借助数据序列化的概念,我们可以在数据进入页面、组件前,将其序列化成统一的数据格式,从而实现代码复用。

而这件事在另一方面又促进了数据层的形成。

前面代码复用的例子中,可以看到形成了如下的结构:

  • 数据结构:class、(type、interface)
  • 业务逻辑:hooks 及 fetch Data 等数据处理内容。
  • 页面表现:view(vue),永远使用经过处理的数据,自身不生产数据。

可以看到我们在此形成了类似 MVC 的分层。

分层思想,也是模块化之后前端必备的思考工具。

参考资料: