背景:最近要在团队内部做一次关于低代码的技术分享,所以最近打算把之前2~3年的低代码平台开发经验,以及对其他大厂和开源的低代码产品的研究和学习做个系统梳理,希望能相对清晰完整地给各位同事介绍低代码的概念和原理、方法和实践、优势和不足,方便各位同事在学习和使用低代码时做个参考。
现在分享低代码相关概念已经非常简单了------因为有各个大厂以及大量开源的低代码产品、白皮书、教程、技术分享和真实系统可以参考------所以文章中对于推荐阅读的内容我会注明出处,方便大家进一步地去了解。
《图解低代码"四搭建"》会以系列文章的形式,有步骤、有重点地分享我对低代码"四搭建"的研究学习和开发经验,目前该系列文章有如下3篇:
以下是《图解低代码"四搭建":(中篇)图解低代码"四搭建"》的文章正文。
2、图解低代码"四搭建"
在《(上篇)怎样理解低代码》中,我们通过对比"零代码""纯代码"和"低代码"的特征和区别得出以下结论:
- "低代码"采用了"零代码"的可视化拖拽和可视化配置的开发形式,试图降低应用开发的上手门槛,使得公民开发者/业务人员等等、通过拖拽和配置也能完成应用的开发工作;
- "低代码"提供了"四搭建"------页面搭建、逻辑搭建、数据搭建和流程搭建------以便完成之前只有"纯代码"才能完成的应用开发。
而低代码"四搭建"(流程、页面、逻辑和数据),正是本系列文章要讨论的核心内容,也是本篇文章我们要进行详细研究和学习的内容。
在这篇文章里,我们不光关注低代码的四个搭建具体是怎么操作、怎么使用的,而且会更关注它背后的设计和概念------通过对低代码各个搭建的设计和概念建立更深入地理解,以便之后更好地去使用它:
2.0 "泛型"的低代码四搭建
虽然经典的低代码搭建有四个,但是这四个搭建在结构上却是相似的,用编程方面的术语来说,低代码搭建其实是"泛型"的,四个搭建我们可以总结成这么一条通用公式------<T>搭建 = <T>组件 + <T>组合:
每个"搭建"------无论是页面搭建、逻辑搭建、数据搭建,还是流程搭建------都是把"组件"作为搭建的基本元素,然后按照某种结构、形成某种"组合"(这种组合结构往往也被称为DSL(即Domain Specific Language,领域特定语言)),比如:
- 页面搭建:页面搭建是把"UI组件"作为搭建的基本元素,最终我们把各个UI组件"组合"成一棵UI树;
- 逻辑搭建:逻辑搭建是把"逻辑组件"作为搭建的基本元素,最终我们把各个逻辑组件"组合"成一棵逻辑树或者逻辑图;
- 数据搭建:数据搭建是把"字段"作为搭建的基本元素,最终我们把各个字段"组合"成一个表结构(由字段组成的数组或者集合);
- 流程搭建:流程搭建是把"流程组件"作为搭建的基本元素,最终我们把各个流程组件"组合"成一张流程图。
也就是说,对于一个低代码搭建(无论是页面、逻辑、数据还是流程)来说,我们需要关注的重点有两个:
- 组件:"组件"是搭建的基本元素,搭建的最终产物无非就是组件的不同组合而已;
- 组合:把组件按照某种结构进行编排的"组合",我们在低代码编辑器中、把组件进行可视化拖拽/编排,往往就是为了形成这种组合。以页面搭建为例,我这里引用《阿里巴巴低代码引擎》官方文档的一个描述------"编排(页面搭建)的本质是生产符合《阿里巴巴中后台前端搭建协议规范》的数据,在这个场景里,协议是通过 JSON 来承载的"。
所以,我们接下来就从"组件"和"组合"这两个核心要素,来分别拆解一下低代码四搭建:
2.1 数据搭建------把数据表/实体看成"字段集合"
在Web应用的开发过程中,数据在某种程度上往往处于核心地位------所以有"以数据为中心"的说法:
- 数据存储:我们通过数据库的SQL等专门语言,进行数据定义(比如创建数据库表等等)和数据操作(数据的原子操作有四种,即我们常说的"增删改查"(SELECT、CREATE、UPDATE、DELETE))。
- 在后端:一种流行的API的接口风格叫做"RESTful API",它把服务器端的内容看做"资源(Resource)",我们通过HTTP的各个动词------比如GET、POST、PUT、DELETE等等------对资源做增删改查等各种操作,我们可以简单地把它们映射成对数据的增删改查操作;
- 在前端:比如React就是一个以数据驱动的UI开发框架,React官方团队用了一个简洁的表达式解释使用React进行UI开发------
UI = fn(state)
------而我们进行前端开发,主要也是以图形化的方式进行数据展示和进行增删改查操作。
数据搭建:"模型驱动"的低代码搭建
以数据为中心的低代码搭建,往往也被称作"模型驱动"的低代码搭建,接下来我们重点看一下数据搭建的模型:
JSON是我们常用的数据交换格式,以下是一个简单的JSON数据,描述了一个用户的信息:
json
{
"name": "张三", // 用户姓名
"age": 26, // 用户年龄
"gender": "MALE", // 用户性别:MALE,男性;FEIMALE,女性;UNKNOWN,未知
"avatar": "https://..." // 用户头像
}
以上的JSON数据主要是通过"键值对"来记录信息------
- 键:其中的"键"是指属性的英文名称(比如name、age、gender等等);
- 值:"值"则是属性的取值(比如张三、26、MALE等等)
- 说明:为了解释说明各个属性的含义和约束等等额外,我还添加了JSON语法中并不允许的注释说明(比如用户姓名、用户年龄、用户性别等等)。
如果我们用Java这种通用编程语言进行以上数据的建模的话,大概是这样的一种结构:
java
public class User {
@(
label = '姓名',
description = '用户姓名',
)
String name = "张三";
@(
label = '年龄',
description = '用户年龄',
)
Integer age = 26;
@(
label = '性别',
description = '用户性别:MALE,男性;FEIMALE,女性;UNKNOWN,未知',
)
String gender = "MALE";
@(
label = '头像',
description = '用户头像',
)
String avatar = "https://...";
}
在使用Java这种通用编程语言进行属性声明时,主要包括以下几个部分:
- 属性类型:属性的类型包括String、Integer等等;
- 属性英文名称���属性的英文名称包括name、age、gender等等;
- 默认值:属性的默认值,比如张三、26、MALE等等;
- 属性中文名称:为了增强Java对模型信息的描述,我假设能够通过一个注解@、通过label属性添加中文名称;
- 属性描述:为了增强Java对模型信息的描述,我假设能够通过一个注解@、通过description属性添加属性描述。
如果我们以一种结构化的JSON数据的形式来描述以上Java的数据建模信息的话,大概可以形成以下结构:
json
{
type: "JavaClass",
name: "User",
properties: [
{
type: "String",
name: "name",
defaultValue: "张三",
label: "姓名",
description: "用户姓名",
},
{
type: "Integer",
name: "age",
defaultValue: 26,
label: "年龄",
description: "用户年龄",
},
{
type: "String",
name: "gender",
defaultValue: "MALE",
label: "性别",
description: "用户性别",
enum: [
{ label: "男", value: "MALE" },
{ label: "女", value: "FEMALE" },
{ label: "未知", value: "UNKNOW" }
],
},
{
type: "String",
name: "avatar",
defaultValue: "https://...",
label: "头像",
description: "用户头像",
},
]
}
这就是我们本小节开篇的图片所展示的内容:
事实上,同样的信息,我们可以通过TypeScript、JSON Schema、百度爱速搭、网易Codewave和开源的Nocobase分别进行搭建:
- TypeScript:
- JSON Schema:
- 百度爱速搭:
- 网易Codewave:
- Nocobase:
我们可以看到,低代码平台在属性建模上有一些不同------要么像Nocobase,多了interface等等前端展示相关的属性,要么像百度爱速搭,直接使用前端组件作为属性类型------我们可以这么理解:
- 属性值模型:比如JSON,我们姑且称它为"属性值模型",只是用来进行数据交换的,不需要太多信息,那么拥有属性英文名称、属性值、属性描述就够了;
- 属性/字段模型:比如Java中的实体,它在上面"属性值模型"之上,还包括了"属性类型"、"默认值"、"属性中文名称"等等内容;
- 表单领域模型:而如果模型能够直接作为一个表单存在的话,则还需要"校验规则"、"联动规则"、"前端展示"(比如Nocobase的interface)等等信息。
以下是对数据实体模型的一个说明:
typescript
/** 数据实体模型 */
interface Entity {
type: String; // 类型
properties: Array<EntityProperty>; // 数据实体属性列表
}
/** 数据实体属性 */
interface EntityProperty {
type: String; // 属性类型
name: String; // 属性英文名称
label: String; // 属性中文名称
description: String; // 属性描述
defaultValue: String; // 默认值
value: String; // 值
enum: Array<{ // 美剧(限定值的可选项)
label: String; // 中文名
value: String; // 枚举值
}>;
interface: String; // 前端展示类型
required: Boolean; // 是否必填
}
下面我们举个百度爱速搭的例子:
百度爱速搭中的数据模型(Data Model),可以理解为数据库中的表(Table)、Java 中的实体(Entity),本质上是对各种数据进行结构化建模,如订单、商品、用户、评论等。
模型字段配置界面
在爱速搭中,配置一个名称为"用户"(Customers)的模型各个字段界面如下:
模型元信息(Model's Meta-model)
百度爱速搭中的数据模型(Data Model),可以理解为数据库中的表(Table)、Java 中的实体(Entity),本质上是对各种数据进行结构化建模,如订单、商品、用户、评论等,下面是数据模型(Data Model)简化后的实现:
typescript
interface DataModel {
name: string; // DataModel的中文名称
key: string; // 不同 DataModel 通过 key 区分
fields: Field[]; // 每个 DataModel 都有若干 Field
}
这个结构化的模型(Data Model),是数据各个属性(Attribute/Property/Field)的组合,下面是 Field 的简化后的实现:
typescript
interface Field {
key: string; // 不同 Field 通过 key 区分
type: string; // Field Type,字段数据类型,比如int, enum, text 等等
name: string; // 中文名称
}
示例:下面以对用户信息(users)进行结构化建模为例,我们将保存用户的昵称(nickname)、电子邮箱(email)、手机号码(phone)、密码(password)、地址(address)等信息,简化后的存储结构为:
json
{
"key": "users",
"name": "用户",
"fields": [
{
"key": "id",
"type": "int",
"name": "ID"
},
{
"key": "nickname",
"type": "text",
"name": "昵称",
"format": "normal"
},
{
"key": "email",
"type": "text",
"name": "电子邮箱",
"format": "email"
},
{
"key": "phone",
"type": "text",
"name": "手机号",
"format": "tel"
},
{
"key": "password",
"type": "password",
"name": "密码"
},
{
"key": "address",
"type": "text",
"name": "地址"
}
]
}
我们可以看到,不同 Field 通过 "key" 区分;其中的"type"是 Field Type,表示字段的数据类型,比如 int, text, enum 等等;而对于"text"类型的字段,使用了 JSON Schema 的 format 关键字进行更详细的字段区分,比如邮箱、手机号等等。
字段类型 (Field Type)
字段类型中文 | 字段类型英文 | 字段存储类型(MySQL) | 字段类型说明 |
---|---|---|---|
单行文本 | text |
varchar(255) |
用来存储小段文本信息,比如:名称、邮箱、网址、身份证号等等 |
多行文本 | textarea |
text |
用来存储大片文本信息,长度不限 |
富文本 | rich_text |
text |
适合用来存文章内容 |
整数(Int) | int |
int |
用来存储整型数据,比如:年龄、长度、距离等等 |
浮点数(Float) | float |
decimal |
用来存储带小数点的数字,比如:经度、纬度等等 |
金额 | money |
bigint |
用来存储金额类型字段,单位为 分 |
枚举 | enum |
varchar(255) |
用来存储固定的某几个值,常用来存储状态。 |
布尔(开关) | boolean |
tinyint |
用来存储是与否。 |
日期 | date |
timestamp |
用来存储日期格式,包含:日期时间、日期、时间和时间戳格式。 |
日期范围 | date_range |
varchar(200) |
用来存储日期范围格式。包含开始和结束: date_range_stime timestamp date_range_etime timestamp |
附件 | attachement |
text |
用来存储文件,一般用于用户上传。 |
图片 | image |
text |
用来存储图片,一般用于用户图片上传。 |
人员信息 | user |
varchar(255) |
可用来存储人员信息,与平台用户表关联。 |
部门信息 | department |
varchar(255) |
可用来存储部门信息,与平台部门表关联。 |
拥有者 | owner |
varchar(255) |
用来存储数据所属人员信息,可用于权限设置。 |
流水号 | serial-number |
varchar(256) |
系统自动生成字段,根据预设规则生成流水号信息。 |
密码 | password |
varchar(255) |
用来存储密文,只能用于结果比对,不可反解。 |
密文 | ciphertext |
varchar(255) |
用来存储加密文本,可反解。 |
地址 | address |
varchar(200) |
用来存储地址,包含省份、城市、地区街道信息。 address_shengfen varchar(200) address_chengshi varchar(200) address_quyu varchar(200) |
位置 | location |
varchar(250) |
用来存储地理位置,包含经度、纬度信息。 location_lat decimal(10,6) location_lng decimal(10,6) |
JSON | json |
json |
用来存储复杂数据,对象、数组、字符、数字等都能支持。但是不可以用于检索和排序。 |
模型列表(表格)和模型表单
爱速搭提供"模型列表"和"模型表单"两个单独的组件,方便用户快速创建基于模型的表单和表格。
模型列表(表格)
爱速搭可以通过"模型列表"组件,快速构建基于模型的表格:
添加到编辑器中的模型表格效果如下:
模型表单
爱速搭可以通过"模型表单"组件,快速构建基于模型的表单:
添加到编辑器中的模型表单效果如下:
有一个开源的模型驱动的低代码实现------叫做"Nocobase"------推荐大家有空去看一下,它的设计和实现可以作为一个非常棒的参考。
我们使用低代码数据模型编辑器进行数据模型搭建时,搭建的最终结果其实是由各个字段组成的数组或者集合:
2.2 逻辑搭建------把逻辑看成"树/图"
我们好多人在写代码之前,都有画流程图、ER图、UML图的习惯,通过流程可视化的方法、把我们要表达的业务逻辑梳理清楚,而我们最终实现的代码逻辑、其实也相当于一个流程图。而计算机科学的预备知识之一,就是类似下面的"流程图":
计算机科学预备知识之一------流程图
说到可视化编程,Google提供了一个叫做Blockly的可视化编程语言,通过类似积木组装的隐喻,开发者通过可视化组块的方式进行编程。麻省理工学院(MIT)基于Google的Blockly进行了Scratch的设计开发,方便少儿进行编程教育。
说到逻辑编排,我们刚好前段时间刚好实现了LiteFlow的逻辑可视化编排(开源仓库地址是:gitee.com/imwangshiji... ):
我们目前初步实现了LiteFlow的以下3类/6种逻辑编排的可视化:
- ① 顺序类:串行编排THEN、并行编排WHEN;
- ② 分支组件:选择编排SWITCH、条件编排IF;
- ③ 循环组件:FOR循环、WHILE循环。
从结构化编程的角度看,逻辑的控制流也无非就是顺序、分支、循环了,而LiteFlow都覆盖到了。
Liteflow是一款轻量、快速、稳定可编排的组件式规则引擎:
-
🧬 强大的EL:简单低学习成本的EL,丰富的关键字,能完成任意模式的逻辑编排。小身材,大能量。
-
🧩 皆为组件:拥有独特的设计理念,所有逻辑皆为组件。上下文隔离,组件单一职责,组件可以复用且互相解耦。
我们使用低代码逻辑编辑器进行逻辑搭建时,搭建的最终结果其实是由各个逻辑组件组成的逻辑树:
2.3 页面搭建------把UI看成"树"
作为一名开发者,你有没有想过、我们是怎样描述一个"UI"界面的?
例如,下面是一个简单的游戏UI界面:
分解一下这个UI界面,看一下它有哪些元素,发现它是由很多有层次的节点(Node)组成:
而这些节点(Node)元素(Element),最终会组成一棵树(Tree):
而这种"树"(Tree)型数据结构,正是搭建UI/用户界面的关键,比如我们熟悉的HTML DOM树。
其实不止是对于Web前端来说,其他包括小程序、APP应用等等在内,也都是使用树形数据结构来描述UI的。
比如,京东有一个Taro框架,可以使用React语法、进行UI页面开发,然后把UI转换成Web端、APP端、各种小程序端等等。
对于使用React框架的开发者来说,可以通过使用react-dom
、react-native
、react-art
等等包,将React应用分别应用到浏览器、iOS/Android原生App和canvas画布上等等。
而对于Vue框架的开发者来说,也有一个名称为uni-app的框架,甚至实现了"一套代码,运行到多个平台"------使用Vue框架语法开发的应用,同时应用到H5、iOS/Android原生App以及输出成各种小程序。
所以,我们可以看到,虽然各个客户端、各个平台提供的原生API很不相同,但是如果只是看UI的描述形式、倒是非常相似,甚至可以使用同一套源代码、直接进行转换。
在React官方文档中,也有"将 UI 视为树"的相关章节:
树是项目和 UI 之间的关系模型,通常使用树结构来表示 UI。例如,浏览器使用树结构来建模 HTML(DOM)与CSS(CSSOM)。移动平台也使用树来表示其视图层次结构。
比如,下面是一个使用React的JSX语法进行UI开发的实现:
jsx
<dialog>
<button className="blue" />
<button className="red" />
</dialog>
而同样的组件元素,可以使用如下的JSON表示:
typescript
{
type: 'dialog',
props: {
children: [{
type: 'button',
props: { className: 'blue' }
}, {
type: 'button',
props: { className: 'red' }
}]
}
}
对普通的JSON对象进行操作,相信绝大部分前端程序员都能做到------而且JSON这种形式,也正好适合在浏览器中进行操作的形式。到了这里,我们应该就能理解官方文档中这句话的意思了:"编排的本质是生产符合《阿里巴巴中后台前端搭建协议规范》的数据,在这个场景里,协议是通过 JSON 来承载的"。
我们使用低代码页面编辑器进行UI页面搭建,搭建的最终结果其实是一棵UI树,而且是一棵用JSON表示的UI树:
2.4 流程搭建------把流程看成"图"
在BPM(Business Process Management,业务流程管理)领域,往往整个工作流程一般都是通过可视化配置的方式进行业务流程的搭建和编辑:
BPM业务流程编排
BPM领域是有一套标准规范的------即BPM2.0规范------场景覆盖审批流程场景、业务流程的场景。
基本概念
业务流程主要由事件、活动、网关、顺序流组成。其中事件、活动、网关在BPMN2.0中也被称为流对象。
要素 | 说明 |
---|---|
事件 | 事件扮演着流程触发器的角色,通过事件可以完成触发流程的启动、暂停、终止等 |
活动 | 即相关的业务活动,包含人工活动(如人工审批活动)和服务活动(级后端自动执行的活动) |
网关 | 即决策节点,通过网关可以判断不同后续分支的执行策略和分支等待汇聚策略。 |
顺序流 | 显示将执行活动的顺序,可以理解为连线 |
事件
事件用来表明流程的生命周期中发生了什么事。事件总是画成一个圆圈, 在BPMN 2.0中事件有两大分类:
- 捕获(Catching)事件。当流程执行到该事件, 它会中断执行,等待被触发,如图中延时节点,抛出一个定时器,等待计时到期后,触发流程继续执行。
- 抛出(Throwing))事件。当流程执行到该事件, 抛出一个结果
分类 | 节点类型 | 简要说明 |
---|---|---|
开始类事件 | 开始事件 | 常规的开始事件,不指定事件的起因,支持人工、API触发。 |
开始类事件 | 定时开始 | 定时开始事件用来在指定的时间启动一个流程,也可以在指定周期内循环启动多次流程,例如每月1号凌晨2点开始启动账务结算处理流程。 |
开始类事件 | 实体事件开始 | 当接收到特定的实体事件时该流程被触发,例如指定实体的增删改事件后,触发当前流程。 |
开始类事件 | 外部http触发 | 由外部http节点触发。 |
中间类事件 | 延时节点 | 当执行到达【延时节点】时中断在这里,引擎会创建一个定时器,当定时器触发后事件结束,流程沿后继路线继续执行。 |
结束类事件 | 结束事件 | 表示流程或分支的自然结束,什么都不做。当流程有多个分支路线被激活时,最后一个分支自然结束后,流程实例结束。 |
结束类事件 | 终止事件 | 表示流程被强制终止,什么都不做。当流程有多个分支路线被激活时,这些分支上的活动任务也被终止。 |
活动
即流程中执行的任务,包含人工活动、自动活动。
-
人工活动:需要人员参与的任务,一般包含任务处理人、任务详情页等。
-
自动活动:无需人员参与的任务,由后端自动执行,也称之为自动任务。
分类 | 活动名称 | 说明 |
---|---|---|
人工活动 | 填写节点 | 即人工填写指定表单,流程流转到该节点,会创建一个流程待办,由任务执行者进行填写任务。 |
人工活动 | 审批节点 | 即人工审批节点,流程流转到该节点,会创建一个人工审批待办,可在待办中心访问,由任务执行者执行审批任务 |
人工活动 | 知会节点 | 即知会指定人员,流程流转到该节点,被知会人员会收到一条知会任务,可在待办中心访问查看 |
自动活动 | 提交记录 | 针对当前流程中的临时数据,提交到对应的实体中,也就是入库 |
自动活动 | 新增记录 | 在实体表单中新增一条、批量新增多条记录 |
自动活动 | 删除记录 | 在实体表单中删除一条、批量删除多条记录 |
自动活动 | 更新记录 | 更新实体中的指定记录,包含更新实体记录字段、批量更新实体记录 |
自动活动 | 查询记录 | 据条件查询平台对象的实例记录,类似于数据库中的SELECT命令 |
自动活动 | 调用服务 | 调用API及服务编 |
网关
网关即流程中的决策点,决定了流程分支执行、汇聚等待的规则。
网关名称 | 图标 | 说明 |
---|---|---|
排他网关 | 排他网关定义了一组分支的唯一决策,所有流出的分支被顺序评估,第一个条件被评估为true的分支被执行,并不再继续评估下面的分支 | |
并行网关 | 并行网关根据前置连线或后继连线,无条件创建分支或回收分支 | |
包容网关 | 包容网关是排他网关和并行网关的综合体。当决策时,与排他网关所不同的是,所有条件为true的后继分支都会被执行 |
我们使用低代码流程编辑器进行流程搭建,搭建的最终结果其实是一张图: