弹性布局(Flexbox)的交互式指南

弹性布局提供强大的布局模式。理解弹性布局的工作原理后,我们可以构建自动响应的动态布局,随时重新布局。

例如:

本文的 demo 主要是受 Adam Argyle 的“4 种布局,1 种价格”代码笔的启发,没有使用媒体/容器查询。没有设置任意断点,而是使用流体原理创建了一个无缝衔接的流动布局。

以下是相关的 CSS:

form {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 16px;
}
.name {
  flex-grow: 1;
  flex-basis: 160px;
}
.email {
  flex-grow: 3;
  flex-basis: 200px;
}
button {
  flex-grow: 1;
  flex-basis: 80px;
}

我之前见过这种 demo,虽然我了解弹性布局的基本原理,但还是觉得很难理解:

我想通过本文帮大家完善弹性布局的心理模型。大家可以通过学习这些特性,了解弹性布局算法的工作原理。无论你是 CSS 小白,还是弹性布局的资深大佬,都能从本文获益。

我们开始吧!

内容警告:本文使用了与食物有关的比喻~~


弹性布局简介

CSS 是由多个不同的布局算法组成的,官方称之为“布局模式”。每种布局模式都是 CSS 的子语言。CSS 的默认布局模式是流式(Flow)布局,但我们可以通过改变父容器上的 display 特性将布局模式变成弹性布局。

display 改变为 flex 时,就创建了一个“弹性排版上下文”。换句话说,所有子元素将默认按照弹性布局算法进行排版。

每种布局算法的设计初衷都是为了解决某个特定问题。CSS 默认的流式布局是为了创建数字文档,它本质上是 Microsoft Word 布局算法。标题和段落作为区块要垂直堆叠,像文本、链接和图像等则分布在这些区块中。

那么,设计弹性布局是为了解决什么问题呢?弹性布局是将一些元素排列成一行或一列,并让我们控制这些元素的分布和对齐方式。顾名思义,弹性布局是关于弹性的。我们可以控制元素,让元素增长或缩小,还可以决定如何分配额外的空间等。

现在还有必要使用弹性布局吗?
大家可能会觉得,现在所有现代浏览器都支持 CSS 网格,还用弹性布局干什么?
CSS 网格是非常优秀的布局模式,但它与弹性布局解决的问题不一样,所以我们要学会这两种布局模式,从而在实际工作中选择最合适的。
设计动态、流体用户界面(将元素排布成垂直或水平列表)时,弹性布局依旧是最佳选择。下文我们会看一个用 CSS Grid 不容易完成的例子——解构的煎饼。
我对 CSS 网格和弹性布局都很熟悉,但我还是经常使用弹性布局!


Flex direction

如上所述,弹性布局控制元素在行或列中的分布。默认情况下,元素会堆叠为并排的一行,我们可以使用 flex-direction 特性将元素改为堆叠成一列:

flex-direction: row 的主轴是水平的,方向为从左到右;改成 flex-direction: column 后,主轴是垂直的,方向为从上到下。

弹性布局的一切都以主轴为基础,垂直或水平抑或是行或列都不重要。所有的规则都是基于主轴和垂直方向的交叉轴来构造的。

只要学会了弹性布局的规则,就可以从水平布局无缝切换到垂直布局。所有的规则都会自动适应。这个功能是弹性布局模式所特有的。

子元素会按照以下 2 条规则排布:

  • 主轴:子元素会聚集在容器的起点
  • 交叉轴:子元素会扩展至填满整个容器

下图是这些规则的快速可视化:

在弹性布局中,我们决定主轴是水平还是垂直运行。这是所有弹性布局计算依赖的根。


对齐

我们可以用 justify-content 特性改变子元素沿主轴的分布方式:

我们一般不从对准单个子元素的角度来考虑主轴,我们考虑的是群体的分布。

