前言
在开发或使用低代码产品时,我们总会想到一个功能,就是预览,用来确认编辑的内容是否符合预期。
市面上的编辑器在预览方面有多种实现方式:
打开一个新的窗口,进行预览
直接在编辑区所见即所得
如果是用户,当然是选择第2种,最直观,最符合预期。
如果是开发者就会知道,之所以有这样多的可能性,是因为所见即所得非常依赖实际生产的运行环境,第1种单独打开窗口的实现成本比较低。
而有的编辑器虽然声称是所见即所得,但实际操作上,只是有相似度的所见即所得,因为实际运行环境和生产有较大的差距,仍然比较容易产生预期外的问题。
在我们团队的珊瑚海项目中,就实现了一个可以做到完全所见即所得的低代码编辑器,如果想了解该项目概况,可以访问下我们过往的两篇基本介绍文章。
《设计无侵入性的低代码编辑器》
《珊瑚海跨端解决方案及在移动端的布局动态化实践》
下面我就来介绍下,我们是如何实现的所见即所得低代码编辑画布。
方案设计
在描述具体实现之前,先声明一些前提:
本文描述的编辑器的运行容器都是浏览器,因此所见即所得的主要是浏览器的页面
本文所描述的所见即所得的“得”,主要指和生产环境最终产物一致的效果,而不是高保真Demo
2.1 通常的画布实现
低代码编辑器中,通常是将组件的实际内容拖拽到编辑器的画布中,然后展示出一个页面的布局情况。
如果想对画布中的元素进行编辑和修改,首先要点击一下画布中的元素,会出现对应元素的属性配置,修改这些配置项,并将修改后的效果展示在画布中,就完成了一个基本的低代码编辑的操作。
这套基本的操作,在时下主流的编辑器中都是将拖拽的元素、选择元素的事件、选框都写在了一个文档流中,而这样的设计就让画布的内容包含了编辑器的样式和脚本,而生产环境并没有这些内容,这就导致在这样的设计下,编辑器中的内容,必然无法完全做到所见即所得。
2.2 通常的预览实现
基于对编辑器画布无法完全做到所见即所得的设计,也就衍生了通过弹出一个单独的容器(可能是一个沙盒窗口、也可能是新建一个页面)的方式来完成生产环境页面的预览,这样是可以达到预览生产环境页面的目的。
但是这就让编辑和对产物的预期产生了一定的割裂,一边编辑,还要一边去打开一个额外的容器去看效果,显得比较笨拙。
2.3 让预览作为编辑环节的一部分
由上面的铺垫我们不难想到,如果我的画布上编辑的内容就是预览的内容,不就能做到直接编辑生产环境的内容了么。
那这个预览的内容放在哪里呢?通过画中画的方式根据修改实时刷新,这或许不错,但是在优先的窗口下,预览要看的清楚就会有些占地方。
既然画布描绘的就是期望和生产一样,那不如把这个预览的窗口垫在画布下面,甚至作为画布的一部分是不是就能达到目的了呢?
像上图一样,我们将预览的容器放在画布区域下面,每当编辑器中的页面产生变化,我们就重新渲染一下容器,这样就可以达到我们在页面中所见即所得的目的了,酷!
但是实际操作的话我们就会发现,原有画布和预览容器都有内容的展示,这会导致上下层级视觉上的冲突,而且因为两个容器的运行环境不同,也导致了内容无法一一对齐,很容易就产生了错位,不过既然整体思路有了,下面我们就来解决问题。
编辑层和渲染层
我们来整理一下将预览容器放在画布下显而易见的问题:
组件的展示会产生重叠
两层内容会产生错位
针对这两个问题,我设计了一套多层结构画布,即由编辑层和渲染层组成的画布。
其中渲染层就是预览的页面,在这一层我们不会做什么工作,因为一旦做了什么就会影响生产版本的还原度。
也因此,渲染层的容器我选择了iframe,用iframe可以较完美的隔绝编辑器和渲染层的上下文,不会受到意外的干扰;同时也可以在操作弹窗这种组件的时候,让覆盖整个页面的蒙版更可控。
而编辑层之于原来的画布,我们也做了一些改变,最大的区别就是编辑层不再感知当前拖拽的组件实体,而是通过一个通用的占位组件结合当前要操作的实际组件信息,组织出一个触发器,以透明的方式遮盖在渲染层对应的组件上方。
上图是一个实际用户从操作编辑层,到看到编辑结果的流程。通过这样的设计,用户操作编辑器的时候,看到的就是原来的预览容器,也就是渲染层展示的内容。
而用户去点击渲染层组件的时候,其实点击到的是覆盖在渲染层上面编辑层中的触发器,然后根据触发器绑定的组件信息,进行实际的编辑。
在编辑过后,通过将编辑器的产物给到渲染层做重新的渲染,就有可能达到百分百的所见即所得的效果了,且解决了组件间重叠的问题。
用户在编辑时想要点击一个画布中的组件,如何能让系统正确地识别到用户点击的这个组件呢?这就要先解决两个层级中组件错位的问题了。
编辑层和渲染层组建的布局同步
为了能让编辑层的触发器和渲染层的实际组件布局同步,我首先尝试的方案是通过收集渲染层组件渲染后的实际展示大小,让编辑层的触发器继承对应组件的这个大小然后根据所设置的默认信息,自然形成编辑层的布局状态,来达到一一对齐。
这里“自然形成”的意思是指如果一个组件在渲染层用的是flex那编辑层对应的触发框也用flex,而收集组件大小是因为,编辑层没有实际的内容,如文本组件里面会因为文字内容的不同,大小不同。
而这种“自然形成”的策略在实践中很快就出现了问题:
受到全局属性的影响,如box-sizing在编辑层和渲染层的定义不一样会导致渲染布局的整体逻辑都不同
一些组件自定义的样式编辑层感知不到,对于这种黑盒的场景无法完全覆盖到
尤其是组件样式的问题,导致了这个方案直接被否定,因此我们转向了另一个思路,获取组件最外层的所有布局信息传递给编辑层。
我在设计这套DSL给定每个节点一个id,在这里我们可以通过将节点id附着在渲染组件的DOM上。
再通过DOM的getBoundingClientRect方法获取DOMRect对象,再结合组件上的overflow属性,我们就可以较精确的定位到组件在渲染层的实际位置和大小。
因为渲染层用的是iframe,我们在编辑器是可以获取到渲染层的上下文的,因此通过前面的方案我可以轻松获取到所有组件的布局信息,并和编辑层一一对应。
实际应用
咱们来看下实际的效果
上图是编辑器中的实际效果,我们可以看到页面就是生产一样的页面,点击内容会触发选框。
为什么有多个选框?这是使用了数据绑定和根据数据循环渲染节点的效果,后面的文章我们会对实现进行展开,本文不做展开。
我们换个图来看下这个页面。
在chrome的devTools中layers模式下我们可以更直观地看出整个分层的关系,每一个触发器都清晰可见。
看到这个图大家应该可以更直观地理解我们的这个方案操作方式。
好处不止所见即所得
通过编辑层和渲染层配合得到的所见即所得的设计,我们可以再发散一下思维,看看在这样的架构下还能做些什么。
低代码编辑器跨技术栈的实现:渲染层的隔离,也就是组件运行环境的隔离。稍加操作,我们完全可以让渲染层以外的内容不用感知组件的实现,如通过声明的方式导入组件。
通过这样的设计就能做到编辑器和渲染层中的组件用不同的技术栈,就能做到react开发的编辑器,拖拽vue开发的组件库,这能让编辑器可复用性更好,也能更加地减少开发资源。
线上页面调试工具:我们也可以换个思维,做一些工具,如chrome的扩展工具,在系统产生的页面上像收集布局信息那样,直接植入编辑器或者做一些调试工作。
在这样灵活的低代码编辑器设计下,你也可以发挥想象力,做更多更酷的事情。
并不完美
这个设计可以让我们的低代码编辑器复用,且更具扩展性,但也有一定的不足:
知识成本:虽然能做到完美还原生产环境的状态,但是是否能真的还原依赖组件库开发者的建设,比如生产环境所有依赖的脚本和样式,即使和组件库并无直接关系。
动态页面的解决方案:如果是纯静态的页面,这个设计很好理解,但如果是要调用接口呢,整个页面是动态的呢,要怎么做到所见即所得,这里就需要编辑器绑定数据的建设,而这里我也做了设计和实践,将在后面的文章中说明。
对图文混排支持不够:还有就是图文混排的场景,选框不够精准,因为行内片段可能是个多边形,这个因为场景比较少,我还没有想到好的方案,希望有想法的同学可以留言给我。
渲染次数过多导致性能问题:再有就是这样的方案会导致编辑层和渲染层的内容重新渲染次数过多,比较消耗浏览器的性能。这里在后面会考虑根据场景策略加一些变更的监听,来减少一些渲染的回流,不过这就是后话了。
总结
随着前端技术的覆盖面越来越广,低代码作为前端页面搭建的一个提效选择,几乎成为了前端团队必须的技术沉淀,为了能让低代码系统本身的开发成本越来越低,大家也在做各种各样的尝试,编辑层和渲染层的分离也是在这个大背景下的产物。
当前阿里的低代码引擎和腾讯的魔方平台也都使用了类似的方案。在开源的内容中,为了让画布层更可控,两个大厂都不约而同的选择了定制化的画布,而我在这里给出的方案是增加了一些开发的知识点,但是更加开放,把掌控权给到了开发者。
另外就像前面说的,我还考虑了数据绑定还有跨技术栈相关的场景,后续的文章中将会更新相关的内容。
什么?大家想看到实际的代码?我们将会在下一篇文章输出的时候同时附上本文所涉及的内容,欢迎大家留言催更。