CSS 基础:盒模型、Flex、Grid
1. 为什么后端开发者觉得 CSS 难
Java 代码里写 int x = 10,变量就是 10,确定。CSS 写 width: 100px,**元素实际多宽?**可能是 100,也可能是 120(加上 padding/border),还可能是 0(flex 容器里被挤没)。
CSS 难的根源:样式是声明式的,最终渲染结果取决于层叠规则 + 布局模型 + 父容器。
但有好消息:现代 CSS 的三大布局工具(盒模型、Flex、Grid)搞定后,90% 的页面都能拼出来。这篇不讲选择器、不讲颜色、不讲字体——只讲"元素怎么摆"。
2. 盒模型:一个元素占多少地方
每个 HTML 元素渲染时都是一个矩形盒子,四层嵌套:
┌─────────────── margin(外边距,和其他元素的距离) ───────────────┐
│ │
│ ┌─────────── border(边框,有颜色有宽度) ─────────────┐ │
│ │ │ │
│ │ ┌───── padding(内边距,盒子内部留白) ────┐ │ │
│ │ │ │ │ │
│ │ │ ┌─── content(内容本身) ───┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ width × height │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └──────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
默认坑:width: 100px 默认只设置 content 的宽度。加了 10px padding 和 2px border,元素实际占 10 + 2 + 100 + 2 + 10 = 124px。
解法:box-sizing: border-box
* {
box-sizing: border-box;
}
改完后 width: 100px = 包含 padding + border 的总宽度。content 区域会被自动压缩。
这是现代 CSS 的标配,几乎所有框架(包括 TailwindCSS)都默认加上。in4vue 的 src/style.css / Tailwind preflight 里已经生效,不用你操心。
Java 对照:
- 默认
content-box:像int[] arr = new int[100],100 是纯数据,实际占用内存 = 100 * 4 + 对象头 border-box:像StringBuilder sb = new StringBuilder(100),100 是总容量,包含了元数据
选 border-box 的理由:我声明 width: 100px 就是希望元素占 100px,而不是 "加上一堆乱七八糟之后再说"。
margin 的坑:外边距合并
两个相邻块级元素,margin 会合并取最大值,不会相加:
<div style="margin-bottom: 20px">A</div>
<div style="margin-top: 30px">B</div>
<!-- A 和 B 的间距是 30px,不是 50px -->
这是 CSS 的历史设计。避开方法:用 padding 或 gap 替代——现代布局里 margin 用得越来越少。
3. display:元素的"显示类型"
每个元素有一个 display 值,决定它自己怎么渲染 + 子元素怎么布局:
| display | 自己 | 子元素 |
|---|---|---|
block |
独占一行,可设宽高 | 默认流式(上下堆) |
inline |
和别人共享一行,宽高无效 | 行内流 |
inline-block |
行内但可设宽高 | 流式 |
flex |
块级 | Flex 布局(一维) |
grid |
块级 | Grid 布局(二维) |
none |
不渲染,不占位 | — |
常见默认值:
<div>/<p>/<h1>→block<span>/<a>/<strong>→inline<img>/<input>→inline-block
现代布局只记两件事:容器设 display: flex 或 display: grid,子元素自然排开。
4. Flex 布局:一维排列的主力
一维的意思:要么横着排一排,要么竖着排一列。90% 的"把几个东西排在一起"场景都用 Flex。
4.1 最小例子
<div class="row">
<div>A</div>
<div>B</div>
<div>C</div>
</div>
<style>
.row {
display: flex;
}
</style>
效果:A、B、C 横向排成一行,挨着左对齐。无需 float、无需 inline-block 魔法。
4.2 主轴方向:flex-direction
Flex 容器有两个轴:主轴(main axis) 和 交叉轴(cross axis)。
.row {
display: flex;
flex-direction: row; /* 主轴横向(默认)→ 子元素横排 */
/* flex-direction: column; 主轴纵向 → 子元素竖排 */
/* flex-direction: row-reverse; 横向反向 */
}
4.3 主轴对齐:justify-content
控制子元素沿主轴怎么排:
.row {
display: flex;
justify-content: flex-start; /* 左对齐(默认) */
/* center 居中 */
/* flex-end 右对齐 */
/* space-between 两端对齐,元素间等距 */
/* space-around 每个元素两侧等距 */
/* space-evenly 所有间距(含两端)都等 */
}
最常用的三种:flex-start、center、space-between。后两个尤其适合顶栏布局:
<!-- 典型顶栏:左边 logo,右边菜单 -->
<nav style="display: flex; justify-content: space-between; align-items: center">
<div>Logo</div>
<div>登录 | 注册</div>
</nav>
4.4 交叉轴对齐:align-items
控制子元素沿交叉轴怎么对齐(当主轴是横向时,交叉轴就是纵向):
.row {
display: flex;
align-items: stretch; /* 拉伸填满(默认) */
/* flex-start 顶部对齐 */
/* center 纵向居中 */
/* flex-end 底部对齐 */
/* baseline 文字基线对齐 */
}
"垂直居中"的圣杯:
.center-everything {
display: flex;
justify-content: center;
align-items: center;
}
父容器设这两行,子元素不管多大、多少个,都会垂直+水平居中。这是 CSS 2 时代困扰后端开发者十年的问题,Flex 一行搞定。
4.5 gap:元素间距
.row {
display: flex;
gap: 16px; /* 子元素间距 16px */
/* gap: 8px 16px; 行间距 8,列间距 16 */
}
比 margin 干净:不用给每个子元素写 margin、不用担心第一个/最后一个元素多余的 margin。现代浏览器全支持。
4.6 子元素的 flex 属性
前面都是容器上的属性。子元素上有三个关键属性:
.item {
flex-grow: 1; /* 容器有富余空间时,我占几份 */
flex-shrink: 1; /* 容器空间不够时,我缩几份 */
flex-basis: auto; /* 我的初始尺寸 */
/* 简写 */
flex: 1; /* 等价于 flex: 1 1 0%,"有多少拿多少" */
flex: none; /* 等价于 flex: 0 0 auto,"尺寸固定不变" */
}
典型布局:左侧固定宽,右侧自适应
<div style="display: flex; gap: 16px">
<aside style="flex: none; width: 240px">侧边栏</aside>
<main style="flex: 1">内容区</main>
</div>
类比 Java Swing 的 BorderLayout:aside 相当于 WEST、main 相当于 CENTER。CSS 不叫方位名,但原理一样。
4.7 换行:flex-wrap
默认 Flex 不换行——子元素挤不下就压缩。想换行:
.row {
display: flex;
flex-wrap: wrap; /* 挤不下就换到下一行 */
gap: 16px;
}
典型场景:商品卡片列表、标签云。
5. Grid 布局:二维网格的杀器
二维:同时控制行和列。Flex 搞不定"12 列栅格"、"仪表盘网格"、"图片画廊"这类需要两个方向都对齐的场景。
5.1 最小例子
<div class="grid">
<div>A</div>
<div>B</div>
<div>C</div>
<div>D</div>
</div>
<style>
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr; /* 三列,每列等宽 */
gap: 16px;
}
</style>
效果:A、B、C 占第一行,D 自动换到第二行。
5.2 核心属性
.grid {
display: grid;
/* 定义列:3 种写法 */
grid-template-columns: 200px 1fr 1fr; /* 第一列 200px,后两列各占剩余一半 */
grid-template-columns: repeat(4, 1fr); /* 4 列等宽,等价于 1fr 1fr 1fr 1fr */
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
/* 最强大的一个:每列至少 240px,空间够就加一列,够就再加 */
/* 定义行(可选,不写就是内容自适应) */
grid-template-rows: auto 1fr auto;
/* 间距 */
gap: 16px;
/* 或分开:row-gap: 8px; column-gap: 16px; */
}
5.3 fr 是什么
fr = fraction,按比例分配剩余空间。
grid-template-columns: 200px 1fr 2fr;
第一列固定 200px,剩余空间按 1:2 分给后两列。
Java 对照:像 GridBagLayout 的 weightx 权重,但 CSS 的 fr 更好记。
5.4 响应式网格的黄金模板
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
}
这一行 CSS 做到:
- 窗口宽时:4 列、5 列自动排
- 窗口窄时:自动减到 2 列、1 列
- 不用媒体查询
in4vue 笔记列表页就该用这个——屏幕大显示多列卡片,手机上自动变成单列。
5.5 子元素跨多行 / 多列
.item-a {
grid-column: 1 / 3; /* 从第 1 条网格线到第 3 条,跨 2 列 */
grid-row: 1 / 3; /* 同理跨 2 行 */
/* 或用 span */
grid-column: span 2; /* 跨 2 列 */
}
做"大图 + 小图"这类 Bento 布局时用得上。
5.6 命名网格区域(可读性杀手锏)
经典后台布局:顶栏、侧栏、内容。
.layout {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: 60px 1fr;
grid-template-areas:
"header header"
"sidebar main";
height: 100vh;
}
.layout > .header { grid-area: header; }
.layout > .sidebar { grid-area: sidebar; }
.layout > .main { grid-area: main; }
比任何布局代码都清晰:CSS 画出来就是页面长什么样。
6. Flex vs Grid:什么时候用哪个
| 场景 | 选 |
|---|---|
| 一行按钮横排 | Flex |
| 顶栏(左 logo 右菜单) | Flex |
| 垂直居中单个元素 | Flex |
| 列表项等宽换行 | Flex(wrap) 或 Grid(auto-fill) |
| 后台 layout(顶/侧/内容) | Grid |
| 仪表盘多卡片网格 | Grid |
| 12 栅格系统 | Grid |
| 不规则拼图(Bento) | Grid |
混用是常态:页面外层用 Grid 定大结构,每个区域内部用 Flex 排具体元素。两者不互斥。
7. 单位:px、rem、em、%、vh/vw、fr
| 单位 | 相对于 | 典型用途 |
|---|---|---|
px |
绝对像素 | 边框、阴影、精确尺寸 |
rem |
根元素字号(<html> 的 font-size,默认 16px) |
响应式字号、间距 |
em |
当前元素字号 | 不常用,易算错 |
% |
父元素对应维度 | 流式宽度 |
vh / vw |
视口高/宽的 1% | 全屏布局 height: 100vh |
fr |
Grid 容器剩余空间的份数 | 只在 grid-template 里用 |
常用规则:
- 边框、阴影、小图标 →
px - 字号、间距 →
rem(更容易做响应式缩放) - 宽度 →
%或fr - 全屏 →
vh/vw
TailwindCSS 的默认单位换算:1 = 0.25rem = 4px。p-4 就是 padding: 16px。
8. 流式布局 vs 定位:position
大多数时候用 Flex/Grid 的流式布局。个别需要脱离文档流的场景用 position:
.floating {
position: absolute; /* 脱离流,相对最近的 position 非 static 的祖先定位 */
top: 10px;
right: 10px;
}
.sticky-header {
position: sticky; /* 滚动到指定位置后粘住 */
top: 0;
}
.modal-bg {
position: fixed; /* 相对视口定位,滚动不动 */
inset: 0; /* top/right/bottom/left 全为 0 */
}
常见用例:
absolute:卡片角标、关闭按钮sticky:页面标题栏粘顶fixed:弹窗遮罩、返回顶部按钮
坑:position: absolute 的参照物是"最近的 position 非 static 祖先"。给父容器加 position: relative 才能让子元素相对父容器定位,这是老手也常踩的坑。
9. 在 in4vue 里怎么落地
9.1 整体 layout:Grid
.default-layout {
display: grid;
grid-template-columns: 280px 1fr;
grid-template-rows: 60px 1fr;
grid-template-areas:
"header header"
"sidebar main";
min-height: 100vh;
}
9.2 顶栏内部:Flex
.header {
grid-area: header;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid var(--border);
}
9.3 笔记卡片列表:Grid 的 auto-fill
.notes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
9.4 笔记详情页:Flex
.note-detail {
display: flex;
gap: 24px;
}
.note-detail > article { flex: 1; } /* 正文自适应 */
.note-detail > aside { flex: none; width: 240px } /* 目录固定宽 */
这 4 个模板覆盖了笔记站 80% 的布局需求。下一篇 TailwindCSS 会把它们翻译成工具类版本。
10. 调试:打开 DevTools 的 Layout 面板
Chrome / Firefox DevTools 的 Elements → Layout 面板:
- 勾选一个 Flex 容器 → 页面上叠加一层蓝色线条,标出每个子元素的主轴/交叉轴对齐
- 勾选一个 Grid 容器 → 标出所有网格线、行号、列号
学 Flex / Grid 必开这个面板。看不懂为什么元素在那里时,看一眼立马明白。
11. 常见坑
坑 1:flex 子元素溢出时不收缩
默认 flex-shrink: 1 会压缩子元素,但有 min-width: auto 的隐性下限(至少要能放下内容)。长文本溢出撑破容器的经典原因。
解法:给子元素加 min-width: 0:
.flex-item {
flex: 1;
min-width: 0; /* 允许无限压缩 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
这个解法值得背下来——每个前端至少写过 10 次。
坑 2:Grid 的隐式行 / 显式行
只写了 grid-template-columns 没写 rows?子元素多出来会自动换行,新行的高度是"内容撑开"。
如果想新行也有固定高度:
grid-auto-rows: 120px;
坑 3:height: 100% 不生效
子元素 height: 100% 要求父元素有明确的高度。如果父元素只写了 height: auto(默认),100% = auto 的 100%,没意义。
解法:层层往上找,直到给某个祖先设 height: 100vh 或明确像素值。或者用 Flex / Grid 的 stretch。
坑 4:margin: auto 的妙用
Flex 容器里给子元素 margin-left: auto → 它自己挤到最右边:
<!-- 顶栏:logo 左,退出按钮右 -->
<nav style="display: flex">
<div>Logo</div>
<div>菜单 1</div>
<div>菜单 2</div>
<div style="margin-left: auto">退出</div>
</nav>
不用 justify-content: space-between,不用额外 div 分组。优雅。
速查表
/* 盒模型 */
* { box-sizing: border-box; }
/* Flex:一维 */
display: flex;
flex-direction: row | column;
justify-content: flex-start | center | flex-end | space-between;
align-items: stretch | center | flex-start | flex-end;
gap: 16px;
flex: 1 | none;
/* Grid:二维 */
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
grid-template-rows: auto 1fr auto;
grid-template-areas: "header header" "sidebar main";
gap: 16px;
grid-column: span 2;
grid-area: main;
/* 垂直居中 */
display: flex; justify-content: center; align-items: center;
/* 脱离流定位 */
position: absolute | sticky | fixed;
inset: 0;
/* 省略号 */
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
min-width: 0; /* flex 子元素 */
小练习
- 打开
HomePage.vue,把笔记列表改成 Grid +auto-fill,缩放浏览器看列数变化 - 用 Flex 做一个顶栏:左边 "in4vue" 标题,右边 "GitHub" 链接,
space-between+align-items: center - 给一个元素加
position: sticky; top: 0,观察滚动时的"粘顶"效果 - 做一个"垂直+水平居中"的登录框:外层
min-height: 100vh+ flex 居中 - 打开 DevTools → Elements → Layout,勾选一个 flex 容器,看叠加的视觉辅助线
做完这 5 步,日常布局需求都能手到擒来。
延伸阅读
- MDN - CSS 盒模型
- A Complete Guide to Flexbox(公认最好的 Flex 参考)
- A Complete Guide to Grid
- Flexbox Froggy(通关游戏学 Flex,1 小时搞定)
- Grid Garden(同作者的 Grid 版)
- 1 行 CSS = 一个布局(Una Kravets 的 10 个经典 layout)