低代码平台架构深度剖析

2,668 阅读13分钟

Forrest 在2014年正式定义了 “低代码” 这个名词。而早在这个名词出现之前,我们就已经开始着力于相关领域的研究,可以说经历过“低代码”在国内发展的全部时期。作为 “低代码” 领域的历史见证者和践行者,我们探索过最初的 “无人区” 也走过不少弯路,并取得了一些阶段性的沉淀成果。

低代码化是现今各企业技术部门调研和践行的热门方向。低代码能够解决哪些问题?低代码是实际能解决问题的技术革新,还是资本炒作起来的概念? 市面上的意见五花八门,有质疑,有看好,也有观望。而对于低代码是什么,低代码平台应当是什么样,我们也能看到许多不同的解释和看法。

要从这些观点的海洋中找到对于企业团队而言正确的决策,需要对 “低代码平台” 的全貌有清晰的认识。故而在这篇文章中,作者将尽可能以不带任何个人倾向的方式,为各位读者客观铺陈一个相对成熟的低代码平台的架构。对于企业而言,如果打算采购低代码平台,可以将本文作为平台能力逐项评估的参照,如果打算自研低代码平台,也可将本文作为项目设计的借鉴。

接下来的内容会基于百度智能云低代码方向已对外披露的两个主要项目,这两个项目分别是:

其中 amis 是低代码前端框架,而爱速搭平台则在包含了 amis 的前提下拓展了数据模型、API 编排等后端能力。

本文将以 平台概览 - amis 框架介绍和细节展开 - 爱速搭可视化编辑器概述 - 后端服务通览 的脉络展开,为各位读者揭示一个在生产环境成熟运转的低代码平台的全貌。

爱速搭低代码平台结构一览

首先我们通过一张图来了解百度爱速搭平台的全貌。如图所示,前端用户感知最多的 “页面拖拽编辑” 部分,主要由 amis 和 amis-editor 来承担;而“后端业务” 则包括一个低代码平台必不可少的 API Proxy、权限、数据模型、流程引擎等;“爱速搭平台功能” 指的是页面管理列表、数据模型列表等平台常规能力。

接下来我们将挑选一些重要的模块,为大家逐一剖析这些构成低代码平台的关键元素。

爱速搭平台全貌

AMIS 的诞生 —— 从前端 UI 库到低代码前端框架

爱速搭的整个历史要从低代码框架 amis 的诞生说起,amis 框架(github.com/baidu/amis)是百度爱速搭低代码平台整个前端能力的基础设施,它承载了“将 json 配置转换成对应的 React 组件并在页面上渲染出来” 的基础能力。

在低代码平台的发展完善过程中,最初提供的能力通常是前端页面的拖拽搭建。在现今低代码平台开发的通常过程中,低代码平台的开发首先建设的必然是类似 amis 这样能够将 json 转为组件渲染的前端框架。而平台能够支持 “多复杂” 的前端页面搭建,则取决于这个前端框架的设计细节。

下面我们来具体聊一聊 amis 的整个设计初衷和一些重要细节。

为什么要做 AMIS ?

一个技术项目的产生,必然是为了解决某些客观存在的核心问题,然后基于这个核心去不断地完善细节以覆盖更多场景。在深入细节之前,我们首先花一点时间来了解 amis 框架最初想要解决的核心问题。

下面以一个前端最常见的例子来说明这个“核心问题”。比方说我们要实现一个最简单的表单组件数据联动效果,如下图:

比方说我们要实现一个最简单的表单组件数据联动效果.webp

如果用传统的前端组件来实现,开发者需要关注的点可能包括

  1. 如何进行异步请求发送
  2. 如何进行状态管理
  3. 组件间的数据共享和界面状态更新

.....

而在前端庞大的生态圈中,上述的每个点可能都有非常多的 “可选方案”,开发者需要逐一去调研并甄选出每个环节的 “最适” 方案,并将其组合起来形成一套团队内部的研发技术栈,同时还需要分出精力来时刻关注技术栈中每个组成因子的升级适配问题。

而在 amis 中, 我们将技术栈的更新升级细节封装到了 “低代码框架” 本身的更新升级范围中,无论前端技术栈如何升级变迁,对于 amis 框架的用户而言,实现 “表单组件数据联动效果” 都只需要通过下面的配置方式来实现:

