第五章:Flex------让布局更灵活
上一章我们认识了 BFC,明白了"容器隔离内外"的核心思想。这一章,我们来学习一个更强大的容器------Flex 容器 。它不仅能隔离内外(本身就是 BFC),还提供了控制内部项目排列的超级能力,让你轻松实现水平垂直居中、等分布局、自适应排版等传统布局难以做到的效果。

5.1 传统布局的痛点
在没有 Flex 和 Grid 的"旧时代",CSS 布局就像是在用原始工具搭建积木。很多今天一行代码就能解决的问题,当时需要各种 hack 和计算。我们拿两个最常见的需求为例:垂直居中 和等宽分布,看看当年开发者是怎么"绕路"的。
一、垂直居中:一个需求,一堆"野路子"
1. 绝对定位 + 负边距(最经典,但最死板)
css
.parent {
position: relative;
}
.child {
width: 200px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -50px; /* 高度的一半 */
margin-left: -100px; /* 宽度的一半 */
}
原理 :先让子元素的左上角定位到父容器的中心点(top:50%; left:50%),再用负边距把子元素向左、向上拉回自身宽高的一半。
痛点:
- 必须知道子元素的精确宽高,否则无法计算负边距。
- 如果子元素宽高变化(如响应式),就得重新计算,不够灵活。
- 代码看起来像在"打补丁",逻辑不直观。
后来有了 transform: translate(-50%, -50%) 可以替代负边距,但仍需要 top/left 定位,且早期浏览器兼容性不佳。

2. line-height + text-align(只对单行文本友好)
css
.parent {
height: 200px;
line-height: 200px; /* 让行高等于容器高度 */
text-align: center;
}
.child {
display: inline-block; /* 转成行内块,让 text-align 生效 */
line-height: 1.2; /* 重置子元素行高,避免文字错位 */
vertical-align: middle;
}
原理 :每个字都有字体大小(font-size) ,但浏览器为一行文字分配的垂直空间是行高(line-height) 。行高可以大于字体大小,多出来的部分会平均分配到文字的上方和下方,称为"半间距"。行高 = 字号 + 行间距。行框的高度就是行高,文字默认在行框内垂直居中(因为上下空白相等)。所以,如果让行高等于容器的高度,且只有一行文字,那这行文字自然就在容器正中间。

这个方法的本质是利用行框的单行特性实现居中。当容器高度等于行高时,只有一个行框,且文字在该行框内垂直居中,所以文字就在容器正中间。
一旦内容变成多行,问题就来了:
- 每一行都会生成一个与行高等高的独立行框。
- 第一行行框从容器顶部开始,文字在其行框内居中 → 第一行文字在容器中居中。
- 第二行行框紧贴第一行行框底部,完全超出容器底部 → 第二行文字显示在容器外面。
- 后续行依此类推,全部溢出容器。
最终结果:只有第一行文字在容器中居中,其他行都跑到外面去了。整个内容块并没有真正实现垂直居中。所以,这个方法只适用于单行文本。多行文本或包含多个元素的复杂内容,用这个方法就会暴露各种问题。
相比之下,Flex 和 Grid 用更简洁的语法就能解决任意行数的居中问题,而且没有这些副作用。

3. table-cell 法(需要额外表格结构)
css
.parent {
display: table;
}
.child {
display: table-cell;
vertical-align: middle;
text-align: center;
}
原理 :利用表格单元格的 vertical-align 属性天然支持垂直居中。
痛点:
- 需要为父元素设置
display: table,子元素设置display: table-cell,改变了元素的原本语义。 - 表格布局对响应式支持不好,宽度分配有时会出乎意料。
- 如果子元素不止一个,还要考虑表格行的结构,代码臃肿。

