作为阿里经济体前端委员会四大技术方向之一,前端智能化项目经历了2019双十一的阶段性考验,交出了不错的答卷,天猫淘宝双十一会场新增模块79.34%的线上代码由前端智能化项目自动生成。在此期间研发小组经历了许多困难与思考,本次《前端代码是怎样智能生成的》系列分享,将与大家分享前端智能化项目中技术与思考的点点滴滴。
概述
在D2C中,设计稿(Sketch、PSD)、图片(JPG、PNG)经过算法处理之后,最终会导出以绝对布局为基础的元素信息。绝对定位的布局不具备扩展性,可读性也很差,对于开发者来说并不具有可维护性。因此,需要经由布局算法层来处理转化成“前端”眼中可用的代码。在布局算法处理之后,结合语义化、智能字段绑定、智能业务逻辑添加,最终为用户提供一份可读、可维护的代码,本文重点阐述布局算法层的相关处理过程。
所在分层
如图所示,经过链路中的物料识别和图层处理加工,会输出一份以绝对定位为基础,包含了元素位置信息和CSS属性的JSON数据。这份JSON数据接下来会进入布局算法层,对元素的布局结构以及CSS属性进行进一步的加工和处理,最终输出一份符合D2CUI图层协议规范的JSON数据。
(D2C 技术能力分层 )
如图所示,经过链路中的物料识别和图层处理加工,会输出一份以绝对定位为基础,包含了元素位置信息和CSS属性的JSON数据。这份JSON数据接下来会进入布局算法层,对元素的布局结构以及CSS属性进行进一步的加工和处理,最终输出一份符合D2CUI图层协议规范的JSON数据。布局算法作为D2C链路中的重要一环,需要为下游输出正确的布局结构、正确的样式属性、元素间的关系。目前包含以下:
-
合理布局嵌套:
- 绝对定位转相对定位
- 合理的绝对定位
- 冗余嵌套删除
- 合理分组嵌套
- 循环识别
-
元素自适应:
- 元素本身扩展性:文本、图片等节点位置自适应,大小可扩展
- 元素间对齐关系
- 元素最大宽高容错性
约束和前提
布局算法的目标是对任意视觉稿都能有良好的布局还原结果,但是在实际情况中,不够规范的视觉稿(图层分类混乱,图片大小不准确,元素位置不准确)都会影响布局的结果,因此正确的布局前提是有一份规范的视觉稿,这也是布局算法在现阶段,取得理想布局结果的一个前提。
此外,在真实使用的时候,虽然还没有编码,但是开发者早已洞悉视觉稿要怎么变成代码。但是由于规则的缺陷,或者视觉稿的不规范,生成的代码结果可能会与开发者心中所想有差别。因此我们需要提供能力让开发者可以干预生成的结果,而不是将整个过程包装在黑匣子中。这也是 imgcook 设计稿协议诞生的来由之一。通过这一套协议,开发者能够精确的控制代码生成的结果。换而言之,可以通过调整设计稿,结合设计稿的协议来达到对布局还原结果的控制,让生成的代码满足开发者的需要。
核心功能
布局算法的核心思路是分析布局中各个元素的位置、大小和类型,结合元素间的关系,将其从绝对定位的布局,转化为相对定位的布局。
(算法流程图)
如上图所示,布局算法的整个流程是顺序流式的,对于 JSON 数据处理的完整流程是:
- 节点预处理
- 矩阵识别
- 阈值处理
- 节点关系分析
- 行列结构生成
- 布局样式生成
输入
为了减少 Design 对还原的结果的干扰,如下图的设计稿,图层的组织结构和代码的 DOM 组织结构不同。在设计稿中,改变图层顺序,视觉结果可能都不会有变化,但是如果在代码中,调整了 DOM 元素位置,渲染结果可能会发生巨大的变化。因此,为了减少设计稿对还原的结果的干扰,我们选择将输入的设计稿的层级结构都去掉,让所有的图层变成一个一维结构,还原的时候不依赖设计稿提供的结构信息(大部分情况下这些信息都是错误的)。
(设计稿示例图)
这带来的问题是,设计稿中的结构信息丢失。最终的 DOM 结构完全由布局算法生成,失去了人为干预的能力。开发者想要调整 DOM 结构,只能在布局算法生成之后。这会大大降低开发体验,提高使用成本。因此,我们设计了成组协议,通过在设计稿图层名称上添加 #group#,告诉布局算法,设计稿中这个结构是正确的,不需要重新计算。通过成组协议,能够满足开发者对于 DOM 结构干预的诉求。
节点预处理
布局的算法输入来源有 Sketch 插件,PS 插件以及图片,同时,随着插件的迭代升级,不同版本的插件导出的格式和内容也会有所不同。所以需要在进入布局算法之前,将这些差异所以抹平,确保布局算法处理的过程中不要为这些兼容性问题进行编码。
矩阵信息
从这一步开始,布局算法正式开始布局的计算。上面已经提到,输入的 JSON 里是不包含元素的结构信息。因此我们需要通过元素的位置和大小来分析出结构信息。
分析的第一步就是构造一个与输入大小一致的矩阵,例如输入是 702x370 的一个模块,那么会构造一个 702x370 大小的矩阵。遍历 JSON 中的所有节点,根据元素的位置和大小在矩阵中标记元素所在的位置。
这一步的主要工作就是填充整个像素矩阵,得到元素的交错记录。
(图层信息转矩阵信息示意图)
如图所示,JSON 结构信息会转化成一个像素矩阵,像素点上会记录包含的元素信息。
阈值处理
设计稿设计的时候可能有误差,图层的位置可能可能会有偏移,肉眼去看的时候不会很明显。但是在还原步骤中,分析像素矩阵的时候,是会对每一个像素都进行分析,所以像素级别的误差都会被记录进来,对结果可能会产生巨大的干扰。所以需要有一个步骤去规范图层,自动去修复这些误差,降低对还原结果的干扰。
( 阈值处理 )
如上图所示,元素间存在的误差需要我们去分析处理,将这些误差消除掉,为后续的处理提供一个可靠的结果。
节点关系分析
得到准确的矩阵信息之后,就可以来分析元素间的关系。根据元素间的位置信息,可以定义一下三种关系:包含、部分重叠、完全重叠。
(节点关系)
如图所示。A 包含 B 说明 B 节点可能是 A 节点的元素;A 和 B 局部重叠,说明A和B中至少有一个元素需要进行绝对定位。A 和 B 完全重叠的情况较为少见,但是也更为复杂,说明可能其中有一个元素是多余的,也有可能是一个节点包含另外一个节点,也有可能是有节点需要进行绝对定位。经过这个步骤,我们能够获得所有节点之间的关系,可以输出给下一步进行节点结构的构造。
行列结构生成
布局算法是通过“行列结构”来描述布局结构。“行列结构”记录了布局的行列信息,一行的信息包含有:行的起始位置,行的结束位置,行包含的元素,行里面包含的列。一列的信息包含有:列的起始位置,列的结束位置,列包含的元素,列里面包含的行。
(行列结构示意图)
如图所示的一个模块结构,首先会被分成两列记为 Col1 和 Col2,可以看到Col2 里面还包含了三行,可以记为 Row1,Row2 和 Row3。Row3 里面又包含了三列,可以记为 Col1、Col2、Col3。如图六右侧结构示意,最终生成从行列结构是一个树状的结构。
通过元素的行列结构信息,我们可以计算元素的布局信息。在图六的这个例子里,根节点包含了两个子节点 Col1 和 Col2,这两个节点在纵向可以通过 margin-Top 来设置与根节点容器的边距,在横向可以通过 spaceBetween 来设置两个节点的位置。再进一步计算 Col2 中的 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%。未来重要努力目标之一,就是不断的提高视图还原准确率,提高代码可用率。
未来展望
目前,经过规则协议的推广,我们得到了大量用户标记了成组和合并协议的视觉稿,通过对比去掉协议之后的结构生成结果,可以分析出算法生成的结构与用户期望的结构的差别。借助这些高质量的样本,我们正在通过机器学习的算法,进行智能的成组和合并,进一步的降低用户使用的门槛和成本,解放生产力。另外,补充目前整在努力的一些方向:
- 布局算法当前只支持 flex 布局,但是中后台场景或者其他场景可能需要不同的布局方案,因此布局算法后续会提供布局的定制,可以从 flex 布局切换到其他布局。
- 提高布局的准确度,目前有些场景下,布局结果和业务实际场景有关,布局还原的效果不理想。接下来会提供辅助的工具,在插件上引导设计师规范视觉稿,在 web 编辑器上给开发者更好的编辑体验。
- 降低开发者的使用成本,目前对于设计师,前端,在使用过程中还有调整设计稿的要求,这也提高了 D2C 的使用成本。我们正在努力通过智能化的手段来降低这些成本,让生成代码更加智能,更加简单。