都只需要通过下面的配置方式来实.webp

{
  "type": "page",
  "body": {
    "title": "",
    "type": "form",
    "mode": "horizontal",
    "body": [
      {
        "label": "选项1",
        "type": "radios",
        "name": "a",
        "inline": true,
        "options": [
          {
            "label": "选项A",
            "value": 1
          },
          {
            "label": "选项B",
            "value": 2
          },
          {
            "label": "选项C",
            "value": 3
          }
        ]
      },
      {
        "label": "选项2",
        "type": "select",
        "size": "sm",
        "name": "b",
        "source": "/amis/api/mock2/options/level2?a=${a}",
        "description": "切换<code>选项1</code>的值,会触发<code>选项2</code>的<code>source</code> 接口重新拉取"
      }
    ],
    "actions": []
  }
}

在这里,最重要的是 select 组件配置中的 "source": "/amis/api/mock2/options/level2?a=a"这一行,通过a={a}" 这一行,通过 a={a} 这样的语法,将下拉框的接口请求和 name 为 a 的 radios 组件进行 “关联”,当 radio a 的值发生 change 的时候,就会触发 select 组件数据接口的请求,并将当前 select a 的取值以 url 参数的形式随接口请求发送给后端。

而常见的基础组件、CRUD 列表等就不在此一一赘述,有兴趣的读者可以自行前往阅读 amis 官方文档。

一言以蔽之,“低代码框架” 的组件体系,本质上都是在通常的 “UI 组件” 之上增加了 “通用业务逻辑” 的实现,将一些常用的业务逻辑提取并固化到组件层面,然后提供相应的配置项,让用户得以通过配置的方式来复用这些 “固化” 的业务逻辑。

故而低代码组件体系,及其配套的低代码可视化编辑器 - 低代码平台,其价值都在于能够提供不断累积的业务逻辑

从长远来看,不同领域都会孵化出贴合自身需要的 “低代码” 组件和框架体系,而像 amis 这样应用于企业中后台业务场景的类型,就目前而言是认知程度和普及率最高的。

一个相对成熟的低代码前端框架

amis 的核心能力是 “将 json 配置转换成对应的 React 组件并在页面上渲染出来”。乍一看这非常容易实现的,但是将低代码框架放到生产实践中,就会发现各种各样需要处理的细节问题。这里挑选了几个从各种业务场景中提炼出来的处理方案,和各位读者共享,细节上的处理,是 amis 框架 “能够承接更复杂页面设计” 的关键所在。

基础核心能力 —— 从 json 配置到 React 组件

这里同样以一个简单的例子来展示在 amis 将 json 配置转换成 React 组件渲染的实际效果(来自amis官网 “概念-配置与组件” aisuda.bce.baidu.com/amis/zh-CN/…),在这个例子中定义了一个简单的组件,它的 amis 配置是这样的

{
  "type": "page",
  "body": {
    "type": "tpl",
    "tpl": "Hello World!"
  }
}

其渲染结果如下,这段配置描述的是在一个页面容器中以 tpl 组件的形式渲染一段 “Hello World!” 文本

其渲染结果如下,这段配置描述的是在一个页面容器中以 tpl 组件的形式渲染一段.webp

那么这个最简单的例子,是如何从 JSON 配置被转化为 React 组件并渲染到页面上的呢?在 amis 的官方文档中对此有所说明 aisuda.bce.baidu.com/amis/zh-CN/…,简单来说就是先通过 json 的 type 找到对应的 Component,然后把其他属性作为 props 传递过去完成渲染。

amis 框架的 render 方法

render 方法是实现配置到组件转换的关键方法。在 amis 框架的快速开始文档 aisuda.bce.baidu.com/amis/zh-CN/… 中,可以看到通过调用框架提供的 render 方法,能够将符合要求的 json 格式配置转化为相应的 React 组件输出。

(注:这里需要插入说明一点,amis 框架下的全部组件状态管理是基于 Mobx mobx.js.org/README.html 和 mobx-state-tree 来实现的,本文后续中提到的 “store” 指的都是 MST 中的数据 store

下图展示了 render 方法处理组件 schema(json 配置)的大致过程,当然整个方法包含了许多细节上的处理,这里仅展示 schema 到组件的主流程部分。)

