CSS排版布局篇(8):Grid 二维布局

初步认识

历史背景:从"一维流"到"二维网格"

Flex 的出现(2009 年左右,W3C 最终定稿于 2017)彻底改变了布局方式,它解决了"盒子之间的空间分配"这一长期痛点------能自动伸缩、自动对齐、自动换行。

但它有一个根本性局限:Flex 是"一维布局系统" ------ 只能在一条主轴(main-axis)上分配空间。

举例:

plain 复制代码
display: flex;

无论你设置 flex-direction: row 还是 column,Flex 只能沿这个方向伸缩。

多行(wrap)虽然能换行,但第二行的尺寸不会自动对齐第一行的列宽。

随着页面复杂度提高,人们遭遇了越来越多 Flex 难以解决的问题:

时代问题 Flex 无法很好解决的场景
仪表盘(Dashboard) 需要行列双维度伸缩的面板
照片墙(Photo Wall) 要求网格状对齐的多列布局
表格型界面 单元格之间要精准对齐、跨行跨列
复杂响应式页面 各区域要"行列"联动,不能仅靠一条轴

Flex 是"一维思维":

Flex 容器只关注主轴(main-axis)方向的空间分配;

交叉轴(cross-axis)只是用来对齐,而不参与主导布局。

于是,人们开始追求:

"能否让 CSS 像 Excel 一样思考布局?"

这就是 Grid Layout(网格布局) 出现的原因。


核心思想:把页面当作"二维坐标系"

Grid 的思想是:

不是先放内容再排列,而是 先画出网格,再放内容进去。

这就像设计报纸、仪表盘、后台面板:先画出"格子线",再把每个模块放入格子中。

plain 复制代码
.container {
  display: grid;
  grid-template-columns: 200px 1fr 1fr;
  grid-template-rows: 100px auto 50px;
}

这里我们实际上创建了一个"二维坐标平面":

plain 复制代码
3列:200px | 1fr | 1fr
3行:100px | auto | 50px

每个子元素通过坐标放入:

plain 复制代码
.item1 { grid-column: 1 / 3; grid-row: 1; }
.item2 { grid-column: 3; grid-row: 1 / 3; }

这意味着:item1 跨两列,item2 跨两行。


Grid 的定位:"线"是比"格子"更基础的东西

在传统表格(HTML <table>)或 Flex 布局中,布局是围绕"单元格"展开的:

→ "我放到第1格、第2格"。

Grid 的革命性设计 就在于:

它不再直接定位格子,而是定位"线(grid line)"------格子之间的边界。

举例说明

假设你定义了:

plain 复制代码
.container {
  display: grid;
  grid-template-columns: 100px 100px 100px;
}

这意味着容器内部的"网格线"(Grid Lines)如下图所示:

plain 复制代码
| line 1 | cell 1 | line 2 | cell 2 | line 3 | cell 3 | line 4 |

有 3 个单元格(cell),但却有 4 条线(line)

线的编号总是"格子数 + 1"。


grid-column 的语义
  • grid-column 是一个 简写属性,等价于:
plain 复制代码
grid-column: <start-line> / <end-line>;

其中 <start-line> 表示"从第几条网格线(grid line)开始",
<end-line> 表示"到第几条网格线结束(不含该线)"。

简写形式

当你写:

plain 复制代码
grid-column: 1;

它其实等价于:

plain 复制代码
grid-column: 1 / auto;

👉 意思是:"从第1条网格线开始,结束线由浏览器自动决定(通常是占一列)。"

这就类似:

  • grid-column: 1 / 2(仅占一列);
  • 如果结合 grid-column-end: span 2,则表示跨两列。

案例:grid-column: 2 / 4 的含义

"从第2条网格线开始,到第4条网格线结束。"

换句话说,它跨越了:

  • 第2条线 → 第3条线 → 第4条线之间的格子。

也就是第2格 + 第3格,共两个格子。

数学上可理解为 区间 [2,4),起点含,终点不含。


为什么要这么设计?(W3C 的深层逻辑)

Grid 的目标是 在二维空间中,精确描述"边界对齐"的盒子系统

如果基于格子编号,会带来许多歧义:

  • "第3列"到底是指起始线3到4,还是格子编号3?
  • "跨2列"是指含第3列吗?

所以 W3C 干脆基于"线",直接描述"边界":

  • 每个网格项由「起始线」和「结束线」界定;
  • 更接近真实布局引擎的矩形逻辑(坐标是边界而不是中心)。

这就像绘图中:

plain 复制代码
矩形 = 左边界 + 右边界 + 顶边 + 底边

Grid 的逻辑就是:

