页面可视化搭建实践

低代码引擎和生态建设实战及思考

Posted by Li Yucang on August 17, 2023

页面可视化搭建实践

活动页面开发之痛

活动页面特点

前端业务中, 经常需要开发产品介绍页/营销页/活动页/图片展示页等页面. 这类需求有以下几个特点:

  • 页面类似: 页面布局和业务逻辑较固定.

  • 需求高频: 每周甚至每天有多个这种需求.

  • 迭代快速: 开发时间短, 上线时间紧.

  • 开发性价比低: 开发任务重复, 消耗各方的沟通时间和人力.

活动页面常规开发流程

流程:

1.运营/产品提出页面需求.

2.走项目流程进入开发环节.

3.开发根据设计稿完成页面开发.

4.测试进行页面测试.

5.运维进行页面上线.

6.运营/产品进行页面验收.

痛点:

1.多方参与, 反复沟通, 串行流程.

2.页面上线周期长, 无法快速响应活动需求.

3.人力陷入重复工作泥潭, 忙碌而低效.

更优的流程

对于高频和重复的活动页面开发, 业界一般将页面做成配置化, 配置工作从开发人员交接给产品/运营等需求方; 开发和设计人员只需提供配置化页面支持. 更优的活动页面生成流程依靠页面可视化搭建系统来实现.

流程:

1.运营/产品提出页面需求.

2.运营/产品在页面可视化搭建系统中选取合适的页面模板进行页面搭建.

3.页面自动化发布上线, 页面需求完成, 流程完结.

4.如果运营/产品没有找到合适的模板.

5.开发进行页面模板开发, 并将页面模板添加到页面可视化搭建系统中.

6.运营/产品继续「流程2」.

同时, 随着页面可视化搭建系统中的页面模板不断丰富, 新的页面需求对开发人员的依赖逐渐减低, 可由运营/产品直接完成.

低代码体系的架构设计思考

什么是低代码

在讨论低代码方案前,首先要明确「低代码」究竟是什么?

这个问题不好直接回答,因为低代码是非常宽泛的概念,有很多产品都声称自己的低代码,但我们很容易反过来回答另一个问题:「什么是低代码产品唯一不可缺少的功能?」

我认为这个功能是可视化编辑,因为非可视化编辑就是代码编辑,而只有代码编辑的产品不会被认为是低代码,因此可视化编辑是低代码的必要条件,低代码其实还有另一个更清晰的叫法是可视化编程。

既然可视化编辑是低代码的必要条件,那从实现角度看,实现可视化编辑有什么必要条件?

我认为可视化编辑的必要条件是「声明式」代码,因为可视化编辑器只支持「声明式」代码。

解释一下什么是「声明式」,除了声明式之外还有另一种代码模式是「命令式」,我们分别举两个例子,如果想绘制一个红色区块,用「声明式」来实现,可以使用 HTML+CSS,类似下面的方法:

<div style="background:red; height:50px"></div>

而换成用「命令式」来实现,可以使用 Canvas API,类似下面的方法:

const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
const rectangle = new Path2D();
rectangle.rect(0, 0, 100, 100);
ctx.fill(rectangle);

虽然最终展现效果是一样的,但这两种代码在实现思路上有本质区别:

  • 「声明式」直接描述最终效果,不关心如何实现。

  • 「命令式」关注如何实现,明确怎么一步步达到这个效果。

从可视化编辑器的角度看,它们的最大区别是:

  • 「声明式」可以直接从展现结果反向推导回源码

  • 「命令式」无法做到反向推导

反向推导是编辑器必备功能,比如编辑器里的常见操作是点选这个红色区块,然后修改它的颜色,在这两种代码中如何实现?

如果是「声明式」的 HTML+CSS,可以直接改 style 的 background 值,而基于 Canvas 的命令式代码则无法实现这个功能,因为无法从展现找到实现它的代码,命令式代码实现同样效果的可能路径是无数的,除了前面的示例,下面这段代码也可以实现一样的效果:

const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(50, 0);
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 100;
ctx.stroke();

甚至有可能这个颜色是多个字符串加随机数拼接而成,即便通过静态分析也找不到来源,从而无法实现可视化修改。

