1. 程式人生 > >移動端Web頁面適配方案

移動端Web頁面適配方案

移動端Web頁面,即常說的H5頁面、手機頁面、webview頁面等。

手機裝置螢幕尺寸不一,做移動端的Web頁面,需要考慮在安卓/IOS的各種尺寸裝置上的相容,這裡總結的是針對移動端裝置的頁面,設計與前端實現怎樣做能更好地適配不同螢幕寬度的移動裝置。

適配的目標

引用一文章的描述:

在不同尺寸的手機裝置上,頁面“相對性的達到合理的展示(自適應)”或者“保持統一效果的等比縮放(看起來差不多)”。

概念理解

在做適配之前,需要先理解一些概念。對於不理解的地方,可以搜尋更多文章看看,本文總結的也是摘抄了其他文章的描述,本文末有附相關連結。

viewport視口

viewport是嚴格的等於瀏覽器的視窗。viewport

與跟viewport有關的meta標籤的關係,詳細建議讀一讀這篇文章:移動前端開發之viewport的深入理解viewport與佈局的關係,可以看下這篇文章:在移動瀏覽器中使用viewport元標籤控制佈局

visual viewport 可見視口 螢幕寬度
layout viewport 佈局視口 DOM寬度
ideal viewport 理想適口:使佈局視口就是可見視口
裝置寬度(visual viewport)與DOM寬度(layout viewport), scale的關係為:

  • (visual viewport)= (layout viewport)* scale

獲取螢幕寬度(visual viewport)

的尺寸:window. innerWidth/Height
獲取DOM寬度(layout viewport)的尺寸:document. documentElement. clientWidth/Height

設定理想視口:把預設的layout viewport的寬度設為移動裝置的螢幕寬度,得到理想視口(ideal viewport):

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

物理畫素(physical pixel)

物理畫素又被稱為裝置畫素,他是顯示裝置中一個最微小的物理部件。每個畫素可以根據作業系統設定自己的顏色和亮度。所謂的一倍屏、二倍屏(Retina)、三倍屏,指的是裝置以多少物理畫素來顯示一個CSS畫素,也就是說,多倍屏以更多更精細的物理畫素點來顯示一個CSS畫素點,在普通螢幕下1個CSS畫素對應1個物理畫素,而在Retina螢幕下,1個CSS畫素對應的卻是4個物理畫素。關於這個概念,看一張"田"字示意圖就會清晰了。

CSS畫素

CSS畫素是一個抽像的單位,主要使用在瀏覽器上,用來精確度量Web頁面上的內容。一般情況之下,CSS畫素稱為與裝置無關的畫素(device-independent pixel),簡稱DIPs。CSS畫素顧名思義就是我們寫CSS時所用的畫素。

裝置畫素比dpr(device pixel ratio)

裝置畫素比簡稱為dpr,其定義了物理畫素和裝置獨立畫素的對應關係。它的值可以按下面的公式計算得到:

裝置畫素比 = 物理畫素 / 裝置獨立畫素

在Retina屏的iphone上,devicePixelRatio的值為2,也就是說1個css畫素相當於2個物理畫素。通常所說的二倍屏(retina)的dpr是2, 三倍屏是3。

viewport中的scale和dpr是倒數關係。
獲取當前裝置的dpr:

  • JavaScript: window.devicePixelRatio
  • CSS: -webkit-device-pixel-ratio, -webkit-min-device-pixel-ratio, -webkit-max-device-pixel-ratio。不同dpr的裝置,可根據此做一些樣式適配(這裡只針對webkit核心的瀏覽器和webview)。

裝置獨立畫素dip或dp

dip或dp,(device independent pixels,裝置獨立畫素)與螢幕密度有關。dip可以用來輔助區分視網膜裝置還是非視網膜裝置。

安卓裝置根據螢幕畫素密度可分為ldpi、mdpi、hdpi、xhdpi等不同的等級。規定以160dpi為基準,1dp=1px。如果密度是320dpi,則1dp=2px,以此類推。
IOS裝置:從IPhone4開始為Retina屏

  • CSS畫素與裝置獨立畫素之間的關係依賴於當前的縮放等級。

螢幕畫素密度PPI(pixel per inch)

螢幕畫素密度是指一個裝置表面上存在的畫素數量,它通常以每英寸有多少畫素來計算(PPI)。螢幕畫素密度與螢幕尺寸和螢幕解析度有關,在單一變化條件下,螢幕尺寸越小、解析度越高,畫素密度越大,反之越小。

螢幕密度 = 對角線解析度/螢幕尺寸

概念關係圖

螢幕尺寸、螢幕解析度-->對角線解析度/螢幕尺寸-->螢幕畫素密度PPI
                                             |
              裝置畫素比dpr = 物理畫素 / 裝置獨立畫素dip(dp)
                                             |
                                       viewport: scale
                                             |
                                          CSS畫素px

前端實現相關方式

下面大致列下前端在實現適配上常採用的方式。百分比、em單位的使用就不必說了。

viewport

設定理想視口

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

設定理想視口,使得DOM寬度(layout viewport)與螢幕寬度(visual viewport)一樣大,DOM文件主寬度即為螢幕寬度。1個CSS畫素(1px)由多少裝置畫素顯示由具體裝置而不同。

動態設定視口縮放為1/dpr

對於安卓,所有裝置縮放設為1,對於IOS,根據dpr不同,設定其縮放為dpr倒數。設定頁面縮放可以使得1個CSS畫素(1px)由1個裝置畫素來顯示,從而提高顯示精度;因此,設定1/dpr的縮放視口,可以畫出1px的邊框。

不管頁面中有沒有設定viewport,若無,則設定,若有,則改寫,設定其scale為1/dpr。

(function (doc, win) {
  var docEl = win.document.documentElement;
  var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
  var metaEl = doc.querySelector('meta[name="viewport"]');
  var dpr = 0;
  var scale = 0;

  // 對iOS裝置進行dpr的判斷,對於Android系列,始終認為其dpr為1
  if (!dpr && !scale) {
    var isAndroid = win.navigator.appVersion.match(/android/gi);
    var isIPhone = win.navigator.appVersion.match(/[iphone|ipad]/gi);
    var devicePixelRatio = win.devicePixelRatio;

    if(isIPhone) {
      dpr = devicePixelRatio;
    } else {
      drp = 1;
    }
    
    scale = 1 / dpr;
  }

  /**
    * ================================================
    *   設定data-dpr和viewport
    × ================================================
    */

  docEl.setAttribute('data-dpr', dpr);
  // 動態改寫meta:viewport標籤
  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');
    document.documentElement.firstElementChild.appendChild(metaEl);
  } else {
    metaEl.setAttribute('content', 'width=device-width, initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
  }

})(document, window);

px單位的適配

設定動態縮放視口後,在iPhone6上,縮放為0.5,即CSS畫素2px最終顯示效果為1px,而在scale=1的裝置,CSS畫素1px顯示效果為1px,那麼,為了達到顯示效果一致,以px為單位的元素(比如字型大小),其樣式應有適配不同dpr的版本,因此,在動態設定viewport: scale的時候,同時在html根元素上加上data-dpr=[dpr]但是這種方式還是不夠,如果dpr為2,3之外的其他數值,px就沒辦法適配到。因此我會選擇都用rem為單位進行適配。
樣式示例:

.p {
    font-size: 14px;

  [data-dpr="2"] & {
    font-size: 14px * 2;
  }

  [data-dpr="3"] & {
    font-size: 14px * 3;
  }
}

為寫樣式方便,可以藉助sass的mixin寫程式碼片段:

// 適配dpr的字型大小
@mixin font-dpr($font-size){
  font-size: $font-size;

  [data-dpr="2"] & {
      font-size: $font-size * 2;
  }

  [data-dpr="3"] & {
      font-size: $font-size * 3;
  }
}
@mixin px-dpr($property, $px) {
  #{$property}: $px;

  [data-dpr="2"] & {
    #{$property}: $px * 2;
  }

  [data-dpr="3"] & {
    #{$property}: $px * 3;
  }
}

// 使用
@include font-dpr(14px);
@include px-dpr(width, 40px); @include px-dpr(height, 40px);

設定縮放視口與設定理想視口有什麼不同

問題:viewport設為理想視口(scale=1),基本已經滿足適配,為什麼要動態設定viewport縮放?
原因:iPhone6為例,dpr為2,縮放設為0.5,則DOM寬度為750,縮放後顯示剛好為螢幕寬度375,而總的CSS畫素其實是750,與裝置畫素一致,這樣1px的CSS畫素,佔用的物理畫素也是1;而viewport設定縮放為1的理想視口情況下,DOM寬度為375,顯示也剛好是螢幕寬度,然而1px的CSS畫素,佔用的物理畫素是2。這樣說來,這樣設定可以實現1px的線條在二倍屏的顯示。因為: CSS畫素與裝置畫素的關係依賴於螢幕縮放。 
驗證:裝置:iPhone6, 
在scale=0.5時,1px邊框顯示效果;
在scale=1.0時,1px邊框顯示效果;
在scale=0.5時,2px邊框顯示效果;
通過對比後發現,在scale=0.5時,1px的線比scale=1.0要細,這也就解決了1px線條的顯示問題。

rem(一個CSS單位)

定義:font size of the root element.

這個單位的定義和em類似,不同的是em是相對於父元素,而rem是相對於根元素。rem定義是根元素的font-size, 以rem為單位,其數值與px的關係,需相對於根元素<html>的font-size計算,比如,設定根元素font-size=16px, 則表示1rem=16px。關於rem更多的解讀,建議可以閱讀本文末附的騰訊一團隊的文章《web app變革之rem》。

根據這個特點,可以根據裝置寬度動態設定根元素的font-size,使得以rem為單位的元素在不同終端上以相對一致的視覺效果呈現。

選取一個裝置寬度作為基準,設定其根元素大小,其他裝置根據此比例計算其根元素大小。比如使得iPhone6根元素font-size=16px。

設 備 裝置寬度 根元素font-size/px 寬度/rem
iPhone5 320 js計算所得 --
iPhone6 375 16 23.4375
i6 Plus 414 js計算所得 --
- 360 js計算所得 --

根元素fontSize公式:width/fontSize = baseWidth/baseFontSize

其中,baseWidth, baseFontSize是選為基準的裝置寬度及其根元素大小,width, fontSize為所求裝置的寬度及其根元素大小

動態設定根元素fontSize

/**
  * 以下這段程式碼是用於根據移動端裝置的螢幕解析度計算出合適的根元素的大小
  * 當裝置寬度為375(iPhone6)時,根元素font-size=16px; 依次增大;
  * 限制當為裝置寬度大於768(iPad)之後,font-size不再繼續增大
  * scale 為meta viewport中的縮放大小
  */
(function (doc, win) {
  var docEl = win.document.documentElement;
  var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
  /**
    * ================================================
    *   設定根元素font-size
    * 當裝置寬度為375(iPhone6)時,根元素font-size=16px; 
    × ================================================
    */
  var refreshRem = function () {
    var clientWidth = win.innerWidth
                      || doc.documentElement.clientWidth
                      || doc.body.clientWidth;

    console.log(clientWidth)
    if (!clientWidth) return;
    var fz;
    var width = clientWidth;
    fz = 16 * width / 375;
    docEl.style.fontSize = fz + 'px';
  };

  if (!doc.addEventListener) return;
  win.addEventListener(resizeEvt, refreshRem, false);
  doc.addEventListener('DOMContentLoaded', refreshRem, false);
  refreshRem();

})(document, window);

rem計算(px2rem)

對於需要使用rem來適配不同·螢幕的元素,使用rem來作為CSS單位,為了方便,可以藉助sass寫一個函式計算px轉化為rem, 寫樣式時不必一直手動計算。sass函式的使用若不熟悉可看下這篇文章:如何編寫自定義Sass 函式, 也可以使用sass的mixin來寫,個人覺得用函式寫更適合。

/* 
 * 此處 $base-font-size 具體數值根據設計圖尺寸而定
 * flexible中設定的標準是【fontSize=16px, when 螢幕寬度=375】,因此,按此標準進行計算,
 * 若設計圖為iPhone6(375*667)的二倍稿,則$base-font-size=32px
 *
 */ 
@function px2rem($px, $base-font-size: 32px) {
  @if (unitless($px)) {
    @warn "Assuming #{$px} to be in pixels, attempting to convert it into pixels for you";
    @return px2rem($px + 0px); // That may fail.
  } @else if (unit($px) == rem) {
    @return $px;
  }
  @return ($px / $base-font-size) * 1rem;
}

// 使用,eg:
font-size: px2rem(18px);

問題思考

我之前一直在想一個問題,選取哪個裝置來做基準、螢幕寬度等分為多少比較合適,設計圖給多大寬度的版本?被選取作為基準的裝置,應當就是前端需要設計提供的設計圖版本,這樣可以避免一些尺寸上的糾纏,而等分為多少等分,除了考慮方便設計,是否需要考慮其他問題?對於根元素font-size沒有手動設定的情況,1rem究竟等於多少?

瞭解到的一些事實:

  • 某些Android裝置會丟掉 rem 小數部分(具體是哪些裝置,搜到的文章中沒有說),那麼1rem對應的px少些,在這些安卓裝置上顯示誤差就會較小,當然如果不存在會丟掉小數這個問題,這一說也就不必考慮了。
  • 未設定font-size情況下,1rem的大小具體看瀏覽器的實現,預設的根元素大小是font-size=16px
  • 目前一般會選取iPhone6作為基準,設計圖便要iPhone6的二倍圖
  • 當動態縮放視口為1/dpr, 計算所得的根元素fontSize也會跟著縮放,即若理想視口(scale=1), iPhone6根元素fontSize=16px; 若scale=0.5, iPhone6根元素fontSize=32px; 因此設定視口縮放應放於設定根元素fontSize之前。

flex佈局

flex佈局對於螢幕適配也很有幫助,有些地方通過flex佈局的實現方式,效果會比較合理。
關於flex佈局,暫時不瞭解的建議閱讀阮一峰老師的教程,分語法和實踐兩篇,講得很清晰易懂實用。
Flex佈局教程:語法篇

vm/vh:CSS單位

vw(view-width), vh(view-height) 這兩個單位是CSS新增的單位,表示視區寬度/高度,視區總寬度為100vw, 總高度為100vh。

視區指瀏覽器內部的可視區域大小:window.innerWidth/Height

一些問題

upsampling/downsampling

DownSampling: 大圖放入比圖片尺寸小的容器中時,出現畫素分割成就近色

不同scale顯示同一圖片基本無問題;
同一sacle,不同倍數圖,存在色差(Downsampling)

關於這個我還不是很瞭解,暫時記一下。

手淘的實現方案

下面主要根據我的理解摘錄了手淘公開的實現方案,詳細可以去gitHub搜尋檢視,文末也附了連結。
圖解設計與前端協作方案:圖:手機淘寶團隊適配協作模式
[淘寶手淘團隊h5頁面終端適配開源庫:lib-flexible]()

方案關鍵點:

  • 動態改寫<meta name="viewport">標籤
  • 給<html>元素新增data-dpr屬性,並且動態改寫data-dpr的值
  • 給<html>元素新增font-size屬性,並且動態改寫font-size的值

通過一段JS程式碼根據裝置的螢幕寬度、dpr設定根元素的data-dpr和font-size, 這段JS程式碼要在所有資源載入之前執行,建議做內聯處理。

px轉rem的mixin

// 使用sass的混合巨集
// 淘寶手淘的方案裡,i6(375pt)螢幕寬度為10rem,即font-size=75px, scale=0.5 因設計圖為二倍圖,$base-font-size=75px
@mixin px2rem($property,$px-values,$baseline-px:16px,$support-for-ie:false){
    //Conver the baseline into rems
    $baseline-rem: $baseline-px / 1rem * 1;
    //Print the first line in pixel values
    @if $support-for-ie {
        #{$property}: $px-values;
    }
    //if there is only one (numeric) value, return the property/value line for it.
    @if type-of($px-values) == "number"{
        #{$property}: $px-values / $baseline-rem;
    }
    @else {
        //Create an empty list that we can dump values into
        $rem-values:();
        @each $value in $px-values{
            // If the value is zero or not a number, return it
            @if $value == 0 or type-of($value) != "number"{
                $rem-values: append($rem-values, $value / $baseline-rem);
            }
        }
        // Return the property and its list of converted values
        #{$property}: $rem-values;
    }
}

小結

  • 適配不同螢幕寬度以及不同dpr,通過動態設定viewport(scale=1/dpr) + 根元素fontSize + rem, 輔助使用vw/vh等來達到適合的顯示;
  • 若無需適配可顯示1px線條,也可以不動態設定scale,只使用動態設定根元素fontSize + rem + 理想視口;
  • 當視口縮放,計算所得的根元素fontSize也會跟著縮放,即若理想視口(scale=1), iPhone6根元素fontSize=16px; 若scale=0.5, iPhone6根元素fontSize=32px; 因此不必擔心rem的計算;
  • !!css單位:以前我認為這樣比較好:適配元素rem為單位,正文字型及邊距宜用px為單位;現在認為全部用rem即可,包括字型大小,不用px
  • px為單位的元素,需根據dpr有不同的大小,如大小12px, dpr=2則採用24px, 使用sass mixin簡化寫法;
  • 配合scss函式,簡化px2rem轉換,且易於維護(若需修改$base-font-size, 無需手動重新計算所有rem單位);
  • px2rem函式的$base-font-size只跟根元素fontSize的基準(此文中是【fontSize=16px when 375】)以及設計圖的大小有關,按此基準,若設計圖為iPhone6二倍稿,則$base-font-size=32px,引數傳值直接為設計圖標註尺寸;
  • 使用iPhone6(375pt)二倍設計圖:寬度750px;
  • 切圖使用三倍精度圖,以適應三倍屏(這個目前我還沒有實際應用過)

相關連結: