本文首发于「Shopee技术团队」公众号,探索更多Shopee技术实践
本文是基于 Shopee Supply Chain WMS(Warehouse Management System,仓库管理系统)团队利用前端低代码系统进行降本增效的一次实践总结。
目录
1. 项目背景
当前开发模式下的痛点
2. 思考破局
业界方案对比
3. ASLINE 方案设计
3.1 核心设计思想
3.2 核心架构
4. 核心难点及解决方案
4.1 设计稿组件智能识别
4.2 布局转换
4.3 接口字段与组件字段的匹配
5. 系统展示
6. 落地情况及后续计划
1. 项目背景
在 Shopee Supply Chain WMS 团队内部,经过近几年的沉淀,我们的设计团队、前端团队、后端团队之间形成了一套行之有效的协作方式。
我们有如下基础资源和规范:
- 前端基础组件库;
- 前端业务组件库;
- 前端和设计共同遵守的一整套设计规范,设计基于组件库的 UI,通过 Figma 输出设计稿;
- 前后端目前的协同开发联调方式:通过 YAPI 在线平台,在接口未完成前,提前给出接口文档。
当前开发模式下的痛点
然而,这些现有的组件、规范及流程虽然能一定程度上帮助我们更快产出页面,但是对开发效率提升的程度有限。
作为以 ToB 业务为主的仓库管理系统,前端页面中有大量的管理后台相关的页面,出现频率较高的就是以表格查询展示、表单输入、信息展示为主的列表查询页面和内容详情页面,如下所示:
并且,经过一两年的沉淀,我们的页面业务组件化程度较高,大部分页面都可以拆分成一个一个的业务组件模块。
我们有类似于 Element UI 的自研基础组件库。而业务组件是基于多个基础组件耦合了一定的业务属性和业务逻辑,进一步封装而成,由此渐渐也形成了一套业务组件库。
同时,在前后端接口联调环节,面对这些页面,前端需要耗费一定量的时间去匹配各个展示字段与接口字段,联调工作量较大。
总结起来,这里的痛点主要有:
- 业务页面相似度高,且需求量大,存在大量列表查询页面、内容详情页面;
- 前后端接口联调工作量大。
基于已有的组件库资源、设计规范、接口文档平台资源以及目前面临的痛点,我们在思考,能否有效将现有工具更好地串联起来?通过什么手段能更好地去提升开发效率?
2. 思考破局
针对上述痛点,我们的核心问题是:
- 如何快速产出不含业务逻辑的前端页面?能否将设计稿直接转化成页面代码?
- 如何有效降低前后端联调时间?
为此,我们自然而然地想到了当前比较火的低代码平台概念。基于痛点及诉求,我们预研了业界的一些优秀的同类低代码平台方案。
业界方案对比
下面对比了几个比较流行的业界低代码平台:imgCook、CodeFun、飞冰、H5-dooring 等。
方案 | imgcook | CodeFun | 飞冰 | H5-dooring |
---|---|---|---|---|
端支持 | PC、移动端 | 移动端为主 | PC、移动端 | PC、移动端 |
面向人群 | 开发、运营 | 主要面向开发 | 开发、运营 | 开发、运营 |
框架支持 | Vue、React、uni-app、React Native、H5 等 15 种开发规范 | Vue、微信小程序 | React、Vue | React |
二次开发体验 | 1)支持可视化编辑 2)支持 schema 源码开发 3)支持样式配置、属性配置、数据源处理 |
1)支持简单的行为增加 2)支持简单的样式名修改 |
1)提供区块代码模版 2)代码只有组件示例,要大量修改 3)自动生成代码环境 |
1)支持可视化编辑 2)可扩展,但是基本无法支持复杂度高的页面二次编辑 |
设计稿支持 | Sketch、PSD、文件图片、Figma | Sketch、Photoshop(内测) | 不支持 | 不支持 |
生成代码质量 | 1)较高的代码质量 2)可维护可迭代性高 |
1)合理的页面结构 2)较为精简的代码 |
1)合理的页面结构 2)UI 组件示例代码 3)基础组件会重复生成 |
1)不容易维护的绝对定位布局 2)存在部分代码冗余 |
支持拖拽 | √ | × | × | √ |
接口对接 | √ | × | × | × |
开源 | × | × | √ | √ |
左滑查看完整表格
总体而言,不同的低代码平台有各自的特点和优势,但是没有完全符合我们需求的。最为核心的是,没法与我们现有的工作流及已有的基础组件、业务组件库资源相打通。
我们的诉求是:
- 需要支持 Figma 设计稿的识别;
- 系统能够和我们自研组件基础库、业务组件库相结合,也就是生成的代码是基于我们自身的基础组件库、业务组件库的;
- 能够有效和 API 对接,加速前后端联调;
- 可二次编辑导出的代码,拥有足够的灵活性保证日后的优化与新的需求。
基于此,我们决定研发基于 UI 设计稿识别和自动 API 接口对接的可视化低代码系统。
当然,这里有一点需要强调:我们做的是低代码而不是零代码,我们的业务页面虽然相似度高,但是其中业务逻辑各不相同且非常复杂,我们希望这个系统产出的只是中段代码,可以理解为“半成品代码”,其目的只是帮完成前端页面布局与部分接口的联调对接工作,而我们需要拿到这个中段代码,二次开发,补充完善剩余业务逻辑。
我们设计的这个低代码系统,目标是兼顾效率与灵活性。
3. ASLINE 方案设计
为此,我们自研了 ASLINE 低代码系统。
ASLINE,意为 Assemble Line 流水线,我们希望针对一些高频、量大、重复的页面,该系统能像流水线一样快速产出相应的高质量代码。
3.1 核心设计思想
该系统核心设计思想是充分利用现有资源,即:
完善的组件库+在线设计稿资源+在线接口文档。
核心流程如下:
- 通过识别设计稿,识别设计稿中所用组件,组合成页面,自动生成基于 flex 的智能布局 UI 代码;
- 通过接口文档,自动生成前端网络请求相关代码,自动匹配替换组件内字段;
- 基于上述两个流程,补全生成大部分框架相关代码(Vue),导出完整可运行的结构化页面代码。
该系统提供的核心能力包括:
- 设计稿到页面布局及组件的智能识别;
- 画布支持组件拖拽和智能吸附;
- 支持可视化配置调整;
- 组件和 API 接口字段自动匹配;
- API 接口部分代码自动生成;
- 代码自动生成合并及导出下载。
3.2 核心架构
再来看看我们对整个系统架构的抽象设计:
整个系统的交互操作都基于低代码平台,通过三个核心算法:设计稿识别算法、智能布局转换算法、API 与组件字段匹配算法将核心流程串联起来。
4. 核心难点及解决方案
该系统的难点比较多,这里围绕核心算法展开讲讲下面三点:
- 如何进行设计稿的识别?
- 布局问题:识别出来的组件如何实现基于画布的绝对定位到导出时的流式布局转换?
- 如何实现接口字段与组件字段的匹配?如何提升匹配准确率?
4.1 设计稿组件智能识别
ASLINE 低代码系统支持设计稿识别,能够智能识别出其中所使用到的对应组件。
这里有一个误区,我们不是去直接比对图像之间的相似度,而是通过将图像抽象化并标准化为一个数字矩阵。最终比较的,其实是两个经过标准化后的数字矩阵的相似度。
在组件识别这一块,大家可能会想到图像识别,业界用的比较多的主流方案是 Mask R-CNN 的依靠机器学习的方法,但是我们觉得这个项目没有必要搞得如此复杂,不需要用到这种复杂算法甚至需要用到机器学习的地步。
同时,还有一种不需要识别,直接通过设计稿标记注明组件名称的方案。最终没有选取这个方案主要还是因为不具备很好的通用性(需要设计团队介入、也不利于推广到其他团队)。
因此,我们折中选取了下面将介绍的这种,实现简单、处理速度快、准确率较高的方式。
我们可以通过 Figma 的 OpenAPI 获取一个用来描述设计稿所有元素的 JSON 文件,通过这个 JSON 文件,能够知道文本的信息、各种元素的 CSS 信息、大小以及位置等。在拿到这些数据以后,可以通过提取出一些有效的特征,将这些特征转变为数字矩阵,通过数据的对比来识别出对应的组件。
4.1.1 数据获取
我们可以通过 Figma 的 OpenAPI 获取任意一个设计稿的 JSON 数据,接口为 https://api.figma.com/v1/files/:file_key/nodes
,获取的返回数据格式大致为:
{
name: 'Component Name',
lastModified: '2021-11-29T08:57:41Z',
version: '1334322378',
role: 'viewer',
nodes: {
id: '6:188',
name: 'Component Name/View',
type: 'FRAME',
children: [{
"id": "6:202",
"name": "bg",
"type": "RECTANGLE",
"blendMode": "PASS_THROUGH",
"absoluteBoundingBox": {
"x":1792.0,
"y":-762.0,
"width":114.0,
"height":32.0
},
"fills":[
{
"blendMode": "NORMAL",
"type":"SOLID",
"color":{"r":1.0,"g":1.0,"b":1.0,"a":1.0}
}
],
"strokes":[
{
"blendMode":"NORMAL",
"visible":false,
"type":"SOLID",
"color": {
"r":0.85882353782653809,
"g":0.85882353782653809,
"b":0.85882353782653809,
"a":1.0
}
}
]
}
// ... 还有 N 多个类似的节点
]
}
}
nodes 中会包含着每个元素的各种各样的信息,这个信息量太多也太大,而且里面包含着大量冗余信息,所以下一步我们需要进行数据清理。
4.1.2 数据清理
在获取设计稿所有的数据之后,我们要进行数据清理的工作。这是因为在设计人员绘制设计稿的时候,有些文本或者元素信息可能使用的是 hide 属性,将其隐藏而并不是删除掉了这个元素。有时候可能多绘制了一些并没有显示效果的图层(类似于一个空的 div 元素),或者设计稿中标注了一些备注信息,这些对于设计稿的识别工作都属于噪声(noise),所以第二步的工作是通过设置一些特殊规则(比如正则匹配以及识别一些特殊的参数名称)来将这些噪声去除。
主要去除的内容包括:
- 剔除 visible 属性为 false 的元素;
- 剔除备注信息;
- 剔除对于展示无意义的元素。
4.1.3 特征数据提取
对于这一步骤的理解,可以想象一下,当我们看到设计稿中有这样的一个组件:
接下来去组件的文档中寻找应该使用哪个组件,我们找到了这个组件:
虽然两个组件内的文本信息不同,长度以及宽度都不相同,但是我们能够知道它们其实是同一个组件。
这是因为我们的大脑自动对组件进行了抽象化(Abstraction),我们将这两个图相似的特征都自动提取出来,进行标准化,然后就能够匹配到:这两个图片其实是一码事。
因此,我们的组件匹配其实也是模仿这个过程的。在这里仅把提取文本作为特征来讲解一下该过程:
- 首先,利用深度优先遍历将所有文本元素的信息都提取出来;
- 然后,生成一个二维数组,二维数组的列数为这个 Table 组件的宽度(也就是实际在设计稿中该组件的像素宽度),行数为这个 Table 组件的高度;
- 接着,将每一个文本元素在这个二维数组的位置都设置为 1,其他没有与文本元素坐标重合的二维数组的元素,值都为 0。
下面用一张图片模拟一下,黑色的部分就是二维数组中标记为 1 的地方,白色的则是二维数组中标记为 0 的地方:
接下来我们就需要做标准化处理,也就是将文本元素的大小、数量,以及这个组件的宽与高,都按照一定的比例进行标准化,变成下面这个样式:
左侧是标准化以后的组件,真实的二维数组是右边的样子,一个二维矩阵。实际是这样的(图片大小有限,实际的 0 和 1 会更多):
4.1.4 数据压缩
得到了上面的二维矩阵以后,如果想要把它保存下来,非常耗费空间,因此应当将数据进行压缩。压缩数据的同时,还需要接下来能够直接进行数据比对,也就是需要一个特殊无损压缩的算法。
可以发现这个 Table 组件的二维数组大部分都是 0,1 的部分是少部分,所以我们先将这个二维数组变成一维数组,然后每 32 位分成一份,就可以发现,每一份里面的值都是一个 32 位的二进制。
我们将这个二进制转为十进制的数字,如果该值为 0,则自动跳过这一份。如果值不为 0,则用一个对象来保存,key 值设为此份数据的 index,value 设为二进制转为十进制的值。这样我们就将一个原本非常大的矩阵对象压缩成了一个小了非常多的对象。
压缩后的数据格式:
{
"Table": {
"matrix": {
"1": 127,
"2": 478,
“10”: 1229332,
// ...
"528": 127,
"529": 2147483392,
"530": 8388607,
// ...
}
}
}
其中,“1”,“2”,“528” 表示的是标准化后的坐标,代表这一块区域的信息不全为 0,而后面对应的数字则表示该区域的信息,我们将原本的 01010101 展示改为了十进制展示。
4.1.5 组件比对
将数据进行压缩后,接下来进行数据比对。在真实的系统中,数据库会保存着一份标准 Table 组件压缩后的数据,我们在开始比对的时候,先把设计稿中的一个组件通过上面的转化,转成压缩后的数据,再把这个数据和数据库中保存的标准组件信息库进行比对。
比对的过程如下,经过上述计算后,可以得到设计稿中组件的数据:
{
"Table": {
"matrix": {
"528": 127,
"529": 2147483392,
"530": 8388607,
// ...
}
}
}
标准组件信息库中的某一个组件的数据:
{
"Table": {
"matrix": {
"528":300,
"529":2147483392,
"530":8388607,
// ...
}
}
}
可以看到,设计稿中组件的数据的 key 值有 528,这时候再去看标准库中的组件是否有 528,发现两边都有 index 是 528,值分别为 127 和 300,也就是说它们的二进制中有一些是不一样的。这时候可以利用位运算,就能够知道哪些位是相同的。
举个例子,利用与运算以后,得到 127 & 300 = 44
,44 是一个二进制数转为十进制以后的值,其中 44 中有多少个数字 1 就代表了两个片段的相似度是多少,那这时候怎么才能知道 44 的二进制中有多少个位数是 1?
我们可以选择将 44 转为 32 位的二进制,然后遍历一遍来获取一共多少个位为 1。但更加快捷的方法是,44 的 32 位二进制中,是 1 的位数肯定是小于 32 的,那么我们利用一个特殊的位运算方法 i & (i-1)
,利用这个位运算,就能够将一个二进制最右侧的 1 去掉。
为什么 i & (i-1)
能清除最右边的 1 呢?因为从二进制的角度讲,n 相当于在 n-1 的最低位加上 1 。44 的二进制为 101100,43 的二进制为 101011,44 & (44-1) = 40
,40 的二进制为 101000,也就是比 44 的二进制少一位。所以可以利用循环让一个数字与上个数字减 1,知道这个数字为 0。
利用此方式,比对的次数一定小于 32 次,我们也就可以基于此来进行算法的优化。在通过此方法循环遍历所有组件后,选取相似度最高的,即可得到比对结果。
以上就是基于图像的信息来识别一个组件的全过程,大致分为 5 个步骤:
- 数据获取;
- 数据清理;
- 特征数据提取;
- 数据压缩;
- 数据比对。
通过上述 5 个步骤,即可找到相似度最高的组件。这种方案的好处在于不需要特别大的样本量,类似于深度学习训练后的模型的函数,我们找到了粗糙版的函数。这种方案的识别速度快,而且文本信息的提取与组件识别是在一起获取的,不需要分成两个部分,这样进一步提高了效率。
但是这种方案有时候肯定也会有识别不准确情况,而且这种方案由于是人工定义的一些特征数据(比如上面的例子,我们就将特征数据定为文本信息,还可以是边框等信息),无法覆盖得过于全面,因此,我们基于识别也有一整套的退化降级方案:
识别程度 | 用户操作 |
---|---|
组件全部识别且识别正确 | 二次确认 |
组件全部识别但部分错误 | 二次确认,识别错误组件可进行人工替换 |
组件识别正确 | 未识别部分可在画布上拖拽组合 |
组件 0 识别 | 完全自由的拖拽组合 |
当然,这里还有一些问题需要解释一下,譬如为什么不直接用一个 ID 在设计稿中明确标识是哪种组件?为什么一定要去用算法去识别?这里有几个原因:
- 考虑到团队内部 Figma 账号与相应设计资源的权限分配,若要求设计师明确标识每一个组件,并带上特定的 ID,将额外增加设计工作量,同时不太利于向其他团队推广这个工具;
- 基于现有的识别方式,假设新增一个组件,无需 UI 侧同步新增这个 ID。
基于以上现状,我们设计了如此一种识别的方式。而且它是通用的,基于任何组件都可以这样去做。
4.2 布局转换
许多低代码平台在画布上进行展示的时候,由于要方便计算和拖动,都是基于绝对定位的,最终产出的页面代码使用的布局方式也大部分是绝对定位。
但是在真实的业务代码中,HTML 代码都需要有层级结构,并且就目前而言,多数利用 flex 进行弹性布局。所以我们需要一个将绝对定位的组件转换为 flex 弹性布局的算法。
我们的做法是:
- 通过各个组件位置计算,自动生成带有层级结构的 HTML 代码;
- 通过组件之间的位置关系,优先以“行”结构进行划分,其次以“列”结构进行计算,生成基于 flex 布局的 HTML/CSS 代码。
如何得知布局的行列信息?我们采用的是投影法,以这样一个布局为例子:
它由 3 个业务组件构成,每一个即为一个 flex item。那么,如何在转化的过程中知道它的嵌套结构——得知它有 2 行,其中第一行有 1 个元素,第二行有并排的 2 个元素呢?
先从左侧往右看,假设一束水平光打过去,投影在墙上的结果——不连续黑色块的个数,即是可以分隔的行的数量:
基于上述结果分隔得到的行,再从上往下投影,又可以得到每一行中有多少列。
最终,我们会将原本基于绝对定位的布局,转换为基于 Flex 的布局,类似于这样:
.container {
display: flex;
flex-direction: column;
}
.row-2 {
margin-top: 12px;
display: flex;
}
.column-4 {
margin-left: 20px;
}
对于整个 container 而言,我们会将它设定为一个竖向排列的 flex 布局,设定 display: flex
与 flex-direction: column
。
而 container 容器之下,则是根据上述的投影分析结果,设定 row 信息(行信息),每个行容器内设定 flex-direction: row
,再根据每行内的垂直投影分析得到行内的 column 分布,同时根据实际物理排布,得到每列之间的间距信息,从而汇总得到整体的 flex 布局代码。
4.3 接口字段与组件字段的匹配
当识别出了设计稿中相应的业务组件,并将其渲染在页面上之后,ASLINE 低代码系统还提供一种能力,能够将每个业务组件与一个接口进行关联匹配,通过输入关联的接口文档 URL,自动生成前端网络请求相关代码,自动匹配替换组件内的字段。
这里我们主要做了几件事:
第一,当用户提供了对应业务组件的接口文档 URL,系统就能帮助生成请求相关的代码,节省部分工作量。
第二,完成了前端页面展示与接口字段的匹配。这是什么意思呢?譬如一个表格组件中,某字段在前端展示为 start time
,后端接口返回给前端的字段可能用的是 start_time
、 begin_time
等形式表示。这里,算法要做的事情,就是将前端展示的字段与后端返回的字段进行匹配,将它们正确地关联起来。
这里的整体流程:
- 选中组件后,能得到组件的字段数据;
- 输入接口文档请求参数,将文档参数和组件字段数据传到我们的 API 处理服务;
- API 处理服务得到接口文档请求参数,拉取用户输入的接口文档数据;
- 组件字段数据和接口文档数据进行匹配,通过相似度算法计算出匹配度最高的接口字段;
- 把组件字段替换为匹配接口文档后的字段值;
- 返回匹配接口文档后的组件数据、API 接口数据;
- 系统接收到响应数据后,将匹配好的组件数据更新到页面上;
- 用户可对匹配后的字段,进行人工二次确认及调整。
其形式大概如下,对于我们这样一个业务组件 ProHeader:
右侧数据面板如下:
通过输入接口文档对应该接口的 URL,我们能拉取到接口相关信息,Label 列表示的是前端展示的字段文案,而 Prop 列则是经过智能匹配后,展示字段与接口字段的匹配结果。
而对于这样一个 PageHeader 组件,经过识别及接口匹配后,导出时会自动生成这样一段代码,以 Vue 项目为例,大致的伪代码如下:
包含了整个 Vue 文件的各个部分,组件入参、接口请求、组件渲染等等。
当然,这里只是一个业务组件,对于完整的页面而言,我们还会基于单个组件,再有一个 index.vue
聚合页面上的所有组件,并且基于 flex 布局完成相关内容的排版。
5. 系统展示
接下来简单展示一下整个系统及其核心步骤。
一开始进到首页只有一个输入框,只需要填入对应的 Figma URL。
在对应的 Figma 页面上,选取一个我们业务中常见的列表页,将其 URL 填入系统的输入框中即可。
完成识别后,系统会给出当前所识别出的业务组件列表,有一个用户二次确认调整的过程:
再点击下一步,即可进入主编辑区域。在画板上得到我们所识别出的组件:
这里分为:
- 可拖拽组件区域:可以对画布内容进行增加,对于一些未被识别的组件可以从这里拖进画布;
- 操作面板区域:一些对画布操作的常规工具栏,譬如撤销、回退、辅助线等等;
- 画布区域:可以调整布局,删减组件;
- 二次编辑面板:对每一个组件进行深度调整,关联接口等操作。
最后,调整完毕,点击右上角导出代码,即可获得每个业务组件单独的代码,和这个页面的完整代码。
6. 落地情况及后续计划
这里还是有必要再强调一下:该平台只是低代码平台,而不是零代码平台,系统帮我们完成的,只是页面布局、接口联调等一些繁琐的重复性工作。拿到系统导出的代码后,我们还是需要基于这部分中段代码,继续补齐一些复杂业务逻辑。
这是我们从自己的业务场景出发,考虑实际日常工作中的痛点,而实现的一套有效减轻部分重复机械劳动的系统工具。
目前,此系统已经接入了 Shopee 供应链下基于 Vue 框架的多个子项目:
- 可适用范围:目前全识别加能用的半识别大概占设计稿的 30%-35%,主要分布在列表页和详情页;
- 对于这些列表页和详情页,从目前接入使用的情况而言,业务组件的识别成功率能够到达 92%,只有少部分情况需要手动修改,或者直接在页面进行拖拽;
- 对于适用页面,平均可节约 80% 的开发时间(单个列表页从 0.5 - 1 个工作日降低至 1 个小时)。
当然,ASLINE 能够明显提升开发效率,降低开发成本,但是也存在一定的局限性,需要各种规范/组件库作为支撑。并且前期开发的工作量不低。
关于后续展望,我们已经开始着手适配不同组件库及开发框架(React),并且引入智能学习,提高识别准确率、接口字段匹配成功率。
同时,我们也在画布的易用性、体验交互上不断进行优化,不断增加支持的组件数量。我们期望在无设计资源投入的情况下,支持快速拖拽组装常用页面,服务于一些非前端同学,产生更大的业务价值。
本文作者
Coco,前端工程师,来自 Shopee Supply Chain 团队。