云凤蝶可视化搭建的推导与实现

云凤蝶可视化搭建的推导与实现

前言

从古老的桌面软件领域的 WPF、JavaFX,到移动端与时俱进的 SwiftUI、JetPack Compose,再到 Salesforce、OutSystems、Mendix 等一众 hpaPaaS (High-productivity Application platform as a service),一直以来许多产品都在尝试通过可视化搭建的手段来降低 GUI 应用的研发门槛,提高生产效率。而云凤蝶则是一个新的挑战者。

本文将以概览性的视角来介绍云凤蝶在 低代码+可视化搭建 这条路上遇到的问题与解决方案,包括:

  • 中后台 Web 应用搭建有哪些关键要素?
  • 可视化编辑器如何对齐 Pro Code 下视图 DSL 的表达能力?
  • 交互逻辑与状态如何进行组织与聚合?
  • 如何基于可视化底盘去探索 10 倍效能提升?

一、云凤蝶产品介绍

云凤蝶产品介绍https://www.zhihu.com/video/1175856121427955712


二、可视化搭建的关键要素

如上图左侧所示是一个简单网页,一个 Tabs 内嵌了一个 Form 表单,在普通用户的视角里看到的世界可能是扁平的,但是在开发者的视角基本都可以看出来右侧的一颗 组件树。

一个组件化的搭建平台其实本质就是一条组件组装流水线,其核心目的就是如何多快好省地帮助用户制作出这颗树。

那么让我们将视角聚焦到 “组件” 这个东西上, 在当前大多数前端框架体系下,组件是一个满足f: props => view 协议的函数,它的入参一般是一些简单的抽象好的配置,返回值则是使用某种视图描述语法(HTML、Virtual DOM)来描述的一块视图,而其计算过程可以使用任意框架或语言来实现。

如此看来,组件还牵扯到函数和运算过程,好复杂 😢 ...

而实际上我们可以先尝试做一个职责切分,因为现如今在蚂蚁前端的 Pro Code 研发体系下,Basement 负责研发流程,Gitlab 负责代码管理, tnpm 负责管理模块,Bigfish 负责组件开发规范。所有这些都已经非常成熟,我们应该把专业的事情交给专业的人来做,尽量站在这些成熟的体系之上,也就是:

“让上帝的归上帝,凯撒的归凯撒”

因此最终云凤蝶决定将 Pro Code 世界下的组件作为原材料去消费,自身专心去做一条高效且全能的组装流水线。一条组装流水线其最小功能闭环至少需要有:

  • 组件的导入与二次拼装能力
  • 组件的入参属性定制能力
  • 组件的逻辑编排与联动能力

细化来看,每一个其实都是相当复杂的课题:

  • 组件的导入与二次拼装能
    • 如何实现中心化的组件注册与加载体系
    • 如何实现编辑与预览时实时更新
    • 如何控制组件之间的加载顺序
    • 多组件的公共依赖如何去重
    • 如何处理组件的不兼容升级
  • 组件的入参属性定制能力
    • 属性面板如何表达 antd 的所有 props 类型
    • 如何支撑对属性的扩展能力
      • 对值的类型的扩展能力
        • JS 基本类型
        • 业务类型:Moment、Regexp、URL、Function、Email、Image...
      • 对值的来源的扩展能力
        • 静态值
        • 数据绑定(单向,双向,一次性)
        • 异步动态数据源(HTTP/TR)
        • 第三方图片素材库...
  • 组件的逻辑编排与联动能力
    • 组件之间组合:如何解决不同屏幕尺寸下的多组件位置关系变化问题
    • 组件之间联动:如何支持外置共享状态的管理与消费

这其中每一个点可能都需要一篇文章的笔墨才能介绍清楚,因此本文只会是一篇目录性质的纲要介绍,后续我们会陆续发表对更多实现细节的分享文章,敬请期待。

三、组件导入与制作

上一小节已经阐释了云凤蝶需要有消费 npm 世界的组件能力。如下图所示,云凤蝶既支持从代码导入也支持拖拽二次拼装。