二、等宽分布:浮动布局的"连环坑"
目标:一个导航栏,三个按钮等宽水平排列,填满父容器。
html
<nav class="nav">
<a href="#">首页</a>
<a href="#">产品</a>
<a href="#">关于</a>
</nav>
1. 浮动 + 宽度百分比(最常用,但问题最多)
css
.nav {
overflow: hidden; /* 尝试清除浮动?等一下,这只是触发BFC,不是清除 */
}
.nav a {
float: left;
width: 33.33%; /* 等宽 */
box-sizing: border-box;
padding: 10px;
text-align: center;
}
问题一:高度塌陷
所有子元素都浮动了,父元素 .nav 失去高度,背景和边框都缩成一条线。需要额外清除浮动:
css
.nav::after {
content: "";
display: table;
clear: both;
}
或者加一个空的 <div style="clear: both;"> ------ 为了布局多一个无意义的标签。
问题二:宽度计算不精确
33.33% 是循环小数,不同浏览器对像素舍入处理不同,可能导致三个按钮宽度总和大于或小于父容器,引发换行。开发者经常要用 calc() 微调:
css
width: calc(33.3333% - 0.1px); /* 这种"黑魔法"很常见 */
问题三:垂直对齐
按钮里的文字如果高度不一,浮动本身不处理垂直对齐,还得靠 line-height 或 padding 硬调。

2. inline-block 方案(也有自己的麻烦)
css
.nav {
font-size: 0; /* 干掉换行符产生的空格 */
}
.nav a {
display: inline-block;
width: 33.33%;
box-sizing: border-box;
font-size: 16px; /* 恢复字体大小 */
}
问题:
- 需要额外处理
inline-block之间的空格(父元素font-size:0,子元素再恢复)。 - 垂直对齐仍需
vertical-align介入。 - 宽度计算同样面临小数像素问题。

三、Flex 登场:用 Flex 实现垂直居中和等宽分布
回头看看这些解法,你会发现它们都在绕路 :为了达到一个简单的效果,不得不处理一堆副作用(塌陷、空格、计算......)。Flex 的设计目标很明确------直接解决排列问题,不让开发者操心副作用。

用 Flex 实现垂直居中:
css
.parent {
display: flex;
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
}
/* 子元素宽高任意,无需知道 */
用 Flex 实现等宽导航:
css
.nav {
display: flex;
}
.nav a {
flex: 1; /* 等分剩余空间 */
text-align: center;
padding: 10px;
}
/* 没有浮动塌陷,没有空格问题,没有宽度计算 */
Flex 的核心思想:把布局逻辑交给容器,让容器告诉项目怎么排。开发者只需要声明"我要居中""我要等分",剩下的浏览器自动计算。

传统布局不是不能实现效果,而是为了达到效果,你需要额外处理很多与目标无关的细节。Flex 的出现,把这些细节都封装进了浏览器底层。它让你从"怎么让浮动不塌陷"的泥潭里解脱出来,真正专注于"我想怎么排列"本身。
下一节,我们就来系统学习 Flex 的核心概念,看看它是如何做到这一点的。
5.2 Flex 容器:一个更智能的 BFC
当你给一个元素设置 display: flex 或 display: inline-flex,它就变成了一个 Flex 容器。这个容器天然具备 BFC 的特性------内部怎么折腾都不会影响外部。但更重要的是,它内部有一套全新的排列规则。
1. 为什么叫 flex?
flex 这个名称来自英文单词 flexible,意为"弹性的、可伸缩的"。
在传统的布局模型中,元素的尺寸和位置往往是固定的,或者只遵循简单的文档流规则。当容器大小发生变化时(例如浏览器窗口缩放),元素不会自动调整来填满剩余空间或避免溢出。
Flex 布局的核心能力正是为了解决这个问题:容器可以根据可用空间的大小,自动伸缩子元素的尺寸与排列方式。
- 如果空间有多余,子元素可以被拉长(
flex-grow)来填满空间 - 如果空间不足,子元素可以被压缩(
flex-shrink)来适应空间 - 如果一行放不下,子元素可以自动换行(
flex-wrap)
因此,flex 这个名称准确描述了该布局模型的本质:一个具有弹性能力的盒子模型。
理解要点:flex ≠ 固定布局,flex = 动态分配空间。

2. 主轴与交叉轴
Flex 容器内部有两条核心轴线:主轴 和交叉轴。
- 主轴(main axis):项目排列的方向(默认水平从左到右)
- 交叉轴(cross axis):垂直于主轴的另一个方向
你可以通过几个简单的属性,控制项目在主轴和交叉轴上的对齐方式、排列顺序、换行行为等。