plain 复制代码
grid-column-start / grid-column-end
grid-row-start / grid-row-end

/ 语法只是简写形式

等价写法如下:

plain 复制代码
.item1 {
  grid-column-start: 2;
  grid-column-end: 4;
  grid-row-start: 1;
  grid-row-end: 2;
}

等价于:

plain 复制代码
.item1 {
  grid-column: 2 / 4;
  grid-row: 1 / 2;
}

还有一种"跨越"写法(更直观):

plain 复制代码
.item1 {
  grid-column: 2 / span 2;  /* 从第2条线开始,跨2列 */
  grid-row: 1 / span 1;     /* 跨1行 */
}

这与 2 / 4 完全等价。

因为第2条线 + 跨2列 → 终止线为 2+2 = 4。


常见的几种写法
写法 意义
grid-column: 1 / 3 从第1线到第3线,占第1、2列
grid-column: 2 / span 2 从第2列起,占两列宽度
grid-column: span 3 从当前位置起,占三列
grid-column: 1 自动结束,占一列
grid-column-start: 1; grid-column-end: -1; 从第1线到最后一线,横跨全部列

Grid 的内部机制:一步步分配空间

阶段 1:建立轨道(Tracks)

Grid 把行和列称为 "轨道(track)",由三部分定义:

plain 复制代码
grid-template-columns: 200px 1fr 1fr;
grid-template-rows: 100px auto 50px;

其中:

  • 固定尺寸(px, em)是绝对长度;
  • fr 是"剩余空间分数"(fraction);
  • auto 是内容自适应大小。

此阶段:

浏览器先统计固定尺寸 → 剩余空间再按 fr 比例分配。


阶段 2:放置网格项(Placement)
浏览器此时在解决什么问题?

"我有一堆格子(行列轨道),也有一堆元素(items)。

谁该放在哪个格子里?"

Grid 的放置分为两大类:

显式放置(Explicit Placement)

即开发者手动指定行列坐标:

plain 复制代码
.item1 {
  grid-column: 2 / 4;  /* 从第2列到第4列 */
  grid-row: 1 / 2;     /* 从第1行到第2行 */
}

→ 浏览器直接放入指定单元格区域。

✅ 这类元素优先放置,不会参与自动算法。


自动放置(Auto Placement)

当某些 grid item 没指定行/列位置时,浏览器会这样处理:

Step 1:建立网格轨道

浏览器根据 grid-template-rowsgrid-template-columns 创建初始网格。

Step 2:从左到右、从上到下扫描

浏览器会维护一个"自动放置光标(auto-placement cursor)",

依次在网格中查找空位,把未指定位置的 item 依次放进去。

比如:

plain 复制代码
grid-template-columns: repeat(3, 100px);
grid-template-rows: repeat(2, 100px);

有 6 个格子。

假如你写:

plain 复制代码
<div class="container">
  <div class="item1" style="grid-column: 1 / 3;"></div>
  <div class="item2"></div>
  <div class="item3"></div>
</div>

放置过程为:

  1. item1 占用第1行第1-2列;
  2. 光标跳过被占的格子,从第1行第3列放 item2;
  3. 然后换行,在第2行第1列放 item3。

自动放置行为的控制属性: **grid-auto-flow**

它决定自动放置的方向与密度。

plain 复制代码
grid-auto-flow: row | column | row dense | column dense;
含义
row (默认) 按行填充:从左到右,满一行换行。
column 按列填充:从上到下,满一列换列。
row dense 按行填充,同时尝试"回填"空隙(密集填充模式)。
column dense 按列填充 + 回填。

密集填充模式(Dense Packing)

普通模式下,浏览器"尊重顺序":

  • 即使前面 item 比较大,后面的 item 也不会插到它前面的小空隙中去。

dense 模式 会尽可能填满空隙,不考虑顺序。

例子👇

plain 复制代码
.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-auto-rows: 100px;
  grid-auto-flow: row dense;
}
.item1 { grid-column: span 2; }
.item2 { grid-column: span 3; }
.item3 { grid-column: span 1; }

布局效果:

  • 普通模式:item3 会在 item2 之后;
  • dense 模式:item3 被"回放"进 item1 留下的小空隙中。

阶段 3:计算隐式轨道(Implicit Tracks)
浏览器此时在解决什么问题?

"元素太多,超出了我定义的格子怎么办?"

比如:

plain 复制代码
.container {
  display: grid;
  grid-template-columns: 100px 100px 100px; /* 只有3列 */
}
.container div:nth-child(5) {
  grid-column: 4; /* 第4列并不存在 */
}

这时浏览器会说:

"既然你要第4列,那我得'临时造'一条轨道。"

