跳到主要内容

移动端适配

问题

移动端适配有哪些方案?viewport 是什么?如何解决 1px 问题?安全区域怎么处理?

答案

viewport(视口)

三种视口

视口说明
布局视口(Layout Viewport)网页布局的基准视口,移动端默认通常是 980px
视觉视口(Visual Viewport)用户实际看到的区域,受缩放影响
理想视口(Ideal Viewport)设备屏幕宽度,通过 <meta viewport> 设置
viewport meta 标签
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
属性说明常用值
width布局视口宽度device-width
initial-scale初始缩放比例1.0
maximum-scale最大缩放比例1.0 / 5.0
minimum-scale最小缩放比例1.0
user-scalable是否允许用户缩放yes / no
viewport-fit视口填充模式cover(刘海屏)
user-scalable=no

禁止缩放会影响无障碍性。建议设置 maximum-scale=5.0 而非完全禁止缩放。iOS Safari 从 iOS 10 起会忽略 user-scalable=no

像素概念

概念说明
CSS 像素(px)CSS 中使用的逻辑像素
设备像素(Device Pixel)屏幕物理像素
设备像素比(DPR)物理像素 / CSS 像素(如 iPhone 14 DPR=3)
设备独立像素(DIP)与 CSS 像素等同
iPhone 14 Pro: 物理分辨率 1179×2556, CSS 分辨率 393×852, DPR = 3
// 获取 DPR
const dpr: number = window.devicePixelRatio; // 2 或 3

适配方案

方案 1:vw/vh 方案(推荐)

直接使用视口单位,1vw = 视口宽度的 1%:

.box {
width: 90vw; /* 视口宽度的 90% */
font-size: 4.267vw; /* 在 375px 宽度下 = 16px,375 × 4.267% ≈ 16 */
padding: 4vw;
}

配合 PostCSS 插件自动转换:

postcss.config.ts
// postcss-px-to-viewport 插件配置
export default {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375, // 设计稿宽度
unitPrecision: 5,
viewportUnit: 'vw',
minPixelValue: 1,
}
}
};

方案 2:rem 方案

根据屏幕宽度动态设置根元素 font-size,所有尺寸用 rem:

动态设置 rem
function setRemUnit(): void {
const docEl = document.documentElement;
const width = docEl.clientWidth;
// 设计稿 375px,1rem = 设计稿宽度/10 = 37.5px
docEl.style.fontSize = width / 10 + 'px';
}

setRemUnit();
window.addEventListener('resize', setRemUnit);
/* 设计稿上 180px 宽的元素 */
.box {
width: 4.8rem; /* 180 / 37.5 = 4.8rem */
}
lib-flexible 原理

阿里的 lib-flexible 就是上述方案的封装。但在 vw 兼容性已经很好的今天,推荐直接使用 vw 方案,更简单且无需 JS。

方案 3:clamp() + vw 弹性方案

:root {
/* 最小 14px,理想 4vw,最大 18px */
font-size: clamp(14px, 4vw, 18px);
}

.container {
padding: clamp(12px, 4vw, 32px);
}

方案对比

方案优点缺点
vw无需 JS,纯 CSS在超大屏上可能过大
rem控制精确需要 JS 设置根 font-size
clamp() + vw有上下限,体验好兼容性要求较高
百分比简单百分比参照物不统一
媒体查询断点精确控制需要写多套样式

1px 问题

在高 DPR 设备上,border: 1px 在视觉上偏粗(1 CSS px = 2-3 物理像素)。

解决方案

方案1: transform + 伪元素(推荐)
.border-bottom-1px {
position: relative;
}
.border-bottom-1px::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 1px;
background: #ccc;
transform: scaleY(0.5); /* DPR 2 时缩小一半 */
}

/* 四边边框 */
.border-1px::after {
content: '';
position: absolute;
top: 0; left: 0;
width: 200%;
height: 200%;
border: 1px solid #ccc;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
border-radius: inherit; /* 继承圆角 */
}
方案2: 0.5px(仅 iOS Safari)
.border {
border: 0.5px solid #ccc;
}
方案3: box-shadow
.border {
box-shadow: 0 0 0 0.5px #ccc;
}
方案4: SVG background
.border {
border: none;
background-image: url("data:image/svg+xml,...");
}

安全区域(Safe Area)

