前端知库

移动端怎样做屏幕适配?1px 边框为什么看起来变粗了?

移动端适配先写 viewport meta,让布局宽度等于设备宽度;尺寸用 rem 或 vw 随屏幕缩放,或者用 Flex、Grid 和媒体查询做流式布局。1px 变粗是因为高 DPR 屏幕上 1 个 CSS 像素对应多个物理像素,常用 transform 缩放或 0.5px 来画细线。

参考答案

简答

同一张设计稿要在宽 375 和宽 414 的手机上都显示得合适,第一步是在 <head> 里写 <meta name="viewport" content="width=device-width, initial-scale=1">。不写的话,手机浏览器会假装自己有一个大约 980 像素宽的屏幕来排版,再整体缩小显示,文字会小得看不清。

有了 viewport,再让尺寸跟着屏幕宽度变。常见做法有两种。一种是 rem 方案,根据屏幕宽度设置 html 的 font-size,元素尺寸都用 rem 写,屏幕变宽字号跟着变大,所有尺寸一起放大。另一种是 vw 方案,1vw 就是视口宽度的 1%,直接按设计稿换算,比如 375 宽的设计稿上 75px 就写成 20vw,现在更推荐这种,不需要 JS 计算。很多页面其实不需要整体等比缩放,用 Flex、Grid 加媒体查询做流式布局,大屏多显示内容,体验反而更好。

1px 边框变粗,要从设备像素比说起。window.devicePixelRatio 表示 1 个 CSS 像素对应几个物理像素,常见手机是 2 或 3。CSS 里写 1px,在 DPR 为 2 的屏幕上实际占 2 个物理像素,比设计稿上那根 1 物理像素的线粗了一倍。常用的解决办法是用伪元素画一个 2 倍大小的边框,再用 transform: scale(0.5) 缩回去;支持的浏览器上也可以直接写 0.5px。

要注意,rem 和 vw 方案都会让字号随屏幕等比放大,大屏手机和平板上文字可能过大,可以用 clamp() 给尺寸设上下限。

viewport 到底改变了什么

没有 viewport meta 时,移动浏览器使用一个比屏幕宽得多的布局视口,大约 980 像素,好让为桌面设计的网页不至于挤成一团,然后再缩小到屏幕宽度显示。所以页面看起来像缩略图,用户要手动放大。

width=device-width 让布局视口的宽度等于设备的 CSS 像素宽度,比如 iPhone 上是 375 或 390,initial-scale=1 表示初始不缩放。这样你写的 width: 100%、媒体查询里的 max-width: 600px,对应的就是用户实际看到的屏幕宽度。

不要为了禁止缩放写 user-scalable=no 或 maximum-scale=1,视力不好的用户需要放大页面,这会影响无障碍。

rem 和 vw 怎么换算

假设设计稿宽 375px,一个按钮宽 75px。

CSS
/* vw 方案:75 / 375 * 100 = 20 */
.button { width: 20vw; }

/* rem 方案:约定 html 字号 = 屏幕宽度 / 10 */
html { font-size: 10vw; }
.button { width: 2rem; } /* 375 宽时 html 是 37.5px,2rem 就是 75px */

两种写法在 375 宽的屏幕上都是 75px,在 414 宽的屏幕上都会等比变大。早年 rem 方案用 JS 读屏幕宽度设置 font-size,比如 lib-flexible,因为当时 vw 支持得不好。现在 vw 已经普遍支持,可以直接用 CSS 实现。换算一般交给构建工具,比如 PostCSS 插件把设计稿上的 px 自动转成 vw 或 rem,代码里照着设计稿写 px 就行。

1px 细线怎么画

最通用的做法是伪元素加缩放:

CSS
.hairline {
  position: relative;
}
.hairline::after {
  content: '';
  position: absolute;
  inset: 0;
  width: 200%;
  height: 200%;
  border: 1px solid #ddd;
  transform: scale(0.5);
  transform-origin: 0 0;
  pointer-events: none;
}

为什么要先放大到 200%?伪元素宽高是原来的 2 倍,边框 1px,整体缩小一半后,盒子正好和原元素一样大,边框变成 0.5 个 CSS 像素,在 DPR 为 2 的屏幕上就是 1 个物理像素。DPR 为 3 时,可以用媒体查询 (min-resolution: 3dppx) 改成放大 300%、缩小到三分之一。

pointer-events: none 是为了不让这个盖在上面的伪元素挡住点击。只需要一条下边框时,更简单的写法是用一个高度 1px 的伪元素,transform: scaleY(0.5)。

直接写 border: 0.5px 最省事,但结果取决于屏幕。在 Chrome 里实测,DPR 为 2 时计算出的边框宽度是 0.5px,DPR 为 1 时会被取整成 1px,因为一个物理像素已经是能画出的最细线了。不同浏览器的取整规则不完全一样,所以用之前要在目标机型上验证。

常见追问

图片在高 DPR 屏幕上发虚怎么办?

一个 CSS 像素对应多个物理像素,只给 1 倍图就会被拉伸。用 srcset 提供 2 倍、3 倍图,让浏览器按 DPR 选择,图标尽量用 SVG。详见《srcset、sizes 和 picture 怎样减少图片浪费?》。

移动端 100vh 为什么会超出屏幕?

移动浏览器的地址栏会随滚动收起和展开,100vh 通常按地址栏收起时的最大高度算,地址栏显示时页面底部就被遮住了。可以改用 100dvh,它会跟着地址栏变化。各种视口单位的区别见《px、rem、em、百分比和动态视口单位怎么选?》。

刘海屏的内容被挡住怎么办?

在 viewport 里加 viewport-fit=cover,再用 env(safe-area-inset-*) 给内容留出安全区域,见《移动端安全区域怎样避免内容被刘海和底部手势条遮挡?》。