一个可以让 CSS(层叠样式表)更直观的思维模式转变
几年前,我好像突然对 CSS 开了窍。
之前我学习 CSS 时,一直把注意力放在属性和值上(比如 z-index:10
或 justify-content: center
),认为如果可以理解每个属性的作用,就一定可以理解整个编程语言。
现在我发现 CSS 不仅是属性的集合,还是一个由紧密相连的布局算法组成的整体。每个算法都是一个复杂的系统,有专属的规则和秘密机制。
只知道每个属性的功能是不够的,还得知道布局算法的运行方式,以及布局算法如何使用我们提供的属性。
你是否有过这样的沮丧经历:你在项目中写了之前用过很多次的 CSS 块,但得到的结果却跟预想的完全不一样,从此之后你觉得编程语言非常不靠谱。
为什么输入相同的 CSS 会得到不同的输出呢?
因为这些属性被应用在一个复杂的系统上,轻微的上下文变动就会影响属性的行为。另外,心智模型不完整也是导致意外情况出现的原因之一。
研究了布局算法之后,困扰我多年的一些疑问迎刃而解,我终于意识到了 CSS 功能之强大,开始真正喜欢上使用它。
本文我们会从一个新的角度来了解 CSS 的运行方式,解决大家经常遇到的问题。
布局算法
什么是 “布局算法”?你可能对下列布局算法很熟悉:
-
弹性盒子布局(Flexbox)
-
定位布局(Positioned,例如:
position: absolute
) -
网格布局(Grid)
-
表格布局(Table)
-
流式布局(Flow)
(严格来说,它们是布局模式,不是布局算法。但我觉得“布局算法”这个标签更有用)。
HTML 在浏览器中渲染时,每个元素都主要用一种布局算法来计算布局。我们可以根据具体的 CSS 声明来选择不同的布局算法。例如,应用 position:absolute
,就切换为定位布局。
我们来看一个例子,假设我有下面的 CSS:
.box {
z-index: 10;
}
我们的首要任务是搞清楚将会用哪种布局算法来渲染 .box
元素。依据给定的 CSS,将使用流式布局渲染 .box 元素。
流式布局是一种传统的布局算法。流式布局出现的年代里,网络还被大家当做一个巨大的超链接文件集,或是全球最大的资料库。流式布局跟 Microsoft Word 等文字处理软件使用的布局算法类似。
除非另有明确要求,非表格 HTML 元素默认使用流式布局算法。
z-index
特性可以确定堆叠顺序,如果发生元素重叠,z-index 可以确定哪个元素在“上面 ”。但是,z-index 特性无法在流式布局使用 。流式布局主要用于创建文档格式类布局,而目前的文字处理软件的元素都不可以重叠。
如果几年前你问我这个问题,我会说:
z-index
特性只能用于 position
布局,只有把 position
设置为“相对定位”或“绝对定位”才能使用 z-index。
这个回答也没错,只是有些歧义 。更准确的说法是,Z-index
特性不用于流式布局算法,如果我们想让这个特性发挥作用,需要用别的布局算法。
你可能觉得我在咬文嚼字,但是小歧义有可能会产生大影响。例如:
以上为 demo 截图,详见原文
在上面的 demo 中,我们用弹性盒子布局算法布局了 3 个元素。
中间的元素设置了 z-index
特性,确实有效 ,但移除这个元素,该元素就被置于其他元素之后。
为什么呢?因为我们没有设置 position: relative
!
上文有效是因为弹性盒子算法执行了 z-index
特性。开发人员在设计弹性盒子算法时,把 z-index
特性和弹性盒子布局算法结合起来,用来控制层叠顺序,这与 z-index
特性在定位布局中的功能一样。
这是关键的心智模式转变 。CSS 特性本身没有意义,要靠布局算法来定义其功能和使用方法。
注意:
-
部分 CSS 特性在所有布局算法中都可以使用,比如:在任一布局算法中使用
color: red
特性都会产生红色的文本。 -
所有布局算法都可以覆盖特性的默认行为。
-
许多特性没有默认行为。
另外,宽度特性会随着布局算法的变化而变化 。
下面的 demo 可以证明:
以上为 demo 截图,详见原文
我们的 .item
元素有一个单一的 CSS 特性: width: 2000px
。
在第一个例子中,我们用流式布局渲染.item
,这个布局会消耗 2000px 宽度。流式布局的宽度是硬性规定,会占用 2000px 的空间。
在第二个例子中,我们用弹性盒子布局渲染.item
,弹性盒子算法只有建议宽度。
弹性盒子规范称建议宽度为假设尺寸 。意思是元素在理想情况下(即没有任何约束或力量作用于它的情况下)的尺寸。这个项目的理想宽度应该是 2000px ,但该项目被放置在一个较窄的容器里,需要缩小尺寸来适应容器。
再强调一下框架的重要性。并不是说弹性盒子格局的 width
需要特别注意,只是弹性盒子格局算法实现 width
属性的方式与流式格局算法不同。*
我们编写的特性是输入 ,类似于向函数传递的参数。而如何处理这些输入,由布局算法来选择。 如果你想要了解 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
元素使用弹性盒子布局算法。实际上,它的子元素应使用弹性盒子布局来定位。
严格来说,display: flex
创建了一个弹性格式化上下文。所有的直接子元素都在这个上下文中,意味着所有的直接子元素都会使用弹性盒子布局,不使用默认的流式布局。
display: flex
也会把行内元素(比如<span>
)变成块级元素,所以它确实会对父元素的布局产生影响,但不会改变当前的布局算法。
布局算法的变体
一些布局算法被分割成多个变体。
例如,我们使用定位布局时,可以使用下列几个不同的“定位方案”:
-
相对定位
-
绝对定位
-
固定定位
-
粘性定位
尽管这些变体确实有共同点(例如,都可以使用 z-index
属性),每个变体都有点像一个迷你布局算法。
同样地,流式布局中的元素可以是块状的,也可以是行内的。下文会细说流式布局~
矛盾
如果把多种布局算法应用于一个元素,会出现什么情况?
比如说:
<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>
这三个列表项都是弹性容器中的子元素,理应按照弹性盒子布局算法来定位。但中间的子元素设置了 position: absolute
,选择了定位布局。
我对此的理解是,每个元素主要使用一种布局模式进行渲染,有点像“特异性”:有的布局模式比其他布局模式享有更高的优先权。
我不清楚确切的层次结构,但定位布局往往享有最大的优先权。因此,这个例子中的中间子元素使用定位布局,而不是弹性盒子布局。
因此,弹性盒子布局的计算就只包括两个子元素。对弹性盒子布局算法来说,不存在中间元素。这对算法没有任何影响。
一般来说,冲突都会很明显,而且是有意为之。但是,如果你发现一个元素的行为与你所设想的不一样,可以试着查看一下这个元素使用的布局算法是啥,有可能会有意外的发现。
相对定位
有一个难题是,如果每个元素都使用单一的布局算法来渲染,如何解释相对定位?
使用
Position: relative
的元素显然使用的是定位布局。这个元素可以只使用定位布局特性,如top
或left
,也可以使用弹性盒子/网格布局!我们跑题到越来越复杂的问题啦。我提供一个简单的解释,供大家参考:
每个元素都在一个特定的格式化上下文 中呈现,由布局算法来决定是否应用这个上下文。通常情况下,“定位”布局算法会忽略这些上下文,但相对定位是一个例外。
当一个相对定位的元素在弹性盒子上下文中渲染时,定位布局算法会允许该元素应用该上下文。一旦该元素使用这个上下文建立尺寸/位置,它就会应用定位布局的设置(例如,用
top
或left
调整位置)。这与构图 类似。定位布局算法为相对定位的元素构成了弹性布局算法。
行内空间
我们来看一个典型的“令人困惑的 CSS”问题,看布局算法如何帮我们解决这个问题。
下面是一张内容为“一篮子猫”的图片:
大家可能会问:为什么图片下方有留白?
如果你用开发工具打开,就会发现这个图片有像素差异:
图片的高度是 250px,容器的高度却有 258.5px!
如果大家熟悉盒子模型,就一定知道元素可以使用间隙属性(padding)、边框属性(border )和 边距属性(margin )来进行间隔。大家可能会猜想:可能因为图片有边距属性,或容器有间隙属性?
实际上,与这些特性无关 。这种情况并不是常见问题导致的,我一直把这种问题称作 “魔幻的行内空间”。
要理解这个问题,我们必须对流式布局有更深入的了解。
流式布局
如上所述,流式布局是为文件设计的,就像一个文字处理软件。
文件结构如下:
- 单个字符组合成单词和句子。这些元素排成一行,互相紧邻,如果单行的横向空间不足,会自动换行。
- 段落被视为块 ,与标题或图像一样。块呈垂直堆叠状态,一个块置于另一个块之上,从上到下。
流式布局以这个结构为基础,把单个元素处理为行内元素(并排,就像段落中的单词) 或块级元素(由上往下堆积的大块)。
语言不同,方向不同
在这个例子中,我们是以英语为例,英语由左到右横向排列的语言,但不是所有语言都是这样排列的。
有些语言(如阿拉伯语和希伯来语)是由右到左的横向书写。另外,汉文化圈内的语言(如汉语、日语和韩语)的传统熟悉方式是是由上至下垂直书写。
现在,CSS 的编写都会考虑到这些差异性。例如,我们可以写
margin-inline-start
来实现英语中的左侧边距和阿拉伯语中的右侧边距。
大多数 HTML 元素都有合理默认值。 默认<p>
和 <h1>
为块级元素,<span>
和 <strong>
为行内元素。
行内元素用于段落内,不属于布局。例如,可以在句中添加小图标。
为确保行内元素不会影响周围文本的可读性,会添加额外的垂直空间。
那么,回到上文我们的疑问:为什么那个图片有多余的像素空间?因为那个图片被默认为行内元素!
流式布局算法把这个图片当作段落中的一个字符,并在图片下方添加了垂直空间,避免图片过于靠近(理论上)下一行的文本字符。
行内元素默认依据“基线”对齐。这意味着图片底部与文本的隐形水平线对齐。这就是图片下方有多余空间的原因——这些空间是为下划线准备的,比如字母 j
和 p
。
因此,不是间隙属性、边框属性或边距属性,是流式布局应用于行内元素产生的行内空间。
怎么解决这个问题?
有很多方法可以解决这个问题,最简单的也许是在流式布局中把这个图片当作一个块:
行内空间对我影响很大,所以我把这个修复方法纳入了我的自定义 CSS Reset 中。
另外,只有使用流式布局才会出现这种情况,所以我们可以使用其他布局算法:
<style>
/*
We flip its *parent* to Flex, so
that the child will use Flexbox
instead of Flow:
*/
.photo-wrapper {
display: flex;
}
</style>
<div class="photo-wrapper">
<img
class="cat-photo"
alt="A basketful of cats"
src="/images/cats.jpg"
/>
</div>
还可以使用 line-height
将多余空间缩小到 0 来解决这个问题:
这个方案把多余空间设置为 0 来删除留白。这样做可能会导致多行文本无法阅读,但例子中这个容器不包含文本,这个方案是可行的。
我个人更建议使用前两个解决方案。我提出第三个方案纯粹是觉得好玩(也因为第三个方案可以证明这个照片的问题是行距造成的!)。
行高和可访问性
说到行高,大家应该知道,“没有样式”的 HTML 是不可访问的,因为行距太近。如果行距不够,阅读障碍者就无法阅读文本。
大部分浏览器的默认行高为 1.1 至 1.2,但根据 WCAG 指南,正文的行高至少得设置为 1.5。
欢迎大家查看我的自定义 CSS 重置,了解我对这个问题的解决方案!
建立直觉
重点来啦:如果你只关注具体的 CSS 特性的功能,你就不会理解为什么会出现这个空间。 MDN 的页面中并没有关于 display
或 line-height
的解释。
如上所述,“魔法行内空间”其实根本不是什么魔法,只是流式布局算法的一条规则的副产品,即行内元素应受到 line-height
的影响。但我这么多年来一直都觉得它很神奇,其实只是因为我的心智模型不完整。
CSS 有很多布局算法,它们都有各自的特性和隐藏机制,特性只是 CSS 的冰山一角。我们需要知道那些真正重要的概念,如堆叠上下文、包含块、级联起源等。
网上的许多 CSS 教学也非常浅显。经常有人写文章或发推,分享好用的 CSS 片段,但没人能说清楚为什么那样做有效果,也没有人讲解布局算法是怎么使用 CSS 的。*
CSS 是一种的难以捉摸的调试语言,没有错误提示,也没有 debugger
或 console.log
,只能凭直觉。如果没有真正理解 CSS 就开始使用 CSS 片段,早晚会被布局算法的隐藏特性带进坑里。
我在几年前逐渐有了一些 CSS 直觉。每当遇到问题时,我会全身心地沉浸其中,深入研究 MDN 文档和 CSSWG 规范,修补代码,直到找到问题的根源。
花费这番功夫绝对值得,只是实在是太耗时了。
为了帮助开发人员节省宝贵的时间,我做了一个全面的在线课程,名为《面向 JavaScript 开发者人员的 CSS 课程》。
我会在这个课程里讲解 CSS 的运行逻辑。另外,课程的重点是为大家提供一个强大的心智模型,它可以帮大家逐步建立 CSS 直觉。我不敢保证大家再也不会再遇到 CSS 问题,但这个课程可以帮大家建立一个解决问题的工具包。
目前已有 9500 多名开发人员参加了该课程,分别来自脸书、谷歌、微软、 网飞等公司。
原文作者:Joshua Comeau
原文链接:Understanding Layout Algorithms