前端代码是怎样智能生成的-布局算法篇

avatar
阿里巴巴 前端委员会智能化小组 @阿里巴巴

文/知浅

作为阿里经济体前端委员会四大技术方向之一,前端智能化项目经历了 2019 双十一的阶段性考验,交出了不错的答卷,天猫淘宝双十一会场新增模块 79.34% 的线上代码由前端智能化项目自动生成。在此期间研发小组经历了许多困难与思考,本次 《前端代码是怎样智能生成的》 系列分享,将与大家分享前端智能化项目中技术与思考的点点滴滴。

概述

在 D2C 中,设计稿(Sketch、PSD)、图片(JPG、PNG)经过算法处理之后,最终会导出以绝对布局为基础的元素信息。绝对定位的布局不具备扩展性,可读性也很差,对于开发者来说并不具有可维护性。因此,需要经由布局算法层来处理转化成“前端”眼中可用的代码。在布局算法处理之后,结合语义化、智能字段绑定、智能业务逻辑添加,最终为用户提供一份可读、可维护的代码,本文重点阐述布局算法层的相关处理过程。

所在分层

如图所示,经过链路中的物料识别和图层处理加工,会输出一份以绝对定位为基础,包含了元素位置信息和 CSS 属性的 JSON 数据。这份 JSON 数据接下来会进入布局算法层,对元素的布局结构以及 CSS 属性进行进一步的加工和处理,最终输出一份符合 D2C UI 图层协议规范的 JSON 数据。

(D2C 技术能力分层)

如图所示,经过链路中的物料识别和图层处理加工,会输出一份以绝对定位为基础,包含了元素位置信息和 CSS 属性的 JSON 数据。这份 JSON 数据接下来会进入布局算法层,对元素的布局结构以及 CSS 属性进行进一步的加工和处理,最终输出一份符合 D2C UI 图层协议规范的 JSON 数据。布局算法作为 D2C 链路中的重要一环,需要为下游输出正确的布局结构、正确的样式属性、元素间的关系。目前包含以下:

  • 合理布局嵌套:
    • 绝对定位转相对定位
    • 合理的绝对定位
    • 冗余嵌套删除
    • 合理分组嵌套
    • 循环识别
  • 元素自适应:
    • 元素本身扩展性:文本、图片等节点位置自适应,大小可扩展
    • 元素间对齐关系
    • 元素最大宽高容错性

约束和前提

布局算法的目标是对任意视觉稿都能有良好的布局还原结果,但是在实际情况中,不够规范的视觉稿(图层分类混乱,图片大小不准确,元素位置不准确)都会影响布局的结果,因此正确的布局前提是有一份规范的视觉稿,这也是布局算法在现阶段,取得理想布局结果的一个前提。

此外,在真实使用的时候,虽然还没有编码,但是开发者早已洞悉视觉稿要怎么变成代码。但是由于规则的缺陷,或者视觉稿的不规范,生成的代码结果可能会与开发者心中所想有差别。因此我们需要提供能力让开发者可以干预生成的结果,而不是将整个过程包装在黑匣子中。这也是 imgcook 设计稿协议诞生的来由之一。通过这一套协议,开发者能够精确的控制代码生成的结果。换而言之,可以通过调整设计稿,结合设计稿的协议来达到对布局还原结果的控制,让生成的代码满足开发者的需要。

核心功能

布局算法的核心思路是分析布局中各个元素的位置、大小和类型,结合元素间的关系,将其从绝对定位的布局,转化为相对定位的布局。

(算法流程图)

如上图所示,布局算法的整个流程是顺序流式的,对于 JSON 数据处理的完整流程是:

  1. 节点预处理
  2. 矩阵识别
  3. 阈值处理
  4. 节点关系分析
  5. 行列结构生成
  6. 布局样式生成

输入

为了减少 Design 对还原的结果的干扰,如下图的设计稿,图层的组织结构和代码的 DOM 组织结构不同。在设计稿中,改变图层顺序,视觉结果可能都不会有变化,但是如果在代码中,调整了 DOM 元素位置,渲染结果可能会发生巨大的变化。因此,为了减少设计稿对还原的结果的干扰,我们选择将输入的设计稿的层级结构都去掉,让所有的图层变成一个一维结构,还原的时候不依赖设计稿提供的结构信息(大部分情况下这些信息都是错误的)。

(设计稿示例图)

这带来的问题是,设计稿中的结构信息丢失。最终的 DOM 结构完全由布局算法生成,失去了人为干预的能力。开发者想要调整 DOM 结构,只能在布局算法生成之后。这会大大降低开发体验,提高使用成本。因此,我们设计了成组协议,通过在设计稿图层名称上添加 #group#,告诉布局算法,设计稿中这个结构是正确的,不需要重新计算。通过成组协议,能够满足开发者对于 DOM 结构干预的诉求。

节点预处理

布局的算法输入来源有 Sketch 插件,PS 插件以及图片,同时,随着插件的迭代升级,不同版本的插件导出的格式和内容也会有所不同。所以需要在进入布局算法之前,将这些差异所以抹平,确保布局算法处理的过程中不要为这些兼容性问题进行编码。

