移动端适配问题终极探讨(上)

6,459 阅读7分钟

为什么要写这篇文章?

最近公司做了很多花里胡哨的H5活动,其实H5页面并不难每个前端都可以写,但细说下来有很多前端细节做的并不是那么完美,其实把H5页面做完善,适配完美也是件挺难的事(至少我觉得是这样),下面我们就来总结下关于H5适配的那些事

说明

为了更好理解此篇文章,你可以先阅读为什么我们常说1px问题而不说2px设备独立像素,css像素,逻辑像素,设备像素比概念有基本的了解

此文是适配系列文章的上

普遍的解决方案

研究之前我们可以看看大厂都是如何适配H5

淘宝

地址: main.m.taobao.com/

方案: Flexible

分析:值得聊的是,虽然Flexible是淘宝团队出的关于移动端适配的方案,但手机淘宝似乎并没有使用此方案,可以看下面几张图得出结论

我们现在改变手机型号

可以发现人家的适配单位直接是px根本没有使用rem,只不过px的值是通过手机屏幕的不同动态计算出来的,所以当我们改变苹果的大小时,网站就会刷新动态计算出对应的px值,从而达到适配的目的

随便进去一个淘宝的内页,发现使用的适配方案是vw

京东

地址:m.jd.com/ 方案:rem

分析:京东的适配比较粗暴,直接使用 媒体查询改变html的根font-size 然后使用rem进行适配

字节跳动

地址:job.bytedance.com/campus/m/po…

方案:responsive.js

分析: 我觉得responsive.js和淘宝的Flexible.js本质上是一个东西,都是动态的改变htmlfont-size然后用rem进行适配

适配总结

通过这些大厂的产品,我们可以总结到,移动端适配的三种方案

  • rem (主流)
  • vw/vh (部分)
  • 直接px (分场景)

那么?这边文章就这么完了?😶其实这才刚刚开始我们今天的干货

说说Flexible

Flexible作为移动端适配的鼻祖,非常具有研究价值,并且现在很多的移动端H5都在用这个方案进行适配,今天我们就来学习下他的原理

  • 0.3.2版本 这个版本Flexible适配原理是通过meta标签改变页面的缩放比例,从而达到适配的目的,同时,这个方案也可以解决1px的问题,源码如下
