跳转至

20 如何仅通过元素间的相对位置来生成有弹性可维护的UI代码?

你好,我是陈旭。

这一讲我来给出一种生成页面布局代码的方法,继续完成第19讲《如何使用平民技术实现UX设计稿转代码?》中未能完成的内容讲解。在这讲中,我们通过组件属性识别、层次结构推断和弹性推断等算法,从UX设计稿中识别出了足够多的信息,足以用于生成页面的静态布局代码了。现在各种素材俱备,只欠一股东风将这些素材转为代码。

第07讲中,我介绍了结构化代码生成法,这是一种用于生成TypeScript代码的方法,其中也涉及到了HTML布局代码的生成方法,那么当时给出的方法与今天这讲的方法有啥异同呢?又有啥关系呢?你可以快速回顾一下,再来思考这个问题。

第07讲给出的生成代码的方法,是专注于处理单个组件本身的代码的生成,无法用于处理界面上多个组件之间的关系。界面上多个组件之间的关系,其实就是界面的布局。相对地,今天这讲我们专注于处理组件之间的关系(界面布局),不会涉及到单个组件的代码生成的细节。所以从这个角度来看,这讲的内容是对第07讲内容的扩充,两讲之间是相辅相成的关系。

基础概念

下图这个大屏应用,界面的元素很多,看起来稍微有点复杂,有图形、表格、文本、图标。

图片

我们需要对实际界面做一点抽象,保留住重要的信息,隐藏掉不相干的干扰细节。组件化,是一种非常常见的界面抽象方法,这个方法贯穿了我们的整个专栏。

组件就是这里需要引入的第一个基础概念,但由于我们对组件的概念已经非常熟悉了,所以这里不再过多解释。但有一点非常重要,需要特别强调,那就是,组件是界面的最小单元,不可拆分

显然,只有组件是不足以生成任意界面的布局代码的,但认识到组件可以对界面做抽象,一个界面是由多个组件组成的,这是一个好的开始。接下来要解决的问题是,要找到一个办法来恰当地对界面上的各个组件做分组。当然了,所谓的恰当的方法就是能更容易用代码来实现的方法。

原图上花花绿绿的干扰信息太多了,我们使用一个个盒子来抽象它们,可以把原图简化成下面这样,图中每个盒子就是一个组件。

面对这样一个线框图,我问你一个问题,如果让你手写代码来画出一个类似这样的页面布局,你会写出什么样的代码呢?

以HTML为例的话,大概率你可能会写出这样的代码。

<div class="row1"></div>
<div class="row2"></div>
<div class="row3"></div>
<div class="row4"></div>

这是一个上下结构的界面,一般对于开发人员来说,这是显而易见的,几乎就是看一眼的事情,但如果要求你使用语言逻辑严谨地说出根据来,这恐怕就要好好组织一些语言了。请你先在心里想一想如何逻辑严谨地描述这个事情,然后我们继续。

显然row2和row4这两块里还有更多细节,我们可以先把眼光聚焦在row4上。

这显然是一个左右布局,如果用HTML来描述,几乎每个人都会写出这样的代码。

<div class="row4">
  <div class="col4-1"></div>
  <div class="col4-2"></div>
  <div class="col4-3"></div>
</div>

row2比row4稍微复杂一些,但也很容易得到这是一个左右布局。你可以采用类似的方法,继续拆分row2的内部细节。

人脑在拆分这些界面时,采用的是很感性的方法。但在计算机的世界里,我们必须使用严谨的逻辑描述出来,计算机才能理解这个过程。为了达到这个目的,我们就需要引入一个新的概念——

它的定义是:在给定的界面上,能够在不贯穿任何组件的前提下,将该界面切开的线,则为一条边。根据边的定义,可以得到一个推论:边只存在于水平或者垂直方向上,在任何斜线方向上是不可能找出边来的,这是因为我们把组件抽象为矩形盒子,任何斜线都会贯穿至少一个组件。

我们在拆分原图整体时,是用了水平的边;而在拆分row2/row4时,用的是垂直的边。

对边的概念做延申,还可以得到一个新的概念——。一个边可以把界面分为两部分,我们将每一部分称为一个组。

切蛋糕法

有了边和组的概念之后,我们就可以将前面我们拆分界面的方法改为逻辑严谨的指令式方法了。基于此,我们就可以进而将这些指令翻译成一行行代码,从而让电脑来替代我们完成代码自动生成的过程。

