1. 程式人生 > >手淘H5移動端適配方案flexible原始碼分析

手淘H5移動端適配方案flexible原始碼分析

移動端適配一直是一個值得探討的問題,在業餘時間我找了一些頁面,查看了一些廠商對於移動端H5頁面的適配方案,看到了幾個典型的例子,今天就來記錄一下我看到的第一個典型的例子,也是我們公司目前普通H5專案正在使用的適配方案。

這個適配方案是lib-flexible,在看這個原始碼的同時,我想先來回顧一下幾個概念:

1.  viewport

 在移動裝置上,viewport是裝置螢幕用來顯示我們網頁的那一塊區域,或者說是瀏覽器(或者Hybird App內的webview)用來展示我們網頁的那部分割槽域,viewport不侷限於瀏覽器可視區域的大小,可能比瀏覽器的可視區域大,也可能比瀏覽器的可視區域小。viewport是一個與html中mate標籤相關的概念,如下:

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

上面這行程式碼的作用是讓當前viewport的寬度等於裝置寬度,頁面初始縮放比例為1,viewport最大的縮放比例為1,viewport最小的縮放比例也為1,同時不允許使用者拖動縮放。

屬性
作用

值型別

width
 規定頁面的寬度

可以為字串值"device-width",或者正整數

initial-scale
 規定頁面的初始縮放比例 為數字,可以為小數
maximum-scale
 規定頁面的最大縮放比例 為數字,可以為小數
minimum-scale
 規定頁面的最小縮放比例 為數字,可以為小數
user-scalable
 規定是否允許使用者進行拖動縮放 yes或no,yes是允許,no則不允許

好了,先熟悉到這裡,後面如果想對viewport有更深入透徹的研究,可以檢視PPK大神的關於viewport的三篇文章。

2.裝置畫素比

 關於裝置畫素比,我們先賣個關子,後面會說。我們先來看一下另一個值得思考的問題,我們CSS中常用的單位px到底和我們移動裝置螢幕上的畫素(pixel)是什麼關係?CSS裡的1px等於移動裝置螢幕上的1畫素嗎?

 首先,我來繞一波,px確實是英文畫素(pixel)的縮寫;但是!!!我們這裡為了將CSS中的px和裝置中的物理畫素加以區分,CSS中的單位描述我們就用熟悉的px,裝置的物理畫素,我們則用pixel來加以區分!!!

 那麼問題來了,我CSS中的1px到底等於裝置物理畫素1pixel嗎?----答案是:不一定!!!

 那麼為什麼是不一定呢?這裡我們又要了解兩個相關概念:

  (1)物理畫素:裝置的物理畫素,顧名思義就是一個移動裝置在出廠時就固定了的畫素,整個螢幕是由一個挨著一個間隙極小的畫素組成的,是螢幕顯示中的基本單元,例如某款手機螢幕解析度:1920*1080畫素,這裡所說的1920就是該款手機螢幕縱向的畫素排布數量,1080就是橫向畫素排布數量,這裡的畫素就是我們所說的物理畫素pixel。

  (2)獨立畫素:獨立畫素也可以稱之為邏輯畫素,一個邏輯畫素是螢幕接受程式控制的最小單位,簡言之我們可以將這裡的邏輯畫素和我們CSS中的px建立起聯絡,即CSS中的1px可以控制1個邏輯畫素的顯示。

 書接前文,前面提到我們CSS中的1px不一定等於我們裝置的物理畫素1pixel,那麼什麼情況下等於?什麼情況下又不等於?

  等於的情況:早在移動端視網膜螢幕上市以前,絕大部分手機的物理畫素和邏輯畫素其實是對等的,比如iphone 3 的手機螢幕(物理畫素:320x480;邏輯畫素:320x480)。這裡就是CSS 中的1px等於移動裝置的物理畫素1pixel。也就是說此時,物理畫素÷邏輯畫素=1,這個比值就是裝置畫素比(dpr)。

  不等於的情況:當 iphone 4 手機問世時,掀起了視網膜平螢幕的浪潮,以iphone 4 手機螢幕為例(物理畫素:640x960;邏輯畫素:320x480),由此可見iphone 4  相比於iphone 3 的手機螢幕,物理畫素多了一倍,但是邏輯畫素卻沒有變化,那麼iphone 4 的裝置畫素比: 物理畫素÷邏輯畫素=2,也就是說  dpr=2 。當然,隨著手機日新月異的發展,dpr=3的情況也是有的,例如總結的下表各主要手機型號的裝置畫素比:

