如何使用 flex 布局
flex 布局是一种新的布局方式,在以前,我们要对元素进行布局,比如把元素横过来,我们只能使用 float,我们知道 float 本身是用来实现文字环绕效果的,就不是专门干这个事的,但 CSS 并没有提供其他更好的替代方案,而 flex 布局就填补了这个漏洞。
当我们将一个盒子的 display 属性设置成了 flex,这个盒子内部,也就是其子元素的布局方式就变成了弹性布局。
html
<div class="test">
<div class="content">1</div>
<div class="content">2</div>
<div class="content">3</div>
</div>css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.test {
width: 400px;
height: 200px;
border: 1px solid black;
display: flex;
}
.test .content {
height: 50px;
width: 100px;
border: 1px solid red;
}
可以看到,当我把父级元素设置为弹性布局之后,其子元素的布局方式默认就是横过来的。
注意,将一个盒子的 display 设置为 flex 是改变其子元素的排布方式,设置 flex 属性的这个元素我们可以称为一个弹性容器,而弹性容器中的子元素我们称为弹性项目。
当我们应用 flex 布局之后,会出现以下变化
- float 和 clear 属性在弹性项目中不生效
- vertical-align 属性在弹性项目中不生效
- 伪元素
::first-line和::first-letter在弹性容器上不生效
inline-flex
除了将 display 设置为 flex 以外,还可以设置为 inline-flex,将一个元素的 display 设置为 flex 之后,弹性容器的行为就类似与一个块级元素,例如独占一行。而如果设置为 inline-flex,弹性容器的行为就类似 inline-block,比如不会独占一行。
注:inline-flex 也可以写成 inline flex,但 inline-flex 兼容性或许会更好,可参考 display - CSS:层叠样式表 | MDN
当设置弹性容器后,弹性容器里面的弹性项目默认会转换为块盒,即
display属性会变成block
主轴和交叉轴
弹性盒子其实就是两个轴,即主轴和交叉轴,我们只关心主轴,与主轴垂直的轴就称为交叉轴。这两个轴垂直,一横一竖,主轴和交叉轴都有方向,默认情况下,主轴是横向从左往右的,交叉轴从上往下,我们可以使用 flex-direction 来调整主轴的方向。

通过上面的描述,我们知道主轴和交叉轴是垂直的,一横一竖,主轴有方向,交叉轴的方向就是列的排布方向。所以我们也很容易想到,flex-direction 一共有以下四个取值:
- row:从左往右
- row-reverse: 从右往左
- column: 从上往下
- column-reverse:从下往上
很显然,可以分为 row 和 column 两组,如果设置成 row 这一组其实主轴就是横轴正方向。如果设置成 row-reverse,则表示倒置一下,也就变成横轴的负方向。如果设置为 column,则表示主轴是竖轴的正方向,设置为 column-reverse,则表示主轴是竖轴负方向。
弹性项目的排布方向是按照主轴的方向来的,由于主轴默认是从左向右,所以我们设置弹性容器后弹性项目默认就给横过来了,我们修改主轴的方向就可以修改弹性项目的排布方向。
我们并不关系交叉轴的方向,交叉轴的方向实际上就是多列元素排布的方向。
给弹性容器设置 flex-direction: column; 将主轴的方向竖过来,就可以让弹性项目竖过来排列。

如果给弹性容器设置 flex-direction: column-reverse;,则会实现以下效果

当我们让主轴方向变成从下往上之后,元素的排列也会按照从下往上来。
元素的换行
下一个问题是,如果我设置很多个弹性项目,让弹性项目的总宽度大于弹性容器的宽度,会发生什么事呢

我们会发现此时并没有产生换行,而是强制压缩我为每个盒子设置的 width 属性,此时每一个盒子的宽度已经不再是 100 像素,而是比 100 更小。从这里也可以看到,这种弹性盒子的一大特性就是会自动伸缩,比如这里就让我每一个盒子的宽度都缩小了。所以弹性布局也叫伸缩布局。
如果我就是要保留它的宽度,让他超出宽度之后换行,就需要给弹性容器使用 flex-wrap 属性,这个属性有以下三个取值
- nowrap
- wrap
- wrap-reverse
默认就是 nowrap,也就是不换行,我们设置为 wrap 后,当总宽度大于容器的时候,就会换行展示,而不压缩弹性项目的尺寸。