在开始之前,我们再次回顾一下这讲的目的:给定的任何界面,自动生成它的布局代码。生成的代码能正确渲染出给定的界面,这是最基本的,在这个基础上,我们还有以下这些额外要求:

  1. 人工可以在生成的代码的基础上,对代码做人工编辑,且有可能会做大范围的修改;
  2. 条件1隐含了生成的代码必须符合人类编码的思维习惯;
  3. 生成的页面布局必须具有良好的弹性效果。

1和3都比较好理解,那啥叫符合人类编码的思维习惯呢?我们在上个小节里使用的方法,就是人类在处理布局编码过程中的典型思维过程,从全局到局部,由外而内,由大到小地将给定的界面逐一拆分到组件粒度为止。我们的算法也采用类似的过程来生成代码。

首先第一步是要确定切的方向。前面给的示例是从水平开始切,那是否可以从垂直方向开始切呢?那是很有可能的,甚至存在在两个方向都可以开始的局面,比如像下面这样。

你从水平或者垂直任意一个方向上开始寻找,只要能在这个方向上找到至少一条边的,就可以在这个方向上开始切下去了。当两个方向上都可以开始切的时候,可以从可用边多的那个方向开始,根据我的经验,这样切分收敛会更快

假设边的数量是N,那么全部切完之后,可以得到N+1个分组。这样就完成了当前层级的切分了,然后就可以生成这个层级的代码了。以图3为例,垂直方向上有2条边,而水平方向上只有1条,因此我们先从垂直方向上开始切,得到左中右3个分组。此时可以生成出类似这样的代码:

<div class="col-1">??</div>
<div class="col-2">??</div>
<div class="col-3">??</div>

每列里的细节现在还不知道,这需要在处理完下一层后才能知晓。

接下来需要遍历左中右各个分组,对每个分组所属的界面,再重复这个过程。对于左侧分组,在水平方向上有2条边,因此可以切出3个子分组来,所以左侧分组的代码可以进一步细化为:

<div class="col-1">
  <div class="row-1-1">??</div>
  <div class="row-1-2">??</div>
  <div class="row-1-3">??</div>
</div>

依然有未知细节,我们可以继续递归到下一层,直至无法切分为止,完成后生成的代码如下:

<div class="col-1">
  <div class="row-1-1">
    <div class="col">
      <pie-chart></pie-chart>
      <pie-chart></pie-chart>
    </div>
    <bar-chart></bar-chart>
    <xx-chart></xx-chart>
  </div>
  <div class="row-1-2">??</div>
  <div class="row-1-3">??</div>
</div>

所生成的代码中,还剩下一些行的内部细节没有展示出来,这些细节就留着作为课后练习,由你来补充完整吧,相信你可以做到的。

到这里,你大概已经感受到了,算法执行的过程和切蛋糕的过程很像,就是把一大块界面切分为小块,继续切到单个组件为止,这也是我将其命名为切蛋糕法的原因。

给界面添加弹性

截至现在,我们完成了前面提到的3个小目标中的第1和第2个,分别是要生成符合人类思维习惯的代码,以及人工可以对生成的代码做开发迭代。接下来我们来说明如何实现有弹性的界面。

是否能够生成有弹性的界面代码,是衡量一个代码生成算法否具有实用性的重要标志,无法生成弹性的页面的算法是几乎没有价值的。

先明确,弹性还可以分为两个方向,水平和垂直方向上。即对一个组件来说,它在水平和垂直方向上可以具有相同或不同的弹性性质。在一个给定的界面上,且不说是否有实际意义,理论上说,任何组件在任何方向上,都可能具有弹性,也可能没有弹性。实际上,有无弹性的组件相互交织在一起的,是更切合实际的,就这一点,你可以自行盘点一下你所使用过的软件的界面是否确实是这样的。

作为一个算法而言,我们不能事先假设啥样的界面有弹性,啥样的界面没有弹性,比如预设大块的组件有弹性而小块组件没有,再比如预设一行上要么全有弹性要么全没有等等。一旦有了这样的假设,算法的可用范围就会大打折扣。所以,任何组件在水平或垂直方向上是否有弹性,只能由开发人员说了算,算法必须能正确生成有无弹性相互交织的组件的布局代码

我们先从最简单的情况开始。一行上只有一个组件,并且它没有弹性。而且为了更简单,我们暂时忽视垂直方向上的弹性。这个情况下,肯定是组件在画布(布局编辑器)上有多大,实际运行时就有多大了。略微复杂一点的情况是,这个组件有弹性,那么不难理解,由于这一行上只有这个组件,那么在实际运行时,这个组件将自然而然地占满这一整行。

稍微再复杂一些,现在一行上出现了2个组件,并且一个有弹性一个没有,此时的界面看起来是这样的,从现在开始,我们约定白色底的组件没有弹性,而蓝色底的有弹性。