一、主轴(main axis)
主轴是 flex 布局中的方向轴 。它决定了子元素在容器中排列的主方向------也就是元素们会沿着哪条线一个接一个地排下去。
主轴的朝向由 flex-direction 属性控制:
| 属性值 | 主轴方向 | 视觉表现 |
|---|---|---|
row |
水平方向 | 从左向右排列 |
row-reverse |
水平反向 | 从右向左排列 |
column |
垂直方向 | 从上向下排列 |
column-reverse |
垂直反向 | 从下向上排列 |
为什么叫"主"轴?
因为这个轴是 flex 布局中最主要的参考线 。所有与位置分配、间距控制相关的核心操作,都沿着这个轴进行。例如:
justify-content:在主轴上对齐元素、分配剩余空间flex-grow/flex-shrink:在主轴上决定元素的伸缩行为
可以这样理解:主轴 = 主方向 = 元素排队的方向。就像一列队伍,所有人面朝同一个方向排列,这个方向就是"主"方向。
理解要点:先确定主轴方向,才能理解其他所有 flex 属性的行为。

二、交叉轴(cross axis)
交叉轴是 flex 布局中的第二根轴 ,它的定义非常简单:永远垂直于主轴。
- 当主轴是水平方向(
row或row-reverse)时,交叉轴就是垂直方向(上→下) - 当主轴是垂直方向(
column或column-reverse)时,交叉轴就是水平方向(左→右)
为什么叫"交叉"轴?
"交叉"(cross)一词来源于几何中的十字交叉 ------两根轴相互垂直,在中心点相交,形成一个十字形。主轴是横线,交叉轴就是竖线;主轴是竖线,交叉轴就是横线。两者始终成 90° 直角。
交叉轴的作用与主轴互补:
- 主轴负责"排队方向"
- 交叉轴负责"对齐方向"
具体来说,与交叉轴相关的核心属性包括:
align-items:单行内,每个元素在交叉轴上的对齐方式align-self:单个元素在交叉轴上的独立对齐方式align-content:多行时,整组行在交叉轴上的空间分配
理解要点:交叉轴 = 与主轴垂直相交的那条轴。它不负责排列顺序,只负责在垂直方向上"摆正"元素的位置。

三、主轴与交叉轴的关系总结
| 对比维度 | 主轴 | 交叉轴 |
|---|---|---|
| 方向 | 由 flex-direction 决定 |
永远垂直于主轴 |
| 作用 | 排列顺序 + 空间分配 | 单行或多行的整体对齐 |
| 核心属性 | justify-content、flex-grow、flex-shrink |
align-items、align-self、align-content |
| 记忆关键词 | 主方向、排队 | 十字交叉、垂直对齐 |
一个简单的判断方法:
- 先看
flex-direction决定主轴是水平还是垂直 - 主轴方向确定了,交叉轴就是剩下的那个方向
- 沿着主轴的方向用
justify系列属性控制 - 沿着交叉轴的方向用
align系列属性控制
5.3 Flex 核心属性(一):容器上的属性
这些属性写在父容器上,决定整个内部项目的布局方式。
1. flex-direction:决定主轴方向
css
.container {
display: flex;
flex-direction: row; /* 默认值:水平从左到右 */
}
可选值:row、row-reverse、column、column-reverse

2. justify-content:主轴上的对齐方式
| 值 | 含义 |
|---|---|
flex-start(默认) |
向主轴起点对齐 |
flex-end |
向主轴终点对齐 |
center |
居中对齐 |
space-between |
两端对齐,项目之间间隔相等 |
space-around |
每个项目两侧间隔相等 |
space-evenly |
项目之间及两端间隔都相等 |
css
.container {
justify-content: center; /* 水平居中 */
}