一般正常来说,当一行显示不下的时候,我们都希望其向下换行,也就是从下面另起一行,我们也可以让它从上方另起一行,即将 flex-wrap 设置为 wrap-reverse

此时可以看到,当一行显示不下的时候,就会从上方另起一行,这个属性也可以看做是在改变交叉轴的方向。
flex-direction和flex-wrap有一个简写属性为flex-flow,改属性可以同时设置这两个属性,顺序无关。
主轴的排列方式
我们可以看到,元素的排列方式默认情况下就是从主轴最开始的地方依次排布的,我们可以使用 justify-content 来修改主轴上的对齐方式。
justify-content 的默认值就是 flex-start,表示沿着主轴的方向,从主轴最开始的地方开始依次排列。我们将其改为 flex-end,元素的对其方式就是按照主轴的末尾为参考进行排列。

注意区别和 direction:row-reverse; 的区别,这里只改变元素的对其方式,并不改变元素的排列顺序,如果设置 direction,让其从右往左排布,也就是直接设置了主轴方向,会直接改变元素的排列顺序。
如果我要让弹性项在主轴上居中对齐,则将 justify-content 设置为 center 即可

除此之外,如果我要让这些弹性项在主轴上均匀地分布,也就是说每两个弹性项中间的间隔距离一样,并且均分整个剩余的空间,要放在以前可能不好实现,但是我们也可以调整对其方式轻易做到,例如我将 justfy-content 设置为 space-between,就会有以下效果:

将对齐方式设置为了 space-between,相当于最左边的弹性项顶在最左边,最右边的弹性项顶在最右边,然后中间的区域其他的弹性项均分,保证每两个弹性项的间隔距离一样。
我们会发现最左边和最右边的元素顶格了,如果我不希望它顶格,我可以设置为 space-around,效果如下所示 :

这个属性会保证每两个弹性项的间隙都相同,并且第一个元素距离容器的距离和最后一个元素距离容器的距离相当于弹性项之间距离的一半。这种情况类似于给每个弹性项都设置了一个 margin。可以看到第一个元素距离容器的距离是两个元素距离的一半吧,那如果我要让每个元素距离左右的距离都一样,也就是说第一个元素距离左边的距离和距离右边的距离也一样,可以把对齐方式设置为 space-evenly,效果如下

这样每个元素距离左右的距离都是一模一样的,包括第一个元素和最后一个元素。
以上只是常用取值,还有一些奇奇怪怪的取值可参考 justify-content - CSS: Cascading Style Sheets | MDN
交叉轴的排列方式
我们这里只是设置了沿着主轴的对齐方式,我们当然也可以设置沿着交叉轴的对齐方式,可以使用 align-items 来设置。
align-items 用于设置单行元素的对齐方式,多行元素不适应。
align-items 这个属性有几个取值其实和 justify-content 一毛一样,比如 flex-start,flex-end,center。分别是交叉轴起始点对齐,交叉轴结束点对齐,居中。
比如将其修改为 flex-end 之后,效果如下所示 :

注意,这是按照交叉轴的结束点对其的,默认情况下交叉轴是从上往下的把,结束点在下面吧,所以设置为 flex-end 之后,就靠下了。但如果我们将 flex-wrap 设置为了 wrap-reverse 之后,相当于让交叉轴转向了,此时交叉轴就是从下往上的,结束点就在上面,所以就变成了向上排布。

如果我要让所有弹性项沿着交叉轴垂直对齐,就将 align-items 设置为 center 即可。
如果配合上 justify-content,就可以轻易实现以往很难实现的元素的垂直居中对其的效果。