我们可以把所有元素都集中在某个特定位置(用 flex-startcenterflex-end),也可以把所有元素分散开来(用 space-betweenspace-aroundspace-evenly)。

交叉轴不一样,我们使用 align-items 特性:

有趣的是,align-itemsjustify-content 有一些相同选项,但又不完全重合。

为什么 align-itemsjustify-content 不共享相同的选项?这个问题我们随后讨论,这里我先分享另一个对齐特性:align-self

justify-contentalign-items 不同的是,align-self 适用于子元素,而不是容器。它可以改变一个特定的子元素沿交叉轴的对齐方式。

align-selfalign-items 的值都一样,它们改变的是完全一样的东西align-items 是语法糖,方便使用,可以一次性自动设置所有子元素的对齐方式。

没有 justify-self。至于为什么没有,需要我们更深入地探索弹性布局算法。


Content 与 items

如上所述,弹性布局似乎非常随意。但为什么是 justify-contentalign-items,而不是 justify-itemsalign-content 呢?

同样的,为什么有 align-self,但没有 justify-self 呢?

这些问题涉及到弹性布局最重要也是被误解最多的一点。为了给大家解释清楚,我决定使用一个比喻。

在弹性布局中,元素沿主轴分布。默认情况下,所有元素整齐的排成一排。 我可以画一条笔直的水平线,把所有的子元素都串起来,就像烤肉串一样:

但是,交叉轴不同。交叉轴是一条笔直的垂直线,只与一个子元素相交。

它不像烤肉串,更像一碟鸡尾酒香肠?

区别在于:每个鸡尾酒香肠都能自由移动且不干扰其他元素。

只需选择一只鸡尾酒香肠,拿起即可!

相比之下,主轴串联每个子元素,任何一个元素都无法在不干扰其他子元素的情况下移动!这就是主轴。大家可以试试拖动中间的那个元素:

以上就是主轴和交叉轴的根本区别。当我们探讨交叉轴的对齐时,每个元素都很自由;但如果要对齐主轴上的子元素,我们只能考虑如何分布每个子元素。

这就是没有 justify-self 的原因。给主轴中间的元素设置 justify-self: flex-start 是要做什么? 那里已经有一个子元素了!

我们给这 4 个我们一直在探讨的术语合适的定义:

  • justify——将各元素沿主轴线分布
  • align——沿交叉轴分布元素
  • content——一组可以分布的元素
  • items——可以单独定位的单一元素

所以,我们要用 justify-content 来控制元素在主轴上的分布,用 align-items 给每个元素在交叉轴上单独定位,我们主要用这两个特性来管理弹性布局。

没有 justify-items 和没有 justify-self 的原因是一样的,涉及主轴时,我们必须把所有元素当作一个整体,当作可以分配的内容。

那么 align-content 呢?事实上,align-content 在弹性布局中确实存在,具体细节我们将下文探讨 flex-wrap 特性时介绍。


假定尺寸

现在,我想谈谈弹性布局最令我惊讶的一点。假设我有以下 CSS:

.item {
  width: 2000px;
}

大家看到这个 CSS 可能会说,“好,我们会得到一个 2000 像素宽的元素”。真的吗?我们来测试一下:

是不是很有趣?

两个元素应用了完全相同的 CSS,都是 width: 2000px。然而,第一个项目比第二个项目要宽得多!这是为什么呢?

区别在于布局模式。第一个项目是用流式布局渲染的,在流式布局中,width 是硬性约束。我们设置 width: 2000px,就会得到一个 2000 像素宽的元素,即使它会溢出容器。

但是,在弹性布局中,width 特性的执行方式不同。它更像是一个建议,而不是一个硬性约束。

规范称之为“假定尺寸”,指的是在理想的不受任何干扰的情况下一个元素的尺寸。

但是,理想情况几乎不存在。这个例子的限制条件是,父元素没有空间来容纳 2000 像素宽的子元素,所以子元素的尺寸要被缩小来适应父元素。