通过上述一套流程,我们可以实现快速导入一个 npm 上已存的代码组件到云凤蝶平台上使用,并且基于构建出的依赖 Manifest 信息和 SystemJS Loader 可以实现对一些公共依赖的去重,加载顺序的控制等等。

更为重要的是,这里面出现了一份 “云凤蝶组件定义”,这是一份非常重要的白皮书,它是我们在摸爬滚打接入 antd 所有组件之后,最终总结出来的对一个组件所有编辑时与运行时公共行为的上层建模与抽象,它指导了云凤蝶如何去理解一个组件的诸多表现。

四、组件属性定制与扩展

对属性值的定制

属性编辑器的意图是完成对组件的属性定制,它本质属于一个复杂的 Schema Form 形态的配置器。但是以下图中截取 antd Table 组件的部分的 props 的为例,ant.design/components/t


可以看到:

  • 有大量 props 的值是非 JavaScript 基本类型
  • 有大量 props 的类型是联合类型
  • 有少量 props 互相之间是逻辑关联的,或者互斥

云凤蝶为此打造了一块极其强大的属性配置面板,力求完整的表达能力和良好的用户体验兼顾,后续文章会专门介绍,敬请期待。

对属性值的扩展

即使将属性面板的表单体验打磨到极致,也只是还原了 antd 的能力而已,要想得到比 antd 更高的效能提升,还需要具备对原生属性的扩展能力,因为属性的多样性就代表了组件能力的多样性,比如:

  • 对值的类型的扩展:除了 JS 基本类型之外,是否可以自定义任意业务类型?比如:国际化文案,图片素材库等。
  • 对值的来源的扩展:除了直接在属性面板里面输入静态值之外,值是否可以来源于变量绑定?来自异步请求?来自数据源?。

以云凤蝶其中一种最重要的对属性值来源的扩展 - binding(数据绑定) 为例,它和 Vue、Angular、小程序 等技术体系下的数据绑定概念一脉相承,交互形式如下:

云凤蝶 - 数据绑定https://www.zhihu.com/video/1175861306061680640

那么这种对 props 的数据绑定最终是如何存储与运行的呢?以 Table 组件的一个 props 示例:

在 Pro Code 的世界里,props 应该是一颗普通的对象树,云凤蝶通过定义一种 { $$__type, $$__body } 的特殊对象结构实现了在一颗对象树上任意位置去扩展复杂的业务实体类型。

每一种业务类型都是一个在云凤蝶系统上注册过的 “实体”,每一个实体都需要实现一个实体 Loader,如下图中的有颜色的节点全部是业务实体:


为什么数据格式不采用 JSON Schema ?因为它写起来和实现起来都繁琐,体积臃肿。

在运行时真正渲染每一个组件之前,它的 props 配置数据都会经历一个 “翻译” 过程,云凤蝶的 Runtime 会扫描 props 对象树,在任意节点一旦遇到满足 { $$___type, $$__body } 形态的 Entity,就会转而调用对应的 Entity Loader 来翻译该节点,具体操作一般是返回一个真正可交给原始组件运行的 plain data 替代该节点,同时也允许 Loader 去执行一些副作用,以数据绑定为例,副作用就可能包括监听数据变化和更新渲染视图。

一个 Entity loader 的伪代码:


对属性本身的扩展

除了上一小节提到的扩展 antd props 的配置形态,云凤蝶平台也会向所有组件的属性面板添加一些 通用高级属性,如:是否渲染、是否隐藏、重复、通用样式,悬浮提示,固钉,徽标,loading 加载中等等,每一个通用高级属性都是 Web 应用研发中某一类常见功能的抽象与封装,力求所有能被模式化的 Feature 都被沉淀下来成为一条通用路径。

而这些高级渲染属性的实现原理则是利用 HOC(Higher-Order Components)去包裹原始组件实现这些增强功能,如图:

五、组件的可视化组合与编排

