【低代码漫谈】拖拽生成视图的要点

1,912 阅读14分钟

导读

【低代码漫谈】从前端三大框架到前端低代码》一文中挖了个坑,说要补上试图可视化生成的部分,现在来填坑了。这部分有较高的技术复杂度,很考验架构设计能力。相信一些同学一直有疑惑,前端能做什么架构?那么希望读完这篇文档之后,能够一定程度的解答你的疑惑。

另外,还是出于职业操守的原因,本文不会出现代码细节,只会是一些理论性的文字描述,看起来可能会有些枯燥,但的确是干货。其实每一个模块的内容都挺多的,有些地方笔者也只能点到即止,如果能够耐心读完全文,不管你是否会做低代码,相信一定会有收获。

正文

让我们从最直观的交互图开始: image.png 图片引自《可视化拖拽组件库一些技术要点原理分析》,质量很高的技术实现细节类文章,推荐一读。我们就从图中的工具栏、组件列表、画布、属性区域四个部分分别说起。

工具栏

这部分的要点主要是两个:撤销/重做实时保存,这两个功能会严重影响项目数据格式与状态管理的设计。

撤销/重做

这个功能一定要在方案设计之初就要考虑进去,因为这与你的状态管理设计强相关。在《三大框架》一文中笔者提到过,现在的视图模型已经可以抽象成 UI = fn(state) 了,所以理论上只需要用一个栈管理好 state,就能够做好撤销/重做功能,具体可以参考类似 redux-undo(其实不太好用)等库,有挺多的,本质都是维护一个栈,这里就不展开了。另外,库的选择还有一个数据格式的考虑因素,请继续看。

实时保存

这里的问题,主要是因为每次保存的数据量可能会很大,会有性能问题。我们要保存的数据主要是已经拖拽生成的页面结构数据,也就是画布中呈现的组件树。理论上这颗树很有可能会很大,嵌套很深。所以如果有实时保存的需求,这里就需要设计一下了。解决思路主要有以下几种:

  1. n 秒主动保存一次。 这种思路的假设前提是数据存储以整棵页面组件树为数据结构,也就是一个递归嵌套很多层的大 JSON 数组。

优点是不用对数据进行多余的处理,直接把前端生成的数据传上去就行,对于前端较友好。同时 n 秒保存一次,一定程度的减少了请求次数。

缺点是并不是真正的实时,而且如果不作任何处理的话,在用户长时间不做操作时,有可能造成无效的保存请求。

  1. 每次操作只把变化的组件信息上传。 这种思路的假设前提是数据存储以组件为单位,通过 parentIDID 属性来还原成树,说白了就是把树拍平了存。

优点是可以实现真正的实时保存,且数据结构对后端较友好,准确的说是对关系型数据库较友好。

缺点是需要对初始数据进行组装,还原成树;每次保存前还需要做 diff,把变化的组件找出来传给后端,有一定的技术复杂度。不过结合撤销/重做功能一起看,有些 undo/redo 的库已经实现了 diff 功能,能够直接给出 diff 结果,如果采用了此方案的同学可以调研一下;另外值得一提的是,遇到把列表中的第一个组件拖到最后一个的情况,所有组件的索引都会变,类似的这种 case 需要特殊处理;

  1. 不采用实时保存的策略,而采用用户主动触发保存,但在前端可以保留草稿。这种采用整棵页面组件树的结构即可,是投入产出比比较高的一个选择。

小节

没想到吧,最不起眼的工具栏竟然隐藏了如此玄机。相信大家已经能够理解为什么笔者会把工具栏放到第一个说了,这部分的功能实现会严重影响项目底层数据方面的设计。之前的项目中就是因为这部分是后来才考虑进来的,所以修修补补实现的很不优雅,算是一个教训。另外提个醒,所有有撤销/重做需求的项目,一定要优先设计这部分内容

画布

这部分看似特别复杂,实际上是相对较简单直接的了。主要需要考虑样式隔离文档流布局问题。

样式隔离

之前笔者实现样式隔离用的 react-frame-component,原理很简单,用 iframe 实现样式隔离,而且使用起来与普通组件没有多大区别,state 的响应式也没有任何问题,笔者比较推荐,不过其他技术栈是否有类似的库,笔者还真不太清楚。