这里的 render 方法是 amis 核心功能的 “入口”,那么它具体做了什么事情呢?

  1. 在已注册的组件池中找到组件并将其依次渲染到页面上
  2. 数据 store 的创建

这里的 SchemaRenderer 在 amis 代码中是一个 React 组件,其部分代码如下,其中 resolveRenderer 这行代码是根据 schema 查找组件的关键操作。

amis 的组件分为容器型组件非容器型组件, 容器型组件的 body 中可包含非容器型组件及多层嵌套容器型组件。在 amis 的官方文档中阐述了组件树的概念(aisuda.bce.baidu.com/amis/zh-CN/…

利用这种树形结构的设计,amis 可以实现复杂页面制作,下面这个例子展示了一个利用容器组件嵌套来实现分栏布局的报表页面

嵌套组件的渲染原理和单层组件类似,其区别在于需要递归地获取容器组件 body 的内容并逐层渲染到嵌套的内层。

数据域和数据链

数据是前端页面构成的核心部分。无论是传统原生模式还是 MVVM 框架,抑或是低代码模式,对数据变更(即状态)的管理都是前端开发的重要课题。

在 amis 中,页面数据使用 “数据域” 和 “数据链” 来进行管理(aisuda.bce.baidu.com/amis/zh-CN/…这是使得 amis 能够支持复杂动态页面的一个重要设计。

这里同样用一个直观的例子来说明 “数据域” 和 “数据链” 在实际场景中是如何生效的。

在下图所示的页面中配置了一个页面初始化接口,该接口在返回结果的 data 域中包含了两个字段,“date” 和 “title”

在页面中,我们可以使用 ${title} 这样的模板语法或类似 this.title === xxx 这样的表达式语法来获取到当前页面 data 域下的值,模板语法主要用于展示,表达式语法一般用于显隐条件判断等。

在上面的例子中,我们用 ${} 语法来获取到了 data 域下的值并在页面中渲染出来。

“数据” 的来源和在页面中的读取方式

在一个 amis / 爱速搭的页面中,“数据” 来源主要包括:

  • 页面常量,如当前用户信息等,仅限爱速搭平台,由后端服务在返回页面配置时进行统一替换

  • 地址栏参数

运行态:

编辑态:

  • 在组件配置 “data” 域中配置的初始静态数据

接口返回的数据,最常用的方式,包括表单提交后接口返回的数据(如保存结果等)。在本节开始的例子中已有展示说明,存在同名字段的情况下,“后来” 的数据会覆盖 “先来” 的数据。

这些不同来源的数据被统一归并到数据域中以树状的结构进行管理。

数据链的继承和覆盖

上面我们说到,数据域是以树状结构进行管理的,其继承和覆盖规则的设计如下:

当页面中出现类似 ${} 的语法时:

  1. 首先会先尝试在当前组件的数据域中寻找变量,当成功找到变量时,通过数据映射完成渲染,停止寻找过程;
  2. 当在当前数据域中没有找到变量时,则向上寻找,在父组件的数据域中,重复步骤12
  3. 一直寻找,直到顶级节点,也就是page节点,寻找过程结束。
  4. 但是如果 url 中有参数,还会继续向上查找这层,所以很多时候配置中可以直接 ${id} 取地址栏参数。

amis 对 API 请求的前置 / 后置处理

传统开发模式下,在发起 API 请求时,开发者通常需要按需收集页面当前的数据,并将其按照和后端约定好的格式拼接起来随请求发送给后端,而在后端返回数据的时候,如果后端数据和前端的需求不一致,前端通常也需要对返回数据来进行一定的格式化处理来满足前端的进一步需求。

而在 amis (或者其他的低代码框架)中,每类组件对数据格式的要求通常是确定的,而出于对尽可能少地编写代码的诉求,API 请求通常以配置方式来声明触发,如下图的 form 组件:

其对应的配置如下,可以看到表单是通过配置 form 组件的 api 字段下的 url 属性来指定表单提交接口的。

{
  "type": "page",
  "body": {
    "type": "form",
    "api": {
      "method": "post",
      "url": "/amis/api/mock2/form/saveForm"
    },
    "body": [
      {
        "type": "input-text",
        "name": "name",
        "label": "姓名:"
      },
      {
        "name": "email",
        "type": "input-email",
        "label": "邮箱:"
      }
    ]
  }
}

在没有特殊约定的情况下,当提交动作被触发时,表单组件会收集当前数据域下的全部数据,整理成一个对象发送给后端(有关数据域的说明,参见上一章)

而在旧业务过渡到低代码平台的过程中,许多用 amis / 爱速搭新建的页面都需要和一些老接口进行通信,而这些老接口通常来说不太可能为了对接低代码平台而进行出参 / 入参格式的二次改造。这也是在企业内部落地低代码方案时必然会遇到,也必须解决的问题。

面对这类场景,低代码平台 / 框架需要前后端数据 “对齐” 的能力。 为此,amis 在前端层面设计了数据映射和 apiAdaptor 等机制(后端也有类似的机制,具体见后文API Proxy)。

当然,这些方案并非此类问题的唯一解决方案,如果有更好的设计方案,也欢迎大家集思广益为我们的开源项目添砖加瓦。

数据映射

数据映射的实现原理非常简单,主要是在请求提交前先拦截一下然后将数据处理成指定的格式,其语法为 "目标字段名": ${原字段名}

具体可查阅官方文档中有关数据映射的说明:aisuda.bce.baidu.com/amis/zh-CN/…

API Adaptor(请求适配器和接收适配器)

API Adaptor 简单而言就是由用户自行实现一段用于请求数据 / 返回数据格式转换的代码,并将其贴到组件 api 的配置中。组件在发起请求时会调用这段函数代码来对相关数据进行处理。

其具体使用方法如下(api 字段下的 requestAdaptor 方法),总的来说就是在请求发送之前调用开发者在配置中定义好的函数修改请求内容,然后将修改后的结果作为实际的请求内容发送给后端。接收适配器也类似,只不过修改的是返回内容。

const schema = {
  type: 'form',
  api: {
    method: 'post',
    url: '/amis/api/mock2/form/saveForm',
    requestAdaptor: function (api) {
      return {
        ...api,
        data: {
          ...api.data, // 获取暴露的 api 中的 data 变量
          foo: 'bar' // 新添加数据
        }
      };
    }
  },
  body: [
    {
      type: 'input-text',
      name: 'name',
      label: '姓名:'
    },
    {
      name: 'text',
      type: 'input-email',
      label: '邮箱:'
    }
  ]
};

注意:在爱速搭平台的页面 runtime 环境中,用户填写的 API 如果没有指定为 raw: 类型,会被转化为平台统一的 Proxy API,由爱速搭平台的后端服务统一进行转发。API 转发相关的内容详见后文后端服务-API Proxy。前端 amis 的映射 / adaptor 和后端的 api proxy 共同构成了爱速搭平台的接口请求发送体系。

自定义组件接入机制

虽然 amis 提供了丰富的默认组件,但是默认的组件必然无法覆盖各种未知的使用场景。

在低代码平台 / 框架的选型和建设过程中,通常平台 / 框架的维护者需要不断地抽取通用业务逻辑并将其作为默认组件固化下来,而自定义组件是需求收集和能力延展的有效补充机制。

amis / 爱速搭同样支持多种形式的自定义组件接入方法,包括 SDK,React 方式等,详见官方文档(aisuda.bce.baidu.com/amis/zh-CN/…

自定义组件的整套体系相对比较庞大,所以本文仅列举,具体细节会在后续的其他文章中进行披露。

爱速搭平台的页面可视化编辑器

amis 作为一个相对成熟的低代码前端框架,提供了通过 json 配置生成复杂页面的全套能力。然而 amis 毕竟是一个前端框架,单靠其本身必然无法实现 “让不懂前端,不会 JavaScript 的人也能制作页面” 这个目的。故而在 amis 之上,我们还开发了一个可视化编辑器。

对整个平台而言,组件的具体行为和页面渲染引擎由 amis 提供,而可视化编辑器主要做的事情是

  • 提供拖拽组件到页面并生成相应 schema 的能力
  • 调用 amis 框架基于页面当前编辑中的 schema 来渲染预览视图

从直观的产品形态而言,爱速搭平台的页面可视化编辑器,对应的是平台中最常用的 “编辑” 界面,如下图。

此类可视化编辑器的实现原理在现今已经不是一个时髦的话题,在低代码框架已经 ready 的情况下,可视化编辑器本质上就是一个 “比较友好地修改组件配置的界面”。一般来说在低代码平台的研发过程中需要投入大量人力去处理可视化编辑器的各种细节问题(如配置项的对齐,界面交互友好程度等),但经过业界多年的探索和沉淀,研发层面的基本思路已经相对固化,此处也就不再赘述。

有兴趣的读者可自行前往研究(github.com/aisuda/amis…

后端服务相关

爱速搭平台的前端主要部分由 amis 及其配套的可视化编辑器组成,而作为一个能够提供完整服务的低代码平台,除前端页面搭建能力外,后端的一系列服务也是必不可少。

爱速搭平台的后端服务目前没有对外开源,但对于低代码平台而言,配套的后端服务是整个平台能力中不可或缺并且重要度逐步提升的一部分。

由于篇幅有限,在此对爱速搭平台的后端服务体系只做一个业务问题-解决方案的概要式列举,以供各位读者一窥低代码平台的“全貌”。

API Proxy

为解决前后端通信中最常见的跨域问题,以及提供 header/body/query 信息的拼装转化能力,爱速搭平台提供了 API Proxy 机制。在 amis 中,许多组件都支持以 API 的形式配置数据源 / 提交接口,如下图的 form 保存接口,在配置时填写了原接口

在运行态时实际访问的接口是经过平台统一 proxy 的类似 api/proxy/:apiId 这样的路径

在爱速搭平台中发起一个 API 请求的处理流程如下图,关于前端的数据映射和 adaptor,见上一章《对 API 请求的前置 / 后置处理》(详见 2021 年 GMTC 深圳站分享 PPT)

API 编排

在实际开发过程中,类似 “以接口A的返回值作为参数去请求接口B” 这类场景是很常见的,在传统开发模式中,往往不得不为此开发一个专门用于 “聚合” 这两个接口的接口 C,而可视化的 API 编排系统则完美地解决了此类问题。API 编排(baidu.gitee.io/aisuda-docs…)本质上是以可视化的方式来编写后端代码,它可以用于不复杂的接口聚合以及增删改查的场景。

API 编排在爱速搭平台中的产品形态如下图,主要做的事情是顺次执行各节点操作并将最终结果输出到返回值中

然后在前端页面中就可以通过选择的方式调用编辑好的 API 编排

其执行原理如下图(详见 2021 年深圳站 GMTC 分享 PPT)

实体模型

实体模型主要提供数据库统一访问和建模的能力,在基于 API / API 编排 的数据连接方式中,一个完整的应用仍需以来于后端提供的 API,但随着业务的发展,会产生大量类似 “对数据库中的数据进行增删改查” “往数据库中新增一个字段” 的重复性高,难度低,但又需要耗费大量沟通成本的后端需求。

低代码平台中的实体模型通常是以可视化的方式提供数据增删查改能力以及更高权的 DDL 操作。将这些重复劳动可视化是为了让不熟悉 CRUD 编程的人也能定制自己的数据库操作,也让宝贵的后端研发人力不必过多浪费在编写 CRUD 代码上。

以下是爱速搭平台中的 “实体管理” 建模功能的截图示例。

实体模型整体架构比较复杂,由于篇幅原因,不在本文具体描述,仅列举以供作为 “低代码平台能力” 的参考,有兴趣的读者可前往观看本人在 GMTC 深圳站的分享(ppt.infoq.cn/slide/downl…)以及期待后续的文章。

流程引擎

流程引擎主要用于审批流和业务流的编排,如请假审批,预算审批等。在低代码平台中,流程引擎通常由流程可视化编辑器 + 集成待办中心两个主要部分组成。

以下是百度爱速搭平台中流程可视化编辑器的截图示例,图中展示了一个 “审批” 节点的配置细节。

而下图展示了待办中心审批页,该审批页的内容是由上图编辑器在设计阶段指定的

流程引擎也是一个比较复杂的独立体系,本文仅列举,有兴趣的读者也可在本人的 GMTC 深圳站分享中找到相关内容。

后续也会有独立文章进行详细说明。

结语

低代码平台这样事物严格来说并不能算是新兴产物,而这个概念突然 “火” 起来,却似乎是近一两年的事。面对这样的现状,我们大可不必偏激地认定它是解决研发困局的银弹或是炒作起来的泡沫。就我们在百度内外的实践经验而言,它是“实际解决了不少业务问题的”“有用的”解决方案。在低代码这个领域,未来也必定还有很多值得不断深挖的细分课题。