马蹄疾 | 聊聊你可能并没有完全掌握的 Flex 布局

4,715 阅读15分钟

很多谈 Flex 布局的文章都是一个一个属性的干聊,有点像约会时的查户口,枯燥而无趣。

如果让你硬记中国 34 个省级行政单位,你不仅会记的很吃力,可能还没法分清省和省之间的相邻关系。而一旦你心中有了一副中国地图,这些知识就变成信手拈来的东西了。

这篇文章立志做 Flex 布局的中国地图。

本文是『horseshoe·Flex专题』系列文章之一,后续会有更多专题推出

GitHub地址(持续更新):horseshoe

博客地址(文章排版真的很漂亮):matiji.cn

如果觉得对你有帮助,欢迎来 GitHub 点 Star 或者来我的博客亲口告诉我

Flexbox叫弹性盒模型,它的使用场景主要是屏幕自适应布局和取代浮动布局。

细节性的知识需要大量实践,系统性的知识则需要真正理解系统。我认为Flexbox就属于系统性的知识。所以这篇文章从概念入手,力求做到只要阅读一遍,就可以让开发者心中有乾坤。

一维布局模型

你他妈可能是三体看多了吧,啥叫一维布局模型?

一维布局模型,简单讲就是,在主轴方向确定的情况下,只有,没有

我们熟悉的二维布局模型有哪些呢?有display: tabledisplay: grid。而我们今天要讲的display: flex就是妥妥的一维布局模型了。

直接看图。

illustration

可以看到,Flexbox有的概念,却没有的概念。这导致它的表达能力是受限的。

概念

我们如何确定一条短线是中文的还是阿拉伯数字的1呢?

你可能觉得我在搞笑。一眼看过去,横向的不就是中文的,纵向的不就是阿拉伯数字的1么。

是的。那是因为我们遵循着一些默认的约定:两眼连成的线确定了水平方向。

Flexbox也是这个道理。它是一个一维布局模型,我们就要找到确定仅有的维度的那根线。

这就引出了Flexbox的第一个概念:主轴(mian axis)与交叉轴(cross axis)。主轴就是那根仅有的维度线。两眼连成的线与主轴方向保持平行,这就是肉眼看待Flexbox的正确方式。否则有时候它会显得很别扭。

然后我们再来说弹性盒子。既然它是一个盒子,那肯定得有容器,也得有内容。

也就引出了Flexbox的第二个概念:弹性容器(flex container)与弹性项目(flex item)。

你可以将弹性容器理解为冷战时期的柏林,各方国际政治势力自然就是若干弹性项目了。几个国际大流氓聚在一起开会讨论什么呢?当然是开会讨论如何瓜分柏林咯。是的,弹性盒模型讨论的就是弹性项目如何瓜分弹性容器这一温馨的话题。

上一小节我们提到过,Flexbox是一维布局模型。它带来的结果就是Flexbox有的概念,却没有的概念。

所以引出Flexbox的第三个概念:行(line)。

直接看图。

illustration

最小长度

如果弹性容器有富余空间,那好说,大家分就是了。而如果弹性容器空间不够,弹性项目不仅没得分,大家还得挤一挤。那么问题就来了,挤不是无限制的挤,咱们就来探讨一下,挤到什么程度,是可忍孰不可忍?

举个例子。

.container {
    display: flex;
    width: 850px;
    padding: 5px;
}
.container .item {
    width: 200px;
    height: 50px;
    margin: 5px;
}
.container .item:nth-child(2) {
    width: 1000px;
}

illustration

两个弹性项目的长度加起来的和已经超过了弹性容器,所以不得不挤压。挤压的比率咱们先不考虑,咱们先观察挤压的方式。有没有发现红色部分都有不同程度的收缩,但是黄色部分却岿然不动?

黄色部分是什么?是弹性容器的padding和弹性项目的margin。盒子模型咱们都了解吧,除了paddingmarginborder之外,是不是只剩content了?

咱们可以大胆猜测,Flexbox只敢欺负盒子模型的content,其余都是大爷,惹不起。

