Skip to main content
移动端适配我最早只会写 px,被 Retina 的 1px 和刘海屏坑过之后,才开始按”视口、单位、约束、细节”分层处理。

Q1: 移动端适配到底在适配什么?

移动端适配不是”把 px 换成 rem”,而是同时处理四件事:视口如何建立、尺寸用什么单位、布局在什么条件下变化、细节如何对齐物理像素。把这四层分开,绝大多数适配问题都能定位到具体一层。

三层视口

不写 viewport meta 时,移动浏览器会用一个约 980px 的布局视口来渲染桌面页面,再把整页缩小塞进屏幕,字就会小到看不清。width=device-width 的作用就是把布局视口设为理想视口。
  • width=device-width:布局视口等于设备宽度。
  • initial-scale=1:初始缩放为 1,避免部分浏览器自行缩放。
  • viewport-fit=cover:允许页面铺满刘海和圆角区域,这是 env(safe-area-inset-*) 生效的前提。
user-scalable=no 和 maximum-scale=1 能”锁死”缩放,但会伤害低视力用户的可访问性,现代项目不应默认使用。

visualViewport 与软键盘

window.visualViewport 暴露视觉视口的 width、height、scale、offsetTop。软键盘弹起时,布局视口通常不变,变化的正是视觉视口,所以底部固定输入框会被键盘顶起或遮挡:
不写 meta 时,布局视口约为 980px,媒体查询和百分比都按 980px 计算,页面会被整体缩小,出现”字很小、点不准”。只写 initial-scale=1 而不写 width=device-width,部分浏览器仍会把布局视口设为一个大于屏幕的值,效果不稳定。两者一起写,布局视口才确定等于设备宽度。

Q2: rem、vw、百分比、dvh 怎么选?

没有一种单位能覆盖所有场景。实际项目里通常是”布局用弹性单位、文字用可读单位、视口高度用动态单位”。

rem:靠根字号整体缩放

4.2667vw 来自 16 / 375。用 clamp() 封顶可以避免平板上字大得像海报。若用 JS 动态设置根字号,要处理首屏脚本加载前的闪烁,并尊重用户在浏览器里调大的字号。

vw:与视口严格等比,但必须封顶

vw 的问题是没有上限:1vw 在窄屏上只有几像素,在宽屏上却很大。所有 vw 字号都应该配合 clamp() 或媒体查询使用。

百分比:只解决”相对父级”

百分比宽度好用,但 padding-top: 56.25% 这类写法依赖父级宽度来撑高度,容易和 gap、border 叠加后溢出,现代项目更推荐 aspect-ratio。

dvh:解决移动端 100vh 的经典坑

移动浏览器地址栏收起/展开时,100vh 取的是较大视口高度,内容会被地址栏盖住。dvh 跟随当前可见高度:
svh 是地址栏展开时的最小高度,lvh 是收起时的最大高度,dvh 随浏览器 UI 实时变化。全屏首屏通常用 svh 更稳,弹层用 dvh 更贴手。
JS 方案要在页面渲染前执行,否则首屏字号会闪一下;同时它把根字号写死,会覆盖用户在浏览器里设置的字号偏好,影响可访问性。如果只是整体等比缩放,clamp() 配合 vw 已能覆盖大部分场景;确实需要按设计稿精确缩放时,再用 JS,并给根字号设置上下限。

Q3: 媒体查询与容器查询怎么配合?

媒体查询看的是视口,容器查询看的是父容器。前者决定页面级断点,后者决定组件在什么宽度下换布局。

移动优先的断点

移动优先的含义是:基础样式服务最小屏,断点只做增强。断点值应来自设计稿的内容变化点,而不是照着机型宽度抄。

容器查询

container-type: inline-size 让容器在行内方向可被查询,同时建立独立的包含块;container-name 用来在同级多个容器间精确指定。容器查询单位 cqw、cqh 分别对应容器宽高的 1%。 选择原则:
  • 页面骨架、导航形态:媒体查询或容器查询都可以,按视口更直观。
  • 可复用组件(卡片、表格、工具栏):优先容器查询,组件放进侧栏或主区域都能自适应。
  • 需要 hover、pointer、prefers-reduced-motion 这类环境能力:只能用媒体查询。
在触屏设备上,悬停样式可能永远不触发或触发后”粘住”,所以悬停效果应该包在 @media (hover: hover) and (pointer: fine) 里。

Q4: 1px 边框在 devicePixelRatio 下为什么会变粗?

