移動端適配知識你到底知多少

劉源泉發表於2019-03-03

本文從裝置畫素,CSS畫素這些基本概念出發,先向大家介紹移動端適配必須要掌握的一些基本畫素知識及viewport理論,最後在向大家介紹移動端適配解決方案:flexible.js和vw/vh

裝置畫素 & CSS畫素 & 裝置獨立畫素

裝置畫素:device pixel,dp,物理畫素,不可變,下圖示紅的部分指的就是裝置畫素

移動端適配知識你到底知多少

CSS畫素:web程式設計用到的,我們在JS和CSS中使用的10px就是CSS畫素,是可變的。CSS畫素受螢幕縮放和裝置畫素比(dpr)的影響。如我們網頁的中的字型在網頁放大之後會變大,還有在移動端看起來會比PC端小一些

裝置獨立畫素:device independent pixel,dip,與裝置無關的畫素

再來聊下它們之間的關係:PC端和移動端

PC

100%縮放

1裝置獨立畫素 = 1裝置畫素

200%縮放

1裝置獨立畫素 = 2裝置畫素

移動端

因為不同裝置的PPI不同,在標準螢幕下(160PPI)

1裝置獨立畫素 = 1裝置畫素

至於CSS畫素和他們之間的關係,等下面講到了裝置畫素比再說

裝置畫素比

device pixel ratio,dpr

DPR = 裝置畫素 / 裝置獨立畫素

下圖是一些手機的裝置畫素比資料

移動端適配知識你到底知多少

在JS中我們可以通過window.devicePixelRatio來得到當前裝置的dpr
在CSS中我們可以通過-webkit-device-pixel-ratio來進行媒體查詢

注意,當我們放大或者縮小螢幕的時候,window.devicePixelRatio是可變的

有了dpr再來說說,CSS畫素和裝置畫素之間的關係

當dpr為1,1個CSS畫素對應1個裝置畫素
當dpr為2,1個CSS畫素對應4個裝置畫素
當dpr為3,1個CSS畫素對應9個裝置畫素
......

針對有些同學後面提出的疑問,我詳細解釋這塊,當dpr為1,此時1個CSS畫素對應1個裝置畫素,這個還是很好理解的,當dpr為2,此時在水平方向上的裝置畫素是dpr為1的兩倍,豎直方向上的裝置畫素也是dpr為1的兩倍,所以此時的1個CSS畫素對應2^2個裝置畫素,這個就相當於我們把一個矩形的長寬放大為之前的兩倍,此時的矩形面積為之前的四倍,當dpr為3時,此時的1個CSS畫素對應3^2個裝置畫素,所以1個CSS畫素對應dpr^2個裝置畫素

下圖生動的表示了他們之間的關係

移動端適配知識你到底知多少

這裡跟大家說一個小技巧,就是在移動端的時候可以根據dpr的值,使用不同解析度的圖片,如2X還是3X,這樣可以保證與在普通螢幕上看到的圖片效果一致,不至於失真

DPI & PPI

DPI(Dots Per Inch)源於印刷行業,表示每英寸印表機噴的墨汁點數
PPI(Pixels Per Inch)計算機借鑑了DPI,創造了PPI,表示每英寸的畫素數量,即畫素密度

PPI的計算公式如下:

移動端適配知識你到底知多少

現在二者都可用於描述計算機顯示裝置的畫素密度,意思一樣

下面一張圖描述了iphone三個機型的引數

移動端適配知識你到底知多少

其中可以發現iphoneX的畫素密度達到了458ppi,完爆其它兩個

Retina

Retina螢幕即視網膜螢幕,是蘋果釋出iphone 4提出的。之所以稱作是視網膜螢幕,是因為ppi太高,人類無法分辨出螢幕上的畫素點,目前很多智慧手機都採用Retina螢幕

移動端適配知識你到底知多少

iphone3G/S 和 iphone4的螢幕尺寸都是3.5寸,但是iphone4在水平和豎直方向的物理畫素都是iphone3G/S的一倍

PC端幾個尺寸

PC端有幾個尺寸我們需要弄懂下,它們是:

  • screen.width
  • window.innerWidth
  • document.documentElement.clientWidth
  • document.documentElement.offsetWidth
  • ...

screen.width

screen.width指的是我們顯示器的水平方向的畫素時,不隨著我們瀏覽器視窗的變化而變化,是用裝置畫素衡量的

移動端適配知識你到底知多少

window.innerWidth

window.innerWidth指的是瀏覽器視窗的寬度,是可以變化的,所以使用的是CSS畫素

下面是100%縮放,window.innerWidth的截圖

移動端適配知識你到底知多少

可以發現在100%縮放情況下,window.innerWidth的值為1192,window.innerHeight的值為455,接著我們嘗試將放大到200%,再來看看效果

移動端適配知識你到底知多少

可以看到當放大2倍之後,window.innerWidth和window.innerHeight都變成了放大之前的1/2,但是此時window.devicePixelRatio變成了放大之前的2倍,為什麼是這樣子呢?

其實這個也不難理解?因為window.innerWidth是用CSS畫素衡量的,放大兩倍之後,瀏覽器視窗只能看到之前一半的內容,所以window.innerWidth是之前的一半,而dpr = 裝置畫素 / 裝置獨立畫素,這裡的裝置獨立畫素就是我們的window.innerWidth,所以dpr變為原來的2倍,如果看的有點暈,不如嘗試縮放自己的瀏覽器看下效果就知道了

document.documentElement.clientWidth

document.documentElement.clientWidth指的是viewport的寬度,與window.innerWidth的區別就只差了一個滾動條

document.documentElement.offsetWidth則是取得html標籤的寬度

移動端適配知識你到底知多少

看到沒document.documentElement.offsetHeight此時為0,我開啟除錯定位了下,發現此時html高度確實是為0,而document.documentElement.clientHeight此時為455,是viewport的高度,只不過此時viewport的高度和window.innerHeight相等

小結

對於pc端,總之記住以下幾點:

  • window.innerWidth指的是瀏覽器視窗的寬度(包含滾動條),用CSS畫素衡量
  • document.documentElement.clientWidth指的是viewport的寬度,等於瀏覽器視窗的寬度(不包含滾動條)
  • document.documentElement.offsetWidth指的是html的寬度,預設為瀏覽器視窗的寬度
  • document.documentElement.offsetHeight指的是html的高度,沒有顯示給html指定高度的話,為0

移動端的三個viewport理論

移動端的話和PC端截然不同,我們必須先要掌握三個viewport:

  • layout viewport
  • visual viewport
  • ideal viewport

layout viewport

佈局layout,和PC端的viewport很像,PC端的viewport的寬由瀏覽器視窗的寬決定的,使用者可以通過拖動視窗或者縮放改變viewport的大小,但是在移動端則不同,在IOS中 layout viewport預設大小980px,在android中layout viewport為800px,很明顯這兩個值都大於我們瀏覽器的可視區域寬度。我們可以通過document.documentElement.clientWidth來獲取layout viewport的寬度

移動端適配知識你到底知多少

visual viewport

有了layout viewport,我們還需要一個viewport來表示我們瀏覽器可視區域的大小,這個就是visual viewport。visual viewport的寬度可以通過window.innerWidth獲取

移動端適配知識你到底知多少

移動端瀏覽器為了不讓使用者通過縮放和滑動就能看到整個網頁的內容,預設情況下會將visual viewport進行縮放到layout viewport一樣大小,這也就解釋了為什麼PC端設計的網頁在手機上瀏覽會縮小,其實這是跟移動瀏覽器預設的行為有關係

ideal viewport

裝置理想viewport,有以下幾個要求:

  • 使用者不需要縮放和滾動條就能檢視所有內容
  • 文字大小合適,不會因為在高解析度手機下就顯示過小而看不清,圖片也一樣

這個viewport就叫做ideal viewport。但是不同的裝置的ideal viewport不一樣,有320px,有360px的,還有384px的......

總之在移動端佈局中我們需要的是ideal viewport。它等於我們移動裝置的螢幕寬度,這樣針對ideal viewport設計的網站,在不同解析度的螢幕下,不需要縮放,也不需要使用者滾動,就可以完美呈現

meta標籤

我們可以通過meta標籤設定我們viewport,下面這段程式碼你應該見過不止一次了

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0">
複製程式碼

這段程式碼的作用就是設定當前的layout viewport的寬度為裝置的寬度,初始化縮放為1.0,同時不允許縮放,這應該是我們大家都想要的效果

關於meta viewport標籤最初是由Apple公司在Safari瀏覽器中引入的,目的就是為了解決移動裝置的viewport的問題,後來其餘公司紛紛效仿,它有以下幾個屬性:

屬性 說明
width 設定layout viewport的寬度,數字或者device-width
height 設定layout viewport的高度,數字或者device-height
initial-scale 頁面初始縮放值
maximum-scale 使用者最大縮放值
minimum-scale 使用者最小縮放值
user-scalable 允許使用者縮放,yes或no

width=device-width

設定當前的layout viewport的寬度為裝置的螢幕寬度,這樣我們的網站就是針對裝置的螢幕寬度進行排版的,而這個不正是上面所說的ideal viewport,所以通過這樣我們可以讓我們layout viewport的寬度等於ideal viewport的寬度

但是在iphone和ipad上,無論是橫屏還是豎屏,device-width都是豎屏的螢幕寬度

移動端適配知識你到底知多少

下面是我設定了width=device-width之後

移動端適配知識你到底知多少

可以看到設定了width=device-width之後,document.documentElement.clientWidth和window.innerWidth都變成了375,即裝置的螢幕寬度

initial-scale=1.0

通過這種方式我們也可以讓我們的layout viewport的寬度等於ideal viewport的寬度,原因是initial-scale是針對ideal viewport進行縮放的,當我們設定為1.0也就是縮放100%,就可以讓我們的layout viewport和ideal viewport一樣大

下面是我設定了initdial-scale=1.0之後

移動端適配知識你到底知多少

效果同設定了width=device-width一樣

移動端適配知識你到底知多少

但這次我們發現在winphone上,無論橫屏還是豎屏,都將layout viewport的寬度設定為豎屏的螢幕寬度

所以為了相容,建議我們同時寫上這兩個屬性,即

width=device-width,initial-scale=1.0

initial-scale=其它值

當我們設定init-scale為其他值又是個什麼情況呢?

下圖是我設定了initial-scale=0.5的效果

移動端適配知識你到底知多少

可以看到document.documentElement.clientWidth和window.innerWidth都變成了750,為initial-scale=1的兩倍,由此我們可以有一個假設:

layout viewport寬度 = ideal viewport寬度 / initial-scale

我們繼續設定initial-scale=3,按照上述的結論,此時document.documentElement.clientWidth和window.innerWidth應該為125

移動端適配知識你到底知多少

事實證明我們上述的假設是正確的

flexible.js原始碼分析

flexible.js是阿里無線前端團隊開源的用於移動端適配的庫。雖然現在官方都承認可以放棄這個解決方案了,但是瞭解其中的思想還是很重要的

由於viewport單位得到眾多瀏覽器的相容,lib-flexible這個過渡方案已經可以放棄使用,不管是現在的版本還是以前的版本,都存有一定的問題。建議大家開始使用viewport來替代此方案。vw的相容方案可以參閱《如何在Vue專案中使用vw實現移動端適配》一文。

廢話不多說,直接上程式碼

(function flexible (window, document) {
  var docEl = document.documentElement
  var dpr = window.devicePixelRatio || 1

  // adjust body font size
  function setBodyFontSize () {
    if (document.body) {
      document.body.style.fontSize = (12 * dpr) + 'px'
    }
    else {
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
  }
  setBodyFontSize();

  // set 1rem = viewWidth / 10
  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))
複製程式碼

由於版本不同,可能大家拿到的程式碼區域性地方有所不同,但是整體思路還是不變的。

var docEl = document.documentElement
var dpr = window.devicePixelRatio || 1

// adjust body font size
function setBodyFontSize () {
    if (document.body) {
      document.body.style.fontSize = (12 * dpr) + 'px'
    }
    else {
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
}
setBodyFontSize();
複製程式碼

setBodyFontSize這個函式的作用就是設定body標籤的fontSize,fontSize的值dpr * 12,這個函式的作用是為了覆蓋html的fontSize

// set 1rem = viewWidth / 10
function setRemUnit () {
    var rem = docEl.clientWidth / 10
    docEl.style.fontSize = rem + 'px'
}

setRemUnit()
複製程式碼

首選獲取document.documentElement.clientWidth的值,這個值表示當前裝置layout viewport的寬度(你可以理解html標籤的寬度),在iphone6 7 8下這個值是750,然後將整個視口分成10份,這樣每一份的寬度為clientWidth / 10,即1rem = clientWidth / 10,之所以分成10份,完全是方便計算,你也可以隨意切分,但是最小值不要小於12,因為在谷歌瀏覽器中有最小fontSize的限制

// reset rem unit on page resize
window.addEventListener('resize', setRemUnit)
window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
      setRemUnit()
    }
})
複製程式碼

這段程式碼是為了在window觸發了resize和pageShow事件之後自動調整html的fontSize值

// 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)
}
複製程式碼

這句話的程式碼是檢測0.5px的支援,但是我自己還沒弄懂,有哪位同學如果弄明白了,可以在下面發個評論,大家互相學習

還有一點差點忘記了,設定viewport

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
複製程式碼

檢視demo

看了下flexible的實現,我自己也簡單的實現了下,基本思想同flexible.js一樣,只不過我新增了一個自動計算scale的功能

var docuEl = document.documentElement
var metaEl = document.createElement('meta')
var dpr = window.devicePixelRatio
scale = 1 / dpr
metaEl.setAttribute('name', 'viewport')
metaEl.setAttribute('content', `initial-scale=${scale},maximum-scale=${scale},minimum-scale=${scale},user-scalable=no`)
docuEl.setAttribute('data-dpr', dpr)
document.head.appendChild(metaEl)

function resizeFontSize() {
    docuEl.style.fontSize = docuEl.clientWidth / 10 + 'px'
}
resizeFontSize()

window.onresize = resizeFontSize
window.onpageshow = function(e) {
    if (e.persisted) {
      resizeFontSize()
    }
}
複製程式碼

vw & vh

如今flexible.js已經成了過去式,我們實現移動端自動適配,還要在head中新增js程式碼,對於任何一個追求完美的人確實不能忍,還好我們有vw和vh,而且它們在如今大部分手機中都得到了支援

移動端適配知識你到底知多少

檢視我能否用vw

未完待續

相關文章