别急,再来验证一下。

.container {
    display: flex;
    width: 850px;
    padding: 5px;
}
.container .item {
    width: 200px;
    height: 50px;
    margin: 5px;
}
.container .item:nth-child(2) {
    width: 1000px;
    padding: 0 450px;
}

illustration

这下是不是很清楚了?第一个弹性项目的content长度已经变成了0;第二个也好不到哪去,因为盒子长度都被padding占据,它的content长度实际上也是0。都把人家挤破产了,却丝毫不敢动其他的属性,势利眼无疑了。

我们再来看一个有意思的例子。

.container {
    display: flex;
    width: 850px;
    padding: 5px;
}
.container .item {
    width: 200px;
    height: 50px;
    margin: 5px;
}
.container .item:nth-child(2) {
    width: 1000px;
    padding: 0 300px;
}
.container .item .inner {
    width: 500px;
    height: 100%;
}

illustration

我在第二个弹性项目中放了一个长500px的元素,结果你猜怎么着,弹性项目的padding竟然有一部分和元素重合了。连子元素都未能幸免。

一般来说盒子模型的content都是被文字撑开的,我们最后再来看看文字的情况。

illustration

在弹性项目显式设置了宽度的情况下,弹性项目并不能完全包裹文字。也就是说文字也帮不了它,既然它声明了宽度,文字撑开的长度最多不能超过显式声明的宽度,超出的文字只能溢出。

illustration

而没有显式声明宽度的情况,文字的宽度就是弹性项目盒子模型的content,Flexbox也拿它没办法。

总结一下:当富余空间不够时,Flexbox只会挤压弹性项目的content,其余如paddingmarginborder完全不受影响。同时弹性项目没有显式声明宽度的情况下,Flexbox也不会挤压文字。

display

从这里开始,我们就要讲具体的CSS属性了。

display有两个和Flexbox相关的属性,分别是display: flexdisplay: inline-flex

对于容器内部的项目来说,效果是一样的。它们的区别在于容器自身应该以块元素还是行内元素的身份立命。包括table也有这样的区分,就不多讲了。

flex-direction

这个属性声明的是主轴的方位和方向。

首先,主轴可不一定是水平的,主轴切换了那可就什么都变了。

其次,主轴声明的信息有两个,分别是:

  • 它是水平的还是垂直的?
  • 它在水平或垂直维度上是从左到右还是从右到左?

所以这里也涉及到四个属性值。

.container {
    flex-direction: row(default) | row-reverse | column | column-reverse;
}

illustration

flex-wrap

这个属性声明的是当容器中的项目一行放不下的时候,是让大家挤一挤呢,还是换行。

其实这里也包含两个信息:

  • 要不要换行?
  • 如果需要换行就会形成多行,多行是从上到下排列还是从下到上排列?
.container {
    flex-wrap: nowrap(default) | wrap | wrap-reverse;
}

illustration

如果把flex-directionflex-wrap结合起来,大家会不会懵逼?上上下下左左右右。其实不管它怎么reverse,flex-direction反转的是主轴的方向,flex-wrap反转的交叉轴的方向。

抓住一些概念性的东西,就不会懵逼了。

flex-flow

这是一个集合属性,可以同时定义flex-directionflex-wrap

也就是说,这一个属性可以一站式声明主轴和交叉轴的特性。

.container {
    flex-flow: row-reverse wrap-reverse;
}

justify-content

这个属性声明每行内的项目如何水平对齐。

把弹性容器一行内的项目想象成一行行内元素,justify-contenttext-align的食用方式是一样的。

.container {
    justify-content: flex-start(default) | flex-end | center | space-between | space-around | space-evenly;
}

illustration

flex-startflex-endcenter非常表意,咱们按下不表。

解释一下后面三个属性值:

  • space-between表示两头的项目对齐容器壁,项目与项目之间的空隙平均分配。所谓的between指的就是项目之间。
  • space-around表示两头的项目与容器壁保留一个单位的空隙,项目与项目之间保持两个单位的空隙。around翻译成中文是周围,指的就是每个项目左右两边的空隙平均分配。
  • space-evenly表示两头的项目与容器壁之间的空隙和项目与项目之间的空隙都保持一个单位。evenly翻译成中文是均匀,指的是所有空隙平均分配。

align-items

这个属性声明每行内的项目如何垂直对齐。

.container {
    align-items: stretch(default) | flex-start | flex-end | center | baseline;
}

illustration

这里有一个问题,如果弹性项目显式的声明了高度,那stretch将不再起作用。所以这里的例子,我往项目中加了一个子元素,把高度显式的声明在子元素上,这样项目的高度就是被撑开的。

<div class="container">
    <div class="item"><div class="inner">1</div></div>
    <div class="item"><div class="inner">2</div></div>
    <div class="item"><div class="inner">3</div></div>
</div>

可以看到,默认情况下,行内的弹性项目会拉伸,相当于声明了高度100%。一旦项目显式的声明了高度,拉伸就不再起作用了。

align-content

这个属性将容器的一行视为最小单位。它声明的是如果容器的交叉轴方向有富余空间,每行应该如何垂直对齐。

.container {
    align-content: stretch(default) | flex-start | flex-end | center | space-between | space-around | space-evenly;
}

illustration

这里也一样,如果弹性项目显式的声明了高度,那stretch将不再起作用。

同时有两点需要注意:

  • 弹性容器需要显式的声明高度,而且高度必须高于所有行的高度和,否则去哪来的富余空间呢?
  • 如果容器内只有一行,也就是说不需要换行或者不允许换行,那align-content属性将无法生效,因为它是作用于行的,只有一行也敢叫老子跑一趟?

为什么没有justify-items

如果你同时了解display: flexdisplay: grid,也许你会发现它们都有xx-itemsxx-content属性。但是别急,再进一步深究,发现Flexbox少了一个justify-items

我们先来阐述一下xx-itemsxx-content作用范围有什么区别。

  • xx-items作用于项目,这意思很明朗。
  • xx-content又是作用于什么呢?肯定是比项目更大的单位,又联想Flexbox的align-content是作用于行,我们可以大胆猜测xx-content作用于行或者列。

点破到这里,大家应该有点眉目了吧?Flexbox是一维布局模型,它根本没有列的概念。

你说不对呀,既然Flexbox没有列的概念,那不是应该没有justify-content属性,而应该有justify-items属性么?

话是这么说,我当初也是这么认为的。

但后来仔细想一想,Flexbox的align-items声明的是一行内的项目如何垂直对齐,与之相对,justify-items声明的就应该是一列内的项目如何水平对齐。好像更离谱。而如果将弹性容器一行内的每个项目都当做一列,justify-content似乎就说的通了。

这就好比讨论螃蟹的螯到底是不是手一样,怎么都觉得别扭。只能找一个相对更合理的说法了。

提出这个问题没别的意思,只是想加深大家对Flexbox的理解。

order

从这里开始,涉及到的属性都是弹性项目自身的属性。在大的格局确定的情况下,项目之间也是可以有一些腾挪空间的。

这个属性声明的是弹性项目自身的次序。只要显式声明了不是默认值0的整数,项目显示的次序将会不同于源代码定义的次序。

这个主要是留给JavaScript动态控制项目的次序用的,非动态直接修改源代码的次序就好了。

.item {
    order: <integer>; /* default is 0 */
}

illustration

flex-grow

这个属性声明的是弹性项目是否要瓜分行内的富余空间,以及如何瓜分。

属性值只允许正整数。

.item {
    flex-grow: <number>; /* default is 0 */
}

illustration

首先解释一下什么是富余空间:它是在弹性容器规则和弹性项目显式宽度或者内容的共同约束下,行内剩余的水平空间。比如图例中除去marginpadding的黄色部分。

行内的富余空间是如何被瓜分的呢?

首先,弹性项目要提出申请。比如第一个项目提出flex-grow: 1,意思是说它想要一份富余空间。至于一份富余空间是多少像素,目前还不能确定。