组件的可视化编排会涉及几个关键问题:

  • 编辑时拖拽组件的二维坐标如何形成嵌套 组件树
  • 屏幕尺寸变化的时候,组件自身,组件与其他组件之间的位置关系如何变化

第一个问题相对简单,我们以是否被包含来决定父子关系。举个例子,画布上有如下 [A, B, C, D] 四个元素。

我们根据坐标和尺寸数据可以得到一个包含集合,例如:

  • A: [B, C, D] (A 包含了 B,C,D)
  • B: [C, D] (B 包含了 C,D)
  • C: [D] (C 包含了 D)

然后进行第二轮算法,如果某个被包含的元素,不再被本集合内的其他元素包含,就可以认为它和宿主之间是属于直接父子关系,得到:

  • A: [B] (A 直接包含 B)
  • B: [C] (B 直接包含 C)
  • C: [D] (C 直接包含 D)

从而最终得到了 A -> B -> C -> D 的父子组件树链路。当然真实情况中还要考虑浮层这种独立图层,以及非法父子关系等细节,但核心就是依据这个直接被包含的关系算法,我们可以将二维的画布根据位置信息建模成一颗组件树。

解决了树算法之后,要想解决屏幕尺寸变化的自适应问题我们就必须引入一套布局系统来解决。

布局系统

自由布局

自由布局是云凤蝶默认的布局类型,使用体验就像 KeyNote 或者 Sketch 等产品一样自由拖拽,随心摆放。


自由拖拽可以解决固定屏幕尺寸下的元素摆放问题,但是当屏幕尺寸变化之后如何自适应?云凤蝶采用了一种类似 iOS Auto Layout 的系统,但是约束规则会更简单直观:


  • 宽度/高度
    • 固定:永远不变
    • 自适应:随着容器(父)的变大和变大
    • 适配内容:随着内容(子)的变大而变大
  • 上下左右间距
    • 固定:永远不变
    • 自适应:随着容器 (父)的变大而变大

根据静态的坐标信息再加上布局系统的约束规则配置,云凤蝶运行时就可以将用户的页面翻译成一个自适应的页面,后续本系列会有专题文章介绍,在此只简要概述整个算法的核心原理:

  • 编辑态用户操作:
    • 自由拖拽:得到 x(水平坐标)、 y(垂直坐标)、 w(宽)、 h(高)
    • 按需配置宽、高、间距的布局约束规则
  • 编译时:
    • 基于直接被包含算法,得到组件树
    • 基于水平垂直轴是否交叉对整个页面递归进行行列分割,将绝对定位的布局系统转换为相对布局
    • 对约束规则进行翻译
  • 运行时
    • 一个普通的自适应页面

弹性布局

云凤蝶同样支持将布局类型从自由切换为弹性,弹性布局更适合流式的页面编排。


画布体验

当然解决了组件树算法和布局算法等基建问题之后, 留给可视化搭建平台的还有一个持久的挑战是,如何打造一块好用的画布?因为如果自由拖拽做的不好,反而可能会导致效率低下,画布这里面涉及到海量的技术细节与产品设计,如:参考线、对齐、成组、分布、拖拽、缩放,相对位置 等等,本文暂不展开,后续会有专题文章介绍。

画布的逻辑表达能力

云凤蝶没有暴露类似 HTML 或者 JSX 的视图 DSL 给用户,只提供了一块平面的二维可视化画布,那么如何来表达视图的条件,循环等逻辑呢?

提供“多状态面板组件”表达 if/else,switch/case 类能力


提供“重复”的高级属性来表达 forEach 的循环能力,提供“满足条件才渲染”来表达条件能力

六、组件的共享状态与联动

上文我们已经基本了解了如何通过一块画布生产一颗组件树,那么如果组件之间要联动或者存储状态,这些逻辑该如何做呢?

前后端实现共享数据的方式一般都是通过共享内存或者打通通信渠道。前端的 Redux,Mobx ,Dva 等架构也都是采取状态外置的思路,用公共的 Model 实例来存储所有应用状态,然后不同组件按需的读写,辅以一些工程化和最佳实践的约束。