另外,之前在调研微前端的时候也有跟样式隔离相关的内容,Qiankun 的作者 kuitos 在他的文章《可能是你见过最完善的微前端解决方案 》和《目标是最完善的微前端解决方案 - qiankun 2.0》中对样式隔离有其他方案的描述,比如 Shadow DOM,由于笔者也没有深研究过,所以就不展开了,有兴趣的同学可以进行一下扩展阅读。

文档流布局

这块实际上与想要生成的 APP 类型关系非常大。如果是 H5 运营页,那么就可能需要放大缩小、旋转、动画、绝对定位等功能。但是,这种情况并不在笔者想要讨论的范围内,所以画布中的组件排布就默认是与 HTML 中的文档流排布完全相同。从左到右,从上到下,并且区分行内/块元素,通过调整 style 的各种属性(属性区域可视化修改),来改变宽高、颜色等样式。

那么表示每个组件的抽象数据结构也就出来了,基本上与 DOM(或者说是虚拟 DOM)类似,只不过要简单许多。

小节

其实画布区域最主要的功能就是展示,样式隔离是一定要考虑到的,否则越后面处理越麻烦。另外,如果再开一下脑洞,做到了微前端那样的彻底隔离,我们甚至可以做到其他部分用 React 写,画布用 Vue 写。这并不是纯炫技的无用设计,当结合用户自定义组件功能时,用户会有使用 ReactVue 实现组件的需求,这时候我们必须要支持才行。除了画布,其他功能模块都只是数据的处理,完全可以复用,唯有画布,如果用户用 Vue 实现了一个组件,你如何在一个 React 实现的画布内展示它呢?当然,这属于锦上添花的功能,短时间内你可能不需要考虑这些。

属性区域

这块实际上就是一个动态表单。什么是动态表单呢?试想下,你点击画布中不同的组件,右侧属性区域会显示不同的表单输入项,表单项是动态生成的,这就是所谓的动态表单。

动态表单主要需要考虑核心解析引擎自定义表单项的设计,这块可以参考 formily(文档写的很好,建议阅读)以及其文档中提到的其他竞品,如果没有客观制约,建议选一个直接用就可以了。下面简单说一下这两方面的要点。

核心解析引擎

这块实际上就是 formily 等框架的核心部分,需要完成用户输入响应、校验、渲染等表单常用功能。不过 formily 其实有一些“过于周到”了,以至于一定程度地影响了易用性。在当前这种需求场景之下,很多功能都用不上,所以如果想自己写一个简单的解析引擎,也不是太难。

另外这里又不可避免的出现了 JSON Schema,它是目前来看最合适的描述表单的业内通用数据结构,最具有代表性的库就是 react-jsonschema-form。但是肯定还不够,需要做扩展,篇幅原因就不展开了,有机会的话再单写一篇文章聊聊 JSON Schema 与表单的事。

自定义表单项

组件,尤其是自定义组件的属性多种多样,同理用来设置属性的表单项也有可能是千奇百怪的。

比如某属性值可能是个对象,设置它的表单项需要弹出个弹窗,填一个表单;再比如类似于国际化手机号的输入,表单项是一个下拉框加输入框;表单项还有可能是一个代码编译器,往里面写代码或表达式,等等。

不管表象多么复杂,我们只要抓住本质,事情就会变的极其简单。表单项的本质就是,只需要暴露出 valueonChange 属性给解析引擎就够了。任你组件长得多奇怪,交互有多复杂,解析引擎都不关心,只要你组件内部处理好 value 如何使用,onChange 何时触发就行。是的,就这么简单,仅此而已。

也许这个结论有些突然,没关系,各位可以先记住这个结论,当自己实现的时候,自然就明白了。

小节

动态表单其实也算是一个比较大的主题,否则也不会有这么多库了。不过要提醒的是,在拖拽生成视图的这个场景下,实际上动态表单的功能需求比较简单,在选型时一定要充分意识到这一点。并不一定支持多的就是好的,因为这通常会牺牲易用性。有兴趣的同学可以尝试自己实现一个动态表单的解析引擎,就像上文说的,其实在此场景下这不复杂。

组件列表