「命令式」代码无法实现可视化编辑,而可视化编辑是低代码唯一不可少的功能,所以我们可以得到结论:所有低代码平台必然只能采用「声明式」代码,这也是为什么所有低代码平台都会有内置的「DSL」。

综合来看这些「声明式」语言有以下优点:

  • 容易上手,因为描述的是结果,语法可以做得简单,非研发也能快速上手 HTML 及 SQL。

  • 支持可视化编辑,微软的 HTML 可视化编辑 FrontPage 在 1995 年就有了,现在各种 BI 软件可以认为是 SQL 的可视化编辑。

  • 容易优化性能,无论是浏览器还是数据库都在不断优化,比如可以自动改成并行执行,这是命令式语言无法自动实现的。

  • 容易移植,容易向下兼容,现在的浏览器能轻松渲染 30 年前的 HTML,而现在的编译器没法编译 30 年前的浏览器引擎代码。

而这些语言的缺点是:

  • 只适合特定领域,命令式的语言比如 JavaScript 可以用在各种领域,但 HTML+CSS 只适合渲染文档及界面,SQL 只适合做查询,所有这些语言都。

  • 灵活性差,比如 SQL 虽然内置了很多函数,但想只靠它实现业务是远远不够的,有些数据库还提供了用户自定义函数功能(UDF),通过代码来扩展。

  • 调试困难,遇到问题时如缺乏工具会难以排查,如果你在Firefox出现前开发过页面就会知道,由于IE6没有开发工具,编写复杂页面体验很差,遇到问题要看很久代码才发现是某个标签没闭合或者 CSS 类名写错了。

  • 强依赖运行环境,因为声明式只描述结果而不关注实现,因此强依赖运行环境,但这也带来了以下问题:

    • 功能取决于运行环境,比如浏览器对 CSS 的支持程度决定某个属性是否有人用,虽然出现了CSS Houdini 提案,但 Firefox 和 Safari 都不支持,而且上手成本太高,预计以后也不会流行。

    • 性能取决于运行环境,比如同一个 SQL 在不同数据库下性能有很大区别。

    • 对使用者是黑盒,使用者难以知道最终实现,就像很少人知道数据库及浏览器的实现细节,完全当成黑盒来使用,一旦遇到性能问题就不知所措。

    • 技术锁定,因为即便是最开放的 HTML 也无法解决,很多年前许多网站只支持 IE,现在又变成了只支持 Chrome,微软和 Opera 在挣扎了很多年后也干脆直接转向用 Chromium。同样的即便有 SQL 标准,现在用的 Oracle/SQL Server 应用也没法轻松迁移到 Postgres/MySQL 上。低代码行业未来也一样,即便出了标准也解决不了锁定问题,更有可能是像小程序标准那样发展缓慢,功能远落后于微信。

因为低代码就是一种声明式编程,所以这些「声明式」优缺点,其实就是低代码的优缺点,了解声明式的历史及现状就能更好理解低代码,因为:

  • 低代码的各种优点是「声明式」所带来的。

  • 低代码被质疑的各种缺点也是「声明式」所导致的。

为什么需要低代码

字不少,尝试提取几个关键词,可视化、配置化、低门槛、快捷交付(GUI、Configuration、Minimal Upfront Investment 和 Rapid Delivery)。我们先不深究这几个词的意思,来看几个例子。

表单场景

办公室行政人员小 A,主任让他收集全办公室100个人的行程信息,他用一个 表单低代码平台 快速搭建了行程记录表单应用,同事填完后,还能从后台实时通过图表看数据。

小程序场景

营销人员小 B,总监让他做个营销小程序,他用一个 小程序低代码平台,拖拖拽拽完成了小程序的搭建,然后一键发布,完成了本不可能完成的任务。

模型驱动场景

开发人员小 C,天天苦于写标准化的表单、表格页面,他感觉是重复劳动,但又不得不做,直到某天他发现用一个 模型驱动的低代码平台,通过定义数据模型后,可以自动化生成页面 CRUD,又快又好。

