几年前,我坐下来准备做一个仪表盘。没什么特别的——只是一个固定的侧边栏、一个主内容区域和一个页脚。Flexbox 看起来是显而易见的选择了,起初它表现得还不错。然后我在主区域放入了一行卡片。整个布局瞬间崩溃了。接下来的三个小时里,我一直在与 flex-grow 比例和嵌套的百分比宽度作斗争,眼睁睁看着容器无论我减去多少 padding 都无法对齐。整个页面感觉就像一座摇摇欲坠的高塔。

那次惨痛的经历教会了我关于现代 CSS 布局最重要的一件事:Flexbox 和 Grid 并不是竞争对手。它们是用于不同任务的工具,大多数布局失败的原因都是因为选错了工具。

区别在于维度

把 Flexbox 想象成一条走廊。项目沿一个方向流动,要么是行,要么是列。它是为一维分布而设计的。浏览器查看内容,然后决定如何沿该单一轴线进行拉伸、收缩或对齐。这使得 Flexbox 非常适合内容决定布局的情况。导航栏、一堆表单字段,或者一组需要等间距排列的标签,都能自然地放在 flex 容器中,因为它们共享同一套逻辑。

另一方面,Grid 就像是一个平面图。它是严格二维的,可以同时处理行和列。你先定义结构,然后将项目放入其中。这使得 Grid 非常适合整体页面架构——即任何你需要同时控制垂直和水平定位的地方。当你试图通过嵌套的 Flexbox 容器来强行实现复杂的二维设计时,最终会导致深层的 HTML 标记、脆弱的包装器以及极其不稳定的数学计算,只要你多添加一个元素,整个布局就会分崩离析。

Flexbox 依然大有可为

不要急于用 Grid 替换每一个 flex 容器。在单个组件内部,Flexbox 通常更简洁、更直观。如果你有一个包含两个按钮的卡片页脚,并且希望一个靠左、一个靠右,justify-content: space-between 正好能实现你的意图。如果你需要在一个高度未知的按钮内垂直居中一个图标,align-items: center 是最简单的路径。可变内容——比如一行长度不同的用户名标签——可以很舒服地放在 Flexbox 中,因为它是一种“内容优先”的模式。容器会适应它所承载的内容。

Grid 则是“容器优先”的。你告诉浏览器存在多少个轨道(tracks)以及它们应该有多大,然后将项目放入这些单元格中。如果你的布局真的只是共享空间的单一兄弟节点行,那么使用 Grid 会增加不必要的复杂性。

页面布局属于 Grid

这就是 Grid 大显身手的地方。还记得我那个仪表盘噩梦吗?以下是该结构从一开始就应该有的样子:

.page {
  display: grid;
  min-height: 100vh;
  grid-template-rows: auto 1fr auto;
  grid-template-columns: 200px 1fr;
}

.header { grid-row: 1; grid-column: 1 / -1; }
.sidebar { grid-row: 2; grid-column: 1; }
.main { grid-row: 2; grid-column: 2; }
.footer { grid-row: 3; grid-column: 1 / -1; }

看看这里发生了什么。.page 容器建立了三行两列。第一行由于使用了 auto,其大小会根据 header 的内容自动调整。中间行通过 1fr 占据了所有剩余的可用空间。页脚落在最后一行,大小由其自身内容决定。与此同时,侧边栏固定为 200 像素宽,而主区域填充剩余部分。

布局逻辑才是真正的解脱。Header 从第 1 条列线延伸到第 -1 条列线,这意味着无论你以后添加多少列,它都会横跨所有列。侧边栏和主内容共享第 2 行,但占据不同的列。页脚在第 3 行回归全宽。你不需要在子元素上使用百分比计算,也不需要嵌套的包装器去争夺高度。只需在一个地方更改侧边栏宽度,主区域就会自动调整。整个布局作为一个统一的系统在“呼吸”。

使用 grid-template-areas 提高可读性

为了更加清晰,你可以为你的区域命名。与其管理行号,不如编写一个视觉地图:

.page {
  grid-template-areas:
    "header header"
    "sidebar main"
    "footer footer";
}

.header { grid-area: header; }
.main { grid-area: main; }

现在你的样式表读起来就像一个线框图。如果你想在不同的断点处让侧边栏出现在右侧,只需在模板字符串中交换两个单词即可。这种结构化的可读性在嵌套的 flex 容器堆叠中是无法实现的。

两条惨痛教训

Grid 并非魔法。有两个错误会迅速毁掉你的布局。

第一种是单位使用不规范。如果你定义了 grid-template-columns: 1fr 1fr 1fr,但你的设计其实只需要两列内容,那么第三个轨道仍然存在。它要么会空在那里并产生一个你意料之外的间隙,要么会迫使你的实际内容以破坏设计的方式挤在一起。应当更加严谨。当你需要两列时,请写成 repeat(2, 1fr)