移动端适配:viewport、rem与视口单位实战

一、移动端适配不是加一行 viewport 就完了

十年前做移动端适配,标准答案是「加一行 viewport 加上媒体查询」。今天这套还在用,但真正让页面在手机上看着不对劲的,往往不是布局本身,而是几个具体到像素和单位的细节:1px 边框变成 0.33px 消失、字在小屏上小到看不清、Safari 的地址栏把 100vh 撑破、横屏时 rem 计算失效。这篇按我改自己站的真实顺序写,从 viewport 到 rem,再到测试方法。

二、viewport meta 的每一个参数都在解决一个具体问题

标准的写法是:

<meta name="viewport" content="width=device-width, initial-scale=1">

width=device-width 让布局视口等于设备的 CSS 像素宽度。不写的话,手机浏览器会假装自己是 980px 宽的桌面浏览器,再把整页缩小塞进屏幕,结果是文字小得要用两根手指放大。

initial-scale=1 让初始缩放为 1 倍。这两个参数一起写,才能保证「视口宽度 = 屏幕宽度 = 缩放 1 倍」这个干净的前提。

然后是两个常被误解的参数。maximum-scale 和 user-scalable=no:很多人加这两个是为了防止用户双指缩放「破坏」布局,但它同时挡住了视力不好的用户放大看字,也违反无障碍要求,iOS 从 10 开始就忽略了 user-scalable=no。正确做法是不限制缩放,把布局做成放大也不乱的——字号用 rem 而不是固定 px,容器用百分比或 max-width。

还有一个坑:iOS 输入框聚焦时自动放大。原因是输入框的 font-size 小于 16px,Safari 会自动放大整个页面去「帮」用户看清。解决办法不是加 maximum-scale,而是把所有 input、select、textarea 的 font-size 设到 16px 以上。这条改完,那类「点一下输入框页面就歪了」的反馈基本就消失了。

三、视口单位:vh 在手机上不是屏幕高度

100vh 听起来就是「屏幕那么高」,但在移动浏览器上,vh 对应的是「地址栏收起时」的最大高度。地址栏显示时,顶部和底部工具栏会吃掉一部分,于是 height:100vh 的区块底部被挤到屏幕外,出现「首屏图片下面永远露出一点白」的经典 bug。

现在的正确解法是用动态视口单位:100dvh 表示当前实际可见高度,随地址栏变化;100svh 是最小高度(地址栏和最宽工具栏都在时);100lvh 是最大高度。做全屏首屏用 100svh 最稳,因为它在所有状态下都不会溢出。降级方案是给老浏览器留一条 height:100vh 在前面,现代浏览器自动覆盖。

四、rem 方案:html 的 font-size 怎么算

rem 是相对根元素字号,配合一段 JS 在页面加载时按视口宽度设置 html 的字号,就能让整页按比例缩放。经典写法是:

(function () {
  var doc = document.documentElement;
  function setRoot() {
    var w = Math.min(doc.clientWidth, 750);
    doc.style.fontSize = (w / 7.5) + 'px';   // 750 设计稿,1rem = 100px 设计像素
  }
  setRoot();
  window.addEventListener('resize', setRoot);
})();

750 是设计稿宽度,除以 7.5 意味着 1rem 等于设计稿上的 100px,设计师给 24px 的字就写 0.24rem,心算简单。Math.min 那个上限很关键:平板和桌面不能无限放大,否则整个页面变成巨型字。

rem 方案的代价是依赖 JS,JS 没跑起来之前字号是浏览器默认的 16px,会出现一次明显的「跳一下」。缓解办法是在 <head> 里内联这段脚本,别放在底部;同时把首屏关键容器的高度交给 CSS(比如 aspect-ratio)而不是 rem。

另外,字体大小本身不要用 rem 无脑缩放。小屏上按比例缩,12px 的设计稿字号在 320px 屏上会变成 5px 左右,完全没法读。正确做法是:布局尺寸(间距、图片高度、圆角)用 rem,正文和按钮文字用固定的 px 或 clamp(),并给一个最小值。比如 font-size: clamp(14px, 4vw, 18px)。

五、1px 边框在 Retina 屏上的消失与变粗

设备像素比(DPR)大于 1 的屏幕上,CSS 的 1px 会被渲染成多个物理像素。放在整格位置时看着粗,落在半像素上时浏览器会做取整,于是有的边出现有的边消失,同一排卡片的边框深浅不一。

常见的稳妥解法是先用 transform: scale 画半像素线:给一个 1px 高的元素加 transform: scaleY(0.5),在 DPR=2 的屏上就是一条真正的 1 物理像素线。或者更简单:卡片之间的分隔线改用 box-shadow 或者干脆用背景色差来做,不做 1px 细线——这是我在移动端改版时最省心的选择,视觉上几乎无区别,还省一次渲染。

六、写 CSS 时三个必须改掉的习惯

第一,别写固定的宽高。 width: 375px 在 320px 的屏幕上直接横向溢出,出现左右滚动条。要么用百分比,要么用 max-width 加 width: 100%。图片和视频统一加 max-width:100%; height:auto,这一条能挡掉大半「页面能左右拖动」的投诉。

第二,flex 和 grid 的子项默认不换行。 长英文单词、长 URL、甚至一长串数字都会把容器撑破。给正文容器加 overflow-wrap: break-word 或 word-break: break-word,成本一行,收益极大。我站上「代码块把整页撑宽」的问题就是这一行解决的。