好了,相信通过这几个例子,大家对低代码有了更形象、更深的理解。

低代码的核心价值

总结一下,我们可以通过可视化界面来配置完成传统的应用程序开发 & 交付过程,而不需要了解太多的开发技能,让办公室行政人员、营销人员等非技术人员轻松完成「研发」工作,让开发人员更快地研发。

所以,我理解的低代码的核心价值是「降本提效」和「角色赋能」。

从公司整体视角看低代码体系

烟囱架构

低代码的核心价值是「降本提效」和「角色赋能」。正因为有了这两点核心价值,公司一些部门很早就已经在低代码领域探索并产生了一些平台,平台之间相互独立,属于典型的烟囱架构,烟囱架构从公司整体视角来看有一些问题,比如有一些通用能力没法复用,导致平台建设的成本高且质量稂莠不齐。

和而不同

为了避免低水平重复建设,从而降低各业务场景中低代码平台的建设成本,提升低代码体系中物料、插件、解决方案、产物等的可流通性,我们决定对低代码技术体系进行拉通共建,制定统一的底层协议,并合力打造一套统一的低代码基础设施。

我们有各种技术栈,有 react / vue / 小程序,我们有各种用户角色,有产品经理、设计师、业务人员、前后端开发,当然,我们还有各种业务场景,有 toC 业务,有 toB 业务,有做数据类业务,有做设计研发一体化的业务。

如何找到平台的共同点?以及支撑平台差异点?这是架构能否成功的关键!思考再三,我们确定了十二字设计原则:协议先行,最小内核和最强生态。

分层架构

最终我们设计了这样一套分层架构,自下而上分别是协议 - 引擎 - 生态 - 平台。底层协议栈定义的是标准,标准的统一让上层产物的互通成为可能,引擎是对协议的实现,同时通过能力的输出,向上支撑生态开放体系,提供各种生态扩展能力,那么生态就好理解了,是基于引擎核心能力上扩展出来的,比如物料、设置器、插件等,还有工具链支撑开发体系,最后,各个平台基于引擎内核以及生态中的产品组合、衔接形成满足其需求的低代码平台。

每一层都明确自身的定位,各司其职,协议不会去思考引擎如何实现,引擎也不会实现具体上层平台功能,上层平台的定制化均通过插件来实现,这些理念将会贯穿我们体系设计、实现的过程。

低代码引擎&UIPaaS

协议栈

首先聊聊协议栈,目前我们有两个:一个叫《中后台前端搭建协议规范》,另一个叫《中后台前端物料规范》。

内容很多,我这里简单做个概括,两份协议定义了 3 方面的内容,分别是术语、结构和行为。

  • 术语是我们沟通的基础,概念相通,我们才能高效沟通。我们根据物料的颗粒度,定义了基础组件、区块、低代码组件、模板等术语,另外还包括低代码生产过程中一些模块名称,比如编辑器、画布、事件绑定、数据绑定、渲染、出码、设置器之类的术语,

  • 结构,包括页面描述的结构,如何定义页面组件树、数据源、生命周期、页面状态等等。

  • 行为,不同的业务场景,我们对物料的配置、约束、扩展各不相同,所以我们在物料描述中有各种各样的钩子来支持自定制。

正是有了几份协议,让上层的互通成为可能,概念互通,物料互通,生态互通。

引擎内核

低代码引擎分为 4 大模块,入料、编排、渲染、出码。

  • 入料模块就是将外部的物料,比如海量的 npm 组件,按照《物料描述协议》进行描述。注意,这里仅是增加描述,而非重写一套,这样我们能最大程度复用ProCode体系已沉淀的组件。将描述后的数据通过引擎 API 注册后,在编辑器中使用。

  • 编排,本质上来讲,就是不断在生成符合《搭建协议》的页面描述,将编辑器中的所有物料,进行布局设置、组件 CRUD 操作、以及 JS/CSS编写/逻辑编排等,最终转换成页面描述。

  • 渲染,顾名思义,就是将编排生成的页面描述结构渲染成视图的过程,视图是面向用户的,所以必须处理好内部数据流、生命周期、事件绑定、国际化等。

  • 出码,就是将页面描述结构解析和转换成应用代码的机制。