云凤蝶也不外如是,只是我们认为这些框架写法太繁琐,概念多,门槛高,云凤蝶对外置状态管理的方案做了极致简化。

如果使用过 React 的开发者可能对 state setState 这两个 API 非常熟悉,React 教会我们开发一个组件只需要做三件事情:

  • 定义 state
  • 使用 state 数据来生成试视图
  • 在必要的时候修改 state, 以自动更新视图

很明显,修改 state 之后随之而来的 vdom diff 与更新都属于自动化流程,你不需要过多关注。

这非常符合现实世界的隐喻,我们下班回到家只会按下控制客厅灯的开关,而不会去细究背后的走线和电路控制信号流程。

可惜 React 还是遗留了几个问题交给用户自由选择,社区也有非常多不同的优秀解决方案:

  • 多组件如何共享状态?需要搭配 Context、Redux 等方案
  • setState 修改深层属性的时候非常痛苦。需要搭配 immer、 immutablejs 等方案

云凤蝶决定沿袭 React state 的主体 API 设计形态,但是要解决上面两个问题:

  • store 外置 :状态是永远 standalone 独立存在的,保证 single source of truth
  • store mutable:no setState,no immutable,请用你最自然舒适的方式修改数据

最终云凤蝶决定为每个页面都提供一个类似 MMVM 架构下的 Controller 角色的 View Model 文件,其内容就是导出一个普通的 TypeScript Class,而 Class 的核心概念正好和状态管理一一对应,美妙且自然:

  • 属性(对应状态)
  • 方法(对应修改状态的行为)

在页面 Controller 中定义好变量和方法之后,用户可以通过属性面板的交互操作:

  • 将组件的 props 绑定到 Controller 内的变量
  • 将组件的 event 绑定到 Controller 内的方法

用自然的 Class 语法来聚合 state + action,开发者无需再学习 Redux 架构下 effects,reducer 等概念和编写大堆模板代码,而且:

voila!可以做响应式:
组件属性所绑定的变量发生变化,组件会精确自动更新

Reactive 理念

上文提到过,云凤蝶的状态管理目标是自动化,即只需要修改状态,不需要关注如何更新视图,那么如何实现这种效果呢?

核心架构就是 Reactive (响应式)和 Observable (可观测)。整个云凤蝶的 Controller Class 都是 observable 的,也就是这个 class 是被 Proxy 化的,云凤蝶的 Runtime 可以追踪任意属性的读写。而属性面板的数据绑定 (Button.props.text = $model.buttonText)的过程实质是在收集依赖,从而可以实现数据变化时检查依赖列表自动精确更新视图,达到响应式。

关于 Observable 和依赖收集理念的优越性,下面这两张来自 WWDC 2019 - Session 204:Introducing SwiftUI: Building Your First App 的 PPT 诠释得相当好:

GUI 程序通常使用场景是处理一个长时间段的用户交互过程,这其中不可避免会涉及到许多用户输入信息,视图临时状态的存储,此外整个页面树上可能有多至数以百计的元素(如一个表单或表格)之间要互相通信和联动。

如果按照传统的事件驱动的编程模型,依靠程序员的大脑去掌控所有数据到视图,数据到数据之间的衍生依赖关系,脑力负担是非常大的,开发者小心翼翼维护着所有关联逻辑,而一旦这个依赖复杂度超越了人脑的算力,就会导致关联关系不正确,也就是 Bug 💥 的产生。

而 “响应式”就是将这个依赖关系的维护交由框架来处理,开发者只需要“声明”出依赖关系,而且这个“声明”是指在描述业务逻辑的过程中来顺带地描述出这种依赖关系,并不需要为此大动干戈,比如:

  • State => View 在定义状态到视图的过程中,生产视图消费(getter)了哪些数据,就可以认为是该视图对该数据建立了依赖 (典型场景是云凤蝶属性面板的数据绑定)
  • State => Drived State 计算属性的定义过程中,该属性的结果依赖了哪些中间数据,就可以认为是该属性对其他属性建立了依赖

