移動端適配-rem(新)

沐曉發表於2020-05-31

概念

對於移動端開發來說,無可避免的就是直面各種裝置不同解析度和不同DPR(裝置畫素比)的問題,在此忽略其他相容性問題的探討。

移動端畫素

  1. 裝置畫素(dp),也叫物理畫素。指裝置能控制顯示的最小物理單位,意指顯示器上一個個的點。從螢幕在工廠生產出的那天起,它上面裝置畫素點就固定不變了。

  2. 解析度,螢幕上物理畫素的數量。

  3. 裝置獨立畫素(dip),又稱密度無關畫素。可以認為是計算機座標系統中的一個點,這個點代表一個可以由程式使用並控制的虛擬畫素。由相關係統轉化為物理畫素在裝置上體現。

  4. css畫素,web程式設計中的概念,屬於裝置獨立畫素中的一種,獨立於裝置,屬於邏輯上衡量畫素的單位。

  5. 裝置畫素比(dpr) = 裝置畫素值(dps) / 裝置獨立畫素值(dips),代表系統轉化時一個css畫素佔有多少個物理畫素。

  6. 畫素密度(ppi),裝置(螢幕)每英寸內有多少個畫素點。

移動端三個視口

移動端視口 viewport(div100%時的css大小):移動裝置上的 viewport 就是裝置的螢幕上能用來顯示我們的網頁的那一塊區域,可能與瀏覽器的可視區域不同。預設比瀏覽器可視區域要大(980px),這也是為什麼一般的PC端網頁放在移動端會出現橫向滾動條的原因。

移動端中的三個不同的可視區域大小,來自於ppk關於移動裝置的viewport研究:

  1. 佈局視口(layout viewport),瀏覽器預設的viewport,一般比瀏覽器可視區域大。

  2. 視覺視口(visual viewport),瀏覽器的可視區域大小(瀏覽器的可見區域css畫素值)

  3. 理想視口(ideal viewport),裝置的實際物理寬度(device-width),是一種與ppi無關的裝置原始的寬度(英寸),例如320px和660px下的iphone的理想視口都是320px。

點陣圖畫素

一個點陣圖畫素是柵格影像(如:png, jpg, gif等)最小的資料單元。每一個點陣圖畫素都包含著一些自身的顯示資訊(如:顯示位置,顏色值,透明度等)。

理論上,1個點陣圖畫素對應於1個物理畫素,圖片才能得到完美清晰的展示。當遇上對應的點陣圖畫素與物理畫素不統一的時候。

  1. 點陣圖畫素 < 物理畫素。 1個點陣圖畫素對應於多個物理畫素,由於單個點陣圖畫素不可以再進一步分割,所以只能就近取色,從而導致圖片模糊。(具體取決於裝置系統的影像演算法,並不是簡單的切割圖片)(圖片拉伸)

  2. 點陣圖畫素 > 物理畫素。1個物理畫素對應多個點陣圖畫素,所以它的取色也只能通過一定的演算法(顯示結果就是一張點陣圖畫素只有原影像素總數四分之一的圖片),肉眼看上去雖然圖片不會模糊,但是會覺得圖片缺少一些銳利度,或者是有點色差(但還是可以接受的)(圖片擠壓)

rem適配

什麼是rem

即以根節點(html)的字型大小作為基準值進行長度計算。

假定 html 的 fontSize 為 16px,則 1rem = 16px

如果我們更改 html 的 fontSize,rem 也會更新,總是保持 1rem = 1 fontSize (html)

為什麼使用rem

開發過移動端專案的同學應該都知道,不同手機裝置的大小是不一樣的,在進行移動端開發時,我們通常會為 html 加上 viewport meta

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">

這裡得結合上面的移動端畫素和移動端視口進行分析,width=device-width將此時的頁面寬度設定為裝置寬度(理想視口),所以此時頁面寬度等於裝置寬度,不同手機的裝置寬度是不同的所以頁面寬度也不同

iPhone4 頁面寬度 = window.innerWidth = 裝置寬度 = 320px

iPhone6 頁面寬度 = window.innerWidth = 裝置寬度 = 375px

所以為了適配不同的裝置寬度,我們通常不直接用px來寫css程式碼,因為在不同手機中頁面寬度不同,此時px的相對大小也是不同的。如果我們把一個元素設定為375px來達到100%寬度效果的話,在320裝置寬度的手機就出問題了。

由此我們引入了 rem 來做適配,在 css 中直接使用 rem 作為計量單位,如果不做些什麼的話,1rem = 16px(瀏覽器預設字型大小),在不同手機上都是一樣,還是無法適配,所以要點在於如何根據裝置寬度在做轉化

// 假定設計稿寬度750px
const designWidth = 750;

// 通過裝置寬度(window.innerWidth)和設計稿寬度(designWidth)的比例來設定 html fontSize
document.documentElement.style.fontSize =  (window.innerWidth / designWidth) + 'px';

通過上面程式碼的設定,我們就可以很輕鬆的適配移動端專案了,假定設計稿上一個元素寬度750px,那我們就在css定義750rem

在裝置寬度為320px的手機上

750rem = 750 * 1rem = 750 * (window.innerWidth / designWidth) px = 750 * (320 / 750) px = 320px

同理,在裝置寬度為375px的手機上

750rem = 750 * 1rem = 750 * (window.innerWidth / designWidth) px = 750 * (375 / 750) px = 375px

可能還有個問題,為什麼不直接用百分比來適配?因為百分比在很多情況下是除不盡或者帶有小數的,顯然帶有小數點的px會帶來各種各樣的誤差

高清適配

如果你覺得移動端適配像上面一樣簡單轉化下就行,那就 too young too sample

1px問題

什麼是 1px 問題?

以 iphone6 為例,大家應該聽過啥視網膜畫素之類的,2倍屏之類的吧。其實也就是此時 裝置畫素比(dpr) = 裝置畫素值(dps) / 裝置獨立畫素值(dips),即一個css畫素對應兩個物理畫素,也就是你在css中寫的1px其實在裝置顯示的是兩個畫素,當你設定 border = 1px 時看起來就沒有那種1px的纖細效果,總感覺不盡如人意,差那麼一點點味道。

你以為的1px

使用者看到的1px(請忽略顏色不同)

追求使用者體驗的公司通常是不能容忍 1px 問題的

圖片的模糊問題

同樣的以 iphone6 為例,我們如果定義一張圖片寬度為375px,如果圖片的畫素(點陣圖畫素),此時一個畫素的圖片會對應兩個物理畫素(參考上面的點陣圖畫素),就會造成圖片模糊的問題了。你可能會問?那我直接載入750px畫素的圖片不就好了(點陣圖畫素大於物理畫素時很多人是看不出失真的)。

答案當然是可以的,但你覺得追求使用者體驗的公司能容忍無故的流量耗費和效能浪費麼?當然不能

解決方案

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">

前面也有介紹過這部分程式碼,但是沒有說明 initial-scale=1 的作用,initial-scale 定義了頁面的初始縮放,1代表不縮放。initial-scale的值也會影響頁面寬度,即此時的css畫素。

前面我們說過,在 viewport meta 的約束下

頁面寬度 = window.innerWidth = 裝置寬度,但其實正確的是 頁面寬度 = window.innerWidth = 裝置寬度 / scale,為什麼是除呢?大家可以想象一下,當頁面縮放時(例如scale=0.5),是不是會導致更多的內容內容展示在當前可見區域中,css畫素(頁面)是變大了。

以 iphone6 為例,當我們設定

<meta name="viewport" content="width=device-width, initial-scale=0.5, user-scalable=no">

此時頁面寬度 = window.innerWidth = 裝置寬度 / scale = 375 / 0.5 = 750px,也就是說現在頁面寬度(對應css畫素)和物理畫素是相等的,所以我們設定的 1px 在手機中將真正顯示 1pt(1個物理畫素),也就解決了1px的問題。

所以解決方法如下

// 獲取裝置dpr
const dpr = window.devicePixelRatio;

// 計算縮放比例
const scale = 1 / dpr;

// 動態設定meta
const metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'width=device-width,initial-scale=' + scale + ', user-scalable=no');

對應圖片而言,要想達到最清晰的顯示狀態則要使圖片的點陣圖畫素與裝置的物理畫素對應,所以可以對圖片做如下適配

[dpr=1] img {
width: 200rem;
background: '@1x.png';
}

[dpr=2] img {
width: 200rem;
background: '@2x.png';
}

此方案的原理就是利用meta來更過css畫素(因為css畫素是虛擬畫素由計算機定義的,見上文),以此達到一個css畫素對應一個物理畫素的效果,1px == 1pt

rem高清適配

利用上文提供的rem移動端適配思路,加上現在的高清適配思路,就可以完成移動端高清適配啦

直接貼程式碼,來自前端:『REM』手機螢幕高清適配方案