这就是弹性布局的核心。一切是流动的、灵活的,可以根据实际约束条件进行调整。

算法的输入
大家一般倾向于把 CSS 语言看作是属性的集合,但我认为这是一个错误的心理模型。正如上个例子中,宽度属性的执行结果取决于使用的布局模式。
因此,我喜欢把 CSS 看作是布局模式的集合。每个布局模式都是一种算法,可以执行或重新定义每个 CSS 属性。我们向算法提供我们的 CSS 声明(键/值对),由算法决定如何使用它们。
也就是说,我们写的 CSS 是这些算法的输入,类似于传递给一个函数的参数。如果想真正掌握 CSS ,只学习这些属性是不够的,还必须学习这些算法是如何使用这些属性的。


增长和缩小

上文我们谈到,弹性布局算法有内置的灵活性,有假定尺寸。想要真正了解弹性布局的流动性,需要了解这 3 个特性:flex-growflex-shrinkflex-basis

接下来,我们依次学习每个特性。


flex-basis

在很长一段时间里,我并不理解 flex-basis 是什么。

简单点说就是,在 Flex 的行中,flex-basis 的作用与 width 相同。在 Flex 的列中,flex- basis 的作用与 height 相同。

我们已经了解了弹性布局中的一切都与主轴和交叉轴相关。例如,justify-content 会沿主轴分配子元素,无论主轴是水平还是垂直,它的工作方式都是一样的。

不过,widthheight 并不遵循这一规则!width 会始终影响水平尺寸。如果我们把 flex-directionrow 转变为 column 时,它不会突然变成 height

因此,弹性布局的作者创建了一个通用的“尺寸”特性,叫做 flex-basis。它就像 widthheight 一样,但与主轴挂钩。它允许我们在主轴方向上设置一个元素的假定尺寸,无论主轴是水平还是垂直。

我们可以试一下。这个例子中的 4 个子元素都被赋予了 flex-basis: 50px,但我们可以调整第一个子元素:

width 特性一样,flex-basis 也类似于一种建议,不是硬性约束。如果某一点没有足够的空间保证元素的尺寸,元素就要改变尺寸,避免溢出。

不完全相同
一般来说,我们可以在 Flex 行中交替使用 width flex-basis,但也有一些例外情况。例如,width 特性对图像等被替换的元素的影响与 flex-basis 不同。此外,width 特性可以将元素的尺寸减小到其最小尺寸以下,而 flex-base 则不能。
这已经超出本文探讨的范围了,我写出来是担心大家偶尔可能会遇到需要区分两种特性的不同效果的情况。


flex-grow

默认情况下,Flex 上下文中的元素会沿着主轴线缩小到其最小的合适尺寸,这样通常会产生额外的空间。

我们可以用 flex-grow 特性指定该空间应如何消除:

flex-grow 的默认值是 0,所以我们可以让该值变大。如果我们想让一个子元素吞掉容器中的额外空间,就要明确告诉该子元素。

如果多个子元素设置了 flex-grow 怎么办?这种情况下,额外的空间将在子元素之间按比例(基于 flex-grow 值)分配。

大家看下图就更容易理解。试着对每个子元素进行递增/递减:

第一个子元素想要 1 个单位的额外空间,第二个子元素想要 1 个单位,所以总的单位数是 2(1+1)。每个子元素都能按比例获得额外的空间。


flex-shrink

但是如果子元素对容器来说太大了怎么办?

我们来试着缩小容器,看看会发生什么:

两个元素都同比例缩小了,第一个子元素的宽度始终是第二个子元素的 2 倍。

大家需要注意的是,flex-basiswidth 的作用相同。我们使用的是 flex-basis,但使用 width 会得到完全相同的结果。

flex-basiswidth 设置元素的假定尺寸。弹性布局算法可能会将元素缩小到假设尺寸以下,但在默认情况下,它们总是一起缩放,保留两个元素之间的比例。