编排

下面我们展开来聊聊编排,首先我们得有一个工作台,我们叫编辑器骨架,分为几个默认可视的区域,以及一些可以展开的区域,可以弹窗显示的区域。中心区域,是编排和渲染的画布。

前面说过,编排的本质是不断生成符合《搭建协议》的页面描述的过程,然后通过渲染器将页面描述渲染成真正的视图。

协议是文本协议,是一个 json 结构,理论上手写也能完成,但是考虑到可编程性,我们设计了一套节点和属性模型,类似于 DOM,这样操作节点 + 配置属性就等价于在操作页面描述,也就是操作 json 结构了。

除了节点模型和属性模型之外,上层还有文档&项目模型,对于物料的管理,有物料注册机制和物料模型,另外我们提供了通用的面板管理、拖拽引擎、resize引擎,设计器辅助层、原地编辑、快捷键等二十几个模块,这里就不细说了。

而这所有的模块的能力,也就是 API,都通过插件进行调用,于是插件成为了扩展编辑器的唯一载体。你可以定制你的面板,可以操作节点树,可以定制节点的扩展操作,可以去操作物料模型,可以去绑定快捷键,可以设定画布大小,可以定制拖拽行为等等。

出码

再来聊聊出码,对于一些常规场景,直接由渲染模块渲染即可。但是考虑到一些特殊情况,比如一些不支持动态化的场景,小程序,或者为了更好的性能,转码成 ProCode 打包部署,或者需要二次开发,因此,我们设计了出码框架。出码框架提供一套流水线式的处理流程,类似 babel 的机制,通过一个个的出码插件 / preset 来定制你的出码产物,市面上的 react 框架、vue 框架、小程序框架都可以支持。

引擎生态的设计

再来聊聊引擎生态的设计,前面也提到,最小内核最强生态是我们的设计原则,因此如何定义什么是内核能力,什么是生态以及如何支撑生态,是我们整个体系设计的重中之重。

经过我们支撑众多平台的经验,我们发现平台的差异性体现在这 3 点,物料、设置器和插件,其中插件是扩展的入口,包括物料和设置器也是通过插件才能注册到引擎。我们定义了引擎的约束,这是唯一不可变的部分,以及引擎 API 的能力,包括面板、画布、物料管理、拖拽等所有能力,都可以通过插件来使用。同时,插件我们设计成高内聚、显性化配置、可流通的形态,这支撑了插件生态的形成,甚至更高层面,让自定义设计器也可以通过可视化配置实现。

多说一嘴,因为生态体系如此重要,我们在生态元素调试能力上也下了一番功夫,目前我们通过工具链 + 调试插件让一切生态元素均可调试,可相互组合调试,可线上调试。

我们具象化一点来看引擎生态,这是一个标准的中后台设计器页面。蓝色部分是插件,这些都是能被看到的插件,因为调用的是面板 API,不仅如此,还有一些不能被看到,比如调用了快捷键 API,拖拽 API、事件 API 等。红色部分就是设置器了,可以定制我们如何给一个节点的属性赋值。橙色部分就是物料了,其实物料本质上是一个模型,也是不可见的,不过这里通过物料面板调用了物料 API 来显性化展示了物料,再通过拖拽 API 和 节点 API 来拖拽并插入到画布中。

支持多业务场景

丰富的生态,让快速、低成本打造低代码平台成为可能。我们有物料生态、设置器生态、插件生态,因此,我们推导出一个简单的公式,打造低代码的设计器等价于引擎 + 选择物料 + 选择设置器 + 选择插件。

支持多技术栈

再来聊聊我们如何通过协议来支持多技术栈。不管是《中后台前端搭建协议规范》,还是《中后台前端物料规范》,都是与语言无关的。你定义一套物料描述,而具体实现可以是 react / vue 或者任何技术栈,而对于搭建页面,你可以在设计态用 react 组件,渲染时也用 react 组件,但注意,因为设计和渲染的中间产物页面描述也是语言无关的,所以渲染时可以是任意语言,可以是 react,可以是 vue,当然也可以是小程序。