除了可以按照交叉轴来对其,如果将
justify-content设置为baseline则会根据准线对其,关于准线可以参考 CSS Flexible Box Layout Module Level 1,用得不多,这里不多提及。
读者可以去看一下, align-items 的默认值并不是 flex-start,这里和 justify-content 不太一样,align-items 的默认值为 stretch,但是在前面的例子中,默认值和 flex-start 的效果其实是一样的,这是由于我们给每一个元素项设置了高度,如果不设置弹性项高度(不设置高度默认为 auto),则会让高度强行撑开为弹性容器的高度,如下所示

而如果将其对齐方式设置为 flex-start,哪怕不设置高度,也是用内容区来撑开高度,并不会强制撑开

align-items 用于设置的是单行元素的对其方式,如果我们要处理多行元素的对齐方式,则要使用 align-content,这个 align-content 不仅可以用于多行,也可以用于单行。
如果设置了
flex-wrap:nowrap;,则align-content将失效。
align-content 常用值有 flex-start,flex-end,center,space-between,space-around,space-evenly,相信哪怕不用解释读者也能明白每一个属性分别表示什么意思,这里取几个值做演示:



align-content的默认值其实也是stretch,所以多行如果不设置高度,也会挨个拉伸到盛满容器,如下:
flex 的对齐特性
读者可能会发现,我们设置主轴和交叉轴的排列方式分别为 justify-content, align-content, align-items,这就像是排列组合一样,这里帮读者归纳一下这些单词的意义:
- justify:水平方向的样式,在 flex 中就是主轴
- align:垂直方向的排列方式,在 flex 中就是侧轴
- content:整体的布局,即多行
- items:全体元素的样式设置
- self:元素自身的样式
flex 目前已经支持了 gap 属性,可以设置 flex 项和项之间的间隙
单个元素的排列顺序
以上属性都是设置在弹性容器上的,可以认为是全局属性,例如对齐方式,是设置所有元素的对齐方式吧,这个不难理解,弹性布局还允许我们给单独的某一个弹性项单独设置一些属性,以下介绍的属性均为单独设置在某一个弹性项的。

这是一个很简单的布局吧,它的 HTML 的结构如下
html
<div class="test">
<div class="content">1</div>
<div class="content">2</div>
<div class="content unique">3</div>
</div>很显然,由于主轴是从左往右的,所以元素的排列顺序就是从左往右依次排的,也就是 1,2,3, 这样一个顺序,但如果我要把某一个元素的顺序提上来呢,比如我要把 3 这个元素给他排到第一个,甚至我要自定义它们的排序顺序,就可以使用 order 属性。

我只要把 3 号块的 order 设置为-1,就可以让 3 号块排到第一,即 order 属性可以手动控制元素的排列顺序,order 属性的值越小的元素排列的越靠前,order 属性值相同的元素则根据元素在 HTML 结构中出现的顺序进行排列,order 属性的默认值为 0。
order 属性可以修改元素的顺序,但是如果我们需要频繁修改元素的顺序,控制 order 属性并不是一个明智的选择,基于性能考虑,更推荐直接操作 DOM 结构
元素层次
我们知道,order 越大的元素越靠后,如果 order 同样大,那么根据 HTML 中的书写顺序排序,排序越靠后的弹性项层次会越高,也就是离用户越近。说简单点,靠后的元素可以盖住前面的元素。
单个元素的侧轴位置
如果我们在容器上设置了 align-items,可以设置所有元素基于交叉轴的对齐方式,我们依然可以对单独的一个元素设置对齐方式,比如我要让所有元素向下对其,但要单独的某一个元素向上对其,这种控制单独元素基于交叉轴的对齐方式,就得使用 align-self 属性。