那么在实际运行时我们希望是什么样的结果呢?很容易想到的合理结果是,浏览器会将宽度先分配给组件A,然后将剩余的宽度全部分配给组件B。注意,组件A该多大就多大,而组件B在实际运行时有可能比上图看上去更大或者更小,甚至被完全压扁,这完全取决于浏览器的实际宽度。当有多个固定尺寸的组件和一个有弹性的组件在一行上,处理方式等价于这个情况。

但是当一行上有多个弹性组件时,情况似乎会复杂一些,我们先从两个弹性组件开始。

一个比较合理的处理方式是,2个组件均分所有宽度。但是,当画布上2个组件的尺寸是这样的时候,均分就不合适了吧。

其实,更合理的处理方式是按找它们在画布上的比例来分摊尺寸,比如实际运行时,浏览器有900px的宽度,那么组件A应该是300px,组件B应该是600px。这样一来,采用弹性组件按比例分配尺寸的原则,就可以套用到一行上有任意多个弹性组件的情形。

到此,我们就可以得到处理弹性的两个原则了。

  1. 无弹性组件尺寸优先分配原则
  2. 剩余尺寸按比例分配原则

等等,界面上组件间的留白该怎么办?这其实是一个简单的问题,界面上的留白当作固定组件来分配尺寸即可。

下面我们利用这2个原则来处理一个稍微复杂点的布局,假如画布上的UI配置如下:

组件A和D尺寸固定,C和D有弹性,先不管这样的界面是否有实际意义,这里纯粹是用于帮助你理解和运用这2个规则。当浏览器的实际宽度超过4n时,各个组件的相对位置应该是这样的:

这个情况不难理解,直接套用两个原则即可解释这样的结果。

但当浏览器小于4n时,各个组件的相对尺寸就有意思了,正确的结果应该是下面这样的:

为啥呢?

首先用第二节的切蛋糕法将这4个组件分组,显然是得到左中右三列。然后再根据尺寸分配优先原则,组件A和D的优先级是最高的,因为它们没有弹性。组件B和组件C的优先级谁高谁低呢?还是一样高呢?

组件B和C的优先级当然是一样高的,但注意到组件B所在的组里有组件A。当实际尺寸小于4n时,浏览器需要优先满足组件A的尺寸需求,此时,组件A/B所在的组就得到了一个最小尺寸为 n 的buff,因此浏览器就不会继续继续压缩组件A/B所在的组的尺寸了,这就造成了组件B的最小尺寸与组件A相等的结果。反观组件C,由于它所在的组是一个完全弹性组,因此,浏览器就只能欺负组件C了,如果继续压缩宽度,最终组件C将被压扁,而组件B依然保持与A相等的宽度。

接下来说说如何实现这2个原则。

网页上控制组件的尺寸,显然首选CSS作为解决方案,切勿用JS去实现。那么,有哪个CSS技术适合呢?原则2是按比例分配,心里默念三遍后,就会有答案。

对了,是flex布局。因为:

  • flex-basis 属性可以用于控制固定尺寸的组件;
  • flex-grow 属性可以用于按比例控制弹性组件的尺寸;
  • flex 有非常良好的嵌套支持特性;
  • 几乎所有浏览器,包括IE,都对flex支持得非常好。

如果你还不了解flex布局,那可以看一看阮一峰的这篇 Flex 布局教程,10分钟就可以入门。

以我们的实现为例,再更具体地说说这2个值具体如何来计算。为了实现简单,我们约定了画布(即编辑器)上的组件都不渲染出弹性效果,即在画布上的所有组件都采用了绝对尺寸来渲染。在这个假设下,flex-basis的值就等于设计时组件的px值了,不仅如此,我们让flex-grow的值也取自组件在画布上的px值。注意flex-grow的值可以是一个很大的数字,甚至是可以支持浮点数的。

所以,代码生成器在处理画布上的组件时,都统一使用flex布局的方式来生成代码,在处理组件的弹性特性时,仅仅是有弹性就用flex-grow,无弹性就用flex-basis,就这么简单。

布局约束与解决方法

前面,我们只用了2个原则,切蛋糕法就能很好地处理掉水平方向上的任何弹性、非弹性混合的布局,但凡事没有完美,这个布局处理算法的问题在于垂直方向上。

你可能会说,垂直方向上直接应用切蛋糕法的2个原则不就行了?是的,这样做是没有问题,我们现在也是这么做的。但采用这样的实现方法生成的代码,这样的网页高度并不具有传统网页的高度由内容撑开的习惯。这句话可能有点晦涩,简答地说,就是这样的网页在垂直方向上的弹性好过头了,导致无法撑出滚动条。这样的页面用起来,会让人觉得很不习惯。