第三,表格和代码块必须单独处理。 宽表格在手机上没法压缩,正确做法是外面套一层 overflow-x:auto 的容器,让表格自己横向滚,而不是让整页横向滚。

块同理,并且要加 -webkit-overflow-scrolling: touch 让 iOS 上滚动有惯性。

七、媒体查询该怎么断点

不要按 iPhone 的具体型号写断点,那是永远追不完的。按内容的「挤压点」写:把浏览器窗口从宽拖到窄,哪一刻布局开始变丑,就在那里设断点。实际项目里三档基本够用:小于 600px 单列、600 到 1024px 两列、大于 1024px 三列加侧栏。

写法上用 min-width 递增(移动优先),基础样式就是手机版,往上加增强。这样手机上加载的 CSS 最少,也是移动端性能最需要的那一份。要小心的是,@media 里的样式覆盖顺序依赖出现次序,同优先级下后写的赢;把媒体查询统一放在文件末尾,能避免大半「为什么这条不生效」。

八、怎么验证:真机、缩放、横屏三件事

开发者工具的设备模拟只能验布局,验不了三样东西:真机的字体渲染、Safari 地址栏对 vh 的影响、以及横屏。所以定稿前我固定做三轮:

  1. 用 iPhone SE(320px 宽)和一台安卓大屏,各看一遍首页、文章页、一个长表格页。
  2. 在 Chrome 里把页面缩放到 200%,确认文字放大后不重叠、不被截断,容器不出现横向滚动。
  3. 把手机横过来,确认导航还能用,不会因为高度变小把菜单挤出屏幕。

另外有一个几乎必现的检查项:打开一个长文章,滚到底部,看横向有没有滚动条。有的话八成是某个代码块或表格没套滚动容器。

九、横屏、折叠屏与分栏:三个被忽略的场景

大部分适配只在大屏竖屏的两款手机上验证过就上线了,真正出问题的是三种边缘状态。

横屏。宽度变大、高度变得很小,所有以「一屏塞下」为前提设计的布局立刻失效:吸顶导航占掉一半高度,全屏首屏图被压成一条,弹窗的按钮跑到屏幕外面。做法是给矮视口单独加一条媒体查询,@media (max-height: 500px) 下收紧顶部留白、缩小编排密度、把只在竖屏有意义的整屏区块改成自然高度。

折叠屏与分屏。这类设备会有两个变化:一是宽度刚好卡在平板和手机之间,二是窗口在页面打开时被拖成新的尺寸。前者要求断点不要只写 600px 和 1024px 两个硬点,中间补一档;后者要求所有按视口计算的数值(尤其是 JS 算出来的 rem)必须监听 resize,而不只是加载时算一次。

系统级字体放大。iOS 和安卓都允许用户在系统设置里调大字号,这会让页面的字比设计稿大一圈,紧贴的单行标签、固定高度的按钮、三个字一行的导航都会溢出。可用空间是有限的:给容器用 min-height 而不是 height,文字容器用 line-height 而不要写死行高,按钮的内边距用 em 而不是 px,让容器跟着文字一起长大。

十、图片在移动端的三个坑

移动端 70% 的体积来自图片,而图片的适配问题比布局更隐蔽。

第一个是固定宽高比。如果不给图片容器预留高度,图片加载前高度为 0,加载完突然撑开,正在读的段落被顶走——这叫布局抖动。解决办法是给容器写 aspect-ratio,或者用老的 padding-top 百分比 hack 兜住,等图片加载完才占位。

第二个是 srcset 的取值。很多人只给两档,结果在 3 倍屏上取到了 1 倍图糊成一片,或者在 1 倍屏上取了 3 倍图白白多下 9 倍数据。srcset 里写实际宽度,sizes 里写这个图在不同断点下的显示宽度,浏览器自己选最合适的一档;sizes 写错是「明明配了响应式图却还是加载了大图」的最常见原因。

第三个是点按区域过小。图标按钮视觉上 20px 就够了,但可点击区域至少要 44×44,否则用户永远点不中。做法是视觉尺寸不变,用内边距或者伪元素把可点击范围扩大出去。

十一、上线前的一张检查清单

所有适配工作最后都会落到一张清单上。我现在的固定顺序是:先在 320px 宽下看一遍有没有横向滚动条,再逐个点开输入框看有没有被自动放大;然后滚到最长的文章底部,确认代码块和表格只在自身容器里横向滚动;接着把手机横过来,确认导航和首屏还能用;最后在系统里把字号调到最大,看按钮和卡片有没有溢出。

这四步每步不超过两分钟,但能拦下我遇到过的九成以上问题。比在开发者工具里对着设备模拟反复调像素有效得多,因为模拟器不会告诉你「用户把字放大了」这件事。

十二、小结:适配的本质是「不要假设屏幕」

viewports 的每一个参数、rem 的每一次计算、断点的每一个选择,本质上都在表达同一个态度:不要假设用户的屏幕有多宽、地址栏在哪、能不能放大、是不是横屏。把那几行 viewport 写对,把 vh 换成 svh/dvh,把字号交给 clamp 兜底,把横向滚动限制在局部容器里,移动端 90% 的「错位」反馈就不会再来了。剩下的 10%,几乎都能用「用真机看一遍」发现。

Last modification:October 30th, 2026 at 09:25 pm

Leave a Comment