以上就将 2 号元素的 align-self 属性设置为了 center,将 3 号元素的 align-self 属性设置为了 flex-end,当给某个弹性项单独设置 align-self 属性之后,这个弹性项基于交叉轴的对齐方式不再依靠 align-items 或者 align-items 而是按照自己的 align-self 决定。
align-self和align-items一样只适用于单行元素,当设置align-content之后,不管align-items还是align-self都是直接失效。
伸缩属性
接下来介绍三个弹性盒比较核心的属性,flex-grow,flex-shrink,flex-basis。
为什么叫弹性盒(伸缩盒)呢,因为它的大小会伸缩,比如当我设置 flex-wrap 设置为 nowrap,并且内部弹性项的总宽度大于了容器宽度,就会按比例自动伸缩我每一个元素的宽度,也就是说我现在设置的宽度就不起作用了,会自动调整宽度。这种自动调整宽度的行为,其实就是弹性。
flex-grow 其实就是生长的意思,也就是让盒子大小的生长,每一个弹性项都会设置一个 flex-grow 的属性,这个属性默认为 0,我可以给多个弹性项 flex-grow 属性,这个属性表示的意思是,如果这一行还有剩余空间,按照什么比例去瓜分剩余空间,默认都是 0,也就是都不参与分配。这种情况下,每一个元素都不会分配剩余空间,剩余的空间就全部让它空着。

如果我将 2 号元素的 flex-grow 设置为 1,而其他元素并不设置 flex-grow 属性,也就是默认值 0,说明其他元素不会分配剩余空间,而 2 号元素会分配 1 份剩余空间,此时相当于 2 号元素会自动占满整个剩余空间,如下图所示:

其实从这里可以发现,一旦这一行元素有一个设置了 flex-grow 属性,这一行元素就一定会把整行占满,而空出来的区域就根据每一个元素的 flex-grow 按照一定比例去瓜分剩余空间。
例如我将 1 号元素的 flex-grow 设置为 1,2 号元素的 flex-grow 设置为 2,3 号元素的 flex-grow 设置为 3,此时空出来的区域会被分成 1+2+3=6 份,1 号元素占 1 分,2 号元素占 2 分,3 号元素占 3 分。

很显然,此时每一个盒子的宽度都不再是我们原来设置的宽度,而是在原来宽度的基础上,flex-grow 属性对宽度二次扩充后的结果。
下面介绍一下 flex-basis 这个属性,这个属性是用来替换 width 这个属性的,并且 flex-basis 权重高于 width,如果同时设置了这两个属性,则会使用 flex-basis 的属性。flex-basis 默认值为 auto,此时每一个弹性项的宽度就按照 width 来决定。
注意,flex-basis 实际上是设置的主轴方向的初始尺寸,如果主轴方向是朝下,那这里说的宽度就不太严谨了。
`flex-basis` 的细节
- 如果只设置了
width,则宽度固定为width值。 - 如果只设置了
flex-basis,则宽度最小值为flex-basis的值,如果内部出现了溢出,会自动撑开元素。 - 如果同时设置了
width和flex-basis,则flex-basis会是元素宽度的下限,width会是元素宽度的上限,如果产生溢出,盒子会被自动撑开,最多撑开到width的数值。
如果说 flex-grow 是在没占满一行空间的情况下扩大盒子体积瓜分剩余空间,那么 flex-shrink 就是在超过一行空间的情况下缩小盒子体积以避免超出剩余空间。
如果我将 flex-shrink 的值设为 0,那么当宽度溢出的时候将不再通过收缩来改变元素的宽度,如下所示