编排和渲染的双层架构设计

再来聊聊编排和渲染的双层架构设计,通过这个架构,我们实现了绝对纯净的编辑态渲染,即模拟器实现。

这个图相信大家都很熟悉,编辑器中内嵌一个所见即所得的渲染模块,但这会有一个问题,css 污染的问题,因为编辑器中各个模块,物料、设置器、插件都来自不同的团队,很容易产生 css 污染。编辑器中的元素互相污染问题都不算太大,但是污染了渲染视图就很严重了,大家可以思考下为什么?

我们的解法是将模拟器放入到一个新的 iframe 中运行,通过编辑器将相关资源注入到模拟器,建立数据通道,使用 facade 模式,即在编辑器和模拟器中各有一个 facade 对象来负责对外的方法暴露和调用,避免深度耦合。

引擎架构

低代码引擎通过协议先行,最小内核,最强生态的理念,形成了 4 大模块以及生态扩展性的整体设计,在灵活性上足以支撑各种类型低代码平台。

还欠缺什么

但是,大家此时可能会有些疑问,这引擎 + 生态的组合似乎还是偏底层,离一个真正生产可用的低代码平台有点距离。比如:

  • 搭建出来的页面描述保存到哪里去?

  • 搭建完成后,产物打包系统哪家强?

  • 页面多人编辑冲突如何解决?

  • 研发流程如何定义?

  • 版本管理,多分支咋搞?

  • 页面区块 / 低代码组件 怎么搭建?怎么使用?

  • 这里的问题可以列出上百个,因为这都是我们遇到并已经解决的问题。

UIPaaS

所以,我们在引擎之上再加上一层,形成一个低代码平台的基座,或者叫孵化器。我们把这个低代码平台的孵化器叫做 UIPaaS,在公司内部,我们更多是基于 UIPaaS 来开始打造低代码平台,这样会更轻松一点。

为什么要做 UIPaaS?两点原因:

  • 解决产品能力的问题,实现了应用管理、研发流程、打包流程、发布流程 等一系列能力

  • 解决快速在找到符合需求的生态元素组合

UIPaaS 拓展能力

我们来看看它包含哪些能力以及支持哪些定制化:

  • 设计器:提供一个开箱即用的标准版页面设计器,开箱即用意味着整合了一批插件,插件都已经跟后端服务相绑定了;提供简单版、进阶版设计器定制方案。

  • 运行时:提供稳定的,功能丰富的运行时 SDK,包括页面描述的获取、路由、layout,甚至还有一套运行时中间件机制

  • 生态:提供「生态中心」,大量组件、插件、解决方案唾手可得;提供「一站式研发平台」,可开发、调试低代码领域的所有物料

  • 管理后台:提供功能完善、方便定制的管理后台模板应用,包括研发流程、应用依赖管理、打包配置、路由配置等

  • 后端服务:官方提供 140+ 网关接口,覆盖设计器、运行时、管理后台等全流程;允许上层平台注册服务到 UIPaaS,供其他平台使用。

百花齐放的低代码平台

最后来看一下百花齐放的低代码平台。我们有各种业务场景,各种用户角色,各种技术栈,因此产生形形色色的低代码平台几乎是个必然结果。我们通过 UIPaaS 孵化器来支撑,低成本、快速地支撑各个平台的开发。

总结了一下目前我们打造的垂直类平台,有耳熟能详的中后台,有运营场景,数据报表类场景,还有以设计类为代表的角色协同、产物互通的平台,还有移动应用、IoT、aPaaS 等类型。

平台很多,因为各种原因没法一一展示,这里我们来看几个典型的平台。

这是一个数据报表类的平台,会对图表库、数据模块、账号权限体系、设置器等做深度定制:

这是一个小程序编排平台,核心是接入一套小程序的组件,定制一些小程序特有的配置,以及对接各个发布渠道:

该不该打造自己的低代码平台

虽然提到了很多低代码平台,似乎让使用低代码开发成为了一种风潮。但是我建议不要盲目跟风,低代码研发也只是一种研发范式,跟以往任何一种研发范式相比,没有孰高孰低。适合的,才是最好的,评估标准只有两点:是否能研发提效?以及是否能角色赋能?

