做中后台前端的时间一长你会发现一个规律几乎每个页面的起点都是 Element UI 布局组件。不管是拿 el-container 搭整套页面外壳还是用 el-row 和 el-col 把内容区切成统计卡片、搜索表单、表格工具栏布局组件决定了这个页面在 1920 宽屏、1366 笔记本和手机竖屏三种状态下到底能不能看。这篇文章不打算照着文档复述一遍 API而是把我这些年反复使用栅格和容器组件时的用法、踩过的坑、以及最终沉淀下来的一套写法整理出来。适合正在做 Vue 2 后台项目、或者刚接触 Element UI 布局组件没多久的同学参考。1. 页面骨架用 el-container 搭出后台管理系统的标准外壳1.1 一套 el-container 能组合出什么样的页面框架后台管理系统最常见的骨架是左侧菜单 右侧内容顶部再来一条 header。用 el-container 拼出来是这样的el-container styleheight: 100% el-aside width200px !-- 菜单 -- /el-aside el-container el-header height56px !-- 顶部栏 -- /el-header el-main !-- 内容区 -- /el-main el-footer height48px !-- 页脚 -- /el-footer /el-container /el-container几个默认值先记住el-header 默认高度 60pxel-aside 默认宽度 300pxel-main 默认没有 padding背景透明。不传 width 或 height 时页面能用但往往会出现侧边栏过宽顶栏太高这种违和感。我实际开发时都会显式写上这几个值尤其是 el-aside 的宽度200px 到 220px 之间是菜单栏最舒服的区间太宽占内容区太窄菜单文字放不下。这里有个容易忽略的点el-aside、el-header、el-main、el-footer 本质上只是带对应 CSS 类名的 div它们本身不负责撑高页面。想让整个框架铺满浏览器窗口需要从 html、body 到 Vue 挂载点一路设置 height: 100%再给外层 el-container 也加 height: 100%否则侧边栏背景色只到内容高度那一截底部露出一大片空白视觉上特别廉价。1.2 direction 规则与铺满屏幕的坑el-container 是 flex 容器它的排列方向由子元素类型决定。规则大致是当且仅当子元素中出现 el-header 或 el-footer 时方向自动变成纵向column如果只有 el-aside 和 el-main则是横向row。这就是为什么上面经典的左菜单布局要嵌套一层内层 el-container——外层负责水平的 aside 加右侧区域内层因为含有 header 和 footer自动变成纵向排列。理解这个规则之后你就不会到处乱嵌套 el-container 了。有的同学为了保险起见给每一层都写上 direction结果反而把结构搞乱。其实只需要记住需要 header 在顶部、footer 在底部时把 header/footer 和 main 放在同一个 el-container 内这个容器会自动纵向排列需要 aside 在左侧时把 aside 和右侧整体再塞进一个外层 el-container它自动横向排列。实践中还有一个崩溃现场明明设置了 height: 100%页面还是出现整个窗口的滚动条而不是内容区内部滚动。罪魁祸首通常是 html 或 body 的 height 没真正变成 100%或者外层 el-container 没设高度。我现在的习惯是全局样式先固定这三行html, body, #app { height: 100%; margin: 0; }然后在最外层 el-container 内联或类里加 height: 100%。注意如果项目里用了 keep-alive 缓存页面路由切换后滚动位置会保留在 el-main 的 scrollTop 上这是缓存机制和滚动容器共同作用的结果不是布局组件的 bug但排查时很容易冤枉它。1.3 侧边栏固定与内容区滚动我常用的两招页面骨架还有一个高频诉求左侧菜单固定不动右侧内容区自己滚动。方案一把外层 el-container 高度设为 100%右侧 el-main 设置 overflow: auto这样内容超长时只在右侧出现滚动条左侧 aside 自然固定。方案二如果侧边栏本身要跟随页面滚动就把布局放在视口流里侧边栏用 position: sticky; top: 0; height: 100vh适合内容不复杂的页面。我倾向于方案一。它把滚动条限制在内容区左侧菜单的 hover 弹出子菜单不会被滚动影响而且主内容区滚动时顶栏上的用户信息和全局搜索按钮始终可见。配合 el-aside width200px 加响应式收起按钮整个骨架就很稳了。2. 栅格系统的基本盘12 栏切割、gutter 间距与 offset 偏移2.1 为什么栅格偏偏是 12 列栅格系统的核心是 el-row 和 el-col一横一纵把内容区切成若干块。el-col 的 span 取值 1 到 24几个 col 的 span 加起来等于 24就是占满一行。为什么是 24 而不是 10、16因为 24 能被 2、3、4、6、8、12 整除意味着一行里既能均匀排 2 列、3 列、4 列、6 列也能排出 8:16、6:18、12:12 这种不对称比例。日常业务里左 8 右 16左 6 右 18一行 4 张卡片全部基于这个能力。如果你用过 Bootstrap 的栅格会发现思路几乎一样只是 Bootstrap 是 12 栏Element UI 为了更灵活扩展成了 24 份。这不是巧合而是栅格系统在长期实践中沉淀下来的通用设计。一个新手常犯的疑问是span 总和必须严格等于 24 吗其实不一定。超过 24 的部分会自动换行小于 24 时右侧留白。工具栏里左侧按钮组占 12、右侧操作区占 12 是常见写法但也偶尔出现左 6 右 12留白 6 的情况这种留白在某些设计中是故意的。不需要为了凑 24 把一个按钮强行拉宽。2.2 gutter 的负 margin 机制与两端对齐gutter 是行内列与列之间的间距单位是像素。设置 gutter16 后Element UI 会给每个 el-col 加上左右各 8px 的 padding列与列之间形成 16px 的视觉间距。同时el-row 的左右两边会各向外扩 8px也就是负 margin。这样做的目的是为了保证第一列左边、最后一列右边和上方未使用 gutter 的内容在垂直方向上依然对齐。这个机制不复杂但出现 bug 时很难一眼看出来。若 row 的父容器没有足够 paddingrow 会比父容器宽出 gutter 的一半导致水平滚动条或左右不对称若你在设置了 gutter 的 row 外面又套了一层有 padding 的卡片数值恰好抵消就会出现明明设置了间距却贴边的现象。先记住这个结论gutter 大于 0 时row 的实际渲染宽度 父容器宽度 gutter 宽度。后面我在第 5 章会专门讲怎么处理。2.3 offset 与 push/pull移动元素位置的三把钥匙el-col 提供了三种位置调整手段。offset 给列增加左侧 margin把列往后推比如 :span12 :offset6 表示这一列在 24 栏中从第 6 栏之后开始。push/pull 则用相对定位改变列的视觉位置适合在不改动 DOM 顺序的情况下调整展示顺序。比如表单先渲染提交时间再渲染创建时间但视觉上想让创建时间靠左可以用 :push6 和 :pull6 互换左右位置。三者有个容易被忽略的差别offset 用 margin-left 实现如果一列已经接近行尾较大的 offset 会直接把它推到下一行push/pull 不占额外空间不会影响换行计算。做复杂 dashboard 时我一般优先用 push/pull 调整顺序用 offset 调整间距尽量避免两者同时叠在同一列上。响应式场景下还可以给具体断点加后缀比如 :lg-push6 只在 lg 及以上屏幕生效这在窄屏重新排列内容时非常实用。3. 响应式断点让页面在 1366 和 1920 下都不翻车3.1 先记住这五个断点值Element UI 把屏幕宽度分成五档对应 el-col 上的五个 props断点最小宽度覆盖场景xs小于 768px手机竖屏sm大于等于 768px平板md大于等于 992px笔记本较小窗口lg大于等于 1200px常见桌面宽度xl大于等于 1920px大屏显示器这里要澄清一个文档不会强调的点不写任何响应式 props 时el-col 只按 span 渲染所有屏幕宽度下都一样。你写 :span12手机是 12桌面也是 12。只有写上 xs/sm/md/lg/xl 中的某个档位才会在当前档位及以上的屏幕宽度下覆盖默认 span。所以一个常见错误是只写了 :lg8然后以为手机端会自动变得好看——其实手机端会继续使用默认 span三个卡片还是并排挤在一起除非你再补一个 xs。只要记住断点值是覆盖式触发、向下兼容这一点大部分响应式问题都能想通。3.2 一个典型的统计卡片四联动布局统计卡片是栅格系统最典型的应用场景。业务需求通常是电脑上四个一排平板上两个一排手机上各占一整行。代码可以这样写el-row :gutter16 el-col v-foritem in cards :keyitem.title :xs24 :sm12 :lg6 div classstat-card {{ item.title }}{{ item.value }} /div /el-col /el-row语义很明确lg 及以上屏幕一张卡片占 6 栏正好 4 个一排sm 到 lg 之间占 12 栏2 个一排xs 小屏占 24 栏每个卡片一行。因为断点是向下兼容的覆盖逻辑顺序没有严格限制但建议按 xs、sm、md、lg、xl 从窄到宽书写review 代码时更清楚。实际项目里我还会在统计卡片后面跟一个 span 总和为 24 的 el-row 放图表形成上卡片、下图表的经典结构。这里有一个细节卡片高度不一致时矮卡片下方会漏出难看的空隙。要么给卡片设置固定高度要么在需要时给 el-row 加 align 属性统一垂直对齐后面讲 flex 栅格时会展开。3.3 隐藏列与移动端降级处理除了缩放布局移动端还经常需要隐藏一些次要字段。Element UI 提供了一组工具类hidden-xs-only、hidden-sm-only、hidden-md-only、hidden-lg-only、hidden-xl-only以及 hidden-sm-and-down、hidden-md-and-down 这类范围类。它们既能用在 el-col 上也能用在任意 div 上。我常用的做法是表格的操作列在手机上只保留两三个核心按钮其他需要宽屏展示的字段直接给 el-table-column 加 classhidden-xs-only。另外这些工具类基于 Bootstrap 的断点语义和 el-col 的断点完全对应。需要注意在表格中它们作用于 td 上个别旧浏览器下 display 覆盖可能不生效实测现代浏览器内核都没问题。真遇到兼容问题我一般退一步用 CSS 媒体查询或者把多余字段收进折叠面板不再依赖栅格工具类。4. flex 栅格与业务布局套路搜索区、操作栏、统计卡片4.1 typeflex 时 justify 和 align 的正确取值el-row 默认是普通文档流列与列之间按 span 宽度排布。但业务里经常需要把一行的元素按不同位置摆放这时只要给 el-row 加上 typeflex它就变成 flex 布局容器同时开放两个属性justify 控制水平方向对齐可选 start、end、center、space-around、space-betweenalign 控制垂直方向对齐可选 top、middle、bottom。注意取值不是 CSS 原生的 flex-start 和 flex-end。Element UI 内部会把类名拼成 is-justify-end、is-align-middle再由样式表映射到 flex-start/flex-end 等。如果你直接传 justifyflex-end生成的类名是 is-justify-flex-end没有任何样式匹配布局会保持默认。这是肉眼特别难排查的坑我第一次踩到的时候还以为是版本问题后来查了源码才发现是类名映射的锅。4.2 搜索区一行三列的经典写法后台列表页基本都有搜索区最合理的密度是一行三列超出的自动换行。这个布局用 el-form 加 el-row、el-col 组合非常舒服el-form :modelquery label-width80px el-row :gutter16 el-col :md12 :lg8 el-form-item label关键词 el-input v-modelquery.keyword / /el-form-item /el-col el-col :md12 :lg8 el-form-item label状态 el-select v-modelquery.status / /el-form-item /el-col el-col :md12 :lg8 el-form-item label创建时间 el-date-picker v-modelquery.date / /el-form-item /el-col /el-row /el-form几个细节值得说。给 el-form-item 加 label 后label 宽度默认 auto字段名长短不一时输入框会错落不齐。我一般给 el-form 设置 label-width80px 或 110px或者统一 label 字数。至于到底是 el-col 包 el-form-item还是 el-form-item 包 el-col文档没规定。我实践下来推荐el-col 在外、el-form-item 在内因为这样整行的栅格划分清晰换行行为也符合直觉。如果你把 el-col 塞进 form-item 内部搜索区换行时经常出现 label 和输入框被拆到两行的错乱。4.3 工具栏左右分离的两个模板列表页顶部工具栏几乎是每页必备左边是新增、导出、批量删除这类主操作右边是刷新、全屏、设置这类次操作。用 flex 栅格只需要一行el-row typeflex justifyspace-between alignmiddle el-col el-button typeprimary新增/el-button el-button导出/el-button /el-col el-col el-button刷新/el-button el-button配置/el-button /el-col /el-row注意这里 el-col 没给 span但宽度依然正常。因为父容器是 flex两个 el-col 各按内容宽度渲染justifyspace-between 会把它们推成左侧一组、右侧一组。这在 Element UI 文档里没有明说但实际渲染完全可行也是我项目里最常用的工具栏模板。如果右侧按钮特别多担心极端宽度下换行可以给它一个固定 span 再用 justifyend 对齐。5. 我在实际项目中踩过的布局坑与调整经验5.1 gutter 与父级 padding 的视觉偏差这是很多人遇到过却说不清的问题。现象是el-card 里放 el-row :gutter16卡片左右 padding 是 20px但 row 第一个 col 距离卡片左边明显不到 20px或者和上方文字没对齐。原因就是 gutter 的负 margin——row 左右各被拉出 8px抵消了卡片 20px padding 的一部分。处理办法分场景。如果不希望卡片内容区视觉左右不对称就在 row 外面再套一个 padding: 0 8px 的 div把负 margin 顶回去如果卡片本身足够宽、这点偏移不影响对齐也可以忽略。但有一个约定我认为值得推广页面级的大 row也就是直接放在 el-main 下的那一层一律不给 gutter靠内容自身 padding 留白卡片内部的小 row 给 gutter同时保证卡片 padding 不小于 gutter 的一半。这套约定能规避掉一半以上的栅格对齐问题。5.2 动态 span 与长列表渲染成本我之前在某个权限后台项目里根据下拉框选择动态改变 el-col 的 span页面上有六十多组统计块。一开始一切正常后来数据量上来每次切换布局都明显卡顿控制台偶尔出现长任务警告。定位后发现问题不只在 span 的响应式更新而是每个 el-col 里的图表组件本身就不轻布局一变整行都重渲染。这个问题的本质是el-row 和 el-col 是普通组件span 作为 prop 变化会触发该节点及其子树更新。我的调整思路有两条第一能不动态改 span 就不改栅格布局尽量在页面级别确定一次而不是在交互中频繁切换第二如果必须动态调整把 span 抽到一个只包含轻量 div 的子组件上图表组件用 v-if 或 keep-alive 隔离避免整棵子树连带重渲染。调试这类性能问题时Vue 开发者工具里看组件更新耗时非常直观能快速定位是不是栅格层引起的连锁更新。5.3 嵌套栅格时 el-form-item 的对齐问题表单里出现明显不对齐的另一种情况是在 el-form-item 内部又嵌套 row/col。比如起始时间到结束时间这种复合字段内部用两个 col 分别放日期选择器外部 el-form-item 的 label 会和内部栅格产生对齐偏差因为 form-item 默认的 align-items 不是顶部对齐。我的解法是给这类复合 form-item 单独处理要么在 el-form-item 里只放一个 el-row让栅格整体作为内容区不再拆出多个平级元素要么给 el-form 设置合适的 label-width并在复合字段上调整对齐方式。遇到 label 在左侧的多行复合表单还可以把 label-position 设为 top让 label 全部统一在输入框上方视觉上更整齐。这个坑花了我一个下午排查最后发现只是对齐方式没设置确实很冤。5.4 升级到 Vue 3 前需要知道的差异最后说一句迁移的事情。如果项目准备从 Vue 2 加 Element UI 升到 Vue 3 加 Element Plus布局组件的 API 表面看着差不多——el-row 还是 el-rowel-col 还是 el-colspan、gutter、offset 这些 props 基本保留。但有个本质差异Element UI 的响应式断点靠 CSS 媒体查询窗口变化是浏览器原生行为Element Plus 的栅格改用 JavaScript 监听容器宽度行为上更接近父容器自适应而不是视口自适应在窄侧栏容器内部也能正确重排。正因如此Element Plus 下对栅格项做动态增删时布局计算的开销会明显一些。所以升级前我建议先做一次布局组件使用盘点有没有依赖窗口宽度的 hidden 类有没有大量动态 span 的场景有没有把栅格用在大列表 v-for 里这三类是升级后最容易出现性能回退的地方。如果让我给一个处理优先级我会先处理大列表内部的栅格再处理动态 spanhidden 类一般问题不大。把这些提前处理干净迁移过程会顺利很多。