CSS——理解布局算法

大家在学习CSS的时候,是不是常常有一些灵感浮现的时刻,或者是看到别人只是用简单的CSS就能做出如此炫酷的效果。其实我也有类似的经历,我那时候总是关注折写出来的CSS的属性和值有哪些情况,它们分别的效果是什么?比如说z-index:10肯定在z-index:5之上; justify-content:center就是在flex布局的时候,让元素框居中;我琢磨着如果能够将CSS的属性和值学习得越多,那么就能够更加深刻的理解这一门语言。

随着时间的推移,我对CSS的关键认知有了变化:CSS不仅仅是一系列属性的集合,还是各种相互关联的布局算法的集合;每个算法都是一个复杂的系统,且有自身的规则和隐秘机制。

了解特定属性和值的作用是不够的;还应该理解布局算法是怎样工作的?这些算法怎样使用我们提供的属性来进行布局。

您也许也遇到过这样的情况,写了以前多次写过的CSS属性,但是却得到了不是预期输出的布局效果,这样的不安会让我们觉得特别沮丧。因此CSS会让我们心理上觉得不一致和不稳定。同时又陷入深深的疑问:为什么相同的输入会产生不同的输出?

发生这种情况是因为这些属性和值工作在一个复杂的系统中,一些微妙的上下文变化改变了属性的行为方式。我们对CSS的认知模型不完整,导致产生了各种各样的惊吓。

当我开始深入理解CSS的布局算法的时候,一切变得豁然开朗。困扰多年的迷惑也云开雾散。我开始认识到CSS是一门强大语言,也开始享受编写的乐趣。

本文将用一个全新的视角来帮助理解CSS背后发生的事情;同时用这个视角来解惑那些惊人的疑团。

布局算法

什么是布局算法呢?您可能已经非常熟悉它们了,布局算法包括:

  • Flexbox
  • Positioned
  • Grid
  • Table
  • Flow

技术上它们也被成为布局模式,而不叫布局算法;我这里称呼布局算法是希望能够显得高大上一点,欢迎拍砖。

浏览器在渲染HTML页面的时候,每个元素都会使用一个主要的布局算法来计算元素的页面布局。我们可以使用特定的CSS声明来选择不同的布局算法。举个例子:position:absolute会将元素切换为Positioned布局;默认是Flow布局。

我们先看一个例子。有下面的CSS代码

.box {
  z-index: 10;
}

我们的首要工作是确定将使用哪种布局算法来渲染.box元素。根据提供的CSS,我们可以确定的是使用Flow布局算法。

Flow是网络上最通用的布局算法。它诞生于网络被视为一系列超连接文档,就像是世界上最大的档案库一样。它和微软的Word文字处理软件中使用的布局算法类似。

Flow也是非表格HTML元素的默认布局算法,除非你明确指定一种布局算法,否则就是Flow布局。

z-index属性用于控制堆叠顺序,如果它们发生重叠,哪一个显示在“顶部”。但是这里有一个事实是:在Flow布局中根本没有实现这个属性。Flow就是创建文档样式的布局,相信您没有看到过允许元素重叠的文字处理软件。

网络上有一种这样的说法:

如果不将position设置为“relative”或“absolute”之类的值,则不能使用 z-index,因为 z-index 属性取决于position属性。

以上说法不完全错,但是却有一点微妙的误解。更准确的说法是z-index属性没有在Flow布局中实现,如果想让这个属性起作用,那么需要选择一个实现了此属性的不同布局算法。

您可能觉得在此我有点小题大做,但是这个小误解会产生大困惑。举例如下:

新建一个html文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="layout.css"/>
  <style>
	  .row {
		display: flex;
		gap: 16px;
	  }
	  .raised.item {
		z-index: 2;
		background: hotpink;
	  }
  </style>
  <title>layout-algorithm</title>
</head>
<body>
	<ul class="row">
	  <li class="item"></li>
	  <li class="raised item"></li>
	  <li class="item"></li>
	</ul>
</body>
</html>

新建一个layout.css文件

.row {
  list-style-type: none;
  padding: 16px;
}
.item {
  width: 50px;
  height: 50px;
  border: 2px solid;
  border-radius: 4px;
  background: white;
}
.raised.item {
  margin-top: 8px;
  margin-right: -32px;
}

效果如图所示:

在这个例子中,我们使用Flexbox布局算法安排了3 个兄弟元素。中间的元素设置的z-index属性,可以看到能正常工作,显示在最顶端。如果将这个属性删掉,可以看到效果如下:

可以看到这个时候元素不再出现在顶层。

为什么会这样?我们没有在任何地方设置position: relative。按照前面所说,这个属性能够正常工作是因为Flexbox布局算法实现了z-index这个属性。当语言作者设计Flexbox布局算法时,他们决定实现 z-index 属性来控制堆叠顺序,就像在Positioned布局算法中一样。

这是关键的心智模式(不会觉得不安了吧)转变。CSS 属性本身是没有意义的(一定要结合具体的布局算法才有意义)。由布局算法来定义它们的作用,以及它们在计算中的工作方式。

