
这个三栏布局的问题但凡参加过前端面试十有八九都撞见过。它不仅仅是问你会不会写代码而是通过一个看似简单的布局把你的 CSS 基本功、对文档流的理解、对边界情况的处理能力全摸一遍。什么“圣杯布局”“双飞翼布局”“Flex 布局”听名字挺唬人其实就是三种实现“左右两栏固定宽度、中间一栏自适应”的经典方案。这篇东西我打算换个聊法不整那些教科书式的定义直接把三种方案的代码掰开揉碎了讲告诉你每一步为什么这么写、坑在哪里、真实项目中到底该用哪种。无论你是准备面试的新人还是写业务写烦了想回头补补基础的老手都能在这篇里找到点有用的东西。1. 先搞明白三栏布局到底在解决什么问题在动手写代码之前我必须先把这个问题本身拆清楚。三栏布局描述的是页面里最常见的一种骨架左侧一栏固定宽度通常是导航或者辅助信息右侧一栏固定宽度通常是工具栏或者广告位中间一栏是主体内容宽度要能跟随浏览器窗口大小自适应变化。这个诉求在 PC 端的后台管理系统、资讯门户、电商列表页里极其常见。听起来很朴素但难度跟着一个关键约束条件暴涨中间主内容区的 DOM 结构必须放在最前面。为什么因为网页是从上到下解析渲染的把核心内容放到 HTML 最前面在 CSS 还没加载完或者加载失败的时候用户也能先看到文字内容这对体验和 SEO 都有好处。这就意味着你不能简单地把三个 div 按“左中右”顺序写进 HTML再让它们排成一行——中间那个 div 在 HTML 里是第一个却要显示在视觉中央这本身就和正常的文档流冲突了。这种“DOM 顺序和视觉顺序不一致”的需求才是圣杯布局和双飞翼布局存在的根本原因。它们的核心难点就是如何把一个排在文档流第一个的元素稳稳地放在视觉中间而且不挡住两边的列。理清了这个背景后面看代码就不会觉得是在背魔板了你是在理解一套空间交换的规则。2. 圣杯布局老派但原理最硬核的方案圣杯布局被称作“CSS 布局历史上的经典之作”它的思路非常巧妙用到的核心技术点有三个浮动、负 margin 和相对定位。这套组合拳打下来能让你对 CSS 的理解上一个台阶。2.1 核心代码与 DOM 结构先看结构注意看 HTML 的顺序div classcontainer div classcolumn main中间主内容区/div div classcolumn left左侧栏/div div classcolumn right右侧栏/div /div所有的子元素都在同一个父容器内中间栏在最前面这满足了我们说的核心约束。再看 CSS 的完整实现.container { padding: 0 200px; /* 为左右两栏预留位置 */ } .column { float: left; /* 三兄弟全部左浮动 */ } .main { width: 100%; /* 中间栏占据父容器全部宽度 */ background: #e8e8e8; } .left { width: 200px; margin-left: -100%; /* 关键向左移动一个父容器宽度 */ position: relative; left: -200px; /* 再往左推一个左栏宽度 */ background: #d0e4f6; } .right { width: 200px; margin-left: -200px; /* 向左移动自身宽度 */ position: relative; left: 200px; /* 再往右推一个右栏宽度 */ background: #f6d0d0; }这里每一步都有讲究我们拆开来看。2.2 负 margin 的魔法百分数与像素的区别很多初学者最懵的就是margin-left: -100%和margin-left: -200px这俩到底怎么算的。先说结论负 margin 的百分比值是相对于父容器内容区的宽度计算的而像素值就是它自己。当三个 .column 都设置为float: left后它们会在一行里依次排列。但 .main 的宽度是 100%占满了整行所以 .left 和 .right 默认被挤到了第二行——这显然不是我们要的效果。此时给 .left 加上margin-left: -100%它的含义是“把自身向左移动父容器内容宽度的 100%”。父容器 .container 在应用 padding 之后内容区宽度是浏览器视口宽度减去左右 400px。如果一个元素要向左移动 100% 的内容区宽度正好就把自己从第二行拽回了第一行的起始位置也就是 .main 的左侧起点此时 .left 完整地覆盖在 .main 上面。同理.right 不需要移动一个整行它只需要向左移动自身宽度 200px就能从第二行被拽到第一行的最右侧——也就是和 .main 的右边缘对齐但还是覆盖在 .main 上面。到这里两个侧栏都悬在中间栏上面还没有“让位”。2.3 relative 定位的补位作用接下来就是圣杯布局和双飞翼布局分道扬镳的地方。圣杯布局选择给左右两栏加上position: relative和left偏移.left 向左挪 200px正好落到父容器左侧 padding 区域里.right 向右挪 200px正好落到右侧 padding 区域里。于是三栏各就各位中间栏、左栏、右栏以一种非常优雅的姿态同时出现在正确的位置。刚才提到父容器 .container 设置了padding: 0 200px这个 padding 区域就是专门给两栏腾出来的“舱位”。为什么不直接给左右两栏留出 margin因为中间栏宽度是 100%它占据的是父容器内容区内容区只算 padding 以内的宽度。所以只有先通过 padding 把内容区的宽度压缩中间栏宽度再取 100% 时两边的空位才会被真正预留出来。这套方案的精妙之处在于负 margin 负责“把侧栏拉回第一行”relative 定位负责“把侧栏推进舱位”。两步分工明确一步都不能少。整个过程没有任何 JavaScript没有绝对定位的脱离文档流问题纯粹依赖 CSS 规则之间的相互作用实现布局这放在十几年前是非常惊艳的。2.4 注意了圣杯布局的软肋我的建议是学习圣杯布局一定要亲自动手写一遍感受负 margin 和定位的协作关系。但真实项目里如果要选这套方案你得清楚它有个著名的短板当浏览器窗口窄到一定程度中间栏宽度小于左栏宽度时左右两栏会重叠在一起整个布局会乱掉。这是因为负 margin 的计算依赖父容器宽度而整个布局有个最小宽度阈值。如果用圣杯布局通常要给 body 设置一个min-width比如左右两栏宽度之和加一些余量body { min-width: 600px; /* 避免窗口过窄导致布局错乱 */ }但设了 min-width 之后窗口小于这个宽度就会出现横向滚动条移动端体验是一言难尽的。所以圣杯布局在当年是桌面端浏览器的产物放到今天它的职位基本被双飞翼布局和 Flex 布局替代了。3. 双飞翼布局更稳的改良版方案双飞翼布局是对圣杯布局的改良它放弃了 relative 定位转而通过给中间栏内部再套一层 div用内边距来给两侧腾位置。这个改动带来了一个关键优势当窗口变窄时布局不会轻易崩溃。3.1 多一层嵌套带来的改变看下结构div classcontainer div classcolumn main div classmain-content中间主内容区/div /div div classcolumn left左侧栏/div div classcolumn right右侧栏/div /div注意中间栏 .main 内部多了一个 .main-content。CSS 这样写.column { float: left; } .main { width: 100%; } .main-content { margin: 0 200px; /* 给左右栏让出空间 */ } .left { width: 200px; margin-left: -100%; background: #d0e4f6; } .right { width: 200px; margin-left: -200px; background: #f6d0d0; }对比一下就能看到双飞翼布局去掉了padding和position: relative父容器无需做任何处理真正干活的是中间栏内部那个 .main-content 的 margin。这里的关键逻辑是.left 通过margin-left: -100%拉回第一行后它仍然覆盖在 .main 上面。但 .main-content 是 .main 的子元素它的 margin 是相对于 .main 的内容区计算的左右各 200px 的 margin 会把内容往中间挤。由于 .main 本身就是第一个元素、排在最左边所以 .main-content 的左侧空出来的 200px正好够 .left 展示右侧空出来的 200px正好够 .right 展示。在多出来的窝棚里圣杯布局用的是父容器 padding侧栏像住进“外屋”一样需要 relative 把侧栏“放进”窝棚双飞翼布局则是在中间栏这个房间里再砌一道“内墙”也就是 .main-content 的 margin侧栏就住在这道内墙和房间外壁之间。3.2 双飞翼的优势和适用场景双飞翼布局最大的优势是无 relative 依赖布局更稳。当窗口变窄时中间栏缩小.main-content 的内容区也跟着缩小但它依然有 200px 的左右 margin 保护左右两栏不会和文字内容重叠。相比圣杯布局它把“内容区”和“视觉区”解耦了这种稳定性正是日常开发更看重的。不过代价也很明显多了一层无意义的嵌套 div。在讲究语义化和精简 DOM 的今天这种为了布局而存在的空壳标签确实不算优雅。但从兼容性角度讲双飞翼布局只要浏览器支持float、margin为负值就能工作IE6 时代都能跑得很欢。在那个 Flex 和 Grid 还没影子的年代双飞翼布局是兼容性和稳定性最佳平衡的选择。现在做项目如果还坚持不用 Flex那我通常推荐双飞翼布局而不是圣杯。它在响应式方面的宽容度更高少踩一个布局崩掉的坑。4. Flex 布局现代实现的最优解如果今天有人问我新项目里三栏布局用什么方案我的回答只有一个默认 Flex除非你明确知道自己为什么不能用。Flex 布局把三栏问题从“技巧活”变成了“常规操作”代码量直接少一半心智负担也小得多。4.1 最简洁的实现方式直接上代码div classcontainer div classcolumn main中间主内容区/div div classcolumn left左侧栏/div div classcolumn right右侧栏/div /div这个 DOM 结构跟圣杯布局一模一样不需要额外嵌套.container { display: flex; } .column { /* 无需 float */ } .main { flex: 1; /* 中间栏占据所有剩余空间 */ order: 2; /* 视觉上排第二 */ background: #e8e8e8; } .left { width: 200px; order: 1; /* 视觉上排第一 */ background: #d0e4f6; } .right { width: 200px; order: 3; /* 视觉上排第三 */ background: #f6d0d0; }就这三板斧搞定。三行关键代码分别解释一下display: flex让容器变成 flex 格式化上下文子元素默认沿主轴默认水平方向排列自动成为 flex item。width: 200px固定左右两栏宽度。flex: 1是flex-grow: 1; flex-shrink: 1; flex-basis: 0%的简写让中间栏吃掉所有剩余空间实现宽度自适应。order控制视觉顺序让 DOM 里排第一的 .main 在视觉上排第二。4.2 order 排序的真面目很多人对order有个误解以为它是用来排版的魔法属性。其实它只改变视觉顺序完全不改变 DOM 顺序也不影响键盘导航、屏幕阅读器和爬虫的顺序。也就是说专注模式的读屏软件会先读到 .main 的内容但视觉上它显示在中间这正好满足我们开头说的“核心内容优先渲染”的需求而且比负 margin 和定位的实现方式健康得多。举个例子一个爬虫或者屏幕阅读器按照 DOM 顺序去解读页面时首先碰到的是 .main 而不是 .leftSEO 和可访问性都很友好。这在传统布局里要靠很别扭的代码顺序才能实现在 Flex 里一个 order 属性就搞定了。不过要提醒一句order 属性虽然好使但也别滥用。用 order 改变视觉顺序时如果页面上还有 tab 键盘导航用户的视觉焦点跳跃顺序和实际阅读顺序会不一致这在复杂的交互页面里容易造成困扰。三栏布局这种简单场景没问题但把它当成万能药就危险了。4.3 flex: 1 背后的三兄弟关于flex: 1我见到太多人只会写不会讲。它其实等于.main { flex-grow: 1; flex-shrink: 1; flex-basis: 0%; }三个属性各管一摊事flex-grow: 1表示当容器有剩余空间时中间栏按比例瓜分。容器里其他元素没有设置 grow所以全部剩余空间都归它。flex-shrink: 1表示当空间不足时中间栏允许被压缩。左右两栏也是默认 1但如果你希望某个侧栏在极端窄屏下不被压没可以改成flex-shrink: 0。flex-basis: 0%表示在分配空间之前中间栏的基础尺寸是 0。之后 grow 分到的空间才是它真正的宽度这样能保证所有主轴空间都参与分配不会因为基础尺寸占位导致计算偏差。如果把flex-basis改成auto那么中间栏会先按内容宽度占位然后再加上 grow 分到的空间。在文字很多的场景下这个计算结果是中间栏实际宽度是内容宽度加剩余空间可能比 0% 方案显得更宽一点影响了想要的“真正自适应”效果。所以我写 flex 布局三栏时中间栏总是用flex: 1简写让 basis 变成 0%这样计算最简单、控制最精确。4.4 响应式场景下的 Flex 调整Flex 布局另一个好处是响应式改造非常顺手。假设 768px 以下我们希望左右两栏折叠到下方代码改动非常小media (max-width: 768px) { .container { flex-wrap: wrap; } .left, .right { width: 50%; } .main { flex-basis: 100%; order: 1; /* 内容最先显示 */ } }三行媒体查询就完成从 PC 三栏到移动端“内容优先 两栏侧栏堆叠”的转换。这在圣杯布局和双飞翼布局里是不可想象的——那套体系里改响应式基本等于重写。这也是为什么我说 Flex 是现代项目的主流答案它不是“能实现”的问题而是“实现得最轻松、最可持续”的问题。5. 三套方案的横向对比与选型建议前面代码细节讲完了很多人会问那我到底该用哪个我做一张表把三套方案的维度直接摊开看对比维度圣杯布局双飞翼布局Flex 布局兼容性IE6IE6IE10需加前缀IE9 及以下不支持实现复杂度较高负 margin relative 配合中等负 margin 嵌套层低几乎零心智负担中间内容优先支持DOM 顺序第一支持DOM 顺序第一支持配合 order窗口过窄时容易重叠、布局崩溃相对稳定配合 wrap 或压缩稳定DOM 结构无额外嵌套需多一层中间内容 div无额外嵌套自适应扩展性差改响应式基本重写中等很好前后端同学都好改维护难易负 margin 原理难懂后期易被改坏原理稍好懂但依然是 hack语义清晰代码即解释从这张表能明显看出Flex 在绝大多数维度上都占优。那圣杯和双飞翼是不是就该被彻底扔进历史堆也不是。我个人的选型逻辑是这样的如果项目有硬性的 IE8/IE9 兼容需求而且你不能引入 polyfill那只能从圣杯和双飞翼里选我优先选双飞翼因为它稳定性更好。如果项目是内部后台系统用户浏览器固定为现代 Chrome那毫不犹豫用 Flex。如果是个博客或资讯站对 SEO 和首屏内容提取比较敏感Flex order 的结构依然是很优的选择。如果团队成员对 CSS 基础不扎实避免用圣杯那种“少一层嵌套但原理诡谲”的方案选双飞翼或 Flex代码不容易被后人改崩。注意就算你决定用 Flex我也建议你把圣杯布局的原理吃透。因为面试官问三栏布局大概率不是真想让你写三种方案而是想看你对 float、负 margin、定位、BFC 这些底层概念有没有真正理解。这些知识在排查复杂 bug 的时候会让你有完全不同的视角。该背的八股要背但思想更要懂。6. 实战中的常见问题与排查技巧这里我整理几个自己在实际项目里踩过的坑每一个都是能让你排查半天的问题建议收藏着看。6.1 为什么左栏的 margin-left 用百分比右栏却用像素这是第一次接触圣杯或双飞翼时最常困惑的点。左栏要回到第一行的起始位置和它自身宽度无关而是和“一行有多宽”有关所以用百分比等值于父容器内容区 100% 宽度。右栏要贴到中间栏右侧只需要移动自身宽度所以用像素值 -200px。如果右栏也用百分比它会跑到更远的位置布局就歪了。6.2 浮动元素父容器高度塌了怎么办圣杯布局和双飞翼布局都依赖 float而 float 元素默认不撑开父容器高度这会导致父容器下面的兄弟元素顶上来。常规解法是给 .container 加上清除浮动.container::after { content: ; display: block; clear: both; }或者给 .container 设置overflow: hidden一样能触发 BFC让父容器包裹住浮动子元素。不过用 overflow 的时候要注意如果容器内部有下拉菜单、气泡提示这类需要溢出显示的元素会被裁剪掉。所以更安全的是用伪元素 clear 方案。6.3 圣杯布局窗口变窄后左右重叠怎么救前面说过这是圣杯布局的固有缺陷。如果你已经用了圣杯布局最简单的兜底是给 body 设置 min-widthbody { min-width: 600px; }但更好的做法是直接换双飞翼或者 Flex不要在错误的路上越走越远。我见过一个老项目圣杯布局加了一堆媒体查询去补各种窄屏漏洞最后代码丑到无法维护这种“硬扛”的性价比太低了。6.4 Flex 布局下左右两栏被压缩怎么办Flex 子项的默认flex-shrink: 1表示空间不足时都会被压缩。如果中间内容很多容器宽度不够左右两栏可能被挤得比设定的 200px 还窄。解决办法是给左右两栏显式加上flex-shrink: 0.left, .right { width: 200px; flex-shrink: 0; }这样无论中间内容多大左右两栏都稳稳保持 200px不会变形。6.5 双飞翼布局中内容区背景没铺满双飞翼布局里真正的内容容器是 .main-content它只有左右 margin没有自己的背景。如果你给 .main 设置了背景色那背景会铺满中间栏整个宽度包含左右 margin 的空间视觉上两块白色侧栏区域会被中间栏背景包围好像“多出两块颜色”。所以实际开发时要么给 .main-content 设置背景要么让侧栏和中间栏背景一致保持视觉自然。这个问题相当隐蔽很多新手调半天都不知道是哪里的问题。6.6 关于 BFC 那点事不管是圣杯、双飞翼还是 Flex原理背后都离不开 BFC块级格式化上下文。圣杯和双飞翼里的 float、overflow 隐藏本质都是触发 BFC 来管理布局Flex 容器本身就是一种独立的格式化上下文。理解了 BFC你就理解了为什么 overflow: hidden 能清浮动为什么 margin 穿透在某些容器里会被隔离。这块属于 CSS 的底层内功面试高频排查 bug 也常用到值得专门花时间啃一啃。7. 写在最后的个人体会聊了这么多回到最初的问题三栏布局的意义到底是什么在我看来它不只是几行 CSS 的拼凑而是一台精密的“空间交换机器”。负 margin 是杠杆relative 是拨片float 是传送带order 是调度员——不同时代的方案用不同的手段达到同一个目的让 DOM、视觉、空间这三者协商出一个最优解。我不建议你背代码真的。面试时候手写不出来或者写完讲不清每一步原理和没写没区别。最好是自己开一个空白 HTML 文件把圣杯布局写一遍再改造成双飞翼再改成 Flex边改边观察浏览器里每一步变化。这样折腾一下午收获比刷十道题都大。最后送大家一个自己常用的判断口诀旧项目看兼容新项目用 Flex研究原理啃圣杯求稳定选双飞翼。布局问题从来不是“会不会写”的问题而是“为什么这么写”的问题。把背后的原理弄懂了什么花活都难不倒你。