做者:周周(沪江资深Web前端开发工程师) 本文为原创文章,转载请注明做者及出处html
近期在小D十周年活动之际,又看到了一个自家H5专题梦工厂生成的页面。前端
我与小D十年回忆 >>vue
回想起了一段往事,如今来看还蛮有趣的。主要是一个将业务逐步抽象成数据的过程,对于当时对数据设计等还不太敏感的本身有不小的促进做用。因而想经过本文分享下当初如何搭建可视化编辑页面系统中的一些开发设计思路,也但愿对前端伙伴们在构建相似中大型应用时有必定帮助,能够更好地设计一些较复杂的数据结构。本文不会把具体的实现代码贴出来,更多的是背后为什么要这样设计的一些思考过程,如何把一些业务映射到数据。有不足之处,还请轻拍。react
让咱们一块儿先来预览下编辑器的后台界面,编辑伙伴能够像在桌面应用中同样进行操做,最后直接生成一个h5页面。jquery
当时14年正值H5比较火热的时期,业务中不少时候会碰到一些重复相似的h5活动页面,开发几乎要变成 ctrl+c 和 ctrl+v 了,略显枯燥。后来涌现出了不少h5页面编辑应用,某秀、某KA等。某一天可爱的老大又在背后看着咱们,忽然来了一句:数据库
咱们要不也搞一个?后端
当时反应是万匹草泥马在头上奔过: 这个有点太复杂了,成本过高,仍是不作了吧。不过冷静下来后,心想作完还能够解放一批开发同窗,挑战也不小,因此心想数组
为何不呢?前端框架
因而就开始了一个可视化编辑页面系统 Web IDE 的打造之旅。从计划开始作的那一刻起,实际上是脑中一片空白,后来作了一些小DEMO后,才开始有了点思绪,也不是一蹴而就的。数据结构
场景: 先后端协做,需求是在页面上加一个异步请求的列表模块。
1.发送请求,处理数据。接口响应回来的数据多是:
{
...,
data: {
list: [
{id: 1, content: '这是第一条数据'},
{id: 2, content: '这是第二条数据'},
{id: 3, content: '这是第三条数据'}
],
total: 4
}
}
复制代码
2.根据 list 中的数据,循环拼接出须要渲染的DOM结构
<li>1.这是第一条数据</li>
<li>2.这是第二条数据</li>
<li>3.这是第三条数据</li>
复制代码
3.找到须要追加或者替换的节点,而后加到页面上。
在上面这个场景中,这类数据的结构多是最常碰到的。整个过程能够理解为,先获取数据,再把数据转换到渲染视图里。看目前流行的框架,react,vue 等都帮咱们省略了关注 DOM 的步骤,在这里咱们先抛开一些框架的便利,回归到原始的步骤,由一个 renderer 方法充当。
+--------+ +----------+ +-------+
| data | => | renderer | => | DOM |
+--------+ +----------+ +-------+
复制代码
伙伴要问了,这个流程比较熟悉,但若是作一个 Web IDE 不是就这么几步吧?
需求分析要详细
要下笔时,发现课题变得复杂了。不少时候就会像下面这张图中。
那先缓一缓,先尝试着拆解成一些小部件来分析。多观察,多联想,多分析。
如何把一个H5页面转换成一个可编辑的状态。
观察一个普通的H5活动页面,试着先抽取几个关键元素。试着滑动页面,大体能够猜想这是一个滑动组件,占满全屏的,交互能够划分为上下滑动,可能还有回调等等,这时能够发现一个H5上可能有一个或多个子页面(分页),这里面可能将会涉及到分页的操做。再看每一个分页上,有图片,连接,文字等等一些元素,展示形式差别很大,样式都不同,看起来很难统一,不像上面的小例子中能够比较容易地拼凑出,先放一放。可是有两个关键点浮现出来了,分页+元素,一个h5页面基本都是由不少页面和元素组成的,目标将转换成如何编辑分页,如何编辑元素。
再单独看一个页面详细地分析下:
能够发现页面中有不少元素,大体有:
试着抽象一层,一个页面可能会变成下面的结构:
页面: [
元素1,
元素2,
元素3
]
复制代码
发现和“小例子”中的结构有点相似,一个块包含多个子块,照着相似的渲染方式,估计也能将元素们渲染到页面上,但样式却各不同,元素之间的差别比较大,类型又不一样,怎么想拼接 list 列表同样拼呢?试着比较下,上面“小例子”中的列表数据包含的是偏内容的数据,把 <li> 当作一个元素的话,分析下,这些元素的类型是同样的,数据里包含的是元素内容,而样式等一些其余属性或事件都是定义在了其余地方,并不在这个数据结构里。再回到元素自己上观察下,如何抽象,有哪些特征?
类型
内容
位置
大小
颜色
背景图
连接
其余特征
元素: { 类型, 内容, 位置, 大小, ... }
元素上的属性比较庞大,但仍是能够放在一个元素的对象里。设想若是把这些部分也放在该元素的数据结构上,不仅仅有内容数据,还有样式上的数据,属性上的数据等等,这样是否就能够渲染了。那么目标有新增,如何去编辑这“庞大”的属性集合。
咱们假设一段须要的带属性样式的元素DOM结构:
<div class="element someClass" style="someKeyA: someValueA;someKeyB: someValueB;" data-custom="someCustomData">
<div class="content">
content's context
</div>
</div>
复制代码
相比“小例子”中的 <li> 多了不少属性和结构,根据上面的 DOM 结构,用对象的形式抽象下,格式大体以下:
element: {
style: {
someKeyA: 'someValueA',
someKeyB: 'someValueB',
},
class: ['someClass'],
attribute: {
custom: 'someCustomData',
},
content: {
text: 'content\'s context'
}
}
复制代码
这样的话,咱们就能够经过一个拼接方法来生成咱们想要的结构。这样一个关于元素的数据结构设计就有了雏形。咱们能够经过修改元素上一些属性的值,改变元素的外在表现。整个过程能够简化成数据的变化引发视图的变化,和如今不少前端框架数据驱动思想有几分类似。
经过相似上面不少小 demo 的积累,最后能够整理拼装下,回到单个页面上,除了元素,可能还有一些其余设置,假想预留一些字段。
那么一个页面抽象下,格式大体以下:
page: {
elements: [
{
style: ...,
class: ...,
attribute: ...,
content: ...,
},
{ element2 },
{ element3 },
{ element4 },
{ element5 },
],
setting: {
propertyA: {},
propertyB: 'valueB',
flagC: false,
}
}
复制代码
生成的 DOM 结构大体以下:
<div class="page" data-flagC="false" ...>
<div class="element element1" ...></div>
<div class="element element2" ...></div>
...
</div>
复制代码
再把一个个单页拼起来,就变成了咱们须要的H5页面,格式大体以下:
h5: {
pages: [
{
elements: ...,
setting: ...,
},
{page2},
{page3},
{page4},
],
setting: {
propertyA: {},
propertyB: 'valueB',
flagC: false,
}
}
复制代码
从一个个小元素组成一个页面,再由一个个页面组成h5活动页面。至此一个对于h5页面的抽象出来的数据结构雏形基本完成了。
上面的结构没有展开,展开后你会发现这个大对象可能上千上万行,接下来关注下数据和操做界面中的映射关系了,如何去操做这些数据,数据怎么展示,元素和页面的关系等等。
为什么要操做数据,而不是去操做DOM?
这也是在前期开发中踩过的坑,照着“所见即所得”的模式,像富文本编辑器同样,输入修改完就是最终输出的内容,也何尝不可,实现时习惯使用 jQuery 的伙伴很容易会联想到直接操做 DOM,好比一个元素的定位,使用 jquery-ui 的 draggble 拖拖拽拽很方便的定位,最后产出的就是最后实际的HTML。但放在实际场景中后,会发现拓展性兼容性不太友好。特别是在后期再去操做一段成品的 DOM 结构会变得比较麻烦,好比一个定位的数据,成品中的数据会看起来比较“死”,在适配不一样屏幕时,计算对应的值会比较累。而若是是操做数据的话,能够在渲染以前对数据进行些处理,最后的产出就变得比较灵活,将数据层和视图层抽离的比较独立,拓展起来也比较容易。
映射关系
如何把这些界面的业务抽象成数据操做,首先仍是简单分析整理下。一个可视化编辑应用的操做有不少,这里只举几个类型的数据操做。用户经过操做(好比输入、拖拽、移动、点击等)来改变元素的属性值。
用脑图发散一下有哪些功能:
让咱们回到已经制定的目标上,如何编辑页面,如何编辑元素。下面举几个例子
页面编辑 一个H5由多个页面组成,由几个{元素}组成的[元素集合],此类关系一般能够用数组来表示。
将页面集合简单成抽象成数据的操做:
+-------------+
| |
+-------------+
=> pages: [], index: -1
复制代码
新增页面时,在 pages 数组中 push 一个'page 1'的实例对象,再经过索引取到该实例数据, 而后经过渲染方法将对应的视图渲染到界面中,这个关系链就基本完成了。
+-------------+
| page 1 |
+-------------+
=> pages: [ page1 ], index: 0
复制代码
交换页面顺序
+-------------+
| page 2 |
+-------------+
| page 1 |
+-------------+
=> pages: [ page2, page1 ], index: 1
复制代码
经过数组的两个值的顺序交换便可以实现两个页面的顺序交换,发现不少场景只须要经过一些数组最基本的操做就能够实现一些看起来复杂的功能,而困难的更可能是如何找到这一层映射的关系。
元素操做
元素有多种属性组成,多个{属性键值对}组成的{元素},此类关系一般能够用对象键值对来表示。
在元素对象上不断拓展须要变化的属性,好比元素的尺寸位置:
element: {
style: {
'top',
'left',
'width',
'height',
},
...
}
复制代码
能够设计如上图四个输入框,每一个输入框对应每个属性值,这样一个简单的元素属性编辑控件就行了,依次类推,每加一个可编辑属性就对应加一个编辑控件。基本上都是以key-value的形式来操做。整个过程简化成用户经过界面的输入修改操做数据,数据更改后视图对应从新渲染一遍。
根据不断的尝试和增长,最后结构变成了相似以下的格式:
element: {
id: 1,
role: {
type,
value
},
style: {
'top',
'left',
'width',
'height',
'transform',
...
},
inner: {
html: 'rich text',
style:{
'background-image',
'background-color',
'background-size',
'opacity',
'color',
'font-size',
'text-align',
'border-radius',
...
}
},
attribute:{
'animation-sequence',
}
}
+--------+--------+----------+---------+-------------+
| id | role | style | inner | attribute |
+--------+--------+----------+---------+-------------+
| 1 | link | ... | ... | ... |
+--------+--------+----------+---------+-------------+
| 2 | text | ... | ... | ... |
+--------+--------+----------+---------+-------------+
复制代码
完善后,一个元素的结构已经变得相对庞大了,包含了很是多的属性,随之而来的也是很是多对应的属性编辑控件,也是相对比较复杂的地方。
历史操做
怎么抽象设计?这在平时业务场景里并很少。先分析下历史要什么?主要就是撤销和恢复,用户能够 ctrl+z 回到上一个状态。历史这个大集合里确定有多个历史状态,由多个{历史}组成[历史集合],因而就想到了数组。新状态和老状态的区别是什么?可能就是有了新的操做,数据有了变化,那么把这时的数据保存起来,塞到历史里,至关因而一个 push 的操做,看起来可行。再假如须要回到上一个状态,能够设置个索引 index, 将 index 指向到前一个,就拿到了前一个状态。
| -- push
+------v------+
index --> | status 3 |
+-------------+
| status 2 |
+-------------+
| status 1 |
+------|------+
v -- shift
复制代码
抽取几个关键点:
- 有多个状态 -> 数组
- 不一样状态之间指向 -> 数组的索引值, 游标
- 能够作个步数限制 -> 数组的长度
复制代码
场景:有一个新的操做,即将新的数据插入到历史中
history.push(statusNew);
复制代码
场景:若是满了,将最早插入的数据拿出来
history.shift();
复制代码
场景:撤销一步,将游标指向到前一个,取到前一个状态。重作一步同理。根据这时的数据从新渲染,那么界面上也就回到了前一步的状态。
cursor --;
callback(history[cursor]);
复制代码
那么history的结构就可能长成以下:
[
{status1},
{status2},
{status3},
{statusNew},
]
复制代码
这样一个简单的历史数据结构设计就完成了。
留个问题: 若是撤回到了上几步,而后继续操做,整个历史状态该怎么处理?
最后再经过组装整合,一个可视化编辑器主要的功能大体就知足了。再从新看下操做界面上的数据,能够划分为两个部分,一个前台页面数据,一个后台交互数据,大体以下:
回顾上面的过程,已经从一个简单的数据列表渲染到具备先后台复杂型数据交互的WebIDE,但从数据结构的设计形式上看,本质上变化其实并非很大,只是<li>变成了<element>,<page>等,里面包含的数据量也增长了许多。会不会发现这个数据虽然看起来十分庞大复杂,但也有几分清晰简单。而你的角色更像是一位建筑设计师,把握整个结构框架,而后再管理一砖一瓦。
上述的过程在开发其余项目时一样适用,在开始设计时,要一会儿脑补出整个设计是比较困难的,特别是对某一个事物从一无所知到有点概念,从0到1的过程,客观的说这并不容易。能够先试着抽离出几个关键步骤,写几个小模块,把关键路径走通,在初期十分有效,随后再这些看似零散的小组件拼装起来,每每这个雏形会比一开始想的清晰不少,如此反复,整个设计也会变得更加清晰饱满。数据的设计也是相对应的,由一个个小的数据组成,渐渐的便会造成一个比较庞大的数据,这时代码可能不是最关键的,而是如何合理有效清晰地管理这些数据,可能更像是后端数据库管理同样。每每须要通过不断的试错走些歪路的过程,最后会慢慢驾轻就熟一点。好设计是不断迭代出来的,勇敢试错,不怕踩坑,有句话叫,坑踩的深才铭心刻骨。
iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。
iKcamp新课程推出啦~~~~~开始免费连载啦~每周2更共11堂iKcamp课|基于Koa2搭建Node.js实战项目教学(含视频)| 课程大纲介绍