CSS_Flex 那些鲜为人知的内幕!
前言
Flex
想必大家都很熟悉,也是大家平时在进行页面布局的首选方案。(反正我是!)。不知道大家平时在遇到 Flex
布局属性问题时,是如何查阅并解决的。反正,我每次记不住哪些属性或者对哪些属性的用法忘记时。我总是求助于 阮一峰
老师写的Flex 布局教程:语法篇。
其实,对于 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的描述
其中 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。
网格布局
网格与弹性盒类似,只要在元素上使用了 display: grid
,就会开始使用网格布局算法。此布局算法将根据网格布局算法显示所有子元素。
Grid
和 Flexbox
的区别在于, Grid
适用于布局具有列和行的二维内容,而 Flexbox
适用于布局具有 一维内容,即单个列或行。
我们后面也会有针对 Grid
的文章,预估在 12 月份或者明年 1 月份。
替换元素
在 CSS 中, 替换元素
( Replaced Element
)是指一个由浏览器根据元素的标签和属性创建的、在渲染时展示的元素,而 不是由文档中的内容决定其显示的元素。这些元素通常是具有外部资源(如图像或嵌入式框架)的元素,其内容由浏览器根据其属性和上下文动态生成。
以下是一些常见的替换元素:
-
元素: 通过
src
属性引用外部图像。1<img src="image.jpg" alt="Description" /> 2
-
和 元素: 通过
src
属性引用外部音频或视频文件。1<audio controls> 2 <source src="audio.mp3" type="audio/mp3" /> 3</audio> 4 5<video width="320" height="240" controls> 6 <source src="movie.mp4" type="video/mp4" /> 7</video> 8
-
元素: 通过
src
属性引用外部网页或嵌入式内容。1<iframe src="https://example.com"></iframe> 2
-
元素: 用于嵌入外部资源,如 Flash 动画。
1<object data="flash.swf" type="application/x-shockwave-flash"> 2 <!-- fallback content or alternate content --> 3</object> 4
-
元素: 通过 JavaScript 绘制图形。
1<canvas width="200" height="200"></canvas> 2
替换元素与非替换元素的主要区别在于,替换元素的渲染不依赖于文档的其他部分。它们的外观和尺寸通常由其属性和外部资源决定。替换元素具有一定的固有尺寸,不受文本或子元素的影响。
在 CSS
中,替换元素还可以通过 object-fit
和 object-position
这样的属性进行进一步控制,以指定元素的替换内容的显示方式。例如:
1img {
2 object-fit: cover; /* 图片按比例缩放并覆盖整个容器 */
3 object-position: center; /* 图片在容器中居中显示 */
4}
5
2. Flexbox 是个啥?
CSS 由许多不同的布局算法组成,官方称之为 布局模式
。 每种布局模式都是 CSS 中的一种小型子语言。默认布局模式是 流式布局
,但我们可以通过更改父容器上的 display
属性来选择使用 Flexbox
:
当我们将 display 设置为 flex 时,我们创建了一个 flex格式化上下文
。这意味着,默认情况下, 所有子元素将根据 Flexbox 布局算法定位。
每种布局算法都是为解决特定问题而设计的。默认的 Flow布局
旨在创建数字文档;它本质上是 Microsoft Word
的布局算法。 标题和段落以块的形式垂直堆叠,而文本、链接和图像等元素则不显眼地位于这些块内部。
Flexbox
专注于在行或列中排列一组项目,并提供对这些项目的分布和对齐具有极大控制权。正如其名称所示, Flexbox
关注的是灵活性。我们可以控制项目是增长还是收缩,额外空间如何分配等。
3. Flex Direction
如前所述, Flexbox
的关键在于 控制在行或列中元素的分布。默认情况下,项目将在 一行中侧边堆叠,但我们可以通过使用 flex-direction
属性切换到列:
使用 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:
1.item {
2 width: 2000px;
3}
4
我们第一直觉就是 我们将得到一个宽度为 2000 像素的项目。其实这句话是不对的!
让我们用一个例子来说明。
1<style>
2 .flex-wrapper {
3 display: flex;
4 }
5 .item {
6 width: 2000px;
7 }
8</style>
9
10<div class="item"></div>
11
12<div class="flex-wrapper">
13 <div class="item"></div>
14</div>
15
结果缺不一样。
两个项目都应用了完全相同的 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
中, 自动边距
变得更加有趣:
,
,
自动边距将吞噬额外的空间,并将其应用于元素的边距。它使我们能够精确控制在哪里分配额外的空间。
一个常见的页眉布局特点是在一侧放置标志,而在另一侧放置一些导航链接。
1<style>
2 ul {
3 display: flex;
4 gap: 12px;
5 }
6 li.logo {
7 margin-right: auto;
8 }
9</style>
10
11<nav>
12 <ul>
13 <li class="logo">
14 <a href="/"> 首页 </a>
15 </li>
16 <li>
17 <a href=""> 语言 </a>
18 </li>
19 <li>
20 <a href=""> 个人中心 </a>
21 </li>
22 </ul>
23</nav>
24
1ul {
2 list-style-type: none;
3}
4ul a {
5 text-decoration: none;
6}
7
列表中的第一项通过给它设置 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
。
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。