CC 咖啡猫的工作空间 Coding Space

移动端适配

视口与像素

像素体系

概念 含义 示例
物理像素 屏幕真实的发光点数量 iPhone 15: 1179 x 2556
逻辑像素 开发者使用的 CSS 尺寸单位(px 375 x 812(iPhone 逻辑像素)
CSS 像素 布局时的抽象单位,与逻辑像素等价 width: 375px
设备像素比(DPR) 物理像素 / 逻辑像素 iPhone 15 DPR = 3(1179/393)
物理像素: 1179px (实际发光点)
逻辑像素: 393px (CSS 尺寸)
DPR = 1179 / 393 = 3
所以 1pt (iOS) = 3x3 物理像素

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 初始缩放比例 1.0
maximum-scale=1.0 最大缩放比例 1.0(可省略,保留用户缩放能力更友好)
user-scalable=no 禁止用户缩放 谨慎使用(影响无障碍)

不设置 viewport 时,移动端浏览器默认使用 980px 宽度渲染页面,然后缩小适配屏幕,导致文字太小。

Retina 屏适配

技术 原理 适用场景
@2x @3x 图片 准备多套倍图 + 服务端/JS 判断 DPR 背景图片
srcset 浏览器根据 DPR 和视口选择资源 <img> 标签
icon font → SVG 矢量图标,任意缩放不模糊 图标、Logo
image-set() CSS 中根据 DPR 选择图片 CSS 背景图
<!-- srcset 响应式按 DPR 选择 -->
<img src="logo.png"
     srcset="logo@2x.png 2x, logo@3x.png 3x"
     alt="logo">

<!-- CSS image-set -->
.background {
  background-image: image-set(
    url(bg.png) 1x,
    url(bg@2x.png) 2x,
    url(bg@3x.png) 3x
  );
}

rem 方案

原理

  • rem = root em,相对于 <html> 元素的 font-size
  • 动态设置根元素的 font-size,使 1rem 对应设计稿的 px 值
// flexible.js 核心逻辑(简化版)
(function() {
  const docEl = document.documentElement;
  const setRem = () => {
    const width = docEl.clientWidth;
    // 设计稿宽度 750px(@2x 尺寸),设置 1rem = 100px(÷7.5)
    const rem = Math.min(width, 750) / 7.5;
    docEl.style.fontSize = `${rem}px`;
  };
  setRem();
  window.addEventListener('resize', setRem);
})();
// iPhone 6/7/8 宽度 375px → 1rem = 375/7.5 = 50px
// 设计稿 750px 上 200px → 200/100 = 2rem → 实际 2*50 = 100px(比例正确)

postcss-pxtorem

// postcss.config.js
module.exports = {
  plugins: {
    'postcss-pxtorem': {
      rootValue: 100,         // 对应 1rem = 100px(设计稿 750)
      propList: ['*'],        // 转换所有属性
      exclude: /node_modules/i, // 排除 node_modules
      minPixelValue: 2,       // < 2px 不转换
    },
  },
};
/* 输入 */
.box {
  width: 200px;    /* → 2rem */
  height: 100px;   /* → 1rem */
  border: 1px;     /* 保留(< minPixelValue) */
}

/* 输出 */
.box {
  width: 2rem;
  height: 1rem;
  border: 1px;
}

rem 换算公式

1rem = html font-size(动态值)
设计稿尺寸(px) / rootValue = rem 值
显示尺寸(px) = rem * 当前根 font-size
设计稿宽度 rootValue 逻辑屏 375px 时 1rem
750(@2x) 100 50px
375(@1x) 37.5 37.5px

rem 的缺点: 切换设备时所有尺寸都重新计算,1rem 在不同屏幕物理尺寸不同。vw/vh 方案逐渐取代 rem。


vw/vh 方案

原理

1vw = 视口宽度的 1%
1vh = 视口高度的 1%
100vw = 视口完整宽度
100vh = 视口完整高度
// postcss-px-to-viewport 配置
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      viewportWidth: 375,    // 设计稿宽度(逻辑像素)
      unitPrecision: 5,      // 精度
      viewportUnit: 'vw',
      minPixelValue: 1,
      exclude: [/node_modules/],
    },
  },
};
/* 输入: 设计稿 375px 上 100px 宽 */
.box { width: 100px; }
/* 输出 */
.box { width: 26.66667vw; }  /* 100/375*100 ≈ 26.667 */