需要明确的是,有一些 CSS 属性在所有布局算法中都是一样的。 color: red 无论如何都会产生红色文本。但是每个布局算法都可以覆盖任何属性的默认行为。许多属性没有任何默认行为。

有一个让人吃惊的例子,您通过width属性的实现方式因为不同算法而有所区别吗?看下面的例子:

新建一个width.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="width.css"/>
  <style>
	  .flex-wrapper {
		display: flex;
	  }
	  .item {
		width: 2000px;
	  }
  </style>
  <title>width-different-with-algorithm</title>
</head>
<body>
	<div class="item"></div>

	<div class="flex-wrapper">
	  <div class="item"></div>
	</div>
</body>
</html>

再新建一个width.css

.row {
  list-style-type: none;
  padding: 16px;
}
.item {
  height: 50px;
  border: 2px solid;
  border-radius: 4px;
  background: hotpink;
  margin: 16px;
}

效果如下:

第一个.item元素有一个宽度为2000px的属性,这个元素使用的是Flow布局算法来渲染,因此它实际占据了2000px的空间宽度;在Flow布局算法中,宽度是硬性规则,没有含糊。

第二个.item元素在一个flex容器中渲染,因此采用的布局算法是Flexbox;而在Flexbox布局算法中,宽度这是一个建议值,没有强制硬性规定。

Flexbox规范称宽度为推荐大小。它是元素在容器中的实际大小,而没有任何约束或者强制一定是多少。在一个足够大的容器中,这个大小可能是2000px;但是在一个比较小的容器中,元素的大小会收缩,以便适应容器的大小。

在这里,视角(不同的布局算法)非常重要。当涉及到Flexbox布局时,宽度并没有什么特别的警告。这是 Flexbox布局算法实现 width属性不同于 Flow布局算法而已。 

您可以这里理解这种机制:

我们编写的CSS属性是输入,就像传递给函数的参数一样。选择如何处理这些输入取决于布局算法。如果我们想深入理解CSS,就需要了解布局算法的工作原理;仅了解属性是远远不够的。

识别布局算法

CSS没有一个布局算法的属性,比如layout-mode;有几个属性可以调整所使用的布局算法,这让布局算法会变得非常棘手!

在某些情况下,应用于元素的 CSS 属性将选择特定的布局模式。看下面的代码

.help-widget {
  /* Uses Positioned layout, because of this declaration: */
  position: fixed;
  right: 0;
  bottom: 0;
}
.floated {
  /* Uses Float layout, because of this declaration: */
  float: left;
  margin-right: 32px;
}

在其他情况下,我们需要查看父级元素使用的CSS属性。例如:

<style>
  .row {
    display: flex;
  }
</style>
<ul class="row">
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
</ul>

当我们应用display: flex时,我们实际上并没有为.row元素使用Flexbox布局算法;相反,我们说它的子元素应该使用 Flexbox 布局来定位。

用技术话语来说,display: flex创建了一个 flex格式化上下文。所有直接子级都将应用此上下文,这意味着子元素将使用 Flexbox 布局而不是默认的 Flow 布局。

display: flex也会把一个内联元素,比如 <span>变成块级元素,所以它确实对父元素的布局有一些影响。但它不会改变使用的布局算法。

布局算法变体

一些布局算法可以分割成多个变体。

例如,当我们使用Positioned布局时,它指的是几种不同的“定位方案”:

  • Relative
  • Absolute
  • Fixed
  • Sticky

每个变体有点像它自己的迷你布局算法,尽管它们确实共享一些共同点(例如,它们都可以使用 z-index 属性)。

同样,在 Flow 布局中,元素可以是块状或内联的。稍后我们将详细讨论 Flow 布局。

算法冲突

当多个布局算法应用在一个元素上会发生什么呢?这是一个有意思的话题。看个例子

<style>
  .row {
    display: flex;
  }
  .primary.item {
    position: absolute;
  }
</style>
<ul class="row">
  <li class="item"></li>
  <li class="primary item"></li>
  <li class="item"></li>
</ul>

所有三个列表项都是Flex 容器中的子项,因此它们应该根据 Flexbox 进行定位。但是那个中间的子项目通过设置 position: absolute 选择了 Positioned 布局。

据我了解,元素将使用主布局模式呈现。这有点像特异性:某些布局模式比其他布局模式具有更高的优先级。

我不知道确切的层次结构,但Positioned布局往往胜过一切。因此,在这个例子中,中间的子项目将使用 Positioned 布局,而不是 Flexbox。

因此,Flexbox 的计算结果就好像只有两个子项目,而不是三个。就 Flexbox 算法而言,那个中间子项目不存在;它对算法完全没有影响。

一般来说,冲突通常是非常明显的/有意的。但是,如果您发现某个元素的行为方式与您期望的不同,则值得尝试确定它使用的是哪种布局算法。答案可能会让你大吃一惊!

相对定位

这里有一个难题,如果每一个元素都使用单一布局算法渲染,那么我们应该如何理解相对定义呢?