3. align-items:交叉轴上的对齐方式(单行)
| 值 | 含义 |
|---|---|
stretch(默认) |
如果项目未设置高度,将占满整个容器高度 |
flex-start |
向交叉轴起点对齐 |
flex-end |
向交叉轴终点对齐 |
center |
居中对齐 |
baseline |
以第一行文字的基线对齐 |
css
.container {
align-items: center; /* 垂直居中(单行) */
}

4. flex-wrap:是否换行
| 值 | 含义 |
|---|---|
nowrap(默认) |
不换行,可能压缩项目宽度 |
wrap |
换行,项目按自身尺寸排列 |
wrap-reverse |
换行,但行序反转 |

5. align-content:交叉轴上的行对齐方式(多行)
当有多行时,用 align-content 控制行与行之间的间距。取值与 justify-content 类似:flex-start、flex-end、center、space-between、space-around、stretch。


5.4 Flex 核心属性(二):项目上的属性
这些属性写在子项目上,控制单个项目的表现。
1. flex-grow:放大比例
- 默认
0,即不放大 - 如果有剩余空间,按比例分配。例如所有项目
flex-grow: 1,则均分剩余空间

2. flex-shrink:缩小比例
- 默认
1,空间不足时会缩小 - 设为
0则不会缩小

3. flex-basis:项目在主轴上的初始尺寸
- 默认
auto,即项目本来的大小 - 可以设为固定值,如
100px
常用缩写:flex: 1 代表 flex: 1 1 0%,即可以放大缩小,初始尺寸为 0。
⚠️ 容易混淆的点
剩余空间 ≠ 总空间
- 总空间 = 父容器宽度
- 剩余空间 = 总空间 − 所有子项
flex-basis之和
所以:
flex-basis: auto:不重新分配"已经占用的那部分",只分"多出来的"flex-basis: 0%:连"已经占用的"都按比例重新分
举个例子(父容器 900px,两个子项)
情况 A:flex-basis: auto
- 子项1:内容宽 200px,
flex-grow: 1 - 子项2:内容宽 100px,
flex-grow: 2 - 剩余空间 = 900 − (200 + 100) = 600
- 子项1 增加:200 + 600 × 1/3 = 400px
- 子项2 增加:100 + 600 × 2/3 = 500px
情况 B:flex-basis: 0%
- 基准都是 0
- 总空间 900 全部按 1:2 分配
- 子项1 = 300px
- 子项2 = 600px

4. align-self:覆盖容器的 align-items
- 允许单个项目有不一样的对齐方式
- 取值同
align-items

5. order:排列顺序
- 默认
0,数值越小越靠前

5.5 实战案例:从零搭建常见布局
案例一:导航栏(水平居中 + 等分布局)
html
<nav class="navbar">
<a href="#">首页</a>
<a href="#">产品</a>
<a href="#">服务</a>
<a href="#">关于</a>
</nav>
css
.navbar {
display: flex;
justify-content: space-around; /* 均匀间隔 */
background: #333;
padding: 1rem;
}
.navbar a {
color: white;
text-decoration: none;
}
案例二:垂直居中(经典弹窗)
html
<div class="modal">
<div class="content">我是内容</div>
</div>
css
.modal {
display: flex;
justify-content: center;
align-items: center; /* 同时水平和垂直居中 */
height: 300px;
border: 1px solid #ccc;
}
案例三:圣杯布局(页眉 + 中间三列 + 页脚)
html
<div class="layout">
<header>页眉</header>
<div class="main">
<aside class="left">左侧边栏</aside>
<article class="center">主要内容</article>
<aside class="right">右侧边栏</aside>
</div>
<footer>页脚</footer>
</div>
css
.layout {
display: flex;
flex-direction: column;
min-height: 100vh; /* 让布局撑满全屏 */
}
.main {
display: flex;
flex: 1; /* 中间区域占满剩余高度 */
}
.left, .right {
flex: 0 0 200px; /* 固定宽度 200px */
background: #f0f0f0;
}
.center {
flex: 1; /* 自适应占满剩余宽度 */
background: #fff;
}

