响应式布局方案

19 分钟

为什么需要响应式布局

同一个页面可能运行在 375px 宽的手机、768px 的平板、1920px 的桌面显示器甚至 2560px 的超宽屏上。如果只用固定像素写死宽高,用户在不同设备上看到的要么是内容溢出需要横向滚动,要么是大片留白浪费空间。

响应式布局的核心目标:一套代码,多端适配。根据视口(Viewport)尺寸动态调整布局、字号、间距,让内容在任意屏幕上都能合理呈现。

面试中,响应式布局不是一个单独的知识点,而是一组方案的集合。面试官通常从"你在项目里怎么做移动端适配"切入,然后逐步深入到具体单位、具体方案的原理和取舍。

Viewport meta 标签

在讨论任何适配方案之前,必须先确保 Viewport 设置正确。这是移动端适配的基础前提。

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

各属性含义:

  • width=device-width:将布局视口宽度设为设备逻辑像素宽度。不设置时,移动端浏览器默认布局视口宽度为 980px,页面会被缩小显示。
  • initial-scale=1.0:初始缩放比例为 1,即 CSS 像素与设备独立像素 1:1 对应。
  • maximum-scale=1.0, user-scalable=no:禁止用户手动缩放。注意,iOS Safari 从 iOS 10 开始忽略 user-scalable=no,出于无障碍考虑不建议完全禁用缩放。

不加这个标签,后续所有适配方案都会失效——这是面试中容易被忽略的前置知识。

CSS 长度单位详解

响应式布局的基础是理解各种 CSS 长度单位。不同单位的参照基准不同,适用场景也不同。

px:绝对单位

px 是 CSS 像素,不是物理像素。在标准屏幕(DPR=1)上,1 CSS px = 1 物理像素;在 Retina 屏(DPR=2)上,1 CSS px = 4 物理像素(2×2)。

.box {
  width: 375px;  /* 固定宽度,不随视口变化 */
  font-size: 16px;
}

px 适合需要精确控制的场景,如边框、图标尺寸、固定布局的桌面端页面。但在需要弹性缩放的移动端场景中,纯 px 方案无法适配不同屏幕宽度。

em:相对于父元素字号

em 的参照对象是当前元素的 font-size(用于非 font-size 属性时),或父元素的 font-size(用于 font-size 属性时)。

.parent {
  font-size: 16px;
}

.child {
  font-size: 1.5em;    /* 16px × 1.5 = 24px */
  padding: 2em;        /* 24px × 2 = 48px(参照自身 font-size) */
}

em 的核心问题是层级嵌套导致值累积

.level-1 { font-size: 1.2em; }  /* 假设根 16px → 19.2px */
.level-2 { font-size: 1.2em; }  /* 19.2px × 1.2 = 23.04px */
.level-3 { font-size: 1.2em; }  /* 23.04px × 1.2 = 27.648px */

嵌套越深,字号越大,且计算结果难以预测。这是 em 在大型项目中难以维护的根本原因。

适用场景:组件内部间距跟随字号等比缩放,例如按钮的 paddingem 表示,当字号变化时,按钮内边距也等比调整。

rem:相对于根元素字号

rem(root em)始终参照 <html> 元素的 font-size,不受嵌套层级影响。

html {
  font-size: 16px;  /* 1rem = 16px */
}

.box {
  width: 20rem;     /* 320px */
  font-size: 1rem;  /* 16px */
  padding: 1.5rem;  /* 24px */
}

.nested .deep .element {
  font-size: 1.5rem;  /* 始终是 24px,不受嵌套影响 */
}

rem 解决了 em 嵌套累积的问题,且只需要修改 <html>font-size 就能全局等比缩放所有使用 rem 的元素。这正是 rem 适配方案的理论基础。

vw / vh:相对于视口尺寸

  • 1vw = 视口宽度的 1%
  • 1vh = 视口高度的 1%
  • 1vmin = vwvh 中较小的那个
  • 1vmax = vwvh 中较大的那个
.full-width-banner {
  width: 100vw;
  height: 50vh;
}

.responsive-text {
  font-size: 4vw;  /* 视口宽 375px 时,字号 = 15px */
}

