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 对照

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 不渲染,不占位

常见默认值:

现代布局只记两件事容器设 display: flexdisplay: 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-startcenterspace-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 做到

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 里用

常用规则

TailwindCSS 的默认单位换算:1 = 0.25rem = 4pxp-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 */
}

常见用例

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 必开这个面板。看不懂为什么元素在那里时,看一眼立马明白。


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 子元素 */

小练习

  1. 打开 HomePage.vue,把笔记列表改成 Grid + auto-fill,缩放浏览器看列数变化
  2. 用 Flex 做一个顶栏:左边 "in4vue" 标题,右边 "GitHub" 链接,space-between + align-items: center
  3. 给一个元素加 position: sticky; top: 0,观察滚动时的"粘顶"效果
  4. 做一个"垂直+水平居中"的登录框:外层 min-height: 100vh + flex 居中
  5. 打开 DevTools → Elements → Layout,勾选一个 flex 容器,看叠加的视觉辅助线

做完这 5 步,日常布局需求都能手到擒来。


延伸阅读