说明
相信很多 同事在刚刚接触图满意项目时,对项目中的数据流向和数据解析过程都很模糊, 不清楚数据到底是如何从一份 JSON 数据变为我们使用的数据对象。
这篇文章将会为你 介绍图满意项目中,数据的解析还原及数据流向,为你揭开这一神秘面纱。
通过这篇文章,你将会明白数据是如何通过接口返回的 JSON 一步步变为我们可以拿来使用的 Model,同时我会介绍为什么需要这样处理,如果不这样处理带来的 弊端是什么。
当阅读完文章后,需要对图满意项目有明确的数据流向认知,清晰 明了的了解 dataObject、ModelData 及 Model 的区别。后续开发业务代码需要明确 认知代码应该存放的位置。避免将代码放到不同阶段的类中,出现后期 不可维护、难以修改的情况。
数据解析流向
首先,需要明确理解这张图,这张图中已经描述了我们对整个 Home 数据的解析、实现过程。我先来介绍一下图中出现的 3 种类型的类分别代表什么。
- 原始数据:JSON, 意思就是可被序列化的 JSON 数据,数据中用到需要保存、传输时的最终结果。
- Data 类:用于将 JSON 数据转为实体类的中间 类,所有对数据的解析、转换、适配都在这种类中处理。
- 实体类:在项目中真正用到的数据类型,也就是数据 Model。
我分别详细解释一下这 3 种类的典型事例及作用:
原始数据
比如一份楼层数据:
{
"id": "10",
"name": null,
"height": 2734.4058001657,
"isMirror": false,
"offset": {
"x": 0,
"y": 0
},
"pictureId": "20190115104131777061547519673942",
"pictureType": 1,
"totalClearArea": 29.01560021174,
"uesGuideline": false,
"useComstomLighting": true,
"useIESLight": true,
"compassRotation": 1.1699375695728,
"areaIds": [],
"switchIds": [],
"scale": 1,
"holes": [],
"lightH5VIewIds": [],
"ceilingIds": [],
"bgWallIds": [],
"parquetIds": [],
"bkShapeTexturesIDs": [],
"walls": [
...
],
"curvedWalls": [
...
],
"rooms": [
...
],
"corners": [
...
],
"pillars": [],
"ceilings": [],
"parquets": [],
"bgWalls": [],
"lightViews": [],
"lightH5Views": [],
"layoutGroups": [],
"layoutModels": [],
"holeTextureIDs": [],
"floorTextureIDs": [
...
],
"wallTextureIDs": [
...
],
"pillarTextureIDs": [],
"ceilingTextureIDs": [],
"parquetTexturesID": []
}
在这份数据中可以看出,该数据可以被序列化来传输,并且在数据中会包含很多嵌套的基础数据,如 Wall,Corner 等。
Data 类
如 ModelData,WallData,CornerData
在 Data 类中,会实现以下几个基础方法:
- build() 用于将
实体类
转为Data类
- rebuild() 用于将
Data类
转为实体类
- extractToData() 用于将
原始数据
转为Data类
- buildData() 用于将
Data 类
转为原始数据
Data 类主要会处理数据解析的各类内容对于一些父级的 Data 类,比如 HomeData,LevelData 可以调用其他子级 Data 类,用于协调解析其他基础内容。
Data 类中同样会处理一些适配内容,比如流程图中出现的 LevelAdapter。就是因为随着业务的不断发展,可能会更改原始数据的数据结构。原有的 Home 数据是一整体的,无法做到对单个楼层保存、序列化等处理。为了实现对单独楼层的序列化处理,我们需要将原有数据做一层处理,处理为所需要的原始数据,交给 LevelData 处理解析,避免 HomeData 直接解析 Level 中的数据。
类似的还有处理 Flash 的一些适配数据,如 CurvedAdapter、RectWindowAdapter 等,都是用于处理一些不好解释的数据。
需要注意的一点是:Adapter 的使用要遵循在 源头及末尾处理,切勿在解析过程中处理,比如 Flash 的 z、y 坐标轴与 H5 的坐标轴刚好相反,处理这类数据一定要在解析的源头处(还未变成 Data 类的时候处理),切勿在 Data 解析为实体类的时候再处理这类 数据。同样的,在输出时做数据适配还原也一定要在数据 已经解析成为原始数据后 再走适配逻辑,切勿直接修改 Data 类做 相关适配,后患无穷。
实体类
如 Home,Level, Model, Wall, Corner
真正在项目中使用最多的数据对象,通过操作数据对象可以同步修改 视图内容及 页面的属性面板(DOM)。
数据对象可以看做一个大的状态管理池,数据对象中存在大量关联关系,如墙体(Wall)中可能关联挂墙模型(Models),墙体(Wall)中可能关联墙角(Corners), 墙角(Corner)中可能关联多个墙(Walls)。
数据对象本身就是一个发布者。 遵循观察者模式,数据对象通过 Mobx 本身可以当做一个发布者,关键数据改变将会触发通知订阅这些关键数据的对象。比如 Model.position.x 改变,Model3D\Model2D 在实例化的时候会订阅这些关键数据。那么,position.x 改变将会通知到对应数据做到实时更新改变。
注意:
上述内容中,我们在方便的使用了各种关联、订阅后,也同样可能会留下很多“后遗症”。
比如 Wall 关联了 Corner,那么 Corner 在销毁的时候就需要一定处理 他与 Wall 之间的关联,否则 JS 的垃圾回收机制并不会将这部分内容销毁。
再比如 Model2D 中订阅了很多 Model 关键数据的内容,那么在 Model 的发布池(_events)中就会存在 Model2D 的引用,如果 Model2D 在销毁的时候没有 处理释放这些订阅,他们之间的关联就会一直存在。那么如果来回切换几次楼层,这种后果不可想象。
所以在使用到 Mobx,EventEmit(on,emit),相互关联时,一定要意识到销毁的时候如何处理,多想一步。
上述内容中会吧所有的订阅返回丢入this._disposeArr
中,这样在基类的 destroy
中,就会处理引用关系了。
常见需求点处理方法
将某个实体类,单独做序列化
我们上面的流程图是按照整个户型数据的维度来看,但是在很多时候,我们可能需要的是将某个基础实体类(Model,Group,Hole)单独处理为序列化的原始数据,或者从 SideBar 中的原始数据单独处理为实体类。
遇到这类情况需要思考的第一思路就是 借助 Data 类,保证 Data 类的四个基础方法是实现并且可以用的,那么整体数据就已经可以实现。
如果发现实现过程中出现数据不统一,户型数据中的解析字段与其他过程的解析字段不同, 尽量借助适配器(Adapter
)转换解析字段,保证 Data 类解析过程尽量统一。这样做的好处是后续如果对解析过程中有修改,或者 增加字段等能够做到错误最小, 耦合性最低。
切勿单独再写一套解析方法,这样很容易导致后续一个 实体类的数据变动引起解析保存失败等严重事故。
数据中需要增加一个字段,该如何做
在不断的迭代中,可能会出现需要对某个实体类的原始数据做字段增加,此时应该如何处理这类需求。
其实在了解了整体的 数据解析流向后,这类需求自然应该迎刃而解。
首先应当在对于的 Data 类中建立对应解析字段,然后分别实现 4 个基础方法动作中对数据的处理步骤。在最终的序列化数据中就已经出现了。
某一部分数据与新版本数据不适配
首先需要确定是,这份数据是否是完整的。完整的意思就是, 关键性数据还是存在的,只是数据 保存的格式与原来数据不适配。
若非完整数据,则根据metaData.compatibilityVersion
最低向下兼容版本,提示数据不兼容异常 。
若完整数据,那么就需要做适配器处理。但有些数据是不需要做保存适配的,也就是数据这样解析后,不需要在保存的时候对数据做原始转换(比如:弧形墙),这类适配就只需要做单向适配。而有些数据的适配是需要在保存的时候依然转换回之前的数据格式,这就需要分别实现 in、out 两种方式
持续优化
- 对 Data 类实现基础接口
IDataExtract