vw 的优势是天然跟随视口宽度变化,不需要 JavaScript 参与计算。但也有局限:

  • vw 字号在大屏上可能过大,小屏上可能过小,需要配合 clamp() 限制范围。
  • vh 在移动端表现不稳定——iOS Safari 的地址栏收起/展开会导致 100vh 的实际高度变化,出现页面跳动。可以用 dvh(dynamic viewport height)解决,但兼容性有限。

vmin / vmax

vminvmax 在横竖屏切换场景中有用:

.square {
  width: 50vmin;
  height: 50vmin;  /* 始终以短边为基准,保证不超出屏幕 */
}

竖屏时 vmin = vw,横屏时 vmin = vh。适合需要在横竖屏下都保持合理比例的元素。

单位对比表

单位参照基准是否受嵌套影响典型用途注意事项
px绝对值边框、阴影、固定尺寸元素不适合弹性布局
em当前/父元素 font-size组件内间距跟随字号缩放嵌套累积难以维护
rem<html> font-size全局等比缩放需设置根字号策略
vw视口宽度全屏元素、响应式字号大屏过大、小屏过小
vh视口高度全屏布局、首屏高度移动端地址栏影响
vmin视口短边横竖屏兼容的正方形使用场景有限
vmax视口长边横竖屏兼容的铺满长边使用场景有限

em 与 rem 的区别(高频面试题)

这是面试中几乎必问的对比题,需要能清晰说出三个层面的差异:

参照基准不同em 参照父元素(或自身)font-sizerem 参照 <html>font-size

嵌套行为不同em 会随嵌套层级累积,rem 不受嵌套影响。

使用场景不同em 适合组件内部"间距跟随字号"的场景;rem 适合全局统一缩放的布局方案。

实际开发中的常见策略:字号用 rem,组件内间距用 em,布局尺寸用 vw 或百分比

媒体查询(@media)

媒体查询是响应式布局最基础的能力,通过检测视口特征来应用不同的样式规则。

基础语法

/* 视口宽度 ≥ 768px 时生效 */
@media screen and (min-width: 768px) {
  .container {
    max-width: 720px;
    margin: 0 auto;
  }
}

/* 视口宽度 < 768px 时生效 */
@media screen and (max-width: 767px) {
  .container {
    padding: 0 16px;
  }
}

断点设计

主流 UI 框架(Bootstrap、Tailwind CSS)的断点选择已经过大量实践验证:

断点名称宽度范围典型设备
xs< 576px小屏手机
sm≥ 576px大屏手机
md≥ 768px平板
lg≥ 992px小屏桌面
xl≥ 1200px标准桌面
xxl≥ 1400px大屏桌面

断点不是越多越好。实际项目中通常只需要 2-3 个关键断点:手机(< 768px)、平板(768px - 1199px)、桌面(≥ 1200px)。

Mobile First vs Desktop First

Mobile First(推荐):默认样式针对手机,用 min-width 向上适配。

/* 默认:手机样式 */
.grid { display: block; }

/* 平板及以上 */
@media (min-width: 768px) {
  .grid { display: grid; grid-template-columns: repeat(2, 1fr); }
}

/* 桌面 */
@media (min-width: 1200px) {
  .grid { grid-template-columns: repeat(4, 1fr); }
}

Desktop First:默认样式针对桌面,用 max-width 向下适配。

/* 默认:桌面样式 */
.grid { display: grid; grid-template-columns: repeat(4, 1fr); }

/* 平板及以下 */
@media (max-width: 1199px) {
  .grid { grid-template-columns: repeat(2, 1fr); }
}

/* 手机 */
@media (max-width: 767px) {
  .grid { display: block; }
}

Mobile First 的优势:手机端加载更少的 CSS(默认样式即可),性能更好;编写逻辑更清晰,逐步增强而非逐步降级。

rem 适配方案

rem 适配的核心思路:将设计稿的 px 值转换为 rem,通过动态设置 <html>font-size 实现等比缩放

flexible.js 原理

淘宝的 lib-flexible 是 rem 适配方案的经典实现。核心逻辑只有几行:

// flexible.js 核心逻辑(简化版)
function setRemUnit() {
  const docElement = document.documentElement;
  // 将屏幕宽度分为 10 份,每份作为 1rem
  const remSize = docElement.clientWidth / 10;
  docElement.style.fontSize = remSize + 'px';
}

