Flex 自适应布局:告别 calc,用一个 height-auto 搞定

Flex 自适应布局:告别 calc,用一个 height-auto 搞定

背景

在 Vue 项目里,经常遇到这种布局:

html 复制代码
<div class="page" style="height: 100%">
  <div class="top">顶部,内容多少不确定</div>
  <div class="bottom">
    <div class="right">
      <div class="right-top">顶部</div>
      <div class="right-bottom">底部,内容高度确定</div>
    </div>
    <div class="left">
      <div class="left-top">顶部,内容多少不确定</div>
      <div class="left-bottom">底部</div>
    </div>
  </div>
</div>

需求很朴素:已知一部分区域的高度,让另一部分吃掉剩余空间。

过去大家的做法是 calc:

css 复制代码
.right-top { height: calc(100% - 60px); }
.left-bottom { height: calc(100% - 40px); }

问题显而易见:

  • 高度写死,改一处要跟着改多处
  • 嵌套层级一多,calc 里套 calc
  • 内容不确定时根本没法算

其实用 Flex,几行 CSS 就能解决。核心思路是:父级声明自己是 flex 容器,需要自适应的子级用 flex: 1 去吃掉剩余空间。


一、CSS 方案

1. 核心规则

css 复制代码
/* 父级:flex 容器 + 允许收缩 */
.parent {
  display: flex;
  flex-direction: column; /* 分配高度时必须是 column */
  min-height: 0;          /* 关键:允许子项收缩 */
}

/* 需要吃掉剩余空间的子级 */
.height-auto {
  flex: 1 1 0;
  min-height: 0;
  overflow: auto;         /* 内容溢出时可滚动,视情况加 */
}

只需要记住这句话:

父级 display:flex + 方向 + min-height:0,自适应子级 flex:1 1 0 + min-height:0。

2. 为什么需要 min-height: 0

这是最容易踩的坑。

Flex 子项默认 min-height: auto,意思是"我的最小高度不能小于我的内容高度"。于是当子项内容很长时,它会拒绝收缩,撑破父级,导致 flex: 1 完全失效。

显式写 min-height: 0 就是告诉浏览器:允许我缩到比内容还小 ,这样 flex: 1 才能真正生效。

同理,横向分宽度时也要写 min-width: 0。

3. 回到开头的例子