于是就诞生了"隐式轨道(Implicit Track)"。


显式 vs 隐式轨道
概念 产生方式 定义来源
显式轨道(Explicit) 由开发者定义 grid-template-rows/columns
隐式轨道(Implicit) 由浏览器临时创建 超出定义的范围

隐式轨道的大小由什么决定?

由以下两个属性控制:

plain 复制代码
grid-auto-rows: <length> | <percentage> | auto;
grid-auto-columns: <length> | <percentage> | auto;

例子:

plain 复制代码
.container {
  display: grid;
  grid-template-columns: 100px 100px;
  grid-auto-columns: 200px;
}
.item3 {
  grid-column: 3; /* 会触发创建一个隐式第3列 */
}

此时浏览器:

  1. 检测到你要第3列;
  2. 发现你定义了 grid-auto-columns: 200px
  3. 创建一个新列,宽度 200px。

如果没写 grid-auto-columns,默认是 auto(即根据内容大小决定)。


隐式轨道何时常见?
  • 自动放置时,元素太多导致溢出;
  • 明确指定坐标超出定义;
  • 启用 grid-auto-flow: column 时元素过多。

隐式轨道与"隐式网格"密切相关:Grid 实际上维护了一个 隐式网格表,所有显式 + 隐式轨道共同组成"渲染用的最终网格"。


阶段 4:空间分配算法(Track Sizing Algorithm)

这是整个 Grid 最复杂的阶段,Flex 的伸缩算法只能算它的"一个特例"。

让我们从"浏览器的角度"一步步看它的思考逻辑。


浏览器此时在解决什么问题?

"我已经知道了有多少行、多少列,也知道每条轨道的类型(px、auto、fr)。

那我该如何分配最终的宽高呢?"

这时,浏览器开始执行 W3C 规范中的 轨道尺寸分配算法(Track Sizing Algorithm)。


固定轨道先分配

优先处理固定尺寸轨道,锁死大小。

plain 复制代码
grid-template-columns: 100px 1fr 2fr;

此时:

  • 第一列固定为 100px;
  • 剩余空间准备分配给第二、三列。

自动轨道(auto)根据内容计算

auto 轨道根据"最小内容尺寸(min-content)"或"最大内容尺寸(max-content)"决定初始大小。

通俗解释:

  • min-content:让内容不换行的最小宽度;
  • max-content:让内容完全展开所需宽度。

浏览器先测量内容大小,再决定轨道宽度。

若设置 minmax(100px, auto),会保证至少 100px。


剩余空间分配给 fr 轨道

类似于 Flex 的 flex-growfr 是剩余空间的分数。

计算步骤:

  1. 统计所有固定 + auto 轨道的宽度;
  2. 剩余空间 = 容器宽度 - 已分配宽度;
  3. 再按 fr 权重分配:

例如:

plain 复制代码
grid-template-columns: 200px 1fr 2fr;
  • 固定列:200px;
  • 剩余空间 = 总宽度 - 200px;
  • 第二列 = 剩余空间 × 1/3;
  • 第三列 = 剩余空间 × 2/3。

行高(cross-axis)同理计算

行的算法几乎一样,只是方向换成垂直。
grid-template-rows 也能使用 px、auto、fr、minmax()。


最终阶段:对齐与收尾

在计算完所有轨道大小后,浏览器再执行:

  • 对齐(align-content / justify-content)
  • gap(行列间距)
  • 溢出修正(overflow)

最终生成一个精确的二维坐标网格,每个格子都能映射到具体像素位置。

相关推荐
中微子3 小时前
别再被闭包坑了!React 19.2 官方新方案 useEffectEvent,不懂你就 OUT!
前端·javascript·react.js
呼啦啦嘎嘎3 小时前
rust中的生命周期
前端
岁月宁静3 小时前
前端添加防删除水印技术实现:从需求拆解到功能封装
前端·vue.js·人工智能
1in3 小时前
一文解析UseState的的执行流程
前端·javascript·react.js
隐林3 小时前
如何使用 Tiny-editor 快速部署一个协同编辑器
前端
Mintopia3 小时前
🧠 对抗性训练如何增强 WebAI 模型的鲁棒性?
前端·javascript·人工智能
恋猫de小郭3 小时前
Flutter 在 iOS 26 模拟器跑不起来?其实很简单
android·前端·flutter
北城笑笑3 小时前
Git 10 ,使用 SSH 提升 Git 操作速度实践指南( Git 拉取推送响应慢 )
前端·git·ssh
FreeBuf_3 小时前
攻击者利用Discord Webhook通过npm、PyPI和Ruby软件包构建隐蔽C2通道
前端·npm·ruby