setRemUnit();
window.addEventListener('resize', setRemUnit);

假设设计稿宽度为 750px(对应 375px 设备宽度,DPR=2),flexible.js 在不同设备上的计算结果:

  • 在 375px 设备上:1rem = 375 / 10 = 37.5px
  • 在 414px 设备上:1rem = 414 / 10 = 41.4px
  • 在 320px 设备上:1rem = 320 / 10 = 32px

开发时直接按设计稿的 750px 标注写代码,配合 PostCSS 插件自动将 px 转为 rem。转换规则:设计稿宽度 750px 对应设备宽度 375px,而 1rem = 37.5px,所以设计稿上标注 200px 的元素,对应设备上 100px,换算为 100 / 37.5 = 2.6667rem

也可以换个角度理解:设计稿 750px 被分为 10rem,所以设计稿上 1rem = 75px,那么 200px = 200 / 75 = 2.6667rem。两种算法结果一致。

这个 rem 值在不同设备宽度上会自动等比缩放为不同的 px 值:在 414px 设备上渲染为 2.6667 × 41.4 = 110.4px,在 320px 设备上渲染为 2.6667 × 32 = 85.3px

配合 PostCSS 自动转换

手动计算 rem 值效率太低,实际项目使用 postcss-pxtorem 自动转换:

// postcss.config.js
module.exports = {
  plugins: {
    'postcss-pxtorem': {
      rootValue: 75,          // 设计稿宽度 / 10(750px 设计稿)
      propList: ['*'],        // 所有属性都转换
      selectorBlackList: ['.no-rem'],  // 排除特定选择器
      minPixelValue: 2        // 小于 2px 不转换
    }
  }
};

开发时正常写 px,构建后自动转为 rem

/* 源码 */
.card {
  width: 200px;
  padding: 20px;
  font-size: 28px;
}

/* 构建产物 */
.card {
  width: 2.6667rem;
  padding: 0.2667rem;
  font-size: 0.3733rem;
}

rem 方案的局限

  • 依赖 JavaScriptflexible.js 需要在页面加载时执行,如果 JS 加载延迟或执行失败,页面布局会异常。
  • 全局等比缩放的副作用:在大屏设备上,所有元素等比放大,可能导致字号过大、布局不合理。实际上大屏设备更适合展示更多内容,而非放大内容。
  • lib-flexible 已不再维护:官方 README 明确表示该方案已过时,推荐使用 vw 方案。

vw 适配方案

vw 方案的思路更直接:设计稿上的 px 值直接按比例转换为 vw,不需要 JavaScript 参与。

换算公式

元素 vw 值 = 设计稿元素 px 值 / 设计稿宽度 × 100

# 750px 设计稿上 200px 的元素:
200 / 750 × 100 = 26.667vw

postcss-px-to-viewport

同样使用 PostCSS 插件实现自动转换:

// postcss.config.js
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      viewportWidth: 750,     // 设计稿宽度
      unitPrecision: 5,       // 小数精度
      viewportUnit: 'vw',     // 转换后的单位
      selectorBlackList: ['.ignore'],
      minPixelValue: 1,
      mediaQuery: false        // 是否转换媒体查询中的 px
    }
  }
};
/* 源码 */
.card {
  width: 200px;
  padding: 20px;
  font-size: 28px;
}

/* 构建产物 */
.card {
  width: 26.66667vw;
  padding: 2.66667vw;
  font-size: 3.73333vw;
}

vw + clamp() 限制范围

纯 vw 在极端屏幕宽度下表现不佳。clamp() 函数可以设置最小值和最大值:

.title {
  /* 最小 14px,理想值 4vw,最大 24px */
  font-size: clamp(14px, 4vw, 24px);
}

.container {
  /* 最小 320px,理想值 90vw,最大 1200px */
  width: clamp(320px, 90vw, 1200px);
  margin: 0 auto;
}

clamp() 的浏览器支持已经非常好(Chrome 79+、Safari 13.1+、Firefox 75+),在现代项目中可以放心使用。

rem 方案 vs vw 方案