具有 position: relative 的元素使用 Positioned 布局清晰呈现。它可以使用独有的定位布局属性,如 top 或 left。然而,它也可以参与 Flexbox / Grid 布局!

这个问题比较复杂,超出了本文讨论的范畴,但是可以快速理解一下

每个元素都在特定的格式化上下文中渲染,由布局算法决定是否参与其中。通常,Positioned 布局算法会忽略此类上下文,但它会排除相对定位的例外情况。

当在 Flexbox 上下文中渲染相对定位的元素时,Positioned 布局算法将允许它参与。一旦使用该上下文确定了它的大小/位置,它就会应用 Positioned 布局内容(例如,使用 top 或 left 调整位置)。

你可以把它想成有点像组合。 Positioned 布局算法将为相对定位的元素组合 Flexbox 布局算法。

内联魔法空间

我们首先来看一个经典的令人困惑的CSS问题,看看布局算法是怎么样帮助我们理解并解决这个问题的。

新建一个inline-magic-space.html的文件

<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>inline magic space</title>
  <link rel="stylesheet" href="normalize.min.css">
  <link rel="stylesheet" href="inline-magic-space.css">
</head>
<body>
<div class="photo-wrapper">
  <img
    class="cat-photo"
    alt="A basketful of cats"
    src="cats.jpg"
  />
</div>
</body>
</html>

新建一个inline-magic-space.css的文件

.photo-wrapper {
  border: 1px solid;
}

.cat-photo {
  width: 250px;
  max-width: 100%;
}

效果如下所示:

为什么图像下面有一些额外的空白呢?

如果试着调试一下,你就会发现,图像的宽度和高度都是250px,但是外面的包装器元素.photo-wrapper的高度实际上会多几个像素,为什么会这样呢?

如果您熟悉盒模型,您就会知道可以使用填充、边框和边距来分隔元素。您可能认为图像上有一些边距,或者容器上有一些填充?

在这个例子下,这些属性均没有作用。这就是为什么多年来,我一直私下将其称为“内联魔法空间”。它不是由通常的罪魁祸首引起的。

要了解这里发生了什么,我们必须更深入地研究 Flow 布局。

Flow布局算法

如前文所述,Flow布局是为文档设计的,类似于文字处理软件。

文档具有以下特点:

  • 单个字符组合成单词和句子。当没有足够的水平空间时,这些元素内联、并排和换行。
  • 段落被视为块,如标题或图像。块将垂直堆叠,一个在另一个之上,从上到下。

Flow布局就是基于这种结构。单个元素可以排列为行内元素(并排,如段落中的单词),或排列为块元素(从上到下堆叠的大块砖):

大多数 HTML 元素都带有合理的默认值。 <p> 和 <h1> 被认为是块级元素,而 <span> 和 <strong> 被认为是内联元素。

行内元素用于段落中间,而不是布局的一部分。例如,也许我们想在句子中间添加一个小图标。

为了确保行内元素不会对周围文本的易读性产生负面影响,添加了一些额外的垂直空间。

那么,回到我们的谜团:为什么我们的图像有一些额外的空间像素?因为图片默认是行内元素!

Flow 布局算法将此图像视为段落中的字符,并在下方添加一点空间以确保它不会令人不舒服地靠近(理论上的)下一行文本中的字符。

默认情况下,内联元素是“基线”对齐的。这意味着图像的底部将与文本所在的不可见水平线对齐。这就是图像下方有一些空间的原因——该空间用于下行,例如字母 j 和 p。

所以它不是边距、填充或边框……它是 Flow 布局应用于内联元素的固有空间位。

解决方案

有多种方法可以解决此问题。也许最简单的方法是在 Flow 布局中将此图像视为一个块:

.cat-photo {
    display: block;
}

或者,因为这种行为是 Flow 布局所独有的,我们可以转向不同的布局算法:

/*
  We flip its *parent* to Flex, so
  that the child will use Flexbox
  instead of Flow:
*/
.photo-wrapper {
    display: flex;
}

最后,我们还可以通过使用line-height将额外空间缩小为 0 来解决这个问题:

.photo-wrapper {
  line-height: 0;
}

此解决方案通过将其设置为 0 来删除所有额外的行间距。这会使多行文本完全不可读,但由于此容器不包含文本,因此这不是问题。

我建议使用前面两种解决方案中的一种。提出这个纯粹是因为它很有趣(并且因为它证明了问题是由于行间距造成的!)。

行高和可访问性

当我们谈论行高时:您知道“无样式”的 HTML 实际上被认为是不可访问的,因为行靠得太近了吗?当行间距不够大时,患有阅读障碍的人很难解析文本。

大多数浏览器的默认行高为 1.1 到 1.2,但根据 WCAG 指南,我们应该将正文的行高至少设置为 1.5。

好啦。本文主要为了给大家建立起一种对CSS的稳定和一致感,每次遇到输出和预期不一致的时候,思考一下当前的元素采用的是什么布局算法,然后再结合具体的属性进行分析,写得越多,积累得越多,并且思考得越多,您就会对CSS更加自信。

 

版权声明:著作权归作者所有。

thumb_up 0 | star_outline 0 | textsms 0