vw 的兼容性问题

问题 原因 解决
100vw 溢出 100vw 包含滚动条宽度,window.innerWidth < 100vw 使用 width: 100% 替代
100vh 在移动端溢出 移动端浏览器地址栏收起时视口高度变化 使用 dvh / svh / lvh

安全区适配(iPhone 刘海/底部指示条)

/* safe-area-inset-* 只在 iPhone X+ 生效 */
.container {
  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);
}

/* 固定底部安全适配 */
.footer {
  padding-bottom: constant(safe-area-inset-bottom); /* 旧语法 iOS 11.0 */
  padding-bottom: env(safe-area-inset-bottom);      /* 新语法 iOS 11.2+ */
}

动态视口单位(dvh / svh / lvh)

单位 含义 说明
100vh 最大视口高度(地址栏收起时) 地址栏展开时内容被遮挡
100dvh 动态视口高度(随地址栏变化) 地址栏展开/收起时实时变化
100svh 最小视口高度(地址栏展开时) 始终可见区域最小高度
100lvh 最大视口高度,同 vh 与 vh 等价
/* 推荐用 dvh 或 svh 替代 vh */
.fullscreen {
  height: 100dvh;  /* 跟随地址栏动态变化 */
  /* 或 */
  min-height: 100svh; /* 确保始终覆盖可见区域 */
}

1px 边框

方案对比

方案 原理 兼容性 说明
伪元素 + transform scale(0.5) 通过 ::after + 缩放 全兼容 最推荐
viewport 方案 设置 initial-scale=0.5 响应式需额外处理 全局影响太大
border: 0.5px 直接设置 0.5px iOS 8+ / 部分 Android 兼容性有限

伪元素 + scale 方案

/* 1px 下边框 */
.one-px-border {
  position: relative;
}
.one-px-border::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: #ddd;
  transform: scaleY(0.5);       /* DPR=2 时缩为一半 */
  transform-origin: 0 0;
}

/* DPR 3 时 */
@media (-webkit-min-device-pixel-ratio: 3) {
  .one-px-border::after {
    transform: scaleY(0.333);   /* DPR=3 缩 1/3 */
  }
}

响应式

媒体查询(移动优先)

/* 移动优先——基础样式默认就是移动端 */
.container {
  display: flex;
  flex-direction: column;
  padding: 16px;
}

/* 平板 */
@media (min-width: 768px) {
  .container {
    flex-direction: row;
    padding: 24px;
  }
}

/* 桌面 */
@media (min-width: 1024px) {
  .container {
    max-width: 1200px;
    margin: 0 auto;
  }
}

弹性布局技术

技术 适用场景 说明
flex 一维布局(行/列内分布) 最常用
grid 二维布局(行列网格) 复杂页面骨架
clamp() 流式排版 字体/间距在一定区间自适应
/* clamp(MIN, PREFERRED, MAX) */
h1 {
  font-size: clamp(1.5rem, 2.5vw + 1rem, 3rem);
  /* 最小 1.5rem, 随视口增长, 最大 3rem */
}
.container {
  padding: clamp(16px, 4vw, 48px);
}

Touch 事件

300ms 延迟

原因 解决方案
浏览器等待 300ms 判断是单次点击还是双击缩放 1. <meta viewport width=device-width> — iOS 9+ 自动消除
2. touch-action: manipulation — 禁止双击缩放
3. FastClick 库(已不推荐,现代浏览器不再需要)
/* 推荐 — 声明所有触摸操作为立即操作 */
html {
  touch-action: manipulation;
}

手势库原理

原理 说明
Hammer.js 基于 Touch Events(touchstart/touchmove/touchend 经典手势库,支持 tap/swipe/pan/pinch/rotate
Pointer Events W3C 标准,统一鼠标/触控/笔事件 pointerdown/pointermove/pointerup,兼容性 >95%
// Hammer.js
const el = document.getElementById('canvas');
const hammertime = new Hammer(el);
hammertime.on('swipe', (e) => {
  console.log('swipe', e.direction, e.distance);
});

// Pointer Events(原生)
element.addEventListener('pointerdown', (e) => {
  // e.pointerType: 'mouse' | 'touch' | 'pen'
});