对比维度rem 方案vw 方案
是否依赖 JS是(需要 flexible.js)否(纯 CSS)
原理动态修改根 font-size直接使用视口相对单位
兼容性IE9+(rem 本身)IE11 部分支持,现代浏览器全支持
大屏表现全局等比放大全局等比放大(需 clamp 限制)
PostCSS 插件postcss-pxtorempostcss-px-to-viewport
维护状态lib-flexible 已停止维护持续活跃
当前推荐度旧项目维护新项目首选

结论:新项目推荐 vw 方案,配合 clamp() 处理极端尺寸。旧项目如果已经使用 rem 方案且运行稳定,没有必要强行迁移。

移动端 1px 问题

在 DPR=2 的 Retina 屏上,CSS 中 border: 1px 实际渲染为 2 个物理像素的宽度,视觉上看起来比设计稿的 1px 线条粗一倍。这就是经典的"1px 问题"。

方案一:transform scale(推荐)

使用伪元素 + transform: scale 缩小边框:

.border-1px {
  position: relative;
}

.border-1px::after {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 200%;
  height: 200%;
  border: 1px solid #e5e5e5;
  border-radius: inherit;
  transform: scale(0.5);
  transform-origin: 0 0;
  pointer-events: none;
  box-sizing: border-box;
}

原理:先将伪元素放大 200%,画 1px 边框,再缩小到 50%,最终边框在物理像素上刚好是 1px。

这是目前兼容性和效果最好的方案,Vant、Ant Design Mobile 等主流组件库都采用这种实现。

方案二:0.5px

iOS 8+ 和部分安卓高版本支持直接写 0.5px

.border-hairline {
  border: 0.5px solid #e5e5e5;
}

简洁直接,但安卓低版本会将 0.5px 渲染为 0px(边框消失),兼容性不够可靠。

方案三:使用 SVG 或 box-shadow

/* box-shadow 模拟 */
.border-shadow {
  box-shadow: 0 0 0 0.5px #e5e5e5;
}

效果尚可,但 box-shadow 无法单独控制某一边的边框,灵活性不足。

1px 方案对比

方案兼容性灵活性复杂度推荐度
transform scale全平台高(可控制单边)⭐⭐⭐⭐⭐
0.5pxiOS 8+,安卓不稳定⭐⭐⭐
box-shadow较好低(难控制单边)⭐⭐

面试高频追问

"说说 em 和 rem 的区别"

em 参照父元素 font-size,存在嵌套累积问题;rem 参照 <html>font-size,全局一致。实际开发中,rem 用于全局缩放,em 用于组件内间距跟随字号。

"flexible.js 是怎么工作的"

核心是将屏幕宽度除以 10 作为 <html>font-size,使得 rem 值与视口宽度建立线性关系。配合 PostCSS 插件将设计稿 px 自动转为 rem,实现等比缩放。

"vw 方案有什么缺点"

纯 vw 在极端宽度下字号会过大或过小,需要 clamp() 限制范围。vh 在 iOS Safari 中受地址栏影响不稳定。此外,vw 方案本质上也是等比缩放,大屏设备可能更适合展示更多内容而非放大内容。

"1px 问题怎么解决"

核心原因是 CSS 像素与物理像素的比例关系(DPR)。推荐 transform scale 方案:伪元素放大 200% 画 1px 边框后缩小到 50%,兼容性最好。iOS 8+ 可以直接用 0.5px

"移动端适配整体思路"

  1. 设置正确的 Viewport meta 标签
  2. 选择适配方案(新项目用 vw,旧项目用 rem)
  3. 配合 PostCSS 插件自动转换单位
  4. 使用 clamp() 处理极端尺寸
  5. 使用 transform scale 处理 1px 问题
  6. 关键断点使用媒体查询做差异化布局

总结

  • 响应式布局不是一种技术,而是一组方案的配合使用。Viewport meta 是前提,CSS 单位是基础,媒体查询是补充,PostCSS 插件是效率工具。
  • 单位选择要根据场景:布局尺寸用 vw / 百分比,全局缩放用 rem,组件内间距用 em,固定尺寸用 px。
  • 新项目推荐 vw + clamp() 方案,纯 CSS 实现,无 JS 依赖,兼容性满足现代浏览器要求。
  • rem 方案仍有大量存量项目在使用,理解 flexible.js 原理是面试必备知识。
  • 1px 问题的本质是 DPR,transform scale 是兼容性最好的解决方案。