移動端適配總結(長文)

佈局

  • 靜態佈局: 定寬定高佈局, 不隨設備和視口改變【定寬居中, pc】
  • 流式佈局: 大小改變,佈局不變,寬度使用百分比和min/max,高固定 【柵欄系統(網格系統)】
  • 自適應佈局: 大小改變,主體內容和佈局沒變,缺點,過小屏幕內容過於擁擠 (大小不變,位置改變)
  • 彈性佈局: 大小位置都會變化(flex/rem/em/vw/vh)
  • 響應式佈局: 展現的內容,大小位置都會變化, 媒體查詢+流式佈局 (媒體查詢, vw, rem/em)

相關概念

設備像素(Device Pixels)

設備屏幕的物理像素,表示屏幕上能夠鋪多少個點點,而不是一個絕對長度單位(例如in,mm); 單位是px,好比iPhone6的 (750 x 1334px)css

分辨率(Resolution)

一個物理概念
對於屏幕,分辨率通常表示屏幕上顯示的物理像素總和。好比,咱們說iPhone6屏幕分辨率是(750 x 1334px)
對於圖像,概念等同於圖像尺寸、圖像大小、像素尺寸等等。好比,咱們說(20 x 20px)的iconhtml

CSS像素(CSS Pixels)

是Web編程的概念,指的是CSS樣式代碼中使用的邏輯像素, 或者稱爲設備獨立像素, 由於只與設備相關;
1個CSS像素在不一樣設備上可能對應不一樣的物理像素數,這個比值是設備的屬性(Device Pixel Ratio,設備像素比),好比,iPhone6:375 x 667pxandroid

經過 document.documentElement.clientWidth/clientHeight / document.documentElement.getBoundingClientRect().width 獲取ios

在CSS規範中,長度單位能夠分爲絕對單位和相對單位。px是一個相對單位,相對的是設備像素(Device Pixels)css3

設備獨立像素(device-independent pixels (DIP) / Density-independent Pixels (DP))

Android設備的特色是屏幕尺寸不少,所以爲了顯示能儘可能和設備無關,提出了dip,參照的density是160。git

// 當屏幕密度density爲160(單位是ppi或者dpi,一個意思)時,px === dip
px = dip * density / 160 
// 因此
dip = px * 160 / density
複製代碼

注: 此處只針對於Android, windows 也有 DIP 概念, 含義不一樣, IOS貌似不存在github

設備像素比 (Device Pixel Ratio (DPR))

Device pixel ratio, the ratio between physical pixels and logical pixels used by cascading style sheets (CSS): other names for it are 「CSS Pixel Ratio」 and 「dppx」 表示1個CSS像素(寬度)等於幾個物理像素(寬度)web

DPR = 物理像素(設備像素) / 邏輯像素(css像素/設備獨立像素) // [未縮放]
複製代碼

像素密度

像素密度也叫顯示密度或者屏幕密度,縮寫爲DPI(Dots Per Inch)或者PPI(Pixel Per Inch)編程

// 屏幕對角線的像素尺寸 / 物理尺寸(inch 英寸)
Math.sqrt(750*750 + 1334*1334) / 4.7 = 326ppi
複製代碼

視口(viewport)

桌面上視口寬度等於瀏覽器寬度,但在手機上有所不一樣。windows

佈局視口(layout viewport)

手機上爲了容納爲桌面瀏覽器設計的網站,默認佈局視口寬度遠大於屏幕寬度,爲了讓用戶看到網站全貌,它會縮小網站 document.documentElement.clientWidth

視覺視口(Visual viewport)

屏幕的可視區域,即物理像素尺寸, 可變, 與當前縮放值和設備的屏幕寬度有關 visual viewport寬度 = ideal viewport寬度 / 當前縮放值 能夠經過window.innerWidth來獲取,但在Android 2, Oprea mini 和 UC 8中沒法正確獲取。

理想視口(ideal viewport)

ideal viewport是最適合移動設備的viewport,ideal viewport的寬度等於移動設備的屏幕寬度 在移動開發時, 在meta[name='viewport']中, 經過width = device-width把當前的viewport寬度設置爲理想視口, 不然寬度將默認爲佈局視口980

ideal viewport並無一個固定的尺寸,不一樣的設備擁有有不一樣的ideal viewport。早期全部iPhone理想視口爲320x480px

因此,在沒有縮放的狀況下,屏幕的CSS像素寬度實際上是指理想視口的寬度,而meta標籤:

<meta name="viewport" content="width=device-width, inital-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
複製代碼

指定了佈局視口=理想視口,而且禁止縮放。因此添上width=device-width的viewport meta後頁面變大了(一開始頁面內容小得看不清),其實是佈局視口變小了

initial-scale=1 解決了 iphone、ipad 不管橫豎屏都把寬度設爲豎屏時 ideal viewport 的寬度 width=device-width 解決了IE 不管橫豎屏都把寬度設爲豎屏時 ideal viewport 的寬度

視覺視口與理想視口關係:

visual viewport寬度 = ideal viewport寬度 / 當前縮放值
當前縮放值 = ideal viewport寬度 / visual viewport寬度
複製代碼

參考: www.ayqy.net/blog/徹底理解px… github.com/jawil/blog/…

rem (Font size of the root element)

一個相對長度單位。做用於根元素,相對於初始值/默認值大小;做用域非根元素,相對於根元素html字體大小(經常使用)

em

Font size of the parent, in the case of typographical properties like font-size, and font size of the element itself, in the case of other properties like width.
做用於font-size屬性,相對於父元素字體大小(經常使用);做用於非font-size屬性,相對於自身字體大小

佈局方案

百分比+媒體查詢

媒體查詢 前提: <meta name="viewport" content="width=device-width"/> mate 中的 device-width / width:

  • device-width 是設備實際的寬度,只和設備的分辨率有關,通常是設備物理像素 / 設備像素比,且不會隨着手機旋轉而改變其值, 所以並不適合開發響應式網站 (css邏輯像素)
  • width 指的是可視區域的寬度, 會和 viewport 的 scale 屬性相關,爲頁面的可視區域的寬度
    可理解爲,把佈局視口設置爲理想視口

最多見的方式,經過屏幕寬度(用CSS像素描述的寬度)來區分各類設備,以下:

@media (min-width:320px) { /* smartphones, portrait iPhone, portrait 480x320 phones (Android) */ }
@media (min-width:480px) { /* smartphones, Android phones, landscape iPhone */ }
@media (min-width:600px) { /* portrait tablets, portrait iPad, e-readers (Nook/Kindle), landscape 800x480 phones (Android) */ }
@media (min-width:801px) { /* tablet, landscape iPad, lo-res laptops ands desktops */ }
@media (min-width:1025px) { /* big landscape tablets, laptops, and desktops */ }
@media (min-width:1281px) { /* hi-res laptops and desktops */ }


min-width: 480px: Will target mobile devices in landscape mode and up
複製代碼
// 
@media screen and (min-width: 320px) {
    html {
        font-size: 50px;
    }
}
@media screen and (min-width: 360px) {
    html {
        font-size: 56px;
    }
}
@media screen and (min-width: 414px) {
    html {
        font-size: 63px;
    }
}
複製代碼

rem佈局

rem: 根元素(html)的字體大小. 即 1rem = html中設置的font-size

獲取設備寬度 document.documentElement.getBoundingClientRect().width / document.documentElement.clientWidth

原理: 實質時比例問題, 根據設計圖比例計算出固定 rem 值 實現: 主要經過修改 html 的 fontSize

  1. 直接引入 [www.jianshu.com/p/b00cd3506…]
(function (doc, win) {
        var docEl = doc.documentElement,
            resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
            recalc = function () {
                var clientWidth = docEl.clientWidth;
                if (!clientWidth) return;
                if(clientWidth>=640){
                    docEl.style.fontSize = '100px';
                }else{
                    docEl.style.fontSize = 100 * (clientWidth / 640) + 'px'; // 640: 可根據設計圖來定
                }
            };

        if (!doc.addEventListener) return;
        win.addEventListener(resizeEvt, recalc, false);
        doc.addEventListener('DOMContentLoaded', recalc, false);
    })(document, window);
複製代碼
  1. 使用flexible.js
    github.com/amfe/lib-fl…

缺點:

  1. iframe 問題
  2. 富文本問題
  3. 高清方案
  4. 部分android機型不兼容
  5. 系統字體縮放時, 發生變化, 致使頁面錯亂,由於和根元素字體緊密聯繫;解決方案: 縮放還原
    1> juejin.im/post/59f678…
    2> 基於 flexible.js 添加如下代碼: juejin.im/post/5b9cb9…
var root = window.document.documentElement;
var fontSize = parseFloat(root.style.fontSize);
// html最終的font-size大小 
var finalFontSize = parseFloat(window.getComputedStyle(root).getPropertyValue("font-size"));
if(finalFontSize !== fontSize) {
    root.style.fontSize = fontSize * fontSize / finalFontSize + "px";
}
複製代碼
  • getComputedStyle Window.getComputedStyle()方法返回一個對象,該對象在應用活動樣式表並解析這些值可能包含的任何基本計算後報告元素的全部CSS屬性的值。 私有的CSS屬性值能夠經過對象提供的API或經過簡單地使用CSS屬性名稱進行索引來訪問。

  • getPropertyValue 此 CSSStyleDeclaration.getPropertyValue() 接口會返回一個 DOMString ,這個返回值將會包含預請求的CSS屬性信息。

lib-flexible

舊版本

舊版本#17 代碼

例如 ios
scale=0.5
innerWidth=750
device-width=375
innerWidth * scale = (device-width = layout-viewport-width)
複製代碼

處理過程以下:

  1. 先取 dpr (dpr = window.devicePixelRatio)
  2. 再設置 scale = 1/dpr
  3. 而後就有 innerWidth 了, innerWidth = 375 / scale = 750px (device-width = document.documentElement.clientWidth)
  4. 最後將 innerWidth 分紅 10rem, font-size = innerWidth / 10 = 75px
;(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 = {});
    
    if (metaEl) {
        console.warn('將根據已有的meta標籤來設置縮放比例');
        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) {
        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;
        }
        scale = 1 / dpr;
    }

    docEl.setAttribute('data-dpr', dpr);
    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(){
        // 動態設置的縮放大小會影響佈局視口的尺寸, 設備物理像素大小
        // 設備邏輯像素 device-width = 設備物理像素 /(devicePixelRatio * scale)
        var width = docEl.getBoundingClientRect().width; // iphone6 => 750
        if (width / dpr > 540) {
            width = 540 * dpr;
        }
        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);

    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'] = {}));
複製代碼

新版本

新版本 2.0代碼

meta 標籤固定爲

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

代碼邏輯爲: 取 width 分爲 10rem, font-size = width / 10

: 手機淘寶首頁 2019-01-16, 也是這個方案, 可是是 width = 3.75rem, font-size = 100px, 估計是爲了方便除

代碼, 添加部分註釋

(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 / 3.6, 當設計稿360px, 更好計算
  function setRemUnit () {
    // var rem = docEl.clientWidth / 10 // 原始代碼
    var rem = docEl.clientWidth / 3.6  // 修改後代碼
    docEl.style.fontSize = rem + 'px'
  }

  setRemUnit()

  // reset rem unit on page resize
  window.addEventListener('resize', setRemUnit)
  window.addEventListener('pageshow', function (e) {
    // 參考: https://juejin.im/post/5c807802f265da2de33f4e1f
    // 頁面從瀏覽器的緩存中讀取該屬性返回 ture, 優化方案 window.performance.navigation.type
    // if (e.persisted) {  // 原始代碼
    //   setRemUnit()
    // }
    if (e.persisted || (window.performance && window.performance.navigation.type === 2)) { // 修改後代碼
      setRemUnit()
    }
  })

  // detect 0.5px supports 檢測是否支持0.5px, 用於1px問題
  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))
複製代碼

vw/vh (css3新增屬性)

  • Viewport Height (vh): This unit is based on the height of the viewport.
  • Viewport Width (vw): This unit is based on the width of the viewport
  • 1vw: 1% of the viewport's width,便可視窗口寬度1%
  • 1vh: 1% of the viewport's height,便可視窗口高度1%
  • vmin:1% of the viewport's smaller dimension,即從vw和vh中取最小Math.min(vw, vh)
  • vmax: 1% of the viewport's larger dimension,即從vw和vh中取最大Math.max(vw, vh)

實現:

<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no" />
設計圖尺寸:設計圖元素尺寸 = 100vw:Y // Y 表示css樣式中設計圖某元素的大小, 單位vw
複製代碼

缺點:

  • 沒有最大/最小限制, 當屏幕過於小,字體或內容過小,看不清,過大同理
  • 兼容性: ios8/andorid4.4 以上(ios6/7部分支持) 參考https://caniuse.com/#feat=viewport-units

vw + rem

優勢:

  1. 實現簡單,不依賴插件及第三方庫,幾行css代碼就能夠實現
  2. 開發方便, 方便換算
  3. 不影響px使用,完美兼容第三方組件庫
  4. 不存在富文本和iframe等兼容問題

實現: 如下實現把屏幕平均分紅10份,即屏幕寬10rem, 以rem與vw關係: rem:vw = 10:1 或者 1rem = 10vw爲依據 100vw === 10 rem === document.documentElement 前提: <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no" />

  1. html 的 font-size 等於 1/10 的視口寬度 (即: 1rem = 1 / 10 * 100vw => 等同於lib-flexible 中 document.documentElement.style.fontSize = clientWidth / 10) [此處取1/10, 由於在淘寶方案也是取這個值, 爲了更好計算能夠去其餘值]
// $vw_design: 設計圖尺寸
// $vw_fontsize: 設計圖尺寸 / 10  假設把設計圖分爲10份, 每份的大小(設計圖的1rem), 並以此爲基數
html {
  font-size: ($vw_fontsize / $vw_design) * 100vw; // 直接寫 10vw

  // 同時,經過Media Queries 限制根元素最大最小值
  @media screen and (max-width: 320px) {
    font-size: 32px;
  }
  @media screen and (min-width: 540px) {
    font-size: 54px;
  }
}
複製代碼
  1. 計算使用scss函數: 設計圖元素尺寸 / (設計圖尺寸 / 10) * 1rem
// $basesize: 設計圖元素尺寸
@function rem($basesize) {
  @return ($basesize / $vw_fontsize) * 1rem; 
}

//簡化
@function rem($basesize) {
  @return ($basesize / $vw_design) * 10rem; 
}
複製代碼

計算原理:

假設設計圖某元素所佔視口大小爲 X, 單位爲: vw

設計圖某元素尺寸 / 設計圖尺寸 = (X)vw / 100vw 
=> (X)vw = (設計圖元素尺寸 / 設計圖尺寸) * 100vw 
// 轉化爲以rem單位的數值Y
因爲 (Y)rem = (X)vw / 10 (平局分紅10份,vw:rem = 10:1)
=> (Y)rem = (設計圖元素尺寸 / 設計圖尺寸) * 10vw 
複製代碼

觀察最後推論,因而也能夠按照如下理解:

$design_width: 750
$divideNum: 10 //屏幕平均分紅份數
@function px2rem($design_dom) {
    @return #{$design_dom/$design_width * $divideNum}rem
}

html {
    font-size: (100 / $divideNum)vw;
    //同時,經過Media Queries 限制根元素最大最小值
    @media screen and (max-width: 320 ) {
        font-size: 32;
    }
    @media screen and (min-width: 640 ) {
        font-size: 64;
    }
}
複製代碼

參考: juejin.im/post/5c0357…

rem + js

js 動態設置 html {font-size: (clientWidth / 屏幕平均分紅份數)px}

$design_width: 750
@function px2rem($dom) {
    @return #{$dom/$design_width * 屏幕分紅份數}rem
}
複製代碼

防止使用rem後,未設置font-size的元素繼承使用根元素的font-zsize,重置body的font-size 爲默認值(通常16px)或使用媒體查詢字體響應式(320, 480, 640移動端尺寸)

body {
    font-size: 16px;
}
複製代碼

總結

響應式佈局方案

縮放佈局 用戶體驗 兼容性 依賴js 支持超大屏幕 須要修正字體
rem+media-query IOS4.1 AN2.1 ×
rem+js IOS4.1 AN2.1
rem+vw IOS6.1 AN4.4 ×
vw IOS6.1 AN4.4 × × ×

總結:

  1. rem兼容性:ios4.1/android2.1
  2. vw兼容性:ios6.1/android4.4
  3. 超大屏幕時,rem可經過media query控制font-size 最大/小值
  4. rem方案都須要修正body字體

其餘

1px 實現

.hairlines li{
  height: 50px;
  line-height: 50px;
  border:none;
  text-align: center;
  position: relative;
  margin-top: 10px;
}
.hairlines li:after{
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  border: 1px solid #cccccc;
  // border-radius: 26px;
  width: 200%;
  height: 200%;
  -webkit-transform: scale(0.5);
  transform: scale(0.5);
  -webkit-transform-origin: left top;
  transform-origin: left top;
}
複製代碼

百分比計算

  1. 子元素的height或width中使用百分比,是相對於子元素的直接父元素
  2. 子元素的top/bottom|left/right中使用百分比,是相對於直接非static定位(默認定位)的父元素的高度|寬度
  3. 子元素的padding/margin中使用百分比,是相對於子元素的直接父元素的寬度
  4. 子元素的border-radius/translate/background-size中使用百分比,是相對於自身的寬|高

參考: github.com/sunmaobin/s…

相關文章
相關標籤/搜索