(function(designWidth, rem2px) {
  var win = window;
  var doc = win.document;
  var docEl = doc.documentElement;
  var metaEl = doc.querySelector('meta[name="viewport"]');
  var dpr = 0;
  var scale = 0;
  var tid;

  if (!dpr && !scale) {
    var devicePixelRatio = win.devicePixelRatio;
    if (win.navigator.appVersion.match(/iphone/gi)) {
      if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
        dpr = 3;
      } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
        dpr = 2;
      } else {
        dpr = 1;
      }
    } else {
      dpr = 1;
    }
    scale = 1 / dpr;
  }

  docEl.setAttribute('data-dpr', dpr);
  if (!metaEl) {
    metaEl = doc.createElement('meta');
    metaEl.setAttribute('name', 'viewport');
    metaEl.setAttribute('content', 'width=device-width,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);
    }
  } else {
    metaEl.setAttribute('name', 'viewport');
    metaEl.setAttribute('content', 'width=device-width,initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
  }
  // 以上程式碼是對 dpr 和 viewport 的處理,程式碼來自 lib-flexible。

  // 一下程式碼是處理 rem,來自上篇文章。不同的是獲取螢幕寬度使用的是 
  // document.documentElement.getBoundingClientRect
  // 也是來自 lib-flexible ,tb的技術還是很強啊。
  function refreshRem(_designWidth, _rem2px){
    // 修改viewport後,對網頁寬度的影響,會立刻反應到 
    // document.documentElement.getBoundingClientRect().width
    // 而這個改變反應到 window.innerWidth ,需要等較長的時間
    // 相應的對高度的反應,
    // document.documentElement.getBoundingClientRect().height 
    // 要稍微慢點,沒有準確的資料,應該會受到機器的影響。
    var width = docEl.getBoundingClientRect().width;
    var d = window.document.createElement('div');
    d.style.width = '1rem';
    d.style.display = "none";
    docEl.firstElementChild.appendChild(d);
    var defaultFontSize = parseFloat(window.getComputedStyle(d, null).getPropertyValue('width'));
    // d.remove();
    var portrait = "@media screen and (width: "+ width +"px) {html{font-size:"+ ((width/(_designWidth/_rem2px)/defaultFontSize)*100) +"%;}}";
    var dpStyleEl = doc.getElementById('dpAdapt');
    if(!dpStyleEl) {
      dpStyleEl = document.createElement('style');
      dpStyleEl.id = 'dpAdapt';
      dpStyleEl.innerHTML = portrait;
      docEl.firstElementChild.appendChild(dpStyleEl);
    } else {
      dpStyleEl.innerHTML = portrait;
    }
    // 由於 height 的響應速度比較慢,所以在加個延時處理橫屏的情況。
    setTimeout(function(){
      var height = docEl.getBoundingClientRect().height;
      var landscape = "@media screen and (width: "+ height +"px) {html{font-size:"+ ((height/(_designWidth/_rem2px)/defaultFontSize)*100) +"%;}}"
      var dlStyleEl = doc.getElementById('dlAdapt');
      if(!dlStyleEl) {
        dlStyleEl = document.createElement('style');
        dlStyleEl.id = 'dlAdapt'
        dlStyleEl.innerHTML = landscape;
        docEl.firstElementChild.appendChild(dlStyleEl);
      } else {
        dlStyleEl.innerHTML = landscape;
      }
    },500);
  }

  // 延時,讓瀏覽器處理完viewport造成的影響,然後再計算root font-size。
  setTimeout(function(){
    refreshRem(designWidth, rem2px);
  }, 1);

})(750, 100);

程式碼比較多,有興趣的可以直接上github上找到原始碼(https://github.com/hbxeagle/rem/blob/master/HD_ADAPTER.md)

後記

這是一篇很早之前寫的總結了,今天又複習修改了一下,寫的有錯誤或者寫的不清楚的地方請大家多多指正。

這麼多年過去,其實現在已經逐漸流行直接使用 vw vh 來做移動端適配了,因為隨著裝置的更新相容性的問題已經大大減少。但使用 rem 模式還是有一定需求的,畢竟vw還沒有全部相容,可以參考vw相容性。還有就是有pc瀏覽器開啟並限制最大寬度的需求使用vw就不可以了。

後面有時間將寫寫利用 vw vh 來進行移動端適配的總結,會比這個簡單。

參考


歡迎到前端學習打卡群一起學習~516913974

相關文章