移动端适配
问题
移动端适配有哪些方案?viewport 是什么?如何解决 1px 问题?安全区域怎么处理?
答案
viewport(视口)
三种视口
| 视口 | 说明 |
|---|---|
| 布局视口(Layout Viewport) | 网页布局的基准视口,移动端默认通常是 980px |
| 视觉视口(Visual Viewport) | 用户实际看到的区域,受缩放影响 |
| 理想视口(Ideal Viewport) | 设备屏幕宽度,通过 <meta viewport> 设置 |
<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(刘海屏) |
禁止缩放会影响无障碍性。建议设置 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-px-to-viewport 插件配置
export default {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375, // 设计稿宽度
unitPrecision: 5,
viewportUnit: 'vw',
minPixelValue: 1,
}
}
};
方案 2:rem 方案
根据屏幕宽度动态设置根元素 font-size,所有尺寸用 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 就是上述方案的封装。但在 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 物理像素)。
解决方案
.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; /* 继承圆角 */
}
.border {
border: 0.5px solid #ccc;
}
.border {
box-shadow: 0 0 0 0.5px #ccc;
}
.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">
.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: 安全区域是什么?怎么适配?
答案:
安全区域是刘海屏/异形屏中不被遮挡的显示区域。适配步骤:
- 设置
viewport-fit=cover - 使用
env(safe-area-inset-*)环境变量 - 底部固定栏增加
padding-bottom: env(safe-area-inset-bottom)
Q5: rem 和 vw 方案各有什么优缺点?
答案:
| 特性 | rem | vw |
|---|---|---|
| 依赖 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);
}