矩阵信息

从这一步开始,布局算法正式开始布局的计算。上面已经提到,输入的 JSON 里是不包含元素的结构信息。因此我们需要通过元素的位置和大小来分析出结构信息。

分析的第一步就是构造一个与输入大小一致的矩阵,例如输入是 702 x 370 的一个模块,那么会构造一个 702 x 370 大小的矩阵。遍历 JSON 中的所有节点,根据元素的位置和大小在矩阵中标记元素所在的位置。
这一步的主要工作就是填充整个像素矩阵,得到元素的交错记录。

(图层信息转矩阵信息示意图)

如图所示,JSON 结构信息会转化成一个像素矩阵,像素点上会记录包含的元素信息。

阈值处理

设计稿设计的时候可能有误差,图层的位置可能可能会有偏移,肉眼去看的时候不会很明显。但是在还原步骤中,分析像素矩阵的时候,是会对每一个像素都进行分析,所以像素级别的误差都会被记录进来,对结果可能会产生巨大的干扰。所以需要有一个步骤去规范图层,自动去修复这些误差,降低对还原结果的干扰。

(阈值处理)

如上图所示,元素间存在的误差需要我们去分析处理,将这些误差消除掉,为后续的处理提供一个可靠的结果。

节点关系分析

得到准确的矩阵信息之后,就可以来分析元素间的关系。根据元素间的位置信息,可以定义一下三种关系:包含、部分重叠、完全重叠。

(节点关系)

如图所示。A 包含 B 说明 B 节点可能是 A 节点的元素;A 和 B 局部重叠,说明A 和 B 中至少有一个元素需要进行绝对定位。A 和 B 完全重叠的情况较为少见,但是也更为复杂,说明可能其中有一个元素是多余的,也有可能是一个节点包含另外一个节点,也有可能是有节点需要进行绝对定位。 
经过这个步骤,我们能够获得所有节点之间的关系,可以输出给下一步进行节点结构的构造。

行列结构生成

布局算法是通过“行列结构”来描述布局结构。“行列结构”记录了布局的行列信息,一行的信息包含有:行的起始位置,行的结束位置,行包含的元素,行里面包含的列。一列的信息包含有:列的起始位置,列的结束位置,列包含的元素,列里面包含的行。

(行列结构示意图)

如图所示的一个模块结构,首先会被分成两列记为 Col 1 和 Col 2,可以看到 Col 2 里面还包含了三行,可以记为 Row 1,Row 2 和 Row 3。Row 3 里面又包含了三列,可以记为 Col 1 、Col 2、Col 3。如图六右侧结构示意,最终生成从行列结构是一个树状的结构。

通过元素的行列结构信息,我们可以计算元素的布局信息。在图六的这个例子里,根节点包含了两个子节点 Col 1 和 Col 2,这两个节点在纵向可以通过 marginTop 来设置与根节点容器的边距,在横向可以通过 spaceBetween 来设置两个节点的位置。再进一步计算 Col 2 中的 3 个 Row 的布局,通过递归的方式,计算所有节点的布局信息,建立完整的布局关系。

这个步骤是布局算法中最为关键的一步,但是行列结构生成的结果并不是唯一确定的。像图六这样的模块,还可以生成下图这样的行列结构。

(另外一种行列结构)

不同的行列结构描述代表了不同的 DOM 结构,从 UI 信息上,有时候并不足够判断哪种行列结构是最优的。所以引入了成组的协议来增加开发者的干预,确保能够生成开发者期望的结构。

需要注意的是,这里生成的 DOM 结构可能不是一棵树,而可能是多棵树,比如有一些 DOM 节点是绝对定位的,这部分节点会独立组成一棵树,而没有在主干上。

布局样式生成

经过上一步行列结构的生成,得到了多棵 DOM 树。接下来进进入了布局样式生成的步骤。这一步会遍历上一步得到的所有行列结构,将分散的多棵树组织成一棵完整的 DOM 树。得到完整的布局结构之后,节点的样式需要进一步优化。

  • 宽高优化:上游给到的输入,所有的节点都会有宽高信息,但是经过布局算法处理之后,部分节点可能不需要这个宽高属性,而是由子节点将容器撑开即可。
  • 样式精简:删除插件导出的多余属性
  • 属性处理:插件中的 transform 是针对单个节点,但是布局之后的transform 对子节点也会生效,所以要对 transform 属性进行处理
  • 扩展性优化:有一些元素是可扩展的,例如【图三】的牛皮癣节点,需要进行样式的优化,如果设置了容器宽度需要转化为 padding 形式的布局
  • 层级优化:对生成的 DOM 树进行层级优化,删除一些多余的层级

经过上述处理之后,就得到了完整的 DOM 树,布局算法的基本任务也已经完成。生成 DOM 树最后,可以进行进一步分析,例如可以对 DOM 树的节点相似度进行检测,标记可能是循环的节点。检测出循环之后,在生成代码的时候节点就可以通过循环的方式渲染。

