前言
Flex想必大家都很熟悉,也是大家平时在进行页面布局的首选方案。(反正我是!)。不知道大家平时在遇到Flex布局属性问题时,是如何查阅并解决的。反正,我每次记不住哪些属性或者对哪些属性的用法忘记时。我总是求助于阮一峰老师写的Flex 布局教程:语法篇[1]。
其实,对于CSS来讲,大家都抱着一种「死记硬背」的东西来对待它。久而久之,就会出现上述我说的问题,一个属性或者一个使用案例,需要去指定的网站去查询。这算是好的呢,有些同学没有自己的知识体系或者收藏资料。 每次遇到问题,都是baidu/google一下,然后CV大发一通。
其实,我们应该把将 CSS 视为一组布局模式。每种布局模式都是一个可以实现或重新定义每个 CSS 属性的「算法」。我们使用 CSS 声明(键/值对)提供算法,算法决定如何使用它们。
换句话说,我们编写的 CSS 是这些算法的输入,就像传递给函数的参数一样。如果我们想真正熟悉 CSS,仅仅学习属性是不够的;我们必须学习算法如何使用这些属性。
只有,我们在对一些布局模式有了一定的掌握之后,我们才会在遇到类似的问题,游刃有余的处理问题。或者说像调用函数一样,输入特定的参数,得到特定的结果。
所以,今天我们来换一种对Flex的思考角度,对它来一次深度解析。
还有一点,需要说明,下文中不会设计到特有属性的介绍,并且还需要大家对Flex布局有一点的知识储备。
比方说,下图中标注的一些概念下文中就不会过多介绍了。推荐大家先把阮老师的那个文章通读几遍,对Flex有一个大体的了解在阅读下文。
图片
好了,天不早了,干点正事哇。
我们能所学到的知识点
前置知识点
Flexbox 是个啥?
Flex Direction
对齐(Alignment)
假设大小(Hypothetical size)
增长(Grow)和萎缩(Shrink)
最小尺寸的陷阱
间距
包裹
1. 前置知识点
「前置知识点」,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。「如果大家对这些概念熟悉,可以直接忽略」同时,由于阅读我文章的群体有很多,所以有些知识点可能「我视之若珍宝,尔视只如草芥,弃之如敝履」。以下知识点,请「酌情使用」。
CSS 布局算法
CSS 有不同的模式,确定它如何在页面上布局元素。这些模式通常被称为布局算法或布局模式。
在 CSS 中有七种布局模式,下图是MDN_CSS_Layout_Mode[2]的描述
图片
其中Multi-column layout估计大家没咋接触过,剩余的或多或少在我们平时开发中都有接触过。
图片
其中四种被使用最多。流动、定位、flex和grid。
流动布局(Flow Layout)
默认情况下,CSS 使用所谓的流动布局算法(也称Normal flow)。流动将页面上的每个元素都视为属于文本文档。
块级元素以垂直方式在页面上重叠显示。它们会尽量占用尽可能多的水平空间,同时尽量减少垂直空间的占用。
内联元素在水平方向上像段落中的文本一样显示在一起。它们通常具有固定的宽度和高度,这就是为什么许多其他我们可能想要使用的属性在这些元素上不起作用的原因。我们可以通过将它们的显示属性更改为inline-block来更改此行为。
图片
定位布局
如果在元素上使用 position 属性,我们现在正在要求 CSS 根据定位布局算法显示该元素。在此布局模式中,我们可以请求几种不同类型的行为:
- 静态(Static)
- 相对(Relative)
- 绝对(Absolute)
- 固定(Fixed)
- 粘性(Sticky)
绝对定位元素往往因为在其他地方无法正常工作而被认为是一种hacky的解决方案。
还有一点需要注意,根据我们使用的值的不同,我们可能需要「考虑元素的父级」。例如,在绝对定位元素中,该元素相对于其最近的定位布局祖先定位。这意味着 CSS 将查找 HTML 树并找到最近的一个祖先,「该祖先也使用了这些值之一」。如果找不到,则绝对定位元素将相对于视口定位。
图片
弹性盒布局
当 display 属性设置为 flex 时,元素将根据弹性盒布局算法布置其子元素。
而它就是我们今天要讲的重点,下文中有更多的介绍。
如果想了解更多的Flex的细节,可以参考w3c_flexbox[3]。
网格布局
网格与弹性盒类似,只要在元素上使用了 display: grid,就会开始使用网格布局算法。此布局算法将根据网格布局算法显示所有子元素。
Grid 和 Flexbox 的区别在于,Grid 适用于布局具有列和行的二维内容,而 Flexbox 适用于布局具有「一维内容」,即单个列或行。
我们后面也会有针对Grid的文章,预估在 12 月份或者明年 1 月份。
替换元素
在 CSS 中,替换元素(Replaced Element)是指一个由浏览器根据元素的标签和属性创建的、在渲染时展示的元素,而「不是由文档中的内容决定其显示的元素」。这些元素通常是具有外部资源(如图像或嵌入式框架)的元素,其内容由浏览器根据其属性和上下文动态生成。
以下是一些常见的替换元素:
「<img> 元素:」通过src属性引用外部图像。
<img src="image.jpg" alt="Description" />
「<audio> 和 <video> 元素:」通过src属性引用外部音频或视频文件。
<audio controls>
<source src="audio.mp3" type="audio/mp3" />
</audio>
<video width="320" height="240" controls>
<source src="movie.mp4" type="video/mp4" />
</video>
「<iframe> 元素:」通过src属性引用外部网页或嵌入式内容。
<iframe src="https://example.com"></iframe>
「<object> 元素:」用于嵌入外部资源,如 Flash 动画。
<object data="flash.swf" type="application/x-shockwave-flash">
<!-- fallback content or alternate content -->
</object>
「<canvas> 元素:」通过 JavaScript 绘制图形。
<canvas width="200" height="200"></canvas>
替换元素与非替换元素的主要区别在于,替换元素的渲染不依赖于文档的其他部分。它们的外观和尺寸通常由其属性和外部资源决定。替换元素具有一定的固有尺寸,不受文本或子元素的影响。
在 CSS 中,替换元素还可以通过 object-fit 和 object-position 这样的属性进行进一步控制,以指定元素的替换内容的显示方式。例如:
img {
object-fit: cover; /* 图片按比例缩放并覆盖整个容器 */
object-position: center; /* 图片在容器中居中显示 */
}
2. Flexbox 是个啥?
CSS 由许多不同的布局算法组成,官方称之为布局模式。「每种布局模式都是 CSS 中的一种小型子语言」。默认布局模式是流式布局,但我们可以通过更改父容器上的display属性来选择使用Flexbox:
display:block
display:flex
当我们将 display 设置为 flex 时,我们创建了一个flex格式化上下文。这意味着,默认情况下,「所有子元素将根据 Flexbox 布局算法定位」。
每种布局算法都是为解决特定问题而设计的。默认的Flow布局旨在创建数字文档;它本质上是Microsoft Word的布局算法。「标题和段落以块的形式垂直堆叠,而文本、链接和图像等元素则不显眼地位于这些块内部」。
Flexbox专注于在行或列中排列一组项目,并提供对这些项目的分布和对齐具有极大控制权。正如其名称所示,Flexbox关注的是灵活性。我们可以控制项目是增长还是收缩,额外空间如何分配等。
3. Flex Direction
如前所述,Flexbox的关键在于「控制在行或列中元素的分布」。默认情况下,项目将在「一行中侧边堆叠」,但我们可以通过使用flex-direction属性切换到列:
flex-direction:row
flex-direction:column
使用flex-direction: row时,「主轴水平运行,从左到右」。当我们切换到flex-direction: column时,「主轴垂直运行,从上到下」。
在Flexbox中,一切都「基于主轴」。算法不关心垂直/水平,甚至不关心行/列。所有规则都围绕这个主轴以及垂直运行的交叉轴结构。
我们可以轻松切换水平布局到垂直布局。所有规则都会「自动适应」。这个特性是 Flexbox 布局模式独有的。
子元素将「默认」根据以下两个规则定位:
- 主轴(Primary Axis):子元素将「紧密」排列在容器的「起始位置」。
- 交叉轴(Cross Axis):子元素将「伸展」以「填充整个容器」。
图片
在Flexbox中,我们决定主轴是水平运行还是垂直运行。这是「所有 Flexbox 计算的基准」。
4. 对齐(Alignment)
我们可以使用justify-content属性来改变「子元素沿主轴」的分布方式:
图片
图片
图片
图片
图片
由于主轴是row和column的情况很类似,下文中我们都按主轴为row来讲解
当涉及到主轴时,我们通常不考虑对齐单个子元素。相反,重点是关于整个组的分布。
我们可以将所有项目紧密堆叠在特定位置(使用flex-start、center和flex-end),或者我们可以将它们分开(使用space-between、space-around和space-evenly)。
对于交叉轴,情况有些不同。我们使用align-items属性:
图片
图片
图片
图片
图片
在align-items中,有一些与justify-content相同的选项,但并「没有完全的重叠」。
图片
为什么它们不共享相同的选项呢?我们将很快揭开这个谜团,但首先,我需要分享另一个对齐属性:align-self。
与justify-content和align-items不同,align-self应用于子元素,而不是容器。它允许我们沿着交叉轴改变特定子元素的对齐方式:
图片
图片
图片
图片
图片
align-self具有与align-items完全相同的值。实际上,它们改变的是完全相同的内容。
align-items是一种语法糖,是一种方便的简写,可以「一次性自动设置所有子元素的对齐方式」。
Content VS items
在 Flexbox 中,项目沿着主轴分布。「默认情况下,它们很好地排列在一起,侧边相邻」。我可以画一条直线,将所有子元素串起来,就像烤肉一样:
图片
然而,交叉轴是不同的。「一条垂直的直线只会与其中一个子元素相交」。
这更像是垂直方向用牙签串的烤肠,而不是烤肉串:
图片
这里有一个显著的区别。对于烤肠而言,「每个项目都可以沿着它的棍子移动,而不会干扰其他项目」:
图片
相比之下,通过我们的主轴串联每个兄弟元素,一个单独的项目如果要移动位置,那势必会影响周围兄弟元素的。
图片
这是主轴和交叉轴之间的基本区别。当我们讨论交叉轴上的对齐时,每个项目都可以随心所欲。然而,在主轴上,我们「只能考虑如何分配整个组」。
针对上面的内容,我们可以给出一个正确的定义:
- justify — 沿「主轴定位」某物。
- align — 沿「交叉轴定位」某物。
- content — 「一组」可以被分配的“东西”。
- items — 可以「单独定位」的单个项目。
因此:我们有justify-content来控制沿主轴分配整个组,我们有align-items来沿交叉轴单独定位每个项目。这是我们用来管理 Flexbox 布局的两个主要属性。
当涉及到主轴时,我们必须将项目视为一个组,作为可以分配的内容。
5. 假设大小(Hypothetical size)
假设我有以下的 CSS:
.item {
width: 2000px;
}
我们第一直觉就是「我们将得到一个宽度为 2000 像素的项目」。其实这句话是不对的!
让我们用一个例子来说明。
<style>
.flex-wrapper {
display: flex;
}
.item {
width: 2000px;
}
</style>
<div class="item"></div>
<div class="flex-wrapper">
<div class="item"></div>
</div>
结果缺不一样。
图片
两个项目都应用了完全相同的 CSS。它们都有width: 2000px。然而,第一个项目比第二个项目宽得多!
差异在于「布局模式」。第一个项目是使用流式布局(flow)渲染的,在流式布局中,width是一个「硬性约束」。当我们设置width: 2000px时,我们肯定能到一个宽度为 2000 像素的元素,即使它已经超过当前视口的宽度。
然而,在 Flexbox 中,width属性的实现方式不同。这「更像是一个建议而不是硬性约束」。
规范对此有一个名字:「假设大小」(Hypothetical size)。
在这种情况下,限制因素是父元素没有足够的空间容纳一个宽度为 2000px 的子元素。因此,子元素的大小被缩小,以「适应空间」。
这是 Flexbox 哲学的核心部分。「事物是流动和灵活的,可以根据世界的限制进行调整」。
6. 增长(Grow)和萎缩(Shrink)
要真正了解 Flexbox 的流动性,我们需要讨论三个属性:flex-grow、flex-shrink和flex-basis。
flex-basis
在 Flex行中,flex-basis的作用与width相同。在 Flex 列中,flex-basis的作用与height相同。
「Flexbox 中的一切都与主/交叉轴有关」。例如,justify-content将沿主轴分布子元素,无论主轴是水平还是垂直,它的工作方式都完全相同。
然而,width和height不遵循此规则!width「始终会影响水平尺寸」。当我们将flex-direction从row切换到column时,它不会突然变成height。
因此,Flexbox 创建了一个通用的“大小”属性,称为flex-basis。它就像width或height,但与其他所有属性一样,「与主轴相关联」。它允许我们设置元素在主轴方向上的假设大小,无论这是水平还是垂直。
下图集中,每个子元素都被赋予了flex-basis: 50px,但可以调整第一个子元素的flex-basis。
图片
图片
图片
就像我们在width中看到的那样,flex-basis更像「是一个建议而不是一个硬性约束」。在某个时候,所有元素都没有足够的空间来保持它们被分配的大小,因此「它们必须妥协,以避免溢出」。
一般来说,在 Flex 行中,我们可以互换使用width和flex-basis,但也有一些例外情况。例如,width属性对替换元素(如图像)的影响与flex-basis不同。此外,width可以将项目减小到其最小尺寸以下,而flex-basis则不能。
flex-grow
默认情况下,Flex 上下文中的元素将缩小到它们在主轴上的「最小舒适尺寸」。这通常「会创建额外的空间」。
我们可以使用flex-grow属性指定如何使用该空间:
图片
图片
flex-grow的「默认值是 0」,这意味着增长是可选的。如果我们希望「子元素吞并容器中的任何额外空间」,我们需要明确告诉它。
如果多个子元素设置了flex-grow怎么办?在这种情况下,「额外的空间将根据它们的flex-grow值成比例地分配给子元素」。
图片
图片
图片
图片
当单个子元素被赋予正的flex-grow值时,它将「吞并所有额外的空间」。在这种情况下,数字是无关紧要的:1 和 1000 具有相同的效果。
flex-shrink
在我们迄今为止看到的大多数示例中,我们有额外的空间可以使用。如果我们的子元素太大而父容器无法容纳怎么办?
图片
图片
图片
<<< 左右滑动见更多 >>>
两个项目都会收缩,但它们会「按比例收缩」。第一个子元素始终是第二个子元素宽度的 2 倍。
flex-basis和width设置了元素的假设大小。Flexbox算法可能会「将元素收缩到低于这个期望大小」,但「默认情况下,它们将始终按比例缩放,保持两个元素之间的比例」。
如果我们不希望元素按比例缩小,可以使用flex-shrink属性。
图片
图片
图片
图片
图片
图片
现在我们有两个子元素,每个都有一个假设大小为 250px。容器至少需要 500px 宽度,以便将这些子元素以其假设大小容纳其中。
假设我们将容器缩小到 400px。嗯,我们不能把 500px 的内容塞进一个 400px 的袋子里!我们有 100px 的亏空。为了使它们适应,我们的元素将需要放弃总共 100px。
flex-shrink属性让我们决定如何处理这个亏空。
与flex-grow类似,它是一个比例。「默认情况下,两个子元素的flex-shrink都是 1,因此每个子元素消化亏空的一半」。它们各自放弃 50px,它们的实际大小从 250px 缩小到 200px。
现在,假设我们将第一个子元素提高到flex-shrink: 3:
图片
我们总的亏空是 100px。通常,每个子元素将支付 1/2,但由于我们已经调整了flex-shrink,第一个元素最终支付了 3/4(75px),第二个元素支付了 1/4(25px)。
「绝对值并不重要,一切都取决于比例」。如果两个子元素都具有flex-shrink: 1,每个子元素将支付总亏空的 1/2。如果两个子元素都增加到flex-shrink: 1000,每个子元素将支付总亏空的 1000/2000。无论如何,最终效果都是相同的。
对flex-shrink:我们可以将其视为flex-grow的“反面”。它们是同一硬币的两面:
- flex-grow 控制当项目小于其容器时额外空间的「分配方式」。
- flex-shrink 控制项目大于其容器时空间的「移除方式」。
这意味着这两个属性中只能有一个生效。如果有额外的空间,flex-shrink没有影响,因为项目不需要缩小。如果子元素太大而无法容纳,flex-grow没有影响,因为没有额外的空间可分配。
防止缩小
有时,我们不希望 Flex 子元素缩小。
让我们看一个例子:
图片
当容器变窄时,我们的两个圆形被挤变形了。如果我们希望它们保持圆形怎么办?
我们可以通过设置flex-shrink: 0来实现:
图片
图片
图片
当我们将flex-shrink设置为 0 时,实质上我们「完全退出了缩小过程」。Flexbox 算法将flex-basis(或width)视为硬最小限制。
7. 最小尺寸的陷阱
假设我们正在构建一个搜索表单:
图片
当容器缩小到一定程度以下时,内容溢出!
「根本原因是flex-shrink 的默认值是 1」,我们在示例中设置了该属性,按道理输入框应该能够缩小到它需要的程度!但是却事与愿违。
原因是:除了假设大小之外,Flexbox 算法还关心另一个重要的大小:「最小大小」。
Flexbox算法拒绝将子元素缩小到其最小大小以下。无论我们如何增加flex-shrink,内容将溢出而不是继续缩小!
文本输入框的默认最小大小为 170px-200px(在不同的浏览器之间有所变化)。
在其他情况下,限制因素可能是元素的内容。
图片
图片
图片
对于包含文本的元素,最小宽度是最长不可断开的字符串的长度。
好消息是:我们可以「使用min-width属性重新定义最小大小」。
图片
通过直接在 Flex 子元素上设置min-width: 0px,我们告诉 Flexbox 算法覆盖内置的最小宽度。因为我们将其设置为 0px,所以元素可以缩小到必要的程度。
8. 间距
gap允许我们在每个 Flex 子元素之间创建空间。
这对于诸如导航标题之类的东西非常有用:
图片
自动边距
margin属性用于在特定元素周围添加空间。在某些布局模式中,如 Flow 和Positioned(前面都有过介绍),它甚至可以用于通过margin: auto将元素居中。
在 Flexbox 中,自动边距变得更加有趣:
图片
图片
图片
「自动边距将吞噬额外的空间,并将其应用于元素的边距」。它使我们能够精确控制在哪里分配额外的空间。
一个常见的页眉布局特点是在一侧放置标志,而在另一侧放置一些导航链接。
<style>
ul {
display: flex;
gap: 12px;
}
li.logo {
margin-right: auto;
}
</style>
<nav>
<ul>
<li class="logo">
<a href="/"> 首页 </a>
</li>
<li>
<a href=""> 语言 </a>
</li>
<li>
<a href=""> 个人中心 </a>
</li>
</ul>
</nav>
ul {
list-style-type: none;
}
ul a {
text-decoration: none;
}
图片
列表中的第一项通过给它设置margin-right: auto,我们「聚集了所有额外的空间,并强制将其放在第一项和第二项之间」。
使用浏览器devtool来查看元素信息。
图片
9. 包裹
到目前为止,我们的所有项目都是并排或纵列的。flex-wrap属性允许我们改变这一点。
如果容器宽度不能包含子元素的话,子元素会被隐藏。
图片
我们可以通过设置flex-wrap:wrap来让子元素自动换行。
图片
图片
当我们设置flex-wrap: wrap时,项目不会收缩到其假设大小以下。
使用flex-wrap: wrap,我们「不再有一个可以穿过每个项目的单一主轴线」。实际上,「每一行都充当其自己的小型 Flex 容器」。
当我们有多行时,交叉轴现在可能与多个项目相交!
图片
图片
图片
图片
每一行都是其自己的小型 Flexbox 环境。align-items将在包围每一行的无形框内上下移动每个项目。
但如果我们想对齐行本身怎么办?我们可以使用align-content属性:
图片
图片
图片
图片
图片
总结一下这里发生的情况:
- flex-wrap: wrap给我们两行东西。
- 在每一行内,align-items允许我们将每个单独的子项上下滑动。
- 然而,在整体上,我们有两行在一个单一的 Flex 上下文内!现在,交叉轴将与两行相交,而不是一行。因此,我们不能单独移动行,我们需要将它们作为一个组进行分配。
- 使用我们上面的定义,我们正在处理内容,而不是项目。但我们仍然在谈论交叉轴!因此,我们想要的属性是align-content。
Reference
[1]Flex 布局教程:语法篇:https://www.ruanyifeng.com/blog/2015/07/flex-grammar.html
[2]MDN_CSS_Layout_Mode:https://developer.mozilla.org/en-US/docs/Web/CSS/Layout_mode
[3]w3c_flexbox:https://www.w3.org/TR/css-flexbox-1/