一些技术实现细节

选择画布布局方案

对于布局方案,我只推荐流式布局和自由布局,其他的附加功能大多是噱头大于实际,具体附加功能根据实际场景决定是否添加。

自由布局适合与大屏与中后台系统,因为其页面自由度高,物料间可相互覆盖。

那为什么流式布局适合H5与小程序呢?

我举个栗子,比如H5页面中,你的商品列表是根据网络接口实时查询,当商品数量发生变化导致商品列表组件高度变化时,我们通过流式布局保证布局不受影响。

单纯流式布局看起或许很low,但是操作越是简单,用户才越是方便。我想做的是能实用的工具,而不是看起来有点炫的玩具。

JSON Schema

基础类型

这是一个基础的属性Form表单,包括输入框、单选器、计数器,它们由各种元素组成。接下来我们用JSON来代表元素来组装它们:

输入框:

计数器:

选择器:

复合类型

我们除了基础类型外,组件经常也需要传入对象、数组、对象数组等复合类型。我们应该支持任何的复合类型。我们来看导航栏数据列表:

业务场景实例

从上面我们知道了怎么用JSON表示基础类型和复合类型数据,而一个组件props能接收任意数量任意类型的数据。接下来我们用导航栏组件来实际分析。

导航栏组件和它的控制面板:

导航栏组件接收styles(对象)、attrs(对象)、tabList(对象数组)

  props: {
    styles: {
      type: Object,
      default: () => {}
    },
    attrs: {
      type: Object,
      default: () => {}
    },
    tabList: {
      type: Array,
      default: () => []
    }
  },

然后我们把各个接收数据合并在一起,再加上组件的基础信息,就构成了一个描述组件的JSON。

我们按照平台JSON Schema协议写的标准JSON可以让平台识别组件信息,再调用属性解析器生成属性控制面板来控制组件。

交互逻辑的实现

前面说到前端界面低代码是比较容易,但交互及逻辑处理却很难低代码话,目前常见有三种方案:

1.使用图形化编程

2.固化交互行为

3.使用 JavaScript

先说第一种图形化编程,这是非常自然的想法,既然低代码的关键是可视化,那直接使用图形化的方式编程不就行了?

但我们发现这么做局限性很大,本质的原因是「代码无法可视化」,这点在 35 年前没有银弹的论文里就提到了。

为什么代码无法可视化?首先想一想,可视化的前提条件是什么?

答案是需要具备空间形体特征,可视化只能用来展现二维及三维的物体,因为一维没什么意义,四维及以上大部人无法理解,所以如果一个事物没有形体特征,它就没法被可视化。

举个例子,下面是一段 amis中 代码,作用是遍历 JSON 并调用外部函数进行处理:

function JSONTraverse(json, mapper) {
  Object.keys(json).forEach(key => {
    const value = json[key];
    if (isPlainObject(value) || Array.isArray(value)) {
      JSONTraverse(value, mapper);
    } else {
      mapper(value, key, json);
    }
  });
}

虽然只有 10 行代码,却包含了循环、调用函数、类型检测、分支判断、或操作符、递归调用、参数是函数这些抽象概念,这些概念在现实中都找不到形体的,你可以尝试一下用图形来表示这段代码,然后给周围人看看,我相信任何图形化的尝试都会比原本这段代码更难懂,因为你需要先通过不同图形来区分上面的各种概念,其他人得先熟悉这些图形符号才能看懂,理解成本反而更高了。

代码的这些抽象思维难以像积木一样进行拼接,积木拼接这种方式只适合用来实现简单的逻辑,比如 scratch。

而前面图形化是低代码唯一不可少的功能,这就使得低代码不适合做复杂的抽象逻辑处理,这是图形化缺陷决定的,因此在复杂逻辑处理方面低代码永远无法彻底取代专业代码开发。

但如果是面向特定领域,低代码平台可以先将这个领域难以图形化的算法预置好,让使用者只需做简单的处理,比如在 Blender 中将 PBR 算法封装了,使用的时候只需要调整参数就行