此时就会直接超出去,正因为 flex-shrink 的默认值为 1,所以当我们元素总宽度超过了容器宽度后,才会按比例收缩盒子以避免超出容器宽度。
我们知道, flex-grow 瓜分剩余空间,就是基于剩余空间的大小来瓜分的,和自身宽度是没有任何关系的,但 flex-shrink 要压缩每一个盒子的宽度,每一个盒子压缩的像素值不止和 flex-shrink 的值有关,还和盒子自身宽度有关。
flex属性是flex-grow,flex-shrink,flex-basis的简写属性,依次填写,中间用空格隔开即可。详细语法见下:
如果读者对自己要求并不是很高,只需要知道,
flex-shrink的值越大,这个元素被压缩的比例越大,其实就足够了。哪怕是 MDN 也没有给出具体算法。
如果设置了flex-basis,内容溢出之后容器会被撑开,不论什么情况下,撑开区域都不会被压缩。
如何计算每一个元素被压缩的值
多出来的空间也是按照一定比例去分掉的,只不过这个比例并不是直接按照 flex-shrink 的数值去分的,接下来介绍如何计算比例,首先,根据每个盒子的宽度,将每个盒子的宽度之比计算出来,然后用每一个盒子宽度之比乘以 flex-shrink 的值,得到一个最终的权重,最后就会按照最终的权重进行划分。
举个复杂一点的例子,假设一行三个元素,分别是 200 像素,300 像素,500 像素,此时盒子宽度为 800 像素,多出来 200 像素,这三个元素的 flex-shrink 分别是 1,2 ,3,此时我们先算出这三个元素宽度的比值,即 200:300:500,化简就是 2:3:5,然后分别乘以其 flex-shrink 的值,即 2:6:15,最后就会按照 2:6:15的比例把剩余的空间划分出去,多出来 200 像素就会被分成 2+6+15=23 份,每一份也就是 200*23=8.70 (保留两位小数四舍五入),第一个元素分得两份,也就是 17.4,所以第一个元素会被压缩 17.4 像素,第二个元素会分得 6 份,也就是 52.2 像素,即第二个元素会被压缩 52.2 像素,第三个元素分得 15 份,也就是 130.5 像素,所以第三个元素会被压缩 130.5 像素。(这里加起来多了 0.1 像素是因为前面的四舍五入,这点误差可以忽略不计)
接下来看这样一个问题
html
<!DOCTYPE html>
<html lang="cmn-hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
.test {
width: 600px;
height: 600px;
border: 1px solid black;
display: flex;
}
.test .content {
width: 200px;
height: 200px;
box-sizing: border-box;
border: 1px solid red;
}
.content.unique {
width: 400px;
flex-shrink: 3;
}
</style>
</head>
<body>
<div class="test">
<div class="content">1</div>
<div class="content">2</div>
<div class="content unique">3</div>
</div>
</body>
</html>在这个环境下,先计算一下每个盒子压缩之后的宽度,首先,三个盒子的宽度比为 200:200:400,即 1:1:2,乘以其各自的 flex-shrink,得到的比例为 1:1:6,所以超出的比例是按照 1:1:6的比例压缩的,容器宽度为 600,弹性项总宽度为 800,超出了 200,这 200 分成 1+1+6=8 份,每一份 25像素,1 号占一份,被压缩 25 像素,宽度为 200-25=175 像素,2 号占 1 份,被压缩 25 像素,也剩 175 像素,三号占 6 份,被压缩 150 像素,宽度为 400-150=250 像素

但最终结果却不尽如意,我们会发现这里每一个盒子和我们算出来的虽然大差不差,但总会有那么一丢丢误差。

如果我在控制台取消掉 box-sizing:border-box; 以及 border 属性之后,发现其结果又正常了。这里其实就是计算上的一个细节,在计算权重的时候,我们需要把盒子宽度的比值和其对应的 flex-shrink 乘起来,得到最终的权值,但这个盒子宽度的比值应该是盒子内容区宽度的比值,或者说是 content-box 的宽度,而非 border-box 的宽度。
在上面的例子中,由于有 1 像素的边框,所以盒子内容区宽度的比值应该是 198:198:398,乘以对应的 flex-shrink 应该是 198:198:1194,超出来的这 200 像素会按照这个比例被压缩,即第一个盒子压缩 (198/1590)*200, 剩下 175.09433962264150943396226415094 px,第二个盒子也一样,第三个盒子被压缩 (1194/1590)*200,剩下 249.81132075471698113207547169811 px,
读者可以看到,这已经很接近实际的数字了,但总会差那么一丁点,这实际上就是浏览器本身的问题了,因为不同浏览器其最终渲染效果也各不相同,主要的差异是对 CSS 中小数的处理,例如 chrome 中前两个盒子的宽度都是 175.09 px,第三个盒子为 249.83 px,这一点和上面 firefox 的数值就不一样,但这一丁点误差也并不会影响开发,读者只需要理解,在计算最终比值时所用到的宽度应该是 content-box 的,而非 border-box。