px 是逻辑单位,devicePixelRatio(DPR)表示 1 个 CSS 像素覆盖多少个物理像素。
  • DPR = 1:1 CSS px = 1 物理像素。
  • DPR = 2:1 CSS px = 2×2 物理像素,一条 1 CSS px 宽的线横向占 2 个物理像素。
  • DPR = 3:同理,横向占 3 个物理像素。
设计稿上的”1px 发丝线”通常指 1 个物理像素,换算成 CSS 单位是 1 / DPR px。直接写 border: 1px 就等于把线画粗了 DPR 倍,这是”1px 问题”的根源。

方案对比

伪元素缩放的通用写法

缩放的是绝对定位的伪元素,不参与布局,所以不会引起周围元素位移。四条边都要时,用 scale() 同时缩放两个方向,或分别用四个伪元素/额外元素处理。
部分渲染实现会把小数边框按设备像素取整,0.5px 可能被舍入为 0,边框直接不见;也可能被舍入为 1 个物理像素,效果又偏细。因此 0.5px 可以作为渐进增强,但不能作为唯一方案。需要稳定发丝线时,用伪元素缩放或背景渐变兜底。
缩放后的线条落在物理像素边界上时是清晰的;如果元素的偏移量带有小数,缩放后可能落在半个物理像素上,出现灰边。排查时检查父级是否有 translate、scale 或非整数尺寸,把 transform-origin 设到边缘,并尽量让容器尺寸是整数。

Q5: 刘海屏、圆角与安全区怎么处理?

viewport-fit=cover 让页面延伸进刘海和圆角区域,此时必须用 env(safe-area-inset-*) 把内容推回安全区,否则文字会被刘海或 Home 指示条压住。
env() 的第二个参数是回退值。不支持安全区的设备会返回 0px,不会把布局撑开。早期 iOS 使用 constant(),现在统一用 env() 即可。 需要注意:安全区只在 viewport-fit=cover 时才有非零值;横屏时左右安全区才明显,竖屏主要看上下。

Q6: 横竖屏、平板和折叠屏怎么适配?

横竖屏

只写 orientation: landscape 会把桌面浏览器也算进去,配合 max-height 才能更准确地命中手机横屏。

平板

平板不是”大号手机”。常见策略是限制内容最大宽度、增加列数,而不是把所有尺寸等比放大:
minmax(0, 1fr) 是为了避免长内容把网格撑破,这一点在平板宽栏里尤其常见。

折叠屏

折叠屏展开后,浏览器可能把窗口拆成两个视口段。CSS Viewport Segments 提供了对应的媒体特性和环境变量:
这段代码的含义是:设备横向有两个视口段时,按两个视口段的宽度分栏,内容不会跨过中间的折痕。该特性依赖浏览器与设备支持,使用前要做能力判断,不能假设所有折叠屏都支持。
先保证不跨折痕:用 env(viewport-segment-*) 或媒体查询把内容限制在单段内,避免关键操作按钮压在折痕上。再保证布局可退化:不依赖双栏也能完整阅读,支持视口段时再增强为双栏。这样在老设备和未实现该特性的浏览器上都不会崩。

Q7: 移动端点击延迟与 touch-action 怎么处理?

早期的 300ms 点击延迟来自”双击缩放”判定:浏览器要等一段时间确认用户不是要双击放大,才派发 click。 现代浏览器在 viewport 设置为 width=device-width 后基本取消了这段延迟,但在以下场景仍可能遇到:
  • 页面仍使用默认的宽布局视口。
  • 自定义手势与浏览器手势冲突。
  • 旧版 WebView 或内嵌浏览器。
touch-action 用来声明元素上允许哪些触摸手势,从而让浏览器提前决定是否接管:
touch-action: manipulation 允许平移和双指缩放,但去掉双击缩放,是最常用的”消除点击延迟”写法。touch-action: none 会同时禁用滚动和缩放,只适合确定要自己接管手势的区域。

事件监听的 passive

touchstart、touchmove 监听器如果调用 preventDefault(),会阻塞滚动,浏览器必须等 JS 执行完才能滚动。声明 passive: true 表示”不会阻止默认行为”,浏览器可以立即滚动:
确实需要阻止滚动时,才用 { passive: false },并把处理逻辑控制得足够轻。需要同时支持鼠标、触控笔和触摸时,优先用 Pointer Events(pointerdown / pointermove / pointerup),不要和 Touch Events 混用。