如果我们不希望元素按比例缩小怎么办?这时候就需要使用 Flex-shrink 特性啦

大家花几分钟时间看一下下面这个演示。看能否弄清楚发生了什么。

假设我们有两个子元素,每个子元素的假设尺寸是 250 像素,那么容器至少要有 500 像素的宽度才能容纳这两个子元素。

如果我们把容器缩小到 400 像素,我们无法把价值 500 像素的内容塞进一个 400 像素的容器里,因为空间小了 100 像素,因此,元素需要放弃 100 像素的总量来适应容器。

flex-shrink 特性会帮我们决定如何缩小这 100 像素。

flex-grow 一样,flex-shrink 也是一个比率。默认情况下,两个元素都有 flex-shrink: 1,每个子元素都要缩小 100 像素的一半,即 50 像素,因此,子元素的实际尺寸从 250px 缩小到 200px。

现在,假设我们把第一个子元素缩小 flex-shrink: 3

容器一共少了 100 像素的空间,一般来说,每个子元素需要缩小 50 像素,但由于我们调整了 flex-shrink,所以第一个元素最终要缩小 3/4(75 像素),第二个元素要缩小 1/4(25 像素)。

注意,绝对值不重要,关键是比例。如果两个子元素都是 flex-shrink: 1,每个子元素将缩小总赤字的 ½。如果两个子元素都是 flex-shrink: 1000,那么每个子元素将缩小 总赤字的 1000/2000。无论哪种方式,其结果都是一样的。


缩减和比例
上个例子中,两个弹性子元素都有相同的假定尺寸(250 像素)。在计算如何缩小它们时,我们可以完全用 flex-shrink 来计算。
缩减算法也保留子元素的比例。如果第一个子元素的大小是第二个子元素的 2 倍,它将缩小的值会更多。
因此,完整的计算包括查看每个子元素的相对 flex-shrink 和相对大小。

不久前,我对 flex-shrink 有了一个顿悟:我们可以把它看作是 flex-grow 的“反面”。 它们是同一枚硬币的两面:

  • flex-grow 控制当元素小于容器时如何分配额外空间。
  • flex-shrink 控制当元素大于其容器时如何移除空间。

所以,这两个特性不能同时激活。如果容器有多余的空间,flex-shrink 没有用,因为元素不需要缩小。而如果子元素大于容器,flex-grow 也没有用,因为没有多余的空间可以分割。

防止缩小

有时候,我们不希望 Flex 子元素缩小。

我经常在 SVG 图标和形状上注意到这一点。我们来看一个简化的例子:

当容器变窄时,两个圆就会被压成椭圆。如果我们想让它们保持圆形呢?

我们可以通过设置 flex-shrink: 0 来做到这一点:

当我们将 flex-shrink 设置为 0 时,我们基本上就完全“退出”收缩过程。弹性布局算法将把 flex-basis(或 width)作为一个硬性最低限制。

这里有这个演示的完整代码:

这个方法是不是更简单?
我在课程中教授这个概念,每隔一段时间,就会有学生问我,既然有更简单的方法,为什么还要使用复杂的 flex-shrink:
.item.ball {
  min-width: 32px;
}
几年前,我都会对学生的疑问表示赞同。如果我们设置了一个最小宽度,那么这个元素就不能缩减到这个点以下了!我们添加了一个硬性约束,而不是宽度属性或 flex-basis 这样的软性约束。
我现在觉得这种想法其实是混淆了“熟悉”和“简单”这两个词。大家可能对“最小宽度”比“弹性收缩”更熟悉,但这并不意味着“最小宽度”就比“弹性收缩”更简单。
经过几年的实践,我觉得设置 flex-shrink: 0 是解决这个特殊问题的更直接的方法。但最小宽度在弹性布局算法中仍有重要作用!我们接下来会讨论这个问题。


最小尺寸

假设我们正在为一个电子商务店铺创建流式搜索表单:

当容器缩小到一定程度时,内容就会溢出来!

为什么呢?flex-shrink 的默认值是 1,而且我们没有删除它,所以搜索输入应该能够尽可能地收缩!为什么它拒绝收缩?

其实,除了假定尺寸外,弹性布局算法还关注另外一个重要尺寸:最小尺寸。

弹性布局算法不能将子元素缩小到其最小尺寸以下。无论我们把 flex-shrink 调得多高,内容都会溢出,不会进一步缩小。

文本输入的默认最小尺寸为 170-200 像素(不同浏览器会有所不同)。这就是我们在上文遇到的限制。

其他情况下,限制因素可能是该元素的内容。例如,尝试调整这个容器的大小:

对于包含文本的元素,最小宽度是最长的不可分割的字符串的长度。

好消息是,我们可以用 min-width 特性重新定义最小尺寸。

通过直接在 Flex 子元素上设置 min-width: 0px,我们让弹性布局算法覆盖“内置”的最小宽度。因为我们将其设置为 0px,所以该元素可以根据需要进行缩减。

这个方法同样可以在具有 min-height 特性的 Flex 列中发挥作用。

谨慎处理
注意:内置的最小尺寸有其作用。它可以避免某些故障的发生。
比如,当我们把 min-width: 0px 应用于含文本的 Flex 子元素时,就可能会出现中断:弹性布局中的 min-width 是一个非常强大的特性。它不止一次帮我拜托困境,但是一个特性的功能越强大,出现问题时后果也越严重,所以大家一定要谨慎使用!


间隙

近年来,弹性布局在最大的改进之一是 gap 特性:

gap 特性可以让每个主轴上排列的每个 Flex 子元素之间保留空间。这对导航标头等内容非常有用。

间隙特性是弹性布局语言里相对较新的特性,但自 2021 年初以来,当前所有浏览器都支持该特性。


自动边距特性

我还想分享一个与间距有关的技巧——自动边距特性。它在弹性布局的早期就已经存在了,但相对来说比较隐蔽。

margin 特性可以在特定的元素周围增加空间。在某些布局模式下(如流式布局和定位布局),它可以使用 margin: auto 将元素居中。

弹性布局中的自动边距特性更有趣:

上文我们探讨了 flex-grow 特性如何消除额外的空间,将其应用于子元素。

自动边距特性能吞噬多余的空间,并将其应用于元素的边距。它让我们能够精确地控制额外空间的分布位置。

一个常见的标头布局的特点是:标志在一侧,导航链接在另一侧。下面我们使用自动边距特性来构建这种布局:

Corpatech 标志是列表中的第一个元素。通过设置 margin-right: auto,我们收集了所有多余的空间,将其放在第一和第二个元素之间。

我们可以用浏览器的 devtools 查看:

还有很多其他解决方案。我们可以把导航链接分组在 Flex 容器中,或者用 flex-grow 增长第一个列表项。但我个人喜欢自动边框的解决方案。


封装

大多数时候,当我们在两个维度上工作时,都会想使用 CSS 网格,但 Flexbox+flex-wrap 有其特别用途!这个特别的例子展示了“解构的煎饼”布局,在一个中型屏幕上,3 个元素堆叠成了一个倒金字塔。

如果我们设置 flex-wrap: wrap,项目就不会缩减到小于其假定尺寸。至少,当包裹到下一行/列是一个选项时,是不会的!

我们的烤肉串/鸡尾酒香肠的比喻呢?

有了 flex-wrap: wrap,我们不再有一个可以串联每个元素的主轴。每一行都充当了它自己的迷你柔性容器。与其说是一个大串联,不如说每一行都有自己的串联:

在这个缩小的范围内,我们迄今为止学到的所有规则都继续适用。例如,justify- content 将分配每根棍子上的两块。

但是,现在我们有了多行,align-items 怎么发挥作用呢?交叉轴现在可以与多个项目相交了!