如果真要用节点实现这个算法会非常复杂,大概长这样

想象一下假设客户做出了上面这个图的复杂逻辑,然后找你排查问题,而客户的程序是部署在内网的,没法导出,只能通过微信拍屏幕给你看。

因此我认为图形化不适合用来实现业务逻辑,只适合用来做更高层次流程控制,比如审批流,审批流是现实真实存在的,没有复杂的抽象逻辑,因此适合图形化。

说完了图形化编程,接下来谈第二种方案:固化交互行为,这是不少低代码平台的做法,我们还是以 amis 为例进行介绍。

amis 将常用的交互行为固化并做成了配置,比如弹框是下面的配置:

{
  "label": "弹框",
  "type": "button",
  "actionType": "dialog",
  "dialog": {
    "title": "弹框",
    "body": "这是个简单的弹框。"
  }
}

除了弹框之外还有发起请求、打开链接、刷新其它组件等,使用固化交互行为有下面两个优点:

  • 可以可视化编辑

  • 整合度高,比如弹框里可以继续使用 amis 配置,通过嵌套实现复杂的交互逻辑

但这个方案最大的缺点是灵活性受限,只能使用 amis 内置的行为。

要实现更灵活的控制,还是得支持第三个方案:JavaScript,目前有的低代码平台只在界面编辑提供可视化编辑,一旦涉及到交互就得写 JavaScript,但这最大缺点就是无法可视化编辑,因此不算是低代码。

流程的实现

这是大部分低代码平台标配的功能,流程的逻辑不像普通代码那么抽象,因此适合用可视化编辑。

流程可视化存在很久了,著名的 BPMN 规范最早版本在 2004 就发布了,因此大部分产品都会支持 BPMN 2.0 规范。

但 BPMN 本质上是一种图形规范,它的最大作用是给事件、动作及分支条件这些抽象概念分配了不同的形体,使得熟悉这个规范的用户有了共同语言。

BPMN 不能解决平台锁定问题,在一个平台开发的流程无法直接迁移到另一个平台。

流程的核心是实现流程流转引擎,流程可视化布局后最终存储的格式是有向图,比如下面这个最简单流程:

简化后的存储数据格式是两条连线和三个节点:

{
  "lines": [
    {
      "id": "d4ffdd0f6829",
      "to": "4a055392d2e1",
      "from": "e19408ecf7e3"
    },
    {
      "id": "79ccff84860d",
      "to": "724cd2475bfe",
      "from": "4a055392d2e1"
    }
  ],
  "nodes": [
    {
      "id": "e19408ecf7e3",
      "type": "start",
      "label": "开始"
    },
    {
      "id": "4a055392d2e1",
      "type": "examine-and-approve-task",
      "label": "审批节点"
    },
    {
      "id": "724cd2475bfe",
      "type": "end",
      "label": "结束"
    }
  ]
}

流程流转算法的核心就是根据当前状态和这个有向图,判断出下个节点是什么,然后执行那个节点的操作。

同时因为主要面向的是审批流,所以还需要处理审批场景特有的逻辑,比如有的审批是全部通过才算通过,有的审批是只需要一个人通过就算通过,还有回退、加签等功能,并处理各种边界条件,比如找不到审批人的时候怎么办。

虽然目前业界有开源的流程引擎,但这些引擎大多是面向代码开发,不太好改造成平台模式,因此我们自己实现了流程引擎,这样才能更好定制功能。

组件拖拽

一个元素如果要设为可拖拽,必须给它添加一个 draggable 属性。另外,在将组件列表中的组件拖拽到画布中,还有两个事件是起到关键作用的:

  • dragstart 事件,在拖拽刚开始时触发。它主要用于将拖拽的组件信息传递给画布。

  • drop 事件,在拖拽结束时触发。主要用于接收拖拽的组件信息。

先来看一下左侧组件列表的代码:

<div @dragstart="handleDragStart" class="component-list">
    <div v-for="(item, index) in componentList" :key="index" class="list" draggable :data-index="index">
        <i :class="item.icon"></i>
        <span></span>
    </div>
</div>
handleDragStart(e) {
    e.dataTransfer.setData('index', e.target.dataset.index)
}

