移動端適配問題終極探討(上)

爲何要寫這篇文章?

最近公司作了不少花裏胡哨的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解決方案,本章主要講的理論,下一章咱們會根據實戰得出移動端適配的最佳實踐,若有興趣記得點贊關注哦💓

相關文章
相關標籤/搜索