刘海屏/异形屏需要处理安全区域:

<!-- 首先设置 viewport-fit=cover -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
使用 env() 环境变量
.content {
/* 安全区域内边距 */
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}

/* 底部固定栏适配 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
/* 底部额外增加安全区域的间距 */
padding-bottom: calc(12px + env(safe-area-inset-bottom));
/* 兼容旧 iOS */
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
}
环境变量说明
safe-area-inset-top顶部安全距离(刘海/状态栏)
safe-area-inset-bottom底部安全距离(Home 指示器)
safe-area-inset-left左侧安全距离
safe-area-inset-right右侧安全距离

常见移动端问题

点击延迟(已解决)

<!-- 现代浏览器已无 300ms 延迟,但 meta viewport 是前提 -->
<meta name="viewport" content="width=device-width">
/* 或使用 touch-action */
html {
touch-action: manipulation; /* 禁止双击缩放,消除延迟 */
}

滚动穿透

/* 弹窗打开时阻止背景滚动 */
body.modal-open {
overflow: hidden;
position: fixed;
width: 100%;
}

/* iOS 需要额外处理 */
.modal-overlay {
overscroll-behavior: contain; /* 阻止滚动链传播 */
}

键盘弹起遮挡输入框

// 移动端输入框获取焦点时,确保可见
function scrollToInput(input: HTMLElement): void {
setTimeout(() => {
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 300); // 等待键盘弹起
}

常见面试问题

Q1: 移动端适配方案有哪些?推荐哪种?

答案

方案推荐度说明
vw⭐⭐⭐⭐⭐纯 CSS,配合 postcss-px-to-viewport
clamp()⭐⭐⭐⭐有上下限的弹性方案
rem⭐⭐⭐需要 JS,但更精确
媒体查询⭐⭐⭐适合断点式适配

2024 年推荐 vw + clamp() 组合方案。

Q2: 什么是设备像素比(DPR)?

答案

DPR = 设备物理像素 / CSS 逻辑像素。iPhone 14 Pro 的 DPR 是 3,意味着 1 个 CSS 像素对应 3×3 = 9 个物理像素。

获取方式:window.devicePixelRatio

DPR 影响:图片需要提供 2x/3x 版本才能在高 DPR 设备上清晰显示。更多参见图片优化

Q3: 如何解决移动端 1px 问题?

答案

推荐 transform + 伪元素 方案:

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

原理:元素放大 200%,边框保持 1px,再整体缩小 50%,视觉上边框变为 0.5px。

Q4: 安全区域是什么?怎么适配?

答案

安全区域是刘海屏/异形屏中不被遮挡的显示区域。适配步骤:

  1. 设置 viewport-fit=cover
  2. 使用 env(safe-area-inset-*) 环境变量
  3. 底部固定栏增加 padding-bottom: env(safe-area-inset-bottom)

Q5: rem 和 vw 方案各有什么优缺点?

答案

特性remvw
依赖 JS
精确度
超大屏表现可限制最大值会无限放大(需 max-width
兼容性好(iOS 8+、Android 4.4+)
维护成本需要 JS 脚本PostCSS 插件自动处理

Q6: 移动端 300ms 点击延迟为什么会存在?现在还有吗?

答案

早期移动端浏览器需要等待 300ms 来判断用户是单击还是双击缩放。现代浏览器中,只要设置了 <meta name="viewport" content="width=device-width">,300ms 延迟就会自动消除。

也可以设置 touch-action: manipulation 来禁止双击缩放。

Q7: 如何处理移动端滚动穿透?

答案

弹窗打开时背景仍然可以滚动的问题:

/* 方案1: overscroll-behavior(推荐) */
.modal {
overscroll-behavior: contain;
}

/* 方案2: body 固定 */
body.no-scroll {
position: fixed;
width: 100%;
top: var(--scroll-y); /* JS 记录滚动位置 */
}
// 记录并恢复滚动位置
function lockScroll(): void {
const scrollY = window.scrollY;
document.documentElement.style.setProperty('--scroll-y', `-${scrollY}px`);
document.body.classList.add('no-scroll');
}

function unlockScroll(): void {
const scrollY = document.documentElement.style.getPropertyValue('--scroll-y');
document.body.classList.remove('no-scroll');
window.scrollTo(0, parseInt(scrollY || '0') * -1);
}

相关链接