概念
- 序列化(Serialization): 将数据结构或对象转换成二进制串的过程,
- 反序列化(Deserialization): 将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程
广义上,不一定要是二进制串,只要是将数据结构或对象状态与可以存储或传输的格式的转换的过程,都可以称为序列化和反序列化。
例子:
c
// 示例:简单的序列化和反序列化
struct Person {
std::string name;
int age;
};
// 序列化
std::string serialize(const Person& p) {
return p.name + "|" + std::to_string(p.age);
}
// 反序列化
Person deserialize(const std::string& data) {
size_t pos = data.find("|");
return {data.substr(0, pos), std::stoi(data.substr(pos + 1))};
}
其他概念:
- 类型转换(convert): 将A类型的源对象转换为目标B类型的对象。
- 格式化(format): 将一个对象输出为显示的格式或者说容易让人理解的格式.以及把从显示的格式解析为对象。
- 数据持久化 (Data Persistence): 数据持久化是指将数据保存到持久存储介质(如硬盘、数据库等)中,以便在未来的某个时间点再次使用。是数据序列化的一种表现。
- 数据映射(Data mapping): 数据映射是将一个源中的数据字段与另一源中的数据字段进行匹配的过程。
- 数据转换(Data Transfer): 就是将数据进行合并、清理和整合,通过转换从一种表现形式变为另一种表现形式,并能够实现不同的源数据在语义上保持一致性的过程。
在中文网上能查找到的定义虽然是明确的。但是实际上英文存在互用的情况。如果查找不到一些相关资料时,可以尝试更换以上名词。
什么情况下需要序列化?
其实序列化最终的目的是为了对象可以跨平台存储和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。
本质上存储和网络传输 都需要经过 把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。
日常开发中,序列化的使用场景,可以总结如下:
- 数据持久化,保存在数据库中(二进制)
- 数据从数据库读取出来到 Java 等后端语言
- 数据从后端中通过网络传输到前端。(JSON)
- 数据从 JSON 变到 Javascript 对象 (JSON.parse 和 JSON.stringfy)
序列化在前端的数据传递中是否适用?
前端的序列化,更多是"对象状态"与"传输"的格式的转换的过程。那其实我们也可以借用这个概念在我们与组件间的数据传输的数据映射、转换、格式化等做一个归纳。
例子:
ini
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'
这样写虽然能实现功能,但也仅仅能实现功能。以后有其他人员的信息接口,又要另外写数据处理的过程,所以是没办法复用的。
序列化写法:
定义一个类,专门来处理数据转换的内容。
ini
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 字段是动态实现的,还可以实现动态改变数据:
ini
// 原处理
list[0].name = '李四';
console.log(list[0].info); // 'name: 张三, age: 19'
// class 序列化
list[0].name = '李四';
console.log(list[0].info); // 'name: 李四, age: 19'
代码如何复用?
假设现在除了学生,还多了老师的数据,如何把学生老师的表,一起展示在页面中?
Hooks:
ini
function usePeopleList({fetchData, dataProcess}) {
const list = ref([]);
const res = await fetchData();
if (res.code === 0 && res.data) {
list = dataProcess(res.data.list)
}
}
数据处理:
javascript
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:
xml
<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>
要明显区分老师,学生,只需要修改最后展示页面的数据结构及页面结构即可:
xml
<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>
是否有工具可以完成?
先插入一下,对这个功能很重要的装饰器模式
装饰器模式
JavaScript装饰器模式是一种常用的设计模式,它可以让你在不改变原有代码的情况下,动态地给对象添加新的功能。
前端的函数缓存,就是很好的一个装饰器。
javascript
function calculate(num) {
console.log('Calculating...');
let result = 0;
for (let i = 0; i < num; i++) {
result += i;
}
return result;
}
function cache(fn) {
const cache = new Map();
return function (num) {
if (cache.has(num)) {
console.log('Cache hit!');
return cache.get(num);
} else {
console.log('Cache miss!');
const result = fn(num);
cache.set(num, result);
return result;
}
};
}
// 装饰器模式,给累加器方法加缓存
const cachedCalculate = cache(calculate);
console.log(cachedCalculate(10000000)); // Calculating... Cache miss! 49999995000000
console.log(cachedCalculate(10000000)); // Cache hit! 49999995000000
Es6 装饰器
ES 6上的装饰器能帮助我们更好的完成装饰器的开发。
javascript
class Example {
@log
instanceMethod() { }
@log
static staticMethod() { }
}
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;
}
如上面代码中,装饰器 @log 分别装饰了实例方法 instanceMethod 和 静态方法 staticMethod。@log 装饰器的作用是在执行原始的操作之前,执行 console.log 来输出日志。
另外,还有带参数的写法。
javascript
class Example {
@log(1)
instanceMethod() { }
@log(2)
static staticMethod() { }
}
function log(id) {
return (target, methodName, descriptor) => {
.....
}
}
工具:json-object-mapper
官方示例:
javascript
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 库拥有更多的日常下载量。
为什么这些库不更新了?
个人猜测一个是装饰器的提案迟迟没有稳定的原因。
装饰器从 stage 0 到 3 阶段,从 2014 年到 2022 年,经过了 8 年的变化。stage 3 的写法跟以前有很大的不同。到2023-01-26,TypeScript 5.0 beta 版本,开始支持 stage3 阶段的装饰器写法。新的装饰器写法并与老的装饰器写法并不兼容。
另外一个可能是 react 从类的写法转换到 hooks 的写法。大家减少了 class 的用法。
总结与思考:序列化与前端分层
借助数据序列化的概念,我们可以将数据进入页面、组件前,序列化成统一的数据格式,从而实现代码复用。
而这件事在另一方面又促进了数据层的形成。
前面代码复用的例子中,可以看到形成了如下的结构:
- 数据结构:class、(type、interface)
- 业务逻辑:hooks 及 fetch Data 等数据处理内容。
- 页面表现:view(vue),永远使用经过处理的数据,自身不生产数据。
可以看到我们在此形成了类似 MVC 的分层。
而分层思想,也是模块化之后前端必备的思考工具。
附录:JSON 缺点及解决:
- 不支持大数据类型BigInt、精度丢失:lossless-json、json-bigint
- 不支持 Date、Function、class、symbol 类型等:telejson
- 排序问题:json-stable-stringify
- 大量数据容易出问题:bfj
- JSON转换时,出错提示不明确:parse-json
前两个可能会在我们项目中使用,其他使用较少。目前来看,JSON 的处理并没有集大成者,或者最后被官方接受的
参考资料:
zhuanlan.zhihu.com/p/661559240
tech.meituan.com/2015/02/26/...