(function(win, lib) {
  var doc = win.document;
  var docEl = doc.documentElement;
  var metaEl = doc.querySelector('meta[name="viewport"]');
  var flexibleEl = doc.querySelector('meta[name="flexible"]');
  var dpr = 0;
  var scale = 0;
  var tid;
  var flexible = lib.flexible || (lib.flexible = {});

  // 如果已经设置<meta name="viewport">属性,就根据当前设置的属性
  if (metaEl) {
    var match = metaEl
      .getAttribute("content")
      .match(/initial\-scale=([\d\.]+)/);
    if (match) {
      scale = parseFloat(match[1]);
      dpr = parseInt(1 / scale);
    }
  } else if (flexibleEl) {
    // 同上
    var content = flexibleEl.getAttribute("content");
    if (content) {
      var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
      var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
      if (initialDpr) {
        dpr = parseFloat(initialDpr[1]);
        scale = parseFloat((1 / dpr).toFixed(2));
      }
      if (maximumDpr) {
        dpr = parseFloat(maximumDpr[1]);
        scale = parseFloat((1 / dpr).toFixed(2));
      }
    }
  }
  if (!dpr && !scale) {
    // 这里就是 flexible 的核心代码
    var isAndroid = win.navigator.appVersion.match(/android/gi);
    var isIPhone = win.navigator.appVersion.match(/iphone/gi);
    var devicePixelRatio = win.devicePixelRatio;
    if (isIPhone) {
      // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
      if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
        dpr = 3;
      } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
        dpr = 2;
      } else {
        dpr = 1;
      }
    } else {
      // 其他设备下,仍旧使用1倍的方案
      dpr = 1;
    }
    // 将 <meta> 根据当前设备的 dpr 标签进行缩放
    scale = 1 / dpr;
  }
  docEl.setAttribute("data-dpr", dpr);
  // 根据当前的 dpr 自动设置 <meta> 属性
  if (!metaEl) {
    metaEl = doc.createElement("meta");
    metaEl.setAttribute("name", "viewport");
    metaEl.setAttribute(
      "content",
      "initial-scale=" +
        scale +
        ", maximum-scale=" +
        scale +
        ", minimum-scale=" +
        scale +
        ", user-scalable=no"
    );
    if (docEl.firstElementChild) {
      docEl.firstElementChild.appendChild(metaEl);
    } else {
      var wrap = doc.createElement("div");
      wrap.appendChild(metaEl);
      doc.write(wrap.innerHTML);
    }
  }
  function refreshRem() {
    // 对ipad等设备的兼容
    var width = docEl.getBoundingClientRect().width;
    if (width / dpr > 540) {
      width = 540 * dpr;
    }
    // 将屏幕10等分,设置 fontSize
    var rem = width / 10;
    docEl.style.fontSize = rem + "px";
    flexible.rem = win.rem = rem;
  }
  win.addEventListener(
    "resize",
    function() {
      clearTimeout(tid);
      tid = setTimeout(refreshRem, 300);
    },
    false
  );
  win.addEventListener(
    "pageshow",
    function(e) {
      if (e.persisted) {
        clearTimeout(tid);
        tid = setTimeout(refreshRem, 300);
      }
    },
    false
  );
  // 设置字体 12 * dpr
  if (doc.readyState === "complete") {
    doc.body.style.fontSize = 12 * dpr + "px";
  } else {
    doc.addEventListener(
      "DOMContentLoaded",
      function(e) {
        doc.body.style.fontSize = 12 * dpr + "px";
      },
      false
    );
  }
  refreshRem();
  flexible.dpr = win.dpr = dpr;
  flexible.refreshRem = refreshRem;
  // 工具函数
  flexible.rem2px = function(d) {
    var val = parseFloat(d) * this.rem;
    if (typeof d === "string" && d.match(/rem$/)) {
      val += "px";
    }
    return val;
  };
  flexible.px2rem = function(d) {
    var val = parseFloat(d) / this.rem;
    if (typeof d === "string" && d.match(/px$/)) {
      val += "rem";
    }
    return val;
  };
})(window, window["lib"] || (window["lib"] = {}));

适配效果如下

我们从源码中很容易看出data-dpr,html的 font-size,body的font-szie及meta的缩放比例是如何计算出来的

下面我们简单看下对dpr的计算

    if (isIPhone) {
      // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
      if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
        dpr = 3;
      } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
        dpr = 2;
      } else {
        dpr = 1;
      }
    } else {
      // 其他设备下,仍旧使用1倍的方案
      dpr = 1;
    }

可以看出,在这个版本中,只对ios的dpr进行了处理,对于安卓机型都是默认dpr = 1,显然这样的处理有点不太合理

关于<meta>标签这一块,我们可以这样理解,你通过一个镜框(手机屏幕375px宽度)看一篇报纸(页面内容 750px 的宽度) ,此时镜框是紧贴着报纸的,那你通过镜框看到的内容,就只能镜框区域的那些内容,为了能看到全部的内容,就要镜头拉远一些,flexible就是做了以上的事情,然后让我们在写尺寸的时候完全可以按照设计稿来写,也不会帮我们除以对应的dpr的倍数,但是会帮我们把视口拉远了到1/dpr

  • flexible-2.0 2.0的版本已经没有针对viewport的缩放了,增加了对0.5px的判断,源码如下:
(function flexible(window, document) {
  var docEl = document.documentElement;
  var dpr = window.devicePixelRatio || 1;
  // 设置 body 字体
  function setBodyFontSize() {
    if (document.body) {
      document.body.style.fontSize = 12 * dpr + 'px';
    } else {
      document.addEventListener('DOMContentLoaded', setBodyFontSize);
    }
  }
  setBodyFontSize();
  // 设置 rem 基准值
  function setRemUnit() {
    var rem = docEl.clientWidth / 10;
    docEl.style.fontSize = rem + 'px';
  }
  setRemUnit();
  // reset rem unit on page resize
  window.addEventListener('resize', setRemUnit);
  window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
      setRemUnit();
    }
  });
  // detect 0.5px supports
  if (dpr >= 2) {
    var fakeBody = document.createElement('body');
    var testElement = document.createElement('div');
    testElement.style.border = '.5px solid transparent';
    fakeBody.appendChild(testElement);
    docEl.appendChild(fakeBody);
    if (testElement.offsetHeight === 1) {
      docEl.classList.add('hairlines');
    }
    docEl.removeChild(fakeBody);
  }
})(window, document);

我们看对0.5px问题的处理


  if (dpr >= 2) {
    var fakeBody = document.createElement('body');
    var testElement = document.createElement('div');
    testElement.style.border = '.5px solid transparent';
    fakeBody.appendChild(testElement);
    docEl.appendChild(fakeBody);
    if (testElement.offsetHeight === 1) {
      docEl.classList.add('hairlines');
    }
    docEl.removeChild(fakeBody);
  }

大概逻辑是,判断设备支不支持0.5px, 如果支持 就在body上面添加一个名为hairlinesclass,所以2我们的代码可以这样写

/* dpr=1的时候*/
.line{
 border:1px solid red;
}
/* dpr>=2且支持0.5px的时候*/
.hairlines .line{
 border:0.5px solid red;
}

但是这也会出现以下两个问题

  • 对于那些dpr>2 且不支持0.5px的安卓机,我们应该如何统一处理呢?
  • 如果 dpr=3那么border就应该是0.3333px而不是0.5px了,但是flexible把这些情况都用一个hairlines包含了

看来 flexible似乎并不完美,但是我们也不能否认flexible适配方案, 抛去1px问题,可以说flexible是完美的

说说 vw/vh

个人认为vw适配原理其实和flexible一样,都是平分窗口,只不过一个分了10份一个分了100

关于vw我们在下章实战用的时候在具体说明

如何解决1px问题

我在为什么我们常说1px问题而不说2px文章中提到过,如果对于UI要求不高的时候,1px其实也不算什么问题,往往项目上有很多比1px更重要的bug需要我们去解决,但了解1px的本质有助于我们很好的理解移动端适配原理

解决思路

既然1个css像素代表两个物理像素,设备又不认0.5px的写法,那就画1px,然后再想尽各种办法将线宽减少一半。基于这种思考,我们有以下解决方案

图片大法及背景渐变

这两种方案原理一样,都是设置元素一半有颜色,一半透明,比如做一个2px高度的图片,其中1px是我们想要的颜色,1px设置为透明,适配过程如下

缩放大法

也是flexible 0.3.2使用的适配方案 我们可以把代码稍微改造下

  if (!dpr && !scale) {
    var isAndroid = win.navigator.appVersion.match(/android/gi);
    var isIPhone = win.navigator.appVersion.match(/iphone/gi);
    var devicePixelRatio = win.devicePixelRatio;
      // 对于2和3的屏,用2倍的方案,其余的用1倍方案
      if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
        dpr = 3;
      } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
        dpr = 2;
      } else {
        dpr = 1;
      }
    // 将 <meta> 根据当前设备的 dpr 标签进行缩放
    scale = 1 / dpr;
  }

原理也很简单,根据对应的dpr调整对应的缩放比例,从而达到适配的目的,直接缩放页面个人感觉有点暴力

使用伪元素缩放

缩放整个页面太暴力,那能不能只是缩放边框呢,答案肯定是可以的我们不是有 transform: scale

.border1px{
  position: relative;
  &::after{
    position: absolute;
    content: '';
    background-color: #ddd;
    display: block;
    width: 100%;
    height: 1px; 
    transform: scale(1, 0.5); /* 进行缩放*/
    top: 0;
    left: 0;
  }
}

总结

本文主要讲解了常见移动端适配及1px解决方案,本章主要讲的理论,下一章我们会根据实战得出移动端适配的最佳实践,如有兴趣记得点赞关注哦💓