等所有项目都申请完毕,计算申请的总份数。已知富余空间长度和申请总份数,就能知道一份富余空间是多少像素。

最后根据每人提出申请的份数,分配富余空间。

只有当行内有富余空间时,flex-grow属性才会生效。行内空间已经预先被瓜分完甚至不够时,该属性就管不了了。

flex-shrink

这个属性声明的是弹性项目是否要瓜分行内的负债空间,以及如何瓜分。

属性值只允许正整数。

.item {
    flex-shrink: <number>; /* default is 1 */
}

illustration

同样,解释一下什么是负债空间:它是在弹性容器规则和弹性项目显式宽度或者内容的共同约束下,行内短缺的水平空间。此时如果不换行的话,就要求挤压弹性项目的长度。

行内的负债空间的瓜分规则与富余空间的瓜分规则大致相同。

首先,弹性项目要提出申请。只不过这时的份额就不再是我要多少,而是我还多少。

等所有项目都申请完毕,计算申请的总份数。

这里的问题在于,计算负债空间的长度稍微比较复杂。我们在文章一开始探讨过弹性项目最小长度的话题,这里再次总结一下:

  • 弹性容器只会挤压弹性项目的content,其余如paddingmarginborder不受影响。
  • 如果弹性项目内有文本或者固定宽度的子元素,这又分两种情况。第一种是项目本身没有显式声明宽度,则最小长度以子元素的长度为准;第二种是项目本身显式声明了宽度,则最小长度以子元素的长度为准,但不能超过项目本身的宽度。结合上面图例中最后两个例子,更容易理解(我知道你已经懵逼了)。

最后根据每人提出申请的份数,分配负债空间。就是还债。

为什么flex-grow属性的默认值是0,而flex-shrink属性的默认值是1呢?

因为默认情况下,如果有富余空间我可以不要的,但是有负债空间又无法换行的话,我不得不要。所以flex-shrink属性的默认值是1,意思就是默认情况下,如果空间不够则大家平均的被挤压。

你可以将所有项目的flex-shrink属性值设置为0,如此这般所有项目都铁骨铮铮、不畏强权了。见上面图例的第四个例子。

flex-basis

这个属性声明的是预先分配给弹性项目的长度。它是width属性的替代品,优先级比width高。

.item {
    flex-basis: <length> | auto; /* default is auto */
}

illustration

如果widthflex-basis都显式的声明了一个非auto的值,那么flex-basis的优先级更高。否则,哪个显式声明了就以哪个为准。

确实不太清楚制定flex-basis属性标准的意义何在。

它们的区别好像仅限于属性值为0的情况。width: 0我们都知道表示没有宽度,见上面图例的第二个例子;而flex-basis: 0表示以内容的宽度为宽度,见上面图例中的第三个例子。

flex

这是一个集合属性,可以同时定义flex-growflex-shrinkflex-basis

你可以集合三个属性的值,也可以只写flex-grow一个属性的值。

.item {
    flex: <'flex-grow'> | <'flex-grow'> <'flex-shrink'> <'flex-basis'>;
}

align-self

这个属性声明的是弹性项目自身在行内的垂直对齐方式。

“我就是我,是颜色不一样的烟火”。

.item {
    align-self: auto(default) | stretch | flex-start | flex-end | center | baseline;
}

illustration

除了auto之外,align-self的属性值和align-items的属性值是一样的,效果也一样。

align-items: auto是说我默认服从集体,不自搞一套,所以才会多这么个属性值。

为什么有align-self属性而没有justify-self属性呢?

这个问题我们讨论过吧?因为没有jusifty-items属性,所以justify-self属性也无从谈起。

你看人家Grid就有justify-self属性。

其他

有一个小游戏 Flexbox Froggy 可以帮助你轻松的实践Flexbox的各项特性。

本文是『horseshoe·Flex专题』系列文章之一,后续会有更多专题推出

GitHub地址(持续更新):horseshoe

博客地址(文章排版真的很漂亮):matiji.cn

如果觉得对你有帮助,欢迎来 GitHub 点 Star 或者来我的博客亲口告诉我