考虑一下:如果我们改变这个特性会发生什么?

每一行都是自己的弹性布局环境。align-items 将在环绕每一行的隐形框内向上或向下移动每一项。

但如果我们想让行本身对齐呢?我们可以用 align-content 特性来做到这一点。

总结一下:

  • flex-wrap: wrap 给我们两行东西;
  • 每一行中,align-items 可以让我们向上或向下滑动每个单独的子元素;
  • 然而,放大后,我们在一个单一的 Flex 上下文中拥有这两行!现在,交叉轴将与两行相交,而不是一行。因此,我们不能单独移动这些行,我们需要把它们作为一个组来分配;
  • 如上所述,我们处理的是内容,不是项目。但我们仍然在谈论交叉轴!所以我们想要的特性是 align-content


成功啦!

这是个冗长的教程。如果你还不够了解弹性布局,可能需要一些时间慢慢消化。

就像 CSS 中的许多东西一样,刚开始使用弹性布局时看起来很简单,但当进入更深入的学习后,其复杂性就会迅速上升。

因此,许多人在使用 CSS 的过程中很早就陷入了瓶颈期。我们有足够的知识来完成工作,但又感觉这个语言难以预测。

这很糟糕,因为 CSS 是大多数前端开发工作中的一个相当大的组成部分。

问题是,CSS 实际上是一种非常强大且一致的语言。我们的大多数心理模型都是不完整且不准确。只有真正掌握 CSS,才能感受其中的乐趣。

去年,我发布了一门综合课程,名为《针对 JavaScript 开发者的 CSS 的课程》。

该课程是“多模式”的,使用了很多不同形式的媒体。我从零开始建立了自己的课程平台,因此它支持:

• 互动文章(如本文)

• 视频(170 多个短视频)

• 练习

• 模拟现实世界项目

• 小型游戏

如果你觉得这篇博文有帮助,你一定会喜欢这门课程。该课程采用了类似的方法,但针对的是整个 CSS 语言,还有练习和项目,能真正帮助你学习新技能。

该课程是专门为使用 React/Angular/Vue 等 JS 框架的人设计的。课程的 80% 讲的是 CSS 基础知识,还包括如何将这些基础知识整合到现代 JS 应用程序中,如何构建我们的 CSS 等知识。

如果你正在努力学习 CSS ,一定要试试这个课程。尤其是如果你已经很熟悉 HTML 和 JS,那么接下来最重要的就是获得使用 CSS 的信心。当你完成神圣的三位一体时,才能真正享受开发网络应用的乐趣。


demo 拆解

本教程的开头有“4 个布局,1 个价格”的 demo。

我们研究了弹性布局算法,现在大家了解弹性布局的工作原理了吗?欢迎大家使用下列代码:

<style>
  form {
    display: flex;
    align-items: flex-end;
    flex-wrap: wrap;
    gap: 8px;
  }
  .name {
    flex-grow: 1;
    flex-basis: 120px;
  }
  .email {
    flex-grow: 3;
    flex-basis: 170px;
  }
  button {
    flex-grow: 1;
    flex-basis: 70px;
  }
</style>

<form>
  <label class="name" for="name-field">
    Name:
    <input id="name-field" />
  </label>
  <label class="email" for="email-field">
    Email:
    <input id="email-field" type="email" />
  </label>
  <button>
    Submit
  </button>
</form>
/* Cosmetic styles */
form {
  padding: 8px;
  border: 1px solid hsl(0deg 0% 50%);
}

label {
  font-weight: 500;
}
input {
  display: block;
  width: 100%;
  height: 2.5rem;
  margin-top: 4px;
}
button {
  height: 2.5rem;
}

非常感谢大家的阅读!希望本文对大家有用!



原文作者:Josh W Comeau
原文链接: https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/#bonus-unpacking-the-demo


推荐阅读
相关专栏
前端与跨平台
90 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。