案例四:响应式卡片列表(自动换行)
html
<div class="card-list">
<div class="card">卡片1</div>
<div class="card">卡片2</div>
<div class="card">卡片3</div>
<div class="card">卡片4</div>
</div>
css
.card-list {
display: flex;
flex-wrap: wrap; /* 允许换行 */
gap: 1rem; /* 项目之间的间距 */
}
.card {
flex: 1 1 300px; /* 基础宽度200px,可放大缩小 */
min-width: 150px; /* 防止过小 */
background: lightblue;
padding: 1rem;
text-align: center;
}
一、排队入场规则
想象这样一个场景:
- 有一条 人行道(容器),宽度不固定(可宽可窄)
- 每个 人(卡片) 都有一个 理想占位(flex-basis: 300px)
- 排队规则如下:
规则 1:先按理想占位试
每个人先说:"我要 300px 宽"
规则 2:如果人行道不够 → 换行
比如人行道只剩 100px,下一个人要 300px 站不下 → 直接换到下一行
规则 3:换行后,这一行重新分配
这一行里的所有人,把这一整行的宽度 重新按比例分 (因为有
flex-grow: 1)
二、代码
css
.card {
flex: 1 1 300px;
/* 相当于 flex-grow: 1; flex-shrink: 1; flex-basis: 300px; */
}
场景 1:容器宽度 = 900px
- 第 1 人:要 300px ✅ 站下(剩 600)
- 第 2 人:要 300px ✅ 站下(剩 300)
- 第 3 人:要 300px ✅ 站下(剩 0)
- 第 4 人:要 300px ❌ 不够 → 换行
第一行:3 个人
剩余空间 = 0(刚好 3×300),不需要分配
第二行:1 个人
一个人占满整行(因为有 flex-grow:1)
→ 宽度 = 900px(虽然它只想 300)
👉 一行 3 个,一行 1 个
场景 2:容器宽度 = 700px
- 第 1 人:要 300 ✅(剩 400)
- 第 2 人:要 300 ✅(剩 100)
- 第 3 人:要 300 ❌ 剩 100 不够 → 换行
第一行:2 个人
剩余空间 = 700 − 600 = 100
按 flex-grow:1 平分 → 每人多得 50
最终:350px + 350px
第二行:2 个人
(第 3、第 4 人)同样逻辑 → 每人 350px
👉 一行 2 个,一行 2 个
场景 3:容器宽度 = 500px
- 第 1 人:要 300 ✅(剩 200)
- 第 2 人:要 300 ❌ 剩 200 不够 → 换行
第一行:1 个人
一个人占满 500px
第二行、第三行... 同理
👉 每行 1 个
三、关键结论(用排队逻辑总结)
flex-basis决定"试站位时我要多大"
试完发现不够 → 换行
换行后,这一行内部再用flex-grow重新分
所以:
- 容器变窄 → 每一行能站的人变少 → 一行个数变少
- 容器变宽 → 每一行能站的人变多 → 一行个数变多
这就是 "变大变小,一行个数会变" 的根本原因。
-
flex-basis: 0%→ 所有人从 0 开始抢 -
flex-basis: auto→ 先保留自己的 size -
flex-basis: 300px→ 理想占位 300px(类似"先要 300,多了再分,少了换行")
排队逻辑完全一样,只是"理想占位"从 0 / auto 变成了一个固定值。

5.6 与 BFC 的呼应
学完 Flex 你会发现,我们一直在使用 BFC 的"隔离"思想:
- Flex 容器本身就是 BFC,所以它不会被外部浮动干扰,也不会让内部边距跑出去。
- 内部的 Flex 项目虽然可以灵活排列,但它们之间的 margin 仍然会合并(因为它们在同一个 BFC 内),但 Flex 容器和外部的 margin 不会合并。
这正是我们第四章所说的:容器隔离内外,内部随便折腾,但内部仍然遵循自己的规则。
本章小结
- Flex 容器 = BFC + 强大的内部排列能力
- 核心在两根轴:主轴(
justify-content)和交叉轴(align-items) - 项目可以通过
flex属性灵活分配空间 - 解决了传统布局中垂直居中、等分布局、自适应等难题
下一章,我们将走进 Grid 布局,体验二维网格的强大,你会发现它同样是 BFC 的延伸------一个更精细的"隔离容器"。