移动端适配

视口设置

<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 设置用户不可手动缩放

主流适配方案

目标是让页面元素能够随屏幕的尺寸变化而等比例缩放

  1. REM
  • 原理: REM是相对于根元素HTML字体大小的单位。通过JS动态设置根元素的 font-size,从而让所有使用REM为单位的元素按比例缩放 根字体大小 = 设备宽度 / 设计稿宽度 * 基准值 元素rem值 = 设计稿中的px值 / 基准值
  • 步骤:
    1. 设置viewport
    2. 给设备宽度动态设置html的font-size
    (function (doc, win) {
      const docEl = doc.documentElement;
      const resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
      
      const recalc = function () {
        const clientWidth = docEl.clientWidth;
        if (!clientWidth) return;
        
        // 设计稿宽度375px,基准值37.5px
        const designWidth = 375;
        const baseSize = designWidth / 10;
        
        // 计算当前设备字体大小
        const fontSize = (clientWidth / designWidth) * baseSize;
        
        // 限制范围,避免极端情况
        fontSize = Math.max(fontSize, 12);
        fontSize = Math.min(fontSize, 64);
        
        docEl.style.fontSize = fontSize + 'px';
      };
      
      // 事件监听
      win.addEventListener(resizeEvt, recalc, false);
      doc.addEventListener('DOMContentLoaded', recalc, false);
      recalc(); // 立即执行
    })(document, window);
    1. 编写css 在 CSS 中,所有需要适配的尺寸(width, height, padding, margin, font-size 等)都使用 REM 单位。
    • 换算技巧: 如果设计稿是 375px(iPhone 6/7/8 标准),则 1rem = 37.5px。一个 100px 的元素就是 100 / 37.5 ≈ 2.6667rem。
    • 工具: 可以使用 VS Code 的 px to rem 插件、PostCSS 的 postcss-pxtorem 插件或 Sass/Less 函数来自动完成换算。

    辅助工具

    • JS转换函数
        function px2rem(px, baseSize = 37.5) {
          return (px / baseSize).toFixed(4) + 'rem';
        }
      
        // 使用示例
        console.log(px2rem(100)); // "2.6667rem"
        console.log(px2rem(15));  // "0.4000rem"
    • Sass/SCSS混合宏
        @mixin px2rem($px, $baseSize: 37.5) {
          $rem: $px / $baseSize;
          @return #{$rem}rem;
        }
        .button {
          width: px2rem(100px);   // 自动转换
          height: px2rem(40px);
          font-size: px2rem(16px);
        }
    • PostCSS自动转换
        // postcss-px-to-viewport配置
        module.exports = {
            plugins: [
              require('postcss-pxtorem')({
                // 根字体大小 - 关键配置
                rootValue: 37.5,  // 375px设计稿 ÷ 10
                
                // 精度控制
                unitPrecision: 5,  // 保留5位小数
                
                // 属性白名单 - 哪些属性需要转换
                propList: [
                  'font',
                  'font-size',
                  'line-height',
                  'letter-spacing',
                  'width',
                  'height',
                  'margin*',
                  'padding*',
                  'border*'
                ],
                
                // 选择器黑名单 - 不转换的选择器
                selectorBlackList: [
                  '.ignore',      // 类名包含ignore的不转换
                  /^\.el-/,      // Element UI的类名不转换
                  /^\.van-/      // Vant UI的类名不转换
                ],
                
                // 替换模式
                replace: true,     // true: 替换px, false: 保留px并添加rem
                
                // 媒体查询转换
                mediaQuery: false, // 不转换媒体查询中的px
                
                // 最小转换值
                minPixelValue: 1,  // 小于1px的不转换(如border: 0.5px)
                
                // 排除文件
                exclude: /node_modules/i  // 不转换node_modules中的文件
              })
            ]
          };
      实际效果:
        /* 实际效果 */
        /* 输入的CSS */
        .container {
          width: 375px;
          height: 200px;
          font-size: 16px;
          padding: 10px 15px;
          border: 1px solid #ccc;
          margin-top: 0.5px;  /* 小于minPixelValue,不转换 */
        }
      
        /* 输出的CSS */
        .container {
          width: 10rem;        /* 375px ÷ 37.5 = 10rem */
          height: 5.33333rem;  /* 200px ÷ 37.5 = 5.33333rem */
          font-size: 0.42667rem; /* 16px ÷ 37.5 = 0.42667rem */
          padding: 0.26667rem 0.4rem; /* 10px和15px转换 */
          border: 0.02667rem solid #ccc; /* 1px转换 */
          margin-top: 0.5px;   /* 保持不变 */
        }
  1. VW
  • 原理: VW(Viewport Width)和 VH(Viewport Height)是直接相对于视口宽高的单位。
  • 步骤:
    1. 设置viewport
    2. 直接使用vw编写css
    • 换算:375px的设计稿,1vw = 3.75px。一个100px的元素就是100/3.75=26.6667vw
    • 页面元素可以直接根据市口宽度等比缩放,无需js
  1. 媒体查询-响应式布局
  • 原理: 通过查询设备的宽度(或高度、分辨率等),来应用不同的 CSS 样式块。通常与 REM 或百分比布局结合使用,用于在特定断点进行较大布局调整(如将两栏布局变成一栏布局)。
/* 为不同尺寸的屏幕设置不同的根字体大小 (配合REM方案) */
@media screen and (min-width: 375px) {
  html {
    font-size: 14px;
  }
}
@media screen and (min-width: 414px) {
  html {
    font-size: 16px;
  }
}

/* 或者直接改变布局结构 */
.container {
  width: 100%;
  display: flex;
  flex-direction: column; /* 默认垂直排列 */
}

@media screen and (min-width: 768px) {
  .container {
    flex-direction: row; /* 屏幕宽度大于768px时改为水平排列 */
  }
}
  1. 弹性布局
  • 弹性盒布局 flex
  • 栅格系统 grid

组合使用

  1. 设置viewport
  2. 核心缩放方案,REM+JS或者VW
  3. 布局辅助:Flex、Grid或者媒体查询

其他适配问题

1. 1px 边框变粗问题

在高清屏下,css的qpx可能对应多个物理像素,导致边框看起来较粗


@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
  .retina-border {
    position: relative;
    border: none;
  }
  .retina-border::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    border: 1px solid #ccc; /* 设置边框颜色 */
    border-radius: 4px; /* 设置圆角 */
    transform: scale(0.5);
    transform-origin: 0 0;
    pointer-events: none; /* 防止伪元素干扰点击 */
    box-sizing: border-box;
  }
}

2. 安全区适配

  • HTML viewport 设置支持安全区域
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
.safe-area-padding{
  padding-top: constant(safe-area-inset-top);
  padding-top: env(safe-area-inset-top);
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
  /* 横屏时处理 */
  padding-left: constant(safe-area-inset-left);
  padding-left: env(safe-area-inset-left);
  padding-right: constant(safe-area-inset-right);
  padding-right: env(safe-area-inset-right);
}

3. 滚动条样式

  • 默认滚动条样式不美观
/* 隐藏滚动条但保持滚动功能 */
.hide-scroll {
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE/Edge */
}
.hide-scroll::-webkit-scrollbar {
  display: none; /* Chrome/Safari */
}

/* Webkit内核浏览器 */
.custom-scroll::-webkit-scrollbar {
  width: 6px; /* 竖直滚动条宽度 */
  height: 6px; /* 水平滚动条高度 */
}

.custom-scroll::-webkit-scrollbar-track {
  background: #f1f1f1;  /* 轨道背景 */
  border-radius: 3px;
}

.custom-scroll::-webkit-scrollbar-thumb {
  background: #c1c1c1; /* 滑块颜色 */
  border-radius: 3px; /* 滑块圆角 */
}

.custom-scroll::-webkit-scrollbar-thumb:hover {
  background: #a8a8a8;
}

4. 点击300ms延迟

  • 早期移动端浏览器为区分单机和双击缩放而存在200ms延迟
  1. 方案1 禁止缩放
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
html {
  touch-action: manipulation; /* 阻止浏览器默认的双击缩放行为,间接消除延迟 */
}
  1. 方案2 如果需要兼容老浏览器 使用FastClick库
import FastClick from 'fastclick';

if ('addEventListener' in document) {
  document.addEventListener('DOMContentLoaded', function() {
    FastClick.attach(document.body);
  }, false);
}

5. 滚动穿透

  • 当弹出层上滚动时,背景页面也会随之滚动
function preventScrollThrough(open) {
  const body = document.body;
  const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  if (open) {
    body.style.overflow = 'hidden';
    body.style.position = 'fixed';
    body.style.top = `-${scrollTop}px`; // 记录并抵消当前滚动位置
    body.style.width = '100%';
  } else {
    body.style.overflow = '';
    body.style.position = '';
    body.style.top = '';
    body.style.width = '';
    window.scrollTo(0, parseInt(scrollTop || '0')); // 恢复滚动位置
  }
}
// 弹窗打开时调用
preventScrollThrough(true);
// 弹窗关闭时调用
preventScrollThrough(false);

6. iOS橡皮筋效果滚动

禁用iOS过度滚动的橡皮筋效果

/* 全局禁止 */
body, html {
  width: 100%;
  height: 100%;
  overflow: hidden; /* 禁止全局滚动 */
}

/* 需要滚动的容器使用上面的 .scroll-container 样式。 */
.scroll-container {
  height: 300px; /* 或 max-height */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch; /* 启用iOS的顺滑滚动 */
}

7. 微信长按图片保存

防止微信中长按图片弹出保存菜单

img {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  pointer-events: none;
}

或者使用背景替代img标签,或者通过js阻止默认行为

document.addEventListener('contextmenu', function(e) {
  if (e.target.nodeName === 'IMG') {
    e.preventDefault(); // 阻止默认右键菜单(对长按菜单影响因浏览器而异)
  }
});

8. 图片资源加载优化

  1. 懒加载
  • 方案1
const lazyImages = document.querySelectorAll('img[data-src]');

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.removeAttribute('data-src');
      observer.unobserve(img);
    }
  });
});

lazyImages.forEach((img) => {
  observer.observe(img);
});
  • 方案2 使用loading=“lazy”
<img src="placeholder.jpg" data-src="real-image.jpg" alt="图片说明" loading="lazy">
  1. 响应式图片
  2. 方案1 使用picture元素
<!-- 不同屏幕尺寸加载不同图片 -->
<picture>
  <source media="(min-width: 800px)" srcset="hero.jpg">
  <source media="(min-width: 400px)" srcset="medium.jpg">
  <img src="small.jpg" alt="后备图片">
</picture>
  1. 方案2 使用srcset和sizes属性
  • srcset 属性允许我们为同一个 元素指定多个不同尺寸的图片源,让浏览器根据设备特性选择最合适的图片。
  • w 单位表示图片的实际宽度(像素)
  • sizes 属性告诉浏览器在不同断点下图片会显示的尺寸
  • 浏览器会自动选择最适合当前设备的图片版本
<img 
  src="small.jpg"
  srcset="small.jpg 300w,
          medium.jpg 600w,
          large.jpg 900w"
  sizes="(max-width: 500px) 300px,
         (max-width: 900px) 600px,
         900px"
  alt="响应式图片"
>

<img 
  src="image-1x.jpg"
  srcset="image-1x.jpg 1x,
          image-2x.jpg 2x,
          image-3x.jpg 3x"
  alt="不同像素密度的响应式图片">

9. 表单输入优化

  • 正确使用input type,唤起更合适的虚拟键盘
    <input type="tel" inputmode="numeric" pattern="[0-9]*"> <!-- 电话号码或数字 -->
    <input type="email" inputmode="email"> <!-- 邮箱 -->
    <input type="search"> <!-- 搜索框 -->
    <input type="text" autocomplete="off"  autocorrect="off" autocapitalize="off" spellcheck="false"> <!-- 禁用自动纠正和首字母大写 -->

10. 软键盘弹出问题

  • 软键盘弹出时可能遮挡输入框
const input = document.querySelector('input');
input.addEventListener('focus', () => {
    setTimeout(() => {
        input.scrollIntoView({ 
            behavior: 'smooth', 
            block: 'center'  // 将输入框滚动到视口中心
        });
    }, 300);
});
作者

panxiao

发布日期

2025 - 09 - 02

许可证

Unlicensed

评论