可以看到给列表中的每一个组件都设置了 draggable 属性。另外,在触发 dragstart 事件时,使用 dataTransfer.setData() 传输数据。再来看一下接收数据的代码:

<div class="content" @drop="handleDrop" @dragover="handleDragOver" @click="deselectCurComponent">
    <Editor />
</div>
handleDrop(e) {
    e.preventDefault()
    e.stopPropagation()
    const component = deepCopy(componentList[e.dataTransfer.getData('index')])
    this.$store.commit('addComponent', component)
}

触发 drop 事件时,使用 dataTransfer.getData() 接收传输过来的索引数据,然后根据索引找到对应的组件数据,再添加到画布,从而渲染组件。

具体拖拽呈现流程如下:

具体拖拽流程就是:

1.使用H5 dragable API拖拽左侧组件(component data)进入目标容器(targetBox)

2.监听拖拽结束事件拿到拖拽事件传递的data来渲染真实的可视化组件

3.可视化组件挂载, schema注入编辑面板, 编辑面板渲染组件属性编辑器

4.拖拽, 属性修改, 更新

撤消、重做

撤销重做的实现原理其实挺简单的,用一个数组来保存编辑器的快照数据。保存快照就是不停地执行 push() 操作,将当前的编辑器数据推入 snapshotData 数组,并增加快照索引 snapshotIndex。

撤销

假设现在 snapshotData 保存了 4 个快照。即 [a, b, c, d],对应的快照索引为 3。如果这时进行了撤销操作,我们需要将快照索引减 1,然后将对应的快照数据赋值给画布。

例如当前画布数据是 d,进行撤销后,索引 -1,现在画布的数据是 c。

重做

明白了撤销,那重做就很好理解了,就是将快照索引加 1,然后将对应的快照数据赋值给画布。

不过还有一点要注意,就是在撤销操作中进行了新的操作,要怎么办呢?有两种解决方案:

1.新操作替换当前快照索引后面所有的数据。还是用刚才的数据 [a, b, c, d] 举例,假设现在进行了两次撤销操作,快照索引变为 1,对应的快照数据为 b,如果这时进行了新的操作,对应的快照数据为 e。那 e 会把 cd 顶掉,现在的快照数据为 [a, b, e]。

2.不顶掉数据,在原来的快照中新增一条记录。用刚才的例子举例,e 不会把 cd 顶掉,而是在 cd 之前插入,即快照数据变为 [a, b, e, c, d]。

一般我们采用的是第一种方案。

低代码平台未来

最开始提到过低代码唯一不可缺少的功能是可视化编辑,这是低代码的最大优势,但是低代码的最大缺陷,因为可视化难以表达复杂的抽象逻辑,因此长远看低代码并不会在所有领域取代专业开发,更多是和专业开发配合来提升效率。

从技术方案上看低代码平台主要有两个方向:

偏向零代码的方案,它的特点是

  • 易用性强

  • 灵活性差

  • 适合小公司,客单价低,但客户数多

  • 标准化程度高,导致功能都很类似,将面临同质化竞争

  • 产品使用简单,客户支持成本低

偏向专业开发的方案,它的特点是

  • 易用性弱

  • 灵活性强

  • 适合中大型公司,客户数少,但客单价高

  • 标准化程度低,每家都有各自的特点

  • 产品使用复杂,客户支持成本高

前面字太多了,总结一下主要观点:

  • 低代码都是一种「声明式」编程,因为只有声明式才能可视化编辑,而可视化编辑是低代码唯一不可少的功能。

  • 低代码的优缺点其实来自于「声明式」本身。

  • 编写代码是一种抽象思维,因此并不适合可视化,导致低代码只能面向特定领域,复杂应用需要和专业开发配合。

  • 前端界面的 HTML+CSS 可以认为是一种低代码 DSL,因此界面的低代码比较容易实现,只需要在 HTML+CSS 基础上抽象一层。

  • 后端存储的低代码有几种方案,但没有哪个方案是完美的,它们都有各自的优缺点,这将决定一个低代码平台的适用范围,建议在选型时重点关注。