所以,可以在垂直方向上不应用这讲给出的2个原则,而是保持内容的高度优先,这样就可以生出与传统使用习惯比较相符的页面代码来了。

不过,有一类应用却恰好要求水平和垂直方向都具有完全弹性,那就是大屏类页面。这讲开头给的示意效果图,就是一个典型的大屏的例子。如果大屏类的页面是你的低代码平台的输出类型之一,那么前面的2个原则就可以同时应用到垂直方向上,用于输出大屏类页面布局。

接下来说一个小问题:如何让组件靠右(或靠下)?考虑生成一个header功能的布局,非常常见,一般header的左侧有logo,右侧有已登录的用户,比如下面这个是我正在写作用的某文档编辑器的header。

图片

请问,如何让右侧我的微信头像始终停靠在网页的右边?

前面小节里我说过,将画布上的空白当作固定尺寸的组件来处理,如果这样的话,在画布上整个header上所有的组件都是固定尺寸的了,那生成的代码就会出现这样的结果。

图片

任由浏览器宽度怎么改变,右侧的logo就是站那不动,这不是我们想要的结果。原因前面已经说过了,就是所有组件都是固定尺寸导致的,解决的办法也很简单,只要想办法让中间的空白有弹性就行了,弹性空白会吃掉浏览器多出来的任何尺寸,因此就可以将右边的图标顶在页面的右侧,达到我们的目的。

由于设计时画布上的空白真的就是空白,无法被选中,所以,我们做了一个叫做占位符的组件,这个组件渲染出来就是一片有弹性的空白,由于它是一个普通组件,因此可以按需添加到画布上,来提供弹性。

下面来说说切蛋糕法的一个真正缺陷——处理界面上的重叠组件的问题。如果只把这个算法用在D2C的话,一般可以无视这个缺陷,但如果你打算把这个算法应用到低代码平台上的话,这是一个不能忽视的问题。

考虑一下,下面画布上的组件的位置关系,两个组件出现了位置重叠,并且其中有一个弹性组件。

由于我们是利用了flex布局来渲染页面,flex布局在处理重叠内容时非常麻烦,基本无法处理得很好,更不要提有的内容还需要保持弹性了。对此我们做了简单化处理,但并未彻底解决问题,那就是直接取消所有组件的弹性特性,然后将它们都归入一组,在组内采用绝对布局来定位它们。

小结

切蛋糕法布局生成算法的输入信息,是一个平面上各种组件之间的相对位置,无需输入任何分组信息,并且这个算法生成代码的过程与我们手写代码时的思考方式基本一致,这使得它生成的代码符合人类思维的逻辑,对人类非常友好,因此这个算法非常适合用于D2C自动生成代码的场合。

UX设计稿给出的主要就是一个平面上各个组件的相对位置和尺寸,以及一些细节信息。这些细节信息对生成代码没有决定性的帮助,唯有组件的位置和尺寸,是对生成代码有决定性的帮助的。能将UX设计稿转为代码的软件很多,但是生成的代码符合人类思维习惯、对人类友好,并且能优雅地处理好页面弹性的,确实很少,而这讲给出的切蛋糕法就是其中之一。

切蛋糕法不仅可以用在D2C自动生成代码,它还可以用在低代码布局编辑器的代码生成上。低代码平台的一种常见编辑器就是给出一个平面画布,应用开发人员将组件从库里拖入到画布上,调整组件间的相对位置和各个组件的尺寸,把这些信息作为输入数据喂给切蛋糕法,算法就可以生成出对应的应用界面布局代码来了。

思考题

在前面我给出了一个利用空白占位组件的小技巧,实现了界面组件的靠右排列的效果。好,现在客户的需求变了,客户要求让搜索框保持居中显示(如下图),同时依然保持左右两侧的logo和图标靠边,请问如何使用空白占位符来达成客户想要的布局效果?

图片

欢迎动手尝试,期待你的反馈!我是陈旭,我们下一讲再见。

精选留言(3)
  • ifelse 👍(1) 💬(0)

    学习打卡

    2023-04-08

  • Lei Yang 👍(0) 💬(0)

    老师您好,如何评价百度amis 前端低代码框架,通过 JSON 配置就能生成各种页面?

    2023-07-07

  • sheeeeep 👍(0) 💬(0)

    老师你好,在第12讲中提到的“在不配置服务端 CORS 策略的前提下,巧妙地“骗”过浏览器,绕过跨域限制,做到在浏览器中可以跨域请求任何服务器数据的效果”会放在动态更新部分讲解,这个内容什么时候会有呢

    2023-05-24