手機型號 物理畫素 獨立畫素(邏輯畫素) dpr 倍圖
iphone  5/5S/5E  640*1136  320*568  2  @2x
 iphone 6/7/8  750*1334  375*667  2  @2x
 iphone 6p/7p/8p  1242*2208  414*736  3  @3x

 安卓手機由於廠商眾多,並且型號尺寸眾多,現僅概括幾個常見比例供參考(不再列舉詳細的手機型號),重要的是理解原理:

手機型號 物理畫素 邏輯畫素 dpr 倍圖
Android 1 320*480 320*480 1 @1x
Android 2 540*960 360*640 1.5 @1.5x
Android 3 640*960 320*480 2 @2x
Android 4 720*1280 360*640 2 @2x
Android 5 1080*1920 360*640 3 @3x

好了,巴拉了這麼多,該切入正題上lib-flexible原始碼了,如下:

;(function(win, lib) {
    var doc = win.document;
    var docEl = doc.documentElement;
    var metaEl = doc.querySelector('meta[name="viewport"]');  // 獲取名為viewport的mate標籤
    var flexibleEl = doc.querySelector('meta[name="flexible"]'); // 獲取名為flexible的mate標籤
    var dpr = 0; // dpr (裝置畫素比)初始化置為0
    var scale = 0; // scale (縮放比例)
    var tid;
    var flexible = lib.flexible || (lib.flexible = {});
    
    if (metaEl) { // 如果名為viewport的mate標籤存在var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);  // 將name=viewport的mate標籤裡的,content屬性裡的initial-scale(初始縮放比)屬性處理成陣列
        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); // IOS機
        var devicePixelRatio = win.devicePixelRatio; // 獲取window物件的 devicePixelRatio屬性值,這個屬性值就是我們所說的裝置畫素比,簡稱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;
        }
        scale = 1 / dpr;
    }

    docEl.setAttribute('data-dpr', dpr); // 給頁面根元素設定自定義屬性data-dpr,值為前面已經賦值好的dpr
    if (!metaEl) { // 當name=viewport的mate標籤不存在時,就給頁面新增一個,各元素值為前面計算好的scale,並不允許使用者拖動縮放
        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(){
        var width = docEl.getBoundingClientRect().width;
        if (width / dpr > 540) { // 對於邏輯畫素大於540的裝置,其寬度就設定為裝置畫素比乘以540
            width = 540 * dpr;
        }
        var rem = width / 10; // 將螢幕寬度分成10份,每一份為1rem 所以整個螢幕的完整寬度為10rem
        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();
   // 後面這段程式碼是將rem單位值轉換成px的和將px單位的值換算成rem單位的值
    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'] = {}));

 原始碼的分析已經註釋到程式碼後面的註釋中了。通過原始碼的整體分析,我們會發現,lib-flexible的工作原理可以概括為:

  通過獲取裝置畫素比dpr進行運算,設定頁面裡name=viewport的mate標籤(包括內部的縮放比例),再在頁面根元素--html上新增data-dpr屬性以及值,並且設定根元素字型大小,來進行頁面適配的。

  隨著技術的飛速發展,當前lib-flexible適配方案也在逐漸被更新的適配方案所替代,但是截止目前為止,還沒有發現哪種方案能完全滿足適配各種機型的需要,也會有一些小的問題。lib-flexible是目前用到的比較成熟的適配方案,所以,讓我們一起繼續探索吧~