这部分看似是最简单的,只是一个列表而已,实际上水是很深的。主要需要考虑自定义组件的接入,要设计的充分灵活解耦。举一个实际场景,假如用户想要使用自己实现的组件,上传上来之后怎么让它显示在列表里,并且能够像其他组件一样拖进画布,设置属性,最后被编译成可执行的代码。

组件规范

要想实现上述场景的功能,就必须提供给用户一个规范,只要用户按照规范实现并上传了自己的组件,那么就能够完美的融入你的低代码系统。这个规范需要具备以下内容:

  1. 组件名称、图标、类型(分组)等信息,在组件列表展示时使用;
  2. 组件的代码实现(React、Vue 等,取决于画布支持什么),主要用于画布展示和编译生成 APP 代码时使用;
  3. 组件属性的描述模型,即上文提到的动态表单的模型输入,如 JSON Schema,右侧动态表单引擎生成表单时使用;

只要满足上述的信息,不管是用单文件还是多文件压缩包的形式,都不重要,可以你自己定。

扩展

一旦上述规范制定好了,就意味着组件开发与其他模块完全解耦。OK,那又怎样呢?从工程角度来说,这是一个质的变化。我们知道,单独实现一个组件的难度是非常低的,这意味着你可以靠堆人来提升效率。理论上,即使你打算实现 99 个组件,只要人手够,也能够在 1~2 天内完成。这就是解耦的威力。其实从另一个角度去理解,这不就是用户在开发自定义组件嘛,千万个用户可以在很短时间内产出千万个自定义组件,本质上没有区别。

另外,这也做到了 UI 库的解耦。不管你的低代码平台用的什么 UI 库,你的组件理论上可以用任何的 UI 组件库,而且切换成本很低。如果想要做到极致,甚至可以做到动态加载。这里面能做的事情很多,就不展开了,大家自己发挥想象力吧。

小节

笔者私以为,组件列表这里是最考验架构设计能力的。关键词有三个,解耦,解耦,还是解耦。如果设计之初就能够站在一个很高的角度去思考问题,事情会变得事半功倍。另外,这也第无数次的验证了“高内聚,低耦合”的真理。

结语

本文从工具栏、画布、属性区域、组件列表四个模块的角度入手,梳理了前端低代码平台的一些核心注意事项,其实更偏向于架构设计的思想。下面简单总结一下关键点:

  • 工具栏:撤销/重做、自动保存会影响前后端数据格式的设计;
  • 画布:主要考虑隔离,样式隔离必须做。如果做得更彻底一些,像微前端那样,理论上可以实现多前端语言的支持;
  • 属性区域:本质就是一个动态表单,这是一个复用性很强的主题,私以为值得所有前端同学研究一下;
  • 组件列表:主要考研规范设计能力,一定要做到彻底解耦,很考验功力,如果设计好了对于工程进度来说绝对是质的提升。

相信绝大多数同学,在看到开篇那个图的时候,很难想象笔者会搞出这么多道道。要不是笔者曾经搞过一段时间的低代码,也的确讲不出来这么多道道。这些都是实践当中摸爬滚打出来的经验教训,希望对大家多少有点用。

笔者也知道,没有代码,光用文字描述会很枯燥,很难让人读的下去。但是职业道德还是非常重要的,笔者写这些文章实际上也是给自己的经验做个总结,多年之后看到这些文字能让自己明白就好了。还望大家多多海涵。

最后,这里实际上只是在框架上梳理了一遍。还有很多难点需要解决,包括但不限于:

  • 表单组件的设计,包括表单项联动、子表单、布局等;
  • 表格组件的设计,包括数据展示、筛选、排序、搜索等;
  • 跨页面数据传输,最典型的就是点击表格的某一行,跳转到一个表单页,数据回填上;
  • 数据容器组件,这个组件比较特殊,内部的组件都能够访问到该组件绑定的数据;

等等等等,还有很多细节难点需要攻克。这些问题暂时不打算在这个系列中展开讲,有机会的话会另起一个系列。前端低代码的坑算是填完了,后续关于后端低代码和编译发布等内容,敬请期待。

“完美不在于无以复加,而在于无可删减,万事莫不如此。”—— Antoine de St.Exupery