css 复制代码
/* 页面纵向排列 */
.page {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* bottom 吃掉 page 的剩余高度,同时自己是个横向容器 */
.bottom {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: row;
}

/* 左右两栏平分宽度,各自纵向排列 */
.bottom > .left,
.bottom > .right {
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* 各个"高度自适应的区域" */
.right-top,
.left-bottom {
  flex: 1 1 0;
  min-height: 0;
  overflow: auto;
}

对应 HTML 稍微调整一下,给需要自适应的元素加上语义化的类:

html 复制代码
<div class="page">
  <div class="top">顶部,内容多少不确定</div>
  <div class="bottom height-auto">
    <div class="right">
      <div class="right-top height-auto">顶部</div>
      <div class="right-bottom">底部,内容高度确定</div>
    </div>
    <div class="left">
      <div class="left-top">顶部,内容多少不确定</div>
      <div class="left-bottom height-auto">底部</div>
    </div>
  </div>
</div>

关键约定:谁有 height-auto,谁就是那个"吃掉剩余空间"的角色。

其余同类元素(.top、.bottom、.left、.right、.right-bottom、.left-top)只承担容器职责,高度由内容决定。

4. 完整可运行示例

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Flex 自适应布局</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }

    html, body {
      height: 100%;
    }

    body {
      padding: 12px;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: #f5f5f5;
    }

    /* ============ 1. 页面:纵向 flex 容器 ============ */
    .page {
      height: 100%;
      display: flex;
      flex-direction: column;
      min-height: 0;
      gap: 8px;
      background: #fff;
      border-radius: 8px;
      padding: 12px;
    }

    /* ============ 2. bottom:吃掉 page 的剩余高度,同时自己是横向容器 ============ */
    .bottom {
      flex: 1 1 0;
      min-height: 0;
      display: flex;
      flex-direction: row;
      gap: 8px;
    }

    /* ============ 3. 左右两栏:平分宽度,各自纵向排列 ============ */
    .bottom > .left,
    .bottom > .right {
      flex: 1 1 0;
      min-width: 0;
      min-height: 0;
      display: flex;
      flex-direction: column;
      gap: 8px;
      background: #fafafa;
      border-radius: 6px;
      padding: 8px;
    }

    /* ============ 4. 通用规则:谁有 height-auto,谁吃掉剩余空间 ============ */
    .height-auto {
      flex: 1 1 0;
      min-height: 0;
      overflow: auto;
    }

    /* ============ 5. 视觉样式(可忽略,仅用于演示) ============ */
    .top,
    .right-top,
    .right-bottom,
    .left-top,
    .left-bottom {
      padding: 12px;
      background: #e8f0fe;
      border-radius: 6px;
      border: 1px solid #c6dafc;
      color: #1a56db;
      font-size: 14px;
    }

    .right-bottom,
    .left-bottom {
      background: #fef3c7;
      border-color: #fcd34d;
      color: #92400e;
    }
  </style>
</head>
<body>
  <div class="page">
    <div class="top">顶部,内容多少不确定</div>

    <div class="bottom height-auto">
      <div class="right">
        <div class="right-top height-auto">
          顶部(自适应)<br />
          内容再多也不会撑破布局,超出会滚动。
        </div>
        <div class="right-bottom">底部,内容高度确定</div>
      </div>

      <div class="left">
        <div class="left-top">顶部,内容多少不确定</div>
        <div class="left-bottom height-auto">
          底部(自适应)<br />
          内容再多也不会撑破布局,超出会滚动。
        </div>
      </div>
    </div>
  </div>
</body>
</html>

5. 用 SCSS 减少重复

项目里层级多的时候,用 mixin 收敛一下:

css 复制代码
// ==================== mixins ====================
@mixin flex-col {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

@mixin flex-row {
  display: flex;
  flex-direction: row;
  min-height: 0;
}

@mixin height-auto {
  flex: 1 1 0;
  min-height: 0;
  overflow: auto;
}

// ==================== 页面 ====================
.page {
  height: 100%;
  @include flex-col;
}

.bottom {
  @include flex-row;
  @include height-auto;
}

.bottom > .left,
.bottom > .right {
  flex: 1 1 0;
  min-width: 0;
  @include flex-col;
}

// 通用:谁有 height-auto 谁吃掉剩余空间
.height-auto {
  @include height-auto;
}

6. 注意事项

场景 说明
父级方向 要分高度 用 column,要分宽度 用 row,别搞反
最外层 必须有确定高度(100% / 100vh),否则整条链没有高度来源
嵌套 中间层要同时承担"子"和"父"两个角色,两套属性都要写
溢出 overflow: auto 会裁剪下拉、弹层,有此类内容时改成 visible
横向 分宽度的场景记得加 min-width: 0

7. 局限性

CSS 方案已经能覆盖 95% 的场景,剩下的 5% 通常是:

  • 第三方组件内部写死了高度,flex 插不进去
  • 计算规则很特殊,比如"和某个兄弟节点高度相等"或"减掉某个固定值"
  • 需要在运行时动态判断

这时候就需要 JS 兜底。


二、JS 指令方案

思路:写一个 v-auto-height 指令,用 ResizeObserver 监听父级和兄弟节点,把剩余高度写到元素上。

1. 指令实现 src/directives/autoHeight.js

js 复制代码
/**
 * v-auto-height
 * 让元素的高度自动等于父级剩余高度。
 * 适用于第三方组件写死高度、flex 无法介入的场景。
 *
 * 用法:
 *   <div v-auto-height>...</div>
 *   <div v-auto-height="{ gap: 16, offset: 2, exclude: [someEl] }">...</div>
 */
export const vAutoHeight = {
  mounted(el, binding) {
    const parent = el.parentElement
    if (!parent) return

    // 保存原样式,卸载时恢复
    el._prevStyle = {
      height: el.style.height,
      minHeight: el.style.minHeight,
    }

    const options = binding.value || {}
    const gap = options.gap || 0
    const offset = options.offset || 0
    const exclude = new Set(options.exclude || [])

    const calc = () => {
      const siblings = [...parent.children].filter(
        (child) => child !== el && !exclude.has(child)
      )

      const used = siblings.reduce((sum, child) => {
        const style = getComputedStyle(child)
        return (
          sum +
          child.offsetHeight +
          parseFloat(style.marginTop || 0) +
          parseFloat(style.marginBottom || 0)
        )
      }, 0)

      const h = parent.clientHeight - used - gap - offset
      el.style.height = Math.max(h, 0) + 'px'
      el.style.minHeight = '0'
    }

    const ro = new ResizeObserver(calc)
    ro.observe(parent)
    ;[...parent.children].forEach((c) => ro.observe(c))

    const mo = new MutationObserver(calc)
    mo.observe(parent, { subtree: true, childList: true, attributes: true })

    el._ro = ro
    el._mo = mo
    el._bindingValue = binding.value
    calc()
  },

  updated(el, binding) {
    el._bindingValue = binding.value
    el._ro?.disconnect()
    el._mo?.disconnect()
    vAutoHeight.mounted(el, binding)
  },

  unmounted(el) {
    el._ro?.disconnect()
    el._mo?.disconnect()
    if (el._prevStyle) {
      el.style.height = el._prevStyle.height
      el.style.minHeight = el._prevStyle.minHeight
    }
  },
}

2. 全局注册 src/main.js

js 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import { vAutoHeight } from './directives/autoHeight'

const app = createApp(App)
app.directive('auto-height', vAutoHeight)
app.mount('#app')

3. 使用

html 复制代码
<!-- 基础用法 -->
<div class="bottom" v-auto-height>
  底部,吃掉剩余高度
</div>

<!-- 带参数 -->
<div class="bottom" v-auto-height="{ gap: 16, offset: 2 }">
  底部,吃掉剩余高度
</div>

4. 指令不适合干什么

  • 不适合做简单的高度自适应。明明三行 CSS 就能解决,非要引一个指令,纯属自找麻烦。
  • 不适合高频变化的布局 。每次 resize 都要读 offsetHeight,会触发重排,性能比 Flex 差一截。
  • 容易和第三方组件的内部样式打架 。指令写 height,组件自己也在写 height,谁赢看加载顺序,很脆弱。

所以我的建议是:

能用 Flex 就用 Flex,实在不行才上 JS 指令。

指令的正确使用场景是兜底------比如某个第三方弹窗组件内部强制 height: 400px,你没法改它源码,那就用指令在外部重新算一遍。


三、总结

一套口诀带走:

角色 需要写的属性
父级(纵向) display:flex; flex-direction:column; min-height:0;
父级(横向) display:flex; flex-direction:row; min-height:0;
自适应子级 flex:1 1 0; min-height:0;(横向时补 min-width:0)
最外层 必须有确定高度,100% 或 100vh

记住这句话:

谁设置了 height-auto,谁就吃掉剩余空间;它的所有祖先节点,从外到内都要是 flex 容器并允许收缩。

Flex 方案零 JS、零计算、自动重排,覆盖 95% 场景。JS 指令只在 Flex 插不进去的地方当兜底用。

优先用第一套 CSS 方案。

相关推荐
程序员阿黄1 小时前
月圆人圆:中秋佳节传统文化站(月夜主题重设计版)项目全解析
javascript·html5·毕设
OpsEye1 小时前
开源 AI 网关自研 vs 商用,企业该怎么权衡取舍
javascript·ai编程
环境栈笔记1 小时前
指纹浏览器检查代理失败怎么排查?先分清代理、网络和检测服务
前端·人工智能·后端·自动化
ShineWinsu1 小时前
对于HTML:HTML常见标签的解析—下
前端·css·面试·html·标签·表单·emmet
恋猫de小郭1 小时前
Kotlin 2.5 的 Companion 改造,对 KMP 的重要升级
android·前端·flutter
前端双越老师2 小时前
程序员 1v1 访谈系列第二期:很焦虑但又不知道干啥
前端·ai编程·全栈
广州华水科技2 小时前
北斗形变监测传感器在大坝安全监测中的应用与优势分析
前端
孔令飞2 小时前
如何学习Go
前端
HUIBUR科技2 小时前
AI 中枢,面向企业场景构建的 AI 智能体
前端·数据库·人工智能