在这个理论体系下,State 始终是 single source of truth,视图只是数据的一个衍生值(Derived Value)

云凤蝶采用响应式的状态管理方案,从而使应用开发者用 “状态驱动” 的思维替代传统的命令驱动,开发者的脑海中始终在思考此刻控制我的视图的数据状态是如何,而背后衍生的视图如何更新,需不需要 create 一个 div 然后 append 到哪个 dom 上去,则不用关注。

依赖收集的实现

Observable 和依赖收集已经是非常成熟的技术了,应该这个技术的框架有 Vue 或者 Mobx 之类,甚至 iOS 最新的 SwiftUI,Android 的 Flutter 也是如此。这部分实现后续文章可能会专门展开来讲,感兴趣的可以展开阅读下面两个开源库的实现:

简化来看其大致实现原理是:

  • 使用 Proxy 来追踪对变量的读与写
  • 组件渲染过程中发生读取的的所有变量都会被记录下来作为依赖
  • 某个变量发生修改的时候,会找到所有依赖自己的组件,触发组件的重新渲染


超越手写代码的性能

因为可视化的画布编辑器天然隔绝了父子组件之间的 props 传递,那么云凤蝶将一个组件可能会更新的因素全部限制在属性面板上发生的数据绑定范围内,因此云凤蝶可以选择隔绝掉父子组件的级联更新,让整个页面的更新性能变得极其精确。


七、与服务端交互

传统的 ajax 请求代码编写细节太多,参数返回值信息全靠文档。而云凤蝶的思路是从源头出发,采用 OpenAPI 的通用规范管理 API 信息,在蚂蚁内部可以通过我们提供的工具自动从 JAVA 代码里导出这份信息并上传到云凤蝶。

一旦录入 OpenAPI 的接口信息之后,开发者在页面里就可以一行代码泛化调用 HTTP API 了,而且附带完整的编辑器类型提示和错误发现:


基于这份信息云凤蝶还可以帮助开发者做自动 Mock,接口测试等等更丰富的功能。

八、结语:10 倍效能提升

10 倍效能提升代表着我们的美好愿景,可视化搭建只是 1,云凤蝶还将持续挖掘更多的 0。

No Silver Bullet – Essence and Accident in Software Engineering 这篇文章提到:
there is no single development, in either technology or management technique, which by itself promises even one order-of-magnitude improvement within a decade in producitivity, in reliability, in simplicity.
并不存在某种技术或管理手段,可以保证在 10 年内给软件开发的生产力,可靠性,简单性上带来量级的提升。

API 驱动

从 CURD API 接口定义智能推导生成表格/表单/列表/详情等页面。

从数据格式描述智能推荐生成图表。


云凤蝶的可视化底盘实则拥有了一整套对 Web 应用的描述 DSL,不仅仅包含视图组件树,还包含数据绑定和交互逻辑,因此让智能生成应用的难度太太降低,并且生成完的应用还可以被云凤蝶编辑器消费进行二次修改和迭代。

智能还原设计稿

  • Ant Design 规范特征明显,容易训练
  • 还原成画布元素后补充缺失交互即可上线

智能布局

  • 摆出大概位置,一键格式化成符合 Ant Design 设计规范的布局

模型驱动

  • 只需制作业务模型,补充字段、逻辑
  • 一键完成 DB、API 和 UI 的全链路生成
  • Low-code ⽅方式书写后端逻辑,Serverless 部署
  • 应⽤ Host,一键发布

相关资料


未来已来,时不我待!

云凤蝶招聘前端、Java、PD、设计岗位,未来等你共创!

如果你感兴趣,欢迎联系 chenyu@antfin.com 或 shuai.shao@antfin.com

编辑于 2019-11-24

文章被以下专栏收录

    关注前端前沿技术,探寻业界深邃思想。https://qianduan.group 欢迎微信/微博搜索『前端外刊评论』,关注我们。欢迎给本专栏投稿,原作译作不限,要求:质量高!如果愿意尝试从事前端技术相关的书籍的编写或翻译工作,请私信外刊君。