Q8: 响应式图片与 CLS 怎么控制?

移动端图片同时影响三件事:流量、清晰度和布局稳定性。布局稳定性就是 CLS(Cumulative Layout Shift,累积布局偏移)。

srcset 与 sizes

  • srcset 提供候选图及其固有宽度。
  • sizes 告诉浏览器图片在布局中占多宽,浏览器据此选择合适密度的图。
  • width / height 提供宽高比,浏览器在图片加载前就能预留空间。

预留空间,避免 CLS

aspect-ratio 和 width / height 属性至少保留一个。只写 width: 100% 而不给高度,图片加载完成的瞬间会把下方内容顶下去,这就是最常见的 CLS 来源。

首屏图与懒加载

首屏大图用 loading="lazy" 会推迟 LCP,应该正常加载并提升优先级;首屏外的图才懒加载。 picture 适合按格式或艺术方向切换:在 source 上用 type 指定图片格式,或用 media 指定不同屏幕下的裁切版本,最后用 img 兜底。
不是。CLS 衡量的是元素位置的非预期移动,和文件体积无直接关系。只要图片在加载前没有预留宽高,加载完成后把内容顶开,就会产生布局偏移;反过来,预留了 aspect-ratio,图片再大也不会造成 CLS(只会影响 LCP 和流量)。自定义字体同理,用 font-display: swap 配合度量接近的回退字体,减少加载前后的推挤。

Q9: 面试现场怎么答这道题?

及格回答(60-70 分):
移动端适配一般用 rem,写个脚本根据屏幕宽度设置根字号,设计稿 750 的话就除以 7.5。1px 就用 transform scale 缩放一下。刘海屏加个 padding。
问题出在:只报方案名,说不出方案解决哪一层问题,也说不出副作用。 满分回答(90-100 分):
我会把适配拆成四层。第一层是视口:width=device-width 建立布局视口,viewport-fit=cover 打开安全区,需要处理键盘或缩放时读 visualViewport。第二层是单位:布局宽度用 % 或 fr,整体缩放用 rem 或带 clamp() 的 vw,全屏高度用 svh / dvh,避免 100vh 被地址栏盖住。第三层是约束:页面级用媒体查询做移动优先断点,可复用组件用容器查询按父容器宽度变形,悬停效果放进 hover / pointer 能力查询。第四层是细节:1px 按 DPR 用伪元素缩放,安全区用 env(safe-area-inset-*),点击延迟用 touch-action: manipulation 配合 passive 监听,图片用 srcset + sizes + 固定宽高比控制 CLS。 这套分层的好处是每个问题都能定位:字太小是视口层,元素溢出是单位层,组件在侧栏变形是约束层,线条粗是细节层。折叠屏和横竖屏属于约束层的边界情况,用视口段环境变量和 orientation 查询做增强,不假设所有设备都支持。

常见追问

答题要点: 按内容差异和维护成本判断,不要只按”响应式更先进”。高分回答:内容结构一致、交互差异小,用一套响应式,配合容器查询和断点增强。移动端需要完全不同的信息架构或性能预算(例如桌面是复杂表格、移动端是卡片流)时,单独一套更清晰,但要接受两套代码的维护成本。
答题要点: 说清两者的参照系和副作用。高分回答:rem 参照根字号,能通过 clamp() 或脚本统一缩放,适合把设计稿尺寸整体映射;缺点是改根字号会影响所有 rem,也可能覆盖用户的字号偏好。vw 参照视口宽度,纯 CSS 就能等比缩放,但无上限,必须用 clamp() 封顶。我的默认选择是 clamp() + vw 做字号,布局用 fr / %,rem 只用于需要跟随根字号的间距。
答题要点: 区分”容器尺寸”和”环境能力”。高分回答:不能。容器查询解决的是组件随父容器宽度变形,媒体查询还负责视口断点、hover、pointer、prefers-reduced-motion、prefers-color-scheme 这类环境能力。合理的组合是:页面骨架用媒体查询,组件内部用容器查询,能力探测用媒体查询。
答题要点: 说清视口模拟与真机验证的边界。高分回答:DevTools 的设备模拟适合验证断点、布局和媒体查询,但它模拟不了真实 DPR 下的发丝线、刘海安全区、软键盘行为和触摸手势。我的流程是:模拟器过一遍断点和单位,再用真机验证安全区、1px、键盘弹起、横竖屏切换和滚动性能。折叠屏这类设备拿不到时,至少用视口段媒体查询的降级路径保证可用。