CSS布局(五):Flex——让布局更灵活

第五章: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-heightpadding 硬调。

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: flexdisplay: 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 布局中的第二根轴 ,它的定义非常简单:永远垂直于主轴

  • 当主轴是水平方向(rowrow-reverse)时,交叉轴就是垂直方向(上→下)
  • 当主轴是垂直方向(columncolumn-reverse)时,交叉轴就是水平方向(左→右)

为什么叫"交叉"轴?

"交叉"(cross)一词来源于几何中的十字交叉 ------两根轴相互垂直,在中心点相交,形成一个十字形。主轴是横线,交叉轴就是竖线;主轴是竖线,交叉轴就是横线。两者始终成 90° 直角

交叉轴的作用与主轴互补:

  • 主轴负责"排队方向"
  • 交叉轴负责"对齐方向"

具体来说,与交叉轴相关的核心属性包括:

  • align-items:单行内,每个元素在交叉轴上的对齐方式
  • align-self:单个元素在交叉轴上的独立对齐方式
  • align-content:多行时,整组行在交叉轴上的空间分配

理解要点:交叉轴 = 与主轴垂直相交的那条轴。它不负责排列顺序,只负责在垂直方向上"摆正"元素的位置。

三、主轴与交叉轴的关系总结
对比维度 主轴 交叉轴
方向 flex-direction 决定 永远垂直于主轴
作用 排列顺序 + 空间分配 单行或多行的整体对齐
核心属性 justify-contentflex-growflex-shrink align-itemsalign-selfalign-content
记忆关键词 主方向、排队 十字交叉、垂直对齐

一个简单的判断方法

  1. 先看 flex-direction 决定主轴是水平还是垂直
  2. 主轴方向确定了,交叉轴就是剩下的那个方向
  3. 沿着主轴的方向用 justify 系列属性控制
  4. 沿着交叉轴的方向用 align 系列属性控制

5.3 Flex 核心属性(一):容器上的属性

这些属性写在父容器上,决定整个内部项目的布局方式。

1. flex-direction:决定主轴方向
css 复制代码
.container {
  display: flex;
  flex-direction: row;      /* 默认值:水平从左到右 */
}

可选值:rowrow-reversecolumncolumn-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-startflex-endcenterspace-betweenspace-aroundstretch



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 的延伸------一个更精细的"隔离容器"。

相关推荐
乘风gg32 分钟前
还在养虾吗?虾王已诞生:微信龙虾 ClawBot
前端·ai编程·claude
小小小小宇1 小时前
LLM 长期记忆构建
前端
lichenyang4531 小时前
从 Express 老项目到 NestJS + Docker:一次车辆管理系统的渐进式重构
前端
Momo__2 小时前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
程序员小富2 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇2 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇2 小时前
React中的forwardRef
前端·react.js·面试
槑有老呆2 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
风骏时光牛马3 小时前
Verilog开发常见问题汇总解析
前端
子兮曰3 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端