当前位置: 首页 > Web前端 > HTML5

沪江前端H5页面引发的一场前端数据结构讨论

时间:2023-04-05 19:59:32 HTML5

作者:周周(沪江资深Web前端开发工程师)本文为原创文章,转载请注明作者及出处转载之际,看到了梦工厂自家H5主题生成的页面。与小D的十年回忆>>回忆一件往事,现在看还蛮有意思??的。主要是一个逐渐将业务抽象成数据的过程,对当时对数据设计还不太敏感的自己有很大的提升。所以想通过这篇文章分享一些关于如何构建可视化编辑页面系统的开发设计思路。也希望对前端小伙伴在构建类似的中大型应用时有所帮助,可以更好的设计一些更复杂的数据结构。本文不会贴出具体的实现代码,更多的是讲一下为什么这样设计背后的思考过程,以及一些业务是如何映射到数据上的。如有不足之处,请轻拍。先预览一下编辑器的后台界面。编辑小伙伴可以像在桌面应用中一样操作,最后直接生成一个h5页面。背景当时2014年是H5比较火的时期,很多时候在业务中会遇到一些重复、雷同的H5活动页面,而且开发差不多要变成ctrl+c和ctrl+v,有点无聊。后来出现了很多h5页面编辑应用,比如某秀,某KA等。有一天,可爱的老板又从后面看了我们一眼,突然说:我们也做一个吧?当时的反应是,万匹草泥马从我头上碾过:这个有点太复杂了,成本太高了,还是不做吧。但是静下心来想着做完了可以解放一批开发同学。挑战不小,所以我想为什么不呢?于是开始了构建可视化编辑页面系统WebIDE的旅程。从计划开始的那一刻起,脑子里其实一片空白。在做了一些小demo之后,我开始有了一些想法,这不是一蹴而就的。举个小例子**场景:前后端协同,需求是在页面添加一个异步请求列表模块。1.发送请求并处理数据。接口响应返回的数据可能是:{...,data:{list:[{id:1,content:'这是第一条数据'},{id:2,content:'这是第第二条数据'},{id:3,content:'这是第三条数据'}],total:4}}2.根据列表中的数据,循环出要渲染的DOM结构

  • 1。这是第一条数据
  • 2。这是第二条数据
  • 3.这是第三条数据
  • 3。找到需要添加或替换的节点,然后添加到页面上。在上面的场景中,这种数据结构可能是最常遇到的。整个过程可以理解为先获取数据,然后将数据转化为渲染视图。纵观目前流行的框架,react、vue等都帮助我们省去了关注DOM的步骤。这里我们抛开一些框架的方便,回归到最初的步骤,由一个renderer方法来完成。+--------++----------++------+|数据|=>|渲染器|=>|DOM|+--------++---------++------+小伙伴要问了,这个过程比较熟悉,但是如果你做一个WebIDE,不就那么几步吧?需求分析怎么写,需要详细写。写的时候发现题目变复杂了。大多数时候它看起来像下图。然后放轻松,试着把它拆成一些小零件进行分析。多观察,多联想,多分析。制定目标如何将H5页面转换为可编辑状态。状态分析观察一个普通的H5活动页面,先尝试提取几个关键元素。尝试滑动页面。你可以大致猜到这是一个占满屏幕的滑动组件。交互分为上下滑动,可能有回调等。这时候你会发现一个H5上可能有一个或多个子页面(分页),可能会涉及到分页操作。再看每个页面,都有一些图片、链接、文字等元素,显示形式大不相同,风格也不一样。似乎很难统一。放手吧。但是两个关键点出现了,分页+元素。一个h5页面基本上是由很多页面和元素组成的。目标将转化为如何编辑分页和如何编辑元素。详细分析,单独看一个页面,详细分析:可以发现页面中有很多元素,大致有:图片、音频、视频、文字、内容、链接、动画等元素。尝试抽象一层,一个页面可能变成如下结构:page:[Element1,Element2,Element3]发现结构有点类似于“小例子”,一个块包含多个子块,按照类似的渲染方式,估计页面上也可以渲染元素,但是样式不同,元素之间的差异比较大,类型也不同。你想如何将列表拼接在一起?试对比一下,上面“小例子”中的列表数据包含了部分内容数据。如果把
  • 看成是一个元素,那么这些元素的类型都是一样的,data就是元素内容。其他一些属性或事件如样式是在其他地方定义的,不在这个数据结构中。我们回到元素本身来观察,它是如何抽象出来的,它有什么特点呢?typecontentpositionsizecolorbackgroundimagelinkothercharacteristicelements:{type,content,position,size,...}元素上的属性比较大,但是还是可以放在一个元素对象中。想象一下,如果这些部分也放在元素的数据结构上,不仅是内容数据,还有样式上的数据,属性上的数据等等,这样就可以渲染出来了。那么目标已经添加,如何编辑这个“庞大”的属性集合。通过反复试验,我们假设元素的必需DOM结构具有属性样式:content'scontext
  • 比“小例子”中的
  • 有更多的属性和结构。根据上面的DOM结构,以对象的形式抽象出来,格式大致如下:element:{style:{someKeyA:'someValueA',someKeyB:'someValueB',},class:['someClass'],attribute:{custom:'someCustomData',},content:{text:'content'scontext'}}这样的话,我们可以通过拼接的方式生成我们想要的结构。这样一个关于元素的数据结构设计是有原型的。我们可以通过修改元素上某些属性的值来改变元素的外在表现。整个过程可以简化为数据的变化引起视图的变化,这有点类似于现在很多前端框架的数据驱动思维。经过像上面这样的很多小demo的积累,终于可以整理拼装,回到一个单一的页面了。除了元素之外,可能还有一些其他的设置,有些字段应该是保留的。那么在一个页面的抽象下,格式大致如下:page:{elements:[{style:...,class:...,attribute:...,content:...,},{element2},{element3},{element4},{element5},],setting:{propertyA:{},propertyB:'valueB',flagC:false,}}生成的DOM结构大致如下:
  • ...把各个单独的页面放在一起,就成了我们需要的H5页面。格式大致如下:h5:{pages:[{elements:...,setting:...,},{page2},{page3},{page4},],setting:{propertyA:{},propertyB:'valueB',flagC:false,}}每个小元素组成一个页面,然后每个页面组成一个h5活动页面。至此,基本完成了一个h5页面的抽象数据结构原型。上面的结构没有展开。展开后你会发现这个大对象可能有几万行。接下来要注意数据与操作界面的映射关系,如何操作这些数据,如何展示数据,元素与页面的关系。ETC。为什么业务映射到数据操作数据而不是DOM?这也是前期开发踩过的坑。按照“所见即所得”的模式,像富文本编辑器一样,输入和修改就是最终输出的内容。很容易想到直接操作DOM,比如元素的定位。使用jquery-ui的draggble定位非常方便,最终输出的就是最终的实际HTML。但是放到实际场景之后,你会发现扩展性兼容性不是很友好。特别是后期操作成品的DOM结构会比较麻烦。比如一个定位数据,成品里的数据看起来会“死”。在适配不同屏幕的时候,计算对应的值会很累。而如果是操作数据,可以在渲染前对数据做一些处理,最终的输出变得更加灵活。数据层和视图层的分离更加独立,也更容易扩展。映射关系如何将这些接口的业务抽象为数据操作,首先简单分析梳理一下。可视化编辑应用中的操作有很多,这里只介绍几种数据操作。用户通过操作(如输入、拖动、移动、点击等)改变元素的属性值。下面用脑图来解释下有哪些功能:页面元素增删改查,历史记录增删改查,用户操作等。这里有几个页面编辑的例子。一个H5由多个页面组成,[元素集合]由多个{元素}组成。这种关系通常可以用数组来表示。将页面集合简化为对数据的抽象操作:+------------+||+------------+=>pages:[],index:-1添加新页面时,将'page1'的实例对象压入pages数组,然后获取该实例通过索引获取数据,然后通过渲染方法将对应的视图渲染到界面上。这个关系链基本就讲完了。+------------+|第1页|+------------+=>pages:[page1],index:0swappageorder+-----------+|第2页|+------------+|第1页|+------------+=>pages:[page2,page1],index:1可以通过交换数组的两个值的顺序来交换两个页面的顺序。发现在很多场景下,一些查看只需要通过数组最基本的操作就可以实现。它是一个复杂的函数,但比较难的是如何找到这一层映射的关系。元素操作元素由多个属性组成,{元素}由多个{属性键值对}组成,这种关系通常可以用对象键值对来表示。不断展开元素对象上需要改变的属性,比如元素的大小和位置:element:{style:{'top','left','width','height',},...}可以设计成如上图4每个输入框对应每个属性值,这样一个简单的元素属性编辑控件就好了,以此类推,每添加一个可编辑属性,就对应添加一个编辑控件。基本上都是以key-value的形式进行操作。整个过程简化为用户通过接口输入修改操作数据,修改数据后重新渲染视图。根据不断的尝试和补充,最终的结构变成了类似下面的格式:element:{id:1,role:{type,value},style:{'top','left','width','height','transform',...},inner:{html:'richtext',style:{'background-image','background-color','background-size','opacity','color','font-size','text-align','border-radius',...}},attribute:{'animation-sequence',}}+------+--------+------------+--------+------------+|编号|角色|风格|内部|属性|+--------+--------+--------+--------+-------------+|1|链接|...|...|...|+--------+--------+--------+------+------------+|2|文字|...|...|...|+--------+--------+---------+--------+------------+完善后,一个元素的结构变得比较大,包含了很多属性,后面是对应的属性编辑控件很多,也比较复杂。如何抽象历史操作的设计?这在正常的业务场景中并不多。分析历史首先需要什么?最主要的是撤消和恢复。用户可以ctrl+z返回到之前的状态。history大集合中肯定有多个历史状态,由多个{history}[historycollection]组成,于是我想到了一个数组。新状态和旧状态有什么区别?可能是有新的操作,数据发生了变化,所以此时保存数据,塞进历史,相当于一个push操作,看起来是可行的。如果需要返回到之前的状态,可以设置一个索引index,将索引指向之前的状态,就会得到之前的状态。|--push+------v-----+index-->|状态3|+------------+|状态2|+-------------+|状态1|+------|------+v--shift提取几个关键点:-有多个状态->数组-不同状态之间指向->数组的索引值,游标-可以是步长限制->数组的长度场景:有一个新的操作,就是往历史中插入新的数据history.push(statusNew);场景:如果满了,取出最先插入的数据history.shift();场景:撤销一步,将光标指向上一步,得到之前的状态。同理重做一步。此时根据数据重新渲染,界面会回到上一步的状态。cursor--;callback(history[cursor]);那么history的结构可能会增长如下:[{status1},{status2},{status3},{statusNew},]这样一个简单的历史数据结构设计就完成了.留个问题:如果退到最后几步再继续操作,整个历史状态怎么办?最后通过组装集成,大致满足了一个可视化编辑器的主要功能。再看操作界面上的数据,可以分为两部分,一个是前台页面数据,一个是后台交互数据,大致如下:回头看上面的流程,已经从一个简单的数据列表渲染到复杂的数据与前后台交互的WebIDE,但从数据结构的设计形式来看,本质上的变化不是很大,只是
  • 变成了等,包含的数据量里面也增加了很多。大家会不会发现,虽然这个数据看起来很大很复杂,但其实也有些清晰简单。而你的角色更像是一个建筑设计师,掌握整个结构框架,然后一砖一瓦地管理。上述流程同样适用于其他项目的开发。在设计之初,很难一下子把整个设计拼凑起来,尤其是从一无所知到对某个事物有概念,从0到1的过程,客观来说是不容易的。可以先尝试抽取几个关键步骤,写几个小模块,让关键路径走通。前期很有效,然后把这些看似零散的小部件组装起来。就清晰多了,如此反复,整个设计会变得更清晰、更饱满。数据的设计也是相应的。它由小数据组成,逐渐形成一个比较大的数据。这个时候,代码可能不是最关键的,但是如何合理、有效、清晰的管理这些数据,可能更像是后端数据库管理一样。往往需要一个不断试错的过程,走一些曲折的路,最后你会变得更舒服。好的设计是不断迭代,勇于试错,不怕踩坑,有句话叫坑深了才会刻骨铭心。iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当网发售。