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

为何要写这篇文章?

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

说明

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

此文是适配系列文章的上前端

广泛的解决方案

研究以前咱们能够看看大厂都是如何适配H5android

淘宝

地址: main.m.taobao.com/ios

方案: Flexiblemarkdown

分析:值得聊的是,虽然Flexible是淘宝团队出的关于移动端适配的方案,但手机淘宝彷佛并无使用此方案,能够看下面几张图得出结论app

咱们如今改变手机型号iphone

能够发现人家的适配单位直接是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解决方案,本章主要讲的理论,下一章咱们会根据实战得出移动端适配的最佳实践,若有兴趣记得点赞关注哦💓

相关文章
相关标签/搜索