1. 从一维到二维:为什么我最终把布局方案全换成了 Grid
做了七八年前端,我经历过表格布局、浮动布局、绝对定位布局、Flexbox 布局,每一次布局方案的更迭都伴随着一堆“祖传代码”的重写。早些年用float加clearfix清浮动,后来 Flexbox 出来之后,一维排列的问题基本被解决了,但真正让我觉得“布局这件事终于可以像画图纸一样直观”的,是display: grid。
Grid 网格布局是 CSS 里第一个真正意义上的二维布局系统。它同时处理行和列,把页面切成一个个格子,然后你把元素往格子里放就行。Flexbox 解决的是一维问题——要么横着排,要么竖着排;而 Grid 解决的是二维问题——横竖同时控制。这个区别听起来简单,但实际用起来,它改变的是整个布局的思维方式。
我最初接触 Grid 是在做一个后台管理系统的仪表盘页面。那个页面有侧边栏、顶部导航、主内容区、底部信息栏,主内容区里还要放一堆卡片,卡片大小不一,有的占两列,有的占一行半。用 Flexbox 写的时候,嵌套了四五层,每层都要调flex-basis、flex-grow、margin,改一个地方就牵一发动全身。后来换成 Grid,整个页面结构用grid-template-areas一画,代码量直接砍掉一半,而且可读性极强——你一眼就能看出哪个区域在哪个位置。
这篇文章我想聊的不是“Grid 是什么”这种教科书式的内容,而是我在实际项目里怎么用 Grid、为什么这么用、踩过哪些坑、哪些场景下 Grid 比 Flexbox 更合适、哪些场景下反而应该用 Flexbox。如果你正在纠结布局方案选型,或者已经用了 Grid 但总觉得没用到点子上,那这篇内容应该能帮你省不少时间。
2. 核心概念拆解:Grid 的思维方式和关键术语
2.1 网格容器与网格项:父子关系的重新定义
Grid 的核心逻辑非常直白:你给一个父元素设置display: grid或display: inline-grid,这个父元素就变成了网格容器,它的直接子元素自动变成网格项。注意,只有直接子元素才是网格项,孙元素不是。这一点和 Flexbox 一样,但很多人第一次用的时候会搞混。
.dashboard { display: grid; grid-template-columns: 240px 1fr; grid-template-rows: 64px 1fr 48px; }上面这段代码定义了一个两列三行的网格。第一列固定 240px,第二列占剩余空间;第一行 64px,第二行占剩余空间,第三行 48px。就这么几行代码,一个经典的后台布局骨架就出来了。换成 Flexbox,你得写嵌套容器、设置flex-direction、处理flex值,代码量至少翻倍。
这里有个容易被忽略的点:网格容器本身的display值会影响到子元素的默认行为。比如你设置display: grid之后,子元素的float、clear、vertical-align这些属性会失效。这不是 bug,是规范这么定的。我当初从浮动布局迁移到 Grid 的时候,发现有些元素的float: left突然不生效了,排查了半天才想起来这回事。
2.2 显式网格与隐式网格:你画的格子和你没画的格子
Grid 里有一个很重要的区分:显式网格和隐式网格。显式网格是你用grid-template-columns和grid-template-rows明确定义的格子。隐式网格是当你的内容超出了你定义的格子范围时,浏览器自动帮你生成的格子。
.grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 120px; }这段代码定义了三列,但没有定义行数。如果你往里面放 10 个元素,浏览器会自动生成 4 行(前 3 行每行 3 个,第 4 行 1 个)。这些自动生成的行高度由grid-auto-rows控制,默认是auto。
我踩过的一个坑是:隐式网格的行高如果不设置,内容多的时候行高会撑得很大,内容少的时候又塌下去,导致整个网格看起来参差不齐。后来我养成了一个习惯——只要用了 Grid,就一定设置grid-auto-rows,哪怕设成minmax(100px, auto)也比不设强。
2.3 网格线、网格轨道与网格区域:定位的三层逻辑
Grid 的定位体系分三层:网格线是最细粒度的,你可以把元素放在第几条线到第几条线之间;网格轨道是行或列本身;网格区域是由四条网格线围成的一个矩形区域。
.item { grid-column: 2 / 4; grid-row: 1 / 3; }这段代码的意思是:这个元素从第 2 条列线开始,到第 4 条列线结束(也就是跨两列),从第 1 条行线开始,到第 3 条行线结束(跨两行)。网格线的编号从 1 开始,也可以从 -1 开始倒数。
我个人的经验是:能用grid-template-areas就用它,实在不行再用网格线编号。因为网格线编号在复杂布局里很容易数错,尤其是列数多的时候。grid-template-areas用字符串画图,直观得多:
.layout { display: grid; grid-template-areas: "header header" "sidebar main" "footer footer"; grid-template-columns: 240px 1fr; grid-template-rows: 64px 1fr 48px; } .header { grid-area: header; } .sidebar { grid-area: sidebar; } .main { grid-area: main; } .footer { grid-area: footer; }这种写法几乎就是“所见即所得”,维护起来非常舒服。唯一需要注意的是,grid-template-areas里的每个字符串必须长度一致,否则整个声明会失效。我当初就因为少写了一个空格导致整个布局崩了,排查了十几分钟。
3. 实操全流程:从零搭建一个响应式仪表盘布局
3.1 需求拆解与布局规划
假设我们要做一个仪表盘页面,需求是这样的:
- 顶部导航栏,固定高度,横跨整个页面宽度
- 左侧边栏,固定宽度,高度撑满剩余空间
- 右侧主内容区,占剩余宽度
- 主内容区里有一组卡片,桌面端每行 3 个,平板端每行 2 个,手机端每行 1 个
- 底部信息栏,固定高度,横跨整个页面宽度
这个需求用 Grid 来做非常合适,因为整体是二维布局,卡片区域也是二维布局。如果用 Flexbox,整体布局需要嵌套,卡片区域还需要计算宽度百分比,维护起来麻烦。
我的规划是:外层用一个 Grid 容器处理整体布局,内层卡片区域再用一个 Grid 容器处理卡片排列。两层 Grid 各司其职,互不干扰。
3.2 外层布局:用 grid-template-areas 画骨架
.dashboard { display: grid; grid-template-areas: "header header" "sidebar main" "footer footer"; grid-template-columns: 240px 1fr; grid-template-rows: 64px 1fr 48px; min-height: 100vh; gap: 0; } .dashboard__header { grid-area: header; background: #1a1a2e; color: #fff; display: flex; align-items: center; padding: 0 24px; } .dashboard__sidebar { grid-area: sidebar; background: #16213e; color: #ccc; padding: 16px; overflow-y: auto; } .dashboard__main { grid-area: main; background: #f5f5f5; padding: 24px; overflow-y: auto; } .dashboard__footer { grid-area: footer; background: #1a1a2e; color: #888; display: flex; align-items: center; justify-content: center; font-size: 12px; }这里有几个关键点值得说明:
第一,grid-template-rows的第二行用了1fr,这意味着主内容区会自动撑满剩余高度。配合min-height: 100vh,整个页面至少占满一屏,内容多的时候主内容区内部滚动,不会把整个页面撑长。
第二,gap: 0是我特意设置的。默认情况下 Grid 的gap是 0,但有些重置样式表可能会改这个值,所以我习惯显式声明。如果你想要区域之间有间距,可以设成gap: 8px之类的值。
第三,侧边栏和主内容区都设置了overflow-y: auto。这是为了防止内容过长时把整个网格撑变形。Grid 的轨道尺寸是优先于内容尺寸的,如果你不设置溢出处理,内容可能会溢出轨道,导致布局错乱。
3.3 内层卡片布局:auto-fit 与 minmax 的黄金组合
卡片区域的布局我用了一个非常经典的 Grid 写法:
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; }这一行代码解决了响应式卡片布局的核心问题。auto-fit会根据容器宽度自动计算能放几列,minmax(280px, 1fr)表示每列最小 280px,最大占满剩余空间。当容器宽度不足以放下 280px 的列时,列数自动减少。
这个组合的好处是:你不需要写任何媒体查询,卡片布局就能自适应。桌面端宽屏可能一行 4 个,笔记本一行 3 个,平板一行 2 个,手机一行 1 个。浏览器自动算,你只管写业务逻辑。
不过这里有个细节需要注意:auto-fit和auto-fill的区别。auto-fit会把空轨道折叠掉,让现有元素拉伸填满容器;auto-fill会保留空轨道,元素不会拉伸。大多数情况下我们用auto-fit,因为不希望卡片之间出现莫名其妙的空白。
3.4 响应式断点处理:Grid 与媒体查询的配合
虽然auto-fit加minmax已经能处理大部分响应式场景,但有些布局变化是它搞不定的。比如在手机端,侧边栏需要从左侧移到顶部,或者直接隐藏。这时候就需要媒体查询配合 Grid 的重新定义。
@media (max-width: 768px) { .dashboard { grid-template-areas: "header" "main" "footer"; grid-template-columns: 1fr; grid-template-rows: 56px 1fr 40px; } .dashboard__sidebar { display: none; } }这段代码在 768px 以下把布局从两列变成一列,侧边栏直接隐藏。注意grid-template-areas里的字符串也要相应调整,否则会报错。我见过有人只改了grid-template-columns没改grid-template-areas,结果布局完全乱掉。
如果你不想隐藏侧边栏,而是想把它变成顶部的一个横向导航,可以这样写:
@media (max-width: 768px) { .dashboard { grid-template-areas: "header" "sidebar" "main" "footer"; grid-template-columns: 1fr; grid-template-rows: 56px auto 1fr 40px; } .dashboard__sidebar { display: flex; overflow-x: auto; padding: 8px 16px; } }这种写法在移动端很常见,侧边栏变成横向滚动的标签栏,既保留了导航功能,又不占用垂直空间。
4. 进阶技巧与性能优化:那些文档里不会写的经验
4.1 网格项的对齐控制:justify-self 与 align-self 的实战用法
Grid 提供了六个对齐属性:justify-items、align-items、justify-content、align-content、justify-self、align-self。前四个作用于容器,后两个作用于网格项。
我实际项目里用得最多的是align-self和justify-self。比如在一个卡片网格里,有些卡片内容少,默认会被拉伸到和同行最高的卡片一样高。如果你不希望拉伸,可以给卡片设置align-self: start。
.card { align-self: start; }这样卡片高度就由内容决定,不会被同行其他卡片撑高。但要注意,这样会导致同行卡片高度不一致,视觉上可能不太整齐。我的经验是:如果卡片背景色明显,建议拉伸对齐;如果卡片是透明背景或者只有边框,可以不拉伸。
另一个常用的是justify-content和align-content。当网格轨道总尺寸小于容器尺寸时,这两个属性控制轨道在容器内的分布。比如你定义了三列固定宽度,容器很宽,轨道默认靠左排列,设置justify-content: center就能让它们居中。
.grid { display: grid; grid-template-columns: repeat(3, 200px); justify-content: center; align-content: center; height: 100vh; }这个写法在做登录页、错误页这种需要居中的场景时特别好用,比 Flexbox 的居中写法更直观。
4.2 网格轨道的灵活定义:fr、min-content、max-content 怎么选
Grid 的轨道尺寸单位有好几种,最常用的是fr、px、%、auto、min-content、max-content。我逐个说一下实际使用感受。
fr是 Grid 特有的弹性单位,表示剩余空间的比例。比如grid-template-columns: 1fr 2fr,表示第二列是第一列的两倍宽。fr的计算逻辑是:先减去所有固定尺寸和间距,剩下的空间按fr比例分配。如果所有轨道都是fr,那就按比例平分。
auto比较特殊,它的行为取决于内容。如果轨道里有内容,auto会尽量容纳内容;如果没有内容,auto会变成 0。我一般用auto来做“内容决定宽度”的列,比如标签栏、按钮组。
min-content表示轨道的最小宽度,通常是内容中最宽的那个单词或不可断行的元素。max-content表示轨道的最大宽度,即内容不换行时的宽度。这两个值在实际项目里用得不多,但在做表格类布局时很有用。
.grid { display: grid; grid-template-columns: min-content 1fr max-content; }这个写法可以实现“左侧列宽度由内容决定,中间列占剩余空间,右侧列宽度也由内容决定”的效果。比用 Flexbox 的flex-shrink: 0和flex-grow: 1组合更直观。
4.3 网格项的重叠与层叠:grid-area 的隐藏玩法
Grid 允许网格项重叠,这是 Flexbox 做不到的。你只需要把多个元素放在同一个网格区域里,它们就会重叠。配合z-index可以控制层叠顺序。
.overlay-grid { display: grid; grid-template-columns: 1fr; grid-template-rows: 1fr; } .overlay-grid > * { grid-area: 1 / 1 / 2 / 2; }这段代码让所有子元素都占据同一个格子,实现重叠效果。我在做图片叠加文字、加载遮罩、模态框背景时经常用这个技巧。比用position: absolute更简洁,因为不需要给父元素设position: relative,也不需要计算偏移量。
不过要注意,重叠的元素默认按 DOM 顺序层叠,后面的盖住前面的。如果你需要控制层叠顺序,还是要用z-index。另外,重叠区域的大小由最大的那个元素决定,如果两个元素尺寸差异很大,小的那个可能会被拉伸,需要配合align-self和justify-self调整。
4.4 性能考量:Grid 会不会比 Flexbox 慢
这个问题我被问过很多次。实测下来,在大多数场景下,Grid 和 Flexbox 的性能差异可以忽略不计。浏览器的布局引擎对两者的优化都很好,除非你在一个页面里放几千个网格项,否则感受不到区别。
真正影响性能的不是 Grid 本身,而是网格轨道的计算复杂度。如果你用了大量auto轨道、min-content、max-content,浏览器需要先计算内容尺寸再确定轨道尺寸,这会触发额外的布局计算。相比之下,固定尺寸和fr的计算要快得多。
我的建议是:能用固定尺寸或fr就用,尽量少用auto和内容相关的尺寸单位。如果确实需要内容决定尺寸,考虑用minmax()给一个范围,避免无限计算。
另外,grid-template-areas虽然写起来舒服,但浏览器解析字符串也需要一点开销。不过这个开销是一次性的,不会在每次重排时都发生,所以不用担心。
5. 常见问题与排查技巧实录
5.1 网格项溢出轨道怎么办
这是 Grid 新手最容易遇到的问题:网格项的内容太多,超出了轨道尺寸,导致布局错乱。比如你定义了一个 200px 宽的列,但里面的文字很长,把列撑到了 300px。
原因在于:Grid 轨道的默认最小尺寸是auto,而auto的最小值是min-content。也就是说,轨道至少会容纳内容的最小宽度。如果你希望轨道严格保持你定义的尺寸,需要设置min-width: 0或min-height: 0。
.grid-item { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }这个组合可以让网格项在轨道内截断,而不是撑开轨道。我当初做表格布局的时候,就是因为没设min-width: 0,导致某一列被长文本撑爆,排查了好久。
5.2 gap 在旧版浏览器上的兼容处理
gap属性在 Grid 里支持得很好,但在 Flexbox 里支持得比较晚。如果你需要兼容较老的浏览器,可能需要用margin来模拟间距。
不过现在主流浏览器对gap的支持已经非常好了,我个人建议直接用gap,不要为了兼容性牺牲代码可读性。如果确实需要兼容,可以用@supports做特性检测:
.grid { display: grid; grid-template-columns: repeat(3, 1fr); } @supports (gap: 1px) { .grid { gap: 16px; } } @supports not (gap: 1px) { .grid { margin: -8px; } .grid > * { margin: 8px; } }这种写法虽然麻烦,但能保证在老浏览器上也有可接受的布局效果。
5.3 网格项顺序与 DOM 顺序不一致的坑
Grid 允许你用order属性改变网格项的视觉顺序,但这会带来一个严重的可访问性问题:键盘导航顺序和视觉顺序不一致。屏幕阅读器用户和键盘用户会按照 DOM 顺序浏览,而不是视觉顺序。
我踩过这个坑:在一个表单布局里,我用order把提交按钮移到了视觉上的最后,但 DOM 里它在中间。结果键盘用户按 Tab 键的时候,焦点会先跳到按钮再跳到后面的字段,体验很割裂。
我的建议是:尽量不要用order改变布局顺序。如果确实需要调整顺序,优先调整 DOM 结构。Grid 的grid-area已经能让你在不改变 DOM 顺序的情况下调整位置,没必要再用order。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 网格项撑破轨道 | 轨道最小尺寸为 auto | 给网格项设min-width: 0或min-height: 0 |
| grid-template-areas 失效 | 字符串长度不一致 | 检查每行字符串的列数是否相同 |
| 隐式网格行高失控 | 未设置 grid-auto-rows | 设置grid-auto-rows: minmax(100px, auto) |
| 网格项不换行 | 列数定义太少 | 用repeat(auto-fit, minmax(...))自动计算列数 |
| gap 不生效 | 浏览器不支持或写法错误 | 检查浏览器版本,确认 gap 写在容器上 |
| 网格项重叠顺序不对 | 默认按 DOM 顺序层叠 | 用z-index控制层叠顺序 |
| 响应式断点布局错乱 | 只改了列定义没改区域定义 | 同步修改grid-template-areas和grid-template-columns |
6. 我的实操心得与场景选型建议
6.1 Grid 和 Flexbox 到底怎么选
这个问题我被问过无数次。我的判断标准很简单:看布局是一维还是二维。
如果只是横着排一排按钮,或者竖着排一个列表,用 Flexbox。如果是一个需要同时控制行和列的复杂布局,用 Grid。如果两者都能做,优先用 Grid,因为 Grid 的代码通常更简洁。
但有一个例外:如果布局是内容驱动的,用 Flexbox 更合适。比如导航栏,你不知道有多少个菜单项,每个菜单项宽度也不固定,用 Flexbox 的flex: 1或者flex: 0 0 auto更自然。Grid 更适合结构驱动的布局,也就是你事先知道有几行几列。
我实际项目里经常混用两者:外层用 Grid 搭骨架,内层用 Flexbox 排列内容。这样各取所长,代码也清晰。
6.2 从旧布局迁移到 Grid 的注意事项
如果你手头有老项目要迁移到 Grid,我有几个建议:
第一,不要一次性全改。Grid 和旧布局可以共存,你可以先在新页面上用 Grid,老页面保持不动。等新方案验证稳定了,再逐步迁移。
第二,注意浮动和清除浮动的影响。Grid 容器内的子元素,float会失效。如果你迁移的时候发现某个元素位置不对,先检查是不是有float残留。
第三,注意百分比宽度的变化。在 Grid 里,百分比是相对于网格轨道计算的,不是相对于容器。如果你原来用width: 50%做两列布局,迁移到 Grid 后应该改成grid-template-columns: 1fr 1fr,而不是继续用百分比。
第四,测试键盘导航和屏幕阅读器。Grid 的视觉顺序和 DOM 顺序可能不一致,迁移后一定要用键盘走一遍,确保焦点顺序合理。
6.3 一个让我印象深刻的调试经历
有一次我做一个商品列表页,用了repeat(auto-fit, minmax(250px, 1fr))来做响应式卡片。在大多数屏幕上表现都很好,但在某个特定宽度下,卡片突然变成了两列,而且第二列明显比第一列宽。
排查了半天,发现原因是minmax(250px, 1fr)里的250px和容器的padding冲突了。容器有 24px 的左右内边距,实际可用宽度是容器宽度减去 48px。当容器宽度刚好在某个临界点时,浏览器计算出的列数和1fr的分配比例出现了偏差。
解决方法很简单:把minmax的最小值调小一点,比如minmax(220px, 1fr),给内边距留出余量。或者用calc()动态计算:
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr)); gap: 16px; }这里用了min(250px, 100%),意思是取 250px 和 100% 中较小的那个。这样在容器很窄的时候,列宽不会超过容器宽度,避免了溢出。这个技巧是我踩了好几次坑之后才总结出来的,非常实用。
6.4 关于 Grid 的未来展望
Grid 的规范还在演进,一些新特性已经在路上了。比如subgrid,允许子网格继承父网格的轨道定义,这对嵌套布局来说是个巨大的改进。还有masonry布局,也就是瀑布流,虽然目前还在讨论阶段,但一旦落地,Grid 的能力又会上一个台阶。
不过就目前而言,Grid 已经足够强大,能覆盖 95% 以上的布局需求。我的建议是:先把现有特性用熟,不要追新。等新特性在主流浏览器上稳定了再考虑使用。
如果你还没在项目里用过 Grid,我强烈建议从下一个新页面开始尝试。不用全站迁移,就从一个简单的卡片列表或者后台布局开始。用上一次,你就会明白为什么我说“布局这件事终于可以像画图纸一样直观”了。