测试和度量

由于布局算法目前还偏向专家规则系统,深度学习等智能化方案应用较少,所以规则分支较多,改动容易引起关联问题,所以布局算法层对功能的稳定性和效果的度量有非常高的要求。

功能稳定保障

单元测试

布局算法许多函数的输入非常复杂,像是矩阵信息,可能就是一个 702 * 458 这样的一个矩阵,构造这样的输入很困难。于是我们另辟蹊径,不直接构造函数的输入,而是在设计稿中构造不同的模块,以一个完整模块的运行作为测试用例。

不仅输入的构造较为困难,输出的对比也不太容易。假如要分析矩阵信息这一步骤构造的矩阵是否正确,我们的结果需要录入 702*458 这样的结果。虽然确实可以通过这种方式测试,但是我们设计了另外一种方法,将多个函数联在一起,最终输出的是一个对象,这样测试结果的可读性也大大增强,同时也能保证的正确性和测试的有效性。

目前单元测试的代码覆盖率达到了78%,核心功能达到了 100% 。

功能测试

除了单元测试外,在布局算法的场景下还可以通过功能测试来保障算法的正确性。精选了线上的 156 个模块作为测试集,包含了无线,pc等各种场景下的模块,运行布局算法,分析算法运行的结果。在每一次上线前,都会运行一轮功能测试,以保证布局算法功能的正确性。

效果度量

测试上保证的是布局算法对不对,有没有报错。但是对于结果的“好不好”,需要通过另外一套体系来度量。这套度量体系包含了三个方面:UI 还原度量,代码可维护性度量,用户真实可用率度量。

UI 还原度量

UI 还原度量是度量经过布局算法后视觉效果上是否 100% 还原,具体是通过对比模块的视觉图和 demo 截图,分析布局算法是否准确地还原了模块。

(UI 还原度量)

布局还原效果度量方案复用了插件的还原度量方案,度量插件的还原度时,使用的是插件导出的 JSON 数据,而度量布局算法的时候使用的是还原的结果。还原精选了线上 160 个模块,覆盖了多种形式的布局,经过还原对比,确保布局算法的准确性。不过 ui 的度量只能保证说渲染出来的 UI 和视觉稿一致,但是不能确保结构的正确,假如没有经过布局算法的加工,直接是插件导出的数据,可能对比设计稿还原度会更高,因此需要其他的方案来进一步度量还原的结果。

代码可维护性度量

可维护性的度量,主要是考虑度量代码本身的质量。代码的质量可以通过结构的合理性来衡量:嵌套不能过深,节点数不能太多,不能有多余的嵌套。从生成的节点结构来分析布局算法的可维护性,具体度量的方法:

  • 嵌套
    • 最大嵌套深度不超过6层
    • 冗余嵌套(1套1)
  • 子节点数
    • 不能超过6个

虽然方法较为简单,但是从结果看能够很好的描述生成的结构是否冗余。但是,这种程度的度量也还不足以描述生成的代码足够好,因此我们还引入了用户真实可用率,来进行共同度量。

用户真实可用率度量

如何判断用户的认可度就比较难,如果直接进行用户调研,会有一定的主观因素,也难以量化。我们想到的方法是,计算代码的改动比例,如果生成的代码得到了用户的认可,满足了用户的需求,那么他就不应该去改动代码;反之,如果生成的代码不好,存在问题,用户拿去使用的时候一定会去修改。用户保留下来的代码的比例可以一定程度上用来衡量用户的认可度,从而反应了还原的效果。

度量的计算方案:

  • 代码可用率: 生成的代码最后留存行数 / 发布阶段的总代码行数

双十一模块的度量结果

此次双十一,双11新增模块总数38,D2C链路产出30个,占比:78.9%,智能生成代码平均可用率:79.34%,视图还原准确度:92.47%。未来重要努力目标之一,就是不断的提高视图还原准确率,提高代码可用率。

未来展望

目前,经过规则协议的推广,我们得到了大量用户标记了成组和合并协议的视觉稿,通过对比去掉协议之后的结构生成结果,可以分析出算法生成的结构与用户期望的结构的差别。借助这些高质量的样本,我们正在通过机器学习的算法,进行智能的成组和合并,进一步的降低用户使用的门槛和成本,解放生产力。另外,补充目前整在努力的一些方向:

  1. 布局算法当前只支持 flex 布局,但是中后台场景或者其他场景可能需要不同的布局方案,因此布局算法后续会提供布局的定制,可以从 flex 布局切换到其他布局。
  2. 提高布局的准确度,目前有些场景下,布局结果和业务实际场景有关,布局还原的效果不理想。接下来会提供辅助的工具,在插件上引导设计师规范视觉稿,在 web 编辑器上给开发者更好的编辑体验。
  3. 降低开发者的使用成本,目前对于设计师,前端,在使用过程中还有调整设计稿的要求,这也提高了 D2C 的使用成本。我们正在努力通过智能化的手段来降低这些成本,让生成代码更加智能,更加简单。

更多推荐: