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

語言: CN / TW / HK

為什麼要寫這篇文章?

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