背景
因K12批改项目需求,需要实现自定义类(如批改、错误定义和批改步骤)数据的存储和还原,即实现序列化和反序列化接口,用以保存求解过程、多线程应用提高批改效率。
现有技术参考
当前需求序列化和反序列化:
- 支持自定义类
- 支持正则数组
1. typestack/class-transformer
优点:TypeScript类型系统可规范JavaScript对象的声明、构造、序列化和反序列化;活跃度、star最高
缺点:公司主用JavaScript,项目改用TypeScript的沉默成本很高
2. yahoo/serialize-javascript
优点:基本支持所有的JavaScript原生类型;代码量极少;活跃度、star次之
缺点:不支持自定义类;反序列化应用正则匹配(replace),可能存在灾难性回溯(虽然已添加UUID唯一性保证)
3. jquery.serializeJSON
优点:基本支持所有的JavaScript原生类型、dom转换;jquery子库,代码质量有保障
缺点:需要先引用jquery.js,添加了许多无用库;
方案选择
综合考虑项目现状、代码量等情况,个人决定自己造轮子,甚是开心。。。那么实现方式上可以
- 为每个需要序列化和反序列化的类,补充类独有的toString 和 construct 方法
- 编写通用的序列化和反序列化接口,但需要额外添加schema标识类型(项目越大TypeScript的优势越明显。。。)
考虑到方案2通用性更强,可避免各种重复的类型转换逻辑,最终选择方案2(可能主要是想造轮子)。 实际上,Java 等强类型语言早已内置了序列化和反序列化实现,如 interface Serializable, 通过implement Serializable, 并实现writeObject 和 readObject接口即可。本实现也参考了接口的应用方式
方案设计
接口设计:
- 继承Serialize对象,并定义自身类的_schema属性、包含的属性含有的类型_classes属性(提供注册式或_classes属性定义两种方式)
- 调用对象的stringify()方法进行序列化
- 调用类的static方法toInstance进行反序列化
实现逻辑:
- 递归遍历序列化对象,转换为引用类型的Object对象
- 应用JavaScript原生的JSON.stringify进行序列化
- 反序列化时先应用JSON.parse解析为Object对象
- 递归遍历Object对象,根据schema生成特定类型的属性
优缺点分析
- 效率方面,进行了两次深度遍历(一次是自定义的序列化和反序列函数,另一次是调用JSON.parse和JSON.stringfy接口)
- 代码量方面,需要额外定义class的转换
- 代码不美观。。。反序列化的类函数需要提前加入内存,即在反序列化继承、组合等情况时,需要先提前加载属性、父类的class。父类可以根据原型链访问,而属性的类型需要额外引入。
目前实现了两种方式:
(1)在声明类时添加保存属性类函数的_classess,用以反序列化
(2)在继承时注册需要反序列化的类(register):应用该方式无法保证引用顺序
具体实现
类图
需要序列化的对象(或父类)有继承Serialize即可
使用示例
// 1. 继承Serialize 类
class 批改步骤 extends Serialize {
// 1.1 添加序列化和反序列化的 _schema 和 属性类型集合 _classes
// 其中_classess内的类方法是递归读取超类的
static _schema = 批改步骤schema
static _classes = [计算错误定义, 未知列式错误定义, 缺少步骤定义, 列式错误定义, 单位错误定义, 批改步骤, 回答错误定义]
constructor() {
// 1.2 增加super调用
super()
}
}
// 2. 使用Serialize方法
const 步骤实例 = new 批改步骤()
// 2.1 序列化stringify
const 步骤实例str = 步骤实例.stringify()
// 2.2 反序列化toInstance(str)
const 新步骤实例 = 批改步骤.toInscance(步骤实例str)
【注意】
-
由于序列化是根据schema的属性遍历实例对象的,因此必须先定义类的schema 并赋值至类的_schema属性
-
由于反序列化是调用类的(无参)构造函数新建实例,再给实例属性赋值,所以定义构造函数时需要保证无参构造函数调用无错误
(1)最好对构造函数的参数设置相应的默认值
(2)没有设置默认值的情况下,不能在构造函数的参数应用解构,再调用参数属性
schema 编写规范
【说明】类型,即type属性不区分大小写
| 基础类型 | 含义 | 使用示例 |
|---|---|---|
| string | 字符串 | type: "string" |
| number | 数字 | |
| regexp | 正则表达式 | |
| json_object | 直接应用JSON.stringfy 和 JSON.parse进行序列化和反序列化 |
对象类型:
// 1. arrays:
// (1)子类型为基础类型
{
type: "set"
subType: “string”
}
// (2)子类型为对象类型
{
type: "set"
subType : {
type: "set",
subType : "string"
}
}
// 2. Map/set
{
type: "set",
subType : {
type: "set",
keyType: "string",
valueType: {
type: "set".
subType: "string"
},
}
}
// 3. object/自定义类
{
type : "自定义类名1",
properties: {
key1 : "string",
key2 : {
type: "object",
properties : {
key3 : "numner"
}
},
key3: "自定义类名2"
}
}
schema完整示例
const 分段计费问题批改schema = {
type: '分段计费问题批改',
properties: {
题目: "json_object",
计费函数: "计费函数",
批改步骤集: {
type: "array",
subType: "批改步骤"
},
讲解: "string"
}
}
未来扩展
- 支持或属性格式: 增加|类型
- 增加类型校验:目前没有进行类型判断,schema类型与实例属性类型不同时,直接应用/保存了实例属性类型
- 支持设置序列化的默认值
- 增加schema merge函数,以支持继承关系的schema扩展