手淘移動端適配的方案學習和相關思考

flexible方案是手淘經過多年的摸索和實戰,總結出來的一套移動端適配方案。這個方案在多屏幕適配以及相關bug修復上做的還是不錯的。這也是在讀了源碼之後纔有了更深一層的理解,後面會詳細解讀。

項目回顧

首先來說一下之前做的一個項目,是關於騰訊衆創空間的H5活動頁面的製作。因爲那會是剛去實習沒多久,算是剛剛熟悉了公司的業務流程,接着項目組長就給我分配了這樣一個任務。說實話,當時剛剛接手這個任務的時候,心理還是有點小興奮的,畢竟之前理論知識學習了這麼久,現在能有機會來個實戰,這對於我這個入職不就的實習生來說會是一個不錯的實踐機會。
在拿到設計部門給的設計稿和思路稿之後,便開始了整個頁面的製作。因爲畢竟第一次接觸移動端的開發,剛開始有點小不適應,上手之後就好了。由於這個項目催的急,自己匆匆忙忙的趕着做,在歷經4天時間終於把它搞定了(扯了這麼白話,下面來具體說說項目的整個實現流程)。

項目實戰

當時設計人員給的設計稿是基於iphone5(640×1136)的。整個頁面的佈局工作還算比較輕鬆,比較麻煩是的關鍵幀動畫的延遲時間的控制和背景音樂的按時播放問題(主要是時間軸把握不好)。當時項目組長跟我說前一個動畫開始的時間加上這個動畫的執行時間就是下一個動畫的開始時間。嘗試了好多次,最後終於搞定了。在把所有的靜態頁面都完成之後,剩下的一個最大的任務就是移動端的適配工作了。

移動端的適配

當時採用的方法是:首先通過JS獲取到當前設備屏幕的寬度(通過document.documentElement.clientWidth獲取到),然後求出當前屏幕的寬度和設計稿寬度的比例(高度的處理方法一致)。最後在腳本文件中,獲取到頁面的所有圖片,根據移動設備的不同,動態修改每一張圖片的寬度和高度,當時也結合了CSS3中的vw和vh特性來進行適配。當時由於時間比較緊張,在匆匆忙忙完成適配之後便把所有頁面打包發給組長了。至此,自己的第一個H5頁面告一段落。

首次嘗試存在的問題

後來在手機端測試:頁面在普通屏幕下是沒有問題,但是在retina屏幕下就會出現圖片模糊的情況,這是什麼鬼?
經過一番網上查閱資料和思考,得到一個結論:是因爲位圖像素點不夠,從而導致圖片模糊。因爲自己之前做適配的時候,就拿設計稿的尺寸來說640×1136,而iPhone5的屏幕尺寸是320×568。根據之前的方案,求出的頁面縮放比爲0.5,而這樣做相當於把圖片的尺寸縮小了一半,結果就導致1個位圖像素對應於4個設備物理像素,就會導致圖片模糊(後來想想,這麼做就把設計稿大小要×2的效果給破壞了)。

理論上:1個位圖像素對應於1個物理像素,圖片才能得到完美清晰的展示
關於移動端像素的知識,在這裏不多說了,詳情見我的這篇博客H5移動端開發學習總結

對於dpr=2的retina屏幕而言,1個位圖像素對應於4個物理像素,由於單個位圖像素不可以再進一步分割,所以只能就近取色,從而導致圖片模糊。
所以,對於圖片高清問題,比較好的方案就是兩倍圖片(@2x)。
如:200×300(css pixel)img標籤,就需要提供400×600的圖片。
如此一來,位圖像素點個數就是原來的4倍,在retina屏幕下,位圖像素點個數就可以跟物理像素點個數形成 1 : 1的比例,圖片自然就清晰了。

手淘flexible方案學習

原理:在所有資源加載之前執行這個JS。執行這個JS後,會在<html>元素上增加一個data-dpr屬性,以及一個font-size樣式。JS會根據不同的設備添加不同的data-dpr值,比如說2或者3,同時會給html加上對應的font-size的值,比如說75px。如此一來,頁面中的元素,都可以通過rem單位來設置。他們會根據html元素的font-size值做相應的計算,從而實現屏幕的適配效果。
之前也用這個方案寫過幾個小Demo,最近又找時間把裏面的實現原理梳理了一下。

;(function(win, lib) {
 //源碼部分 
})(window, window['lib'] || (window['lib'] = {}));

這個插件也採用了傳統插件的封裝形式,採用了匿名函數自執行的方式將代碼封裝起來。這樣做的好處是可以避免全局變量的污染,此外將window作爲實現傳入匿名函數中,這樣一來可以減少全部變量的查找,提高性能。
另外一個參數window[‘lib’] || (window[‘lib’] = {} –> 如果lib已經定義(window[‘lib’]能獲取到),就傳這個lib,如果沒有定義就給lib賦值空對象,並傳入lib。爲了避免重複定義。
這個時候在flexible.js裏面的lib其實就已經是window.lib了(js中對象按引用傳遞)。

flexible.js源碼分析

    var doc = win.document;//獲取到document
    var docEl = doc.documentElement;//獲取到html
    var metaEl = doc.querySelector('meta[name="viewport"]');//獲取到視口標籤
    var flexibleEl = doc.querySelector('meta[name="flexible"]');//獲取手動設置的meta來控制dpr值
    var dpr = 0;//設備縮放比
    var scale = 0;//屏幕縮放比  dpr與scale是倒數關係
    var tid;//定時器變量
    var flexible = lib.flexible || (lib.flexible = {});

這段代碼對相應的dom元素進行了緩存獲取,這樣可以減少dom的訪問次數,畢竟dom操作太昂貴,我們在實際編程中應該儘量減少dom操作。

 //如果頁面中存在meta標籤
    if (metaEl) {
        console.warn('將根據已有的meta標籤來設置縮放比例');
        var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
        // console.log(match);
        if (match) {
            scale = parseFloat(match[1]);
            // console.log(scale);
            dpr = parseFloat(1 / scale);//兩者是倒數關係
            // console.log(dpr);
        }
    } else if (flexibleEl) {
        /*
        這裏是判斷是否存在手動設置的meta標籤
        其中initial-dpr會把dpr強制設置爲給定的值。如果手動設置了dpr之後,不管設備是多少的dpr,都會強制認爲其dpr是你設置的值。
        在此不建議手動強制設置dpr,因爲在Flexible中,只對iOS設備進行dpr的判斷,對於Android系列,始終認爲其dpr爲1。
         */
        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));
            }
        }
    }

這段代碼首先會判斷頁面中是否已經存在相應的meta標籤,如果存在,將會給出一個警告:將根據已有的meta標籤來設置縮放比例。

/*
    在Flexible中,只對iOS設備進行dpr的判斷,對於Android系列,始終認爲其dpr爲1。
     */
    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;//scale和dpr成倒數關係
    }

下面這段代碼在頁面中不存在相應的meta標籤時,會自動創建一個meta標籤,並會根據頁面的dpr來設置相應的頁面縮放比。個人覺得這一點設計的很人性化,開發人員可以自己定義meta標籤,如果沒有定義,則代碼會自動幫你根據不同的設備生成相應的meta標籤,這個很不錯。

 //給html標籤設置自定義屬性data-dpr
    docEl.setAttribute('data-dpr', dpr);
    //通過JS來動態改寫meta標籤
    //如果不存在metaEl,則動態創建meta標籤
    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) {
            // console.log(docEl.firstElementChild);//head
            //這裏是將新創建的meta標籤插入到head標籤中
            docEl.firstElementChild.appendChild(metaEl);
        } else {
        //如果沒有head標籤,則新創建一個包裹元素
            var wrap = doc.createElement('div');
            wrap.appendChild(metaEl);
            doc.write(wrap.innerHTML);
        }
    }

這裏寫圖片描述
在dpr爲2的時候,scale爲0.5
這裏寫圖片描述
在dpr爲3的時候,scale爲0.3333333
這樣做目的:當然是爲了保證頁面的大小與設計稿尺寸的一致性,比如設計稿如果是750的橫向分辨率,那麼實際頁面的device-width,以iphone6來說,也等於750,這樣的話設計稿上標註的尺寸只要除以基準值就能夠轉換爲rem了。

//刷新當前頁面的rem基準值
    function refreshRem(){
        //獲取設備的寬度
        // console.log(docEl.getBoundingClientRect());
        var width = docEl.getBoundingClientRect().width;
        if (width / dpr > 540) {
            //給屏幕設置最大的寬度值(1080,dpr是2),防止頁面在PC端展示遭到破壞
            width = 540 * dpr;
        }
        var rem = width / 10;//Flexible會將視覺稿分成100份(主要爲了以後能更好的兼容vh和vw)
        // console.log(rem);
        //設置html元素的字體大小作爲基準值
        docEl.style.fontSize = rem + 'px';
        //當前頁面的rem基準值
        flexible.rem = win.rem = rem;
    }

getBoundingClientRect();該方法獲得頁面中某個元素的左,上,右和下分別相對瀏覽器視窗的位置以及這個元素的寬和高,這個方法返回的是一個對象,即Object,該對象有是個屬性:top,left,right,bottom,width和height。
此外,手淘對於頁面大小設置了一個臨界點,當設備豎着時橫向物理分辨率大於1080時,html的font-size就不會變化了,原因是:這樣的分辨率已經可以去訪問電腦版頁面了,防止移動端頁面在PC端展示遭到破壞。

//監聽resize事件
 //當設備屏幕尺寸發生變化時,更新當前頁面的rem基準值
    win.addEventListener('resize', function() {
        clearTimeout(tid);
        tid = setTimeout(refreshRem, 300);
    }, false);
 //監聽pageshow事件
    win.addEventListener('pageshow', function(e) {
        if (e.persisted) {
            clearTimeout(tid);
            tid = setTimeout(refreshRem, 300);
        }
    }, false);

火狐和Opera有一個特性,名叫”往返緩存”(back-forward cache,或bfcache),可以在用戶使用瀏覽器的”後退”和”前進”按鈕時加快頁面的轉換速度。這個緩存中不僅保存着頁面數據,還保存了DOM和JavaScript的狀態;實際上是將整個頁面都保存在了內存裏。如果頁面位於bfcache中,那麼再次打開該頁面時就不會觸發load事件。
此外,火狐還提供了一些新事件:
pageshow事件:這個事件在頁面顯示時觸發,無論該頁面是否來自bfcache。在重新加載的頁面中,pageshow會在load事件觸發後觸發;而對於bfcache中的頁面,pageshow會在頁面狀態完全恢復的那一刻觸發。
另外要注意:雖然這個事件的目標是document,但必須將其事件處理程序添加到window。pageshow事件的event對象還包含一個名爲persisted的布爾值屬性。如果頁面被保存在了bfcache中,則這個屬性的值爲true,否則這個屬性值爲false。

    /*
    針對不同的瀏覽器做domReady兼容
    IE6,7,8都不支持DOMContentLoaded事件
     */
    if (doc.readyState === 'complete') {//針對不支持DOMContentLoaded事件做兼容
        //根據不同的dpr來設置不同的字體大小,因爲防止頁面設置了縮放scale屬性值而導致不同設備上字體大小不一致
        doc.body.style.fontSize = 12 * dpr + 'px';
    } else {//如果支持DOMContentLoaded事件,則直接使用
        doc.addEventListener('DOMContentLoaded', function(e) {
            // alert("DOMContentLoaded");
            doc.body.style.fontSize = 12 * dpr + 'px';
        }, false);
    }

window的load事件會在頁面中的一切都加載完畢時觸發,但這個過程可能會因爲要加載的外部資源過多而頗費周折。而DOMContentLoaded事件則在形成完整的DOM樹之後就會觸發,不理會圖片、js文件、css文件或者其他資源是否已經下載完畢。
與load事件不同,DOMContentLoaded支持在頁面下載的早期添加事件處理程序,這就意味着用戶能夠儘早地與頁面進行交互。

document.readyState:返回當前文檔的狀態

  • uninitialized - 還未開始載入
  • loading - 載入中
  • interactive - 已加載,文檔與用戶可以開始交互
  • complete - 載入完成
//把rem轉換爲px
    flexible.rem2px = function(d) {
        var val = parseFloat(d) * this.rem;
        if (typeof d === 'string' && d.match(/rem$/)) {
            val += 'px';
        }
        return val;
    }
    //把px轉換爲rem
    flexible.px2rem = function(d) {
        var val = parseFloat(d) / this.rem;
        if (typeof d === 'string' && d.match(/px$/)) {
            val += 'rem';
        }
        return val;
    }

上面的代碼是用於px和rem之間的轉換的,當然我們也可以採用less和sass這樣的css處理器中的混合宏來實現。
less使用舉例:

//定義一個變量和一個mixin
@baseFontSize: 75;//基於視覺稿橫屏尺寸/100得出的基準font-size
.px2rem(@name, @px){
    @{name}: @px / @baseFontSize * 1rem;
}
//使用示例:
.container {
    .px2rem(height, 240);
}
//less翻譯結果:
.container {
    height: 3.2rem;
}

小結

  • 動態設置viewport的scale
scale = 1 / dpr;
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
  • 動態計算html的font-size
var width = docEl.getBoundingClientRect().width;
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
  • 佈局的時候,各元素的css尺寸=設計稿標註尺寸/設計稿橫向分辨率/10
    注:設計稿橫向分辨率/10即頁面佈局基準值

目前手淘已經給我們提供了一個開源的解決方案,具體請查看:傳送門

相關問題思考

retina下,border: 1px問題

我們正常的寫css,像這樣border: 1px;,在retina屏幕下,會有什麼問題呢?
這裏寫圖片描述
注:圖片來源於傳送門
對於一條1px寬的直線,它們在屏幕上的物理尺寸的確是相同的,不同的其實是屏幕上最小的物理顯示單元,即設備物理像素,所以對於一條直線,iphone5它能顯示的最小寬度其實是圖中的紅線圈出來的灰色區域,用css來表示,理論上說是0.5px。
所以,設計師想要的retina下border: 1px;,其實就是1物理像素寬,對於css而言,可以認爲是border: 0.5px;,這是retina下(dpr=2)下能顯示的最小單位。
然而,無奈並不是所有手機瀏覽器都能識別border: 0.5px;,ios7以下,android等其他系統裏,0.5px會被當成爲0px處理,那麼如何實現這0.5px呢?
對於iphone5(dpr=2),添加如下的meta標籤,設置viewport(scale 0.5)

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

這樣,頁面中的所有的border: 1px都將縮小0.5,從而達到border: 0.5px;的效果。

如何在css編碼中還原視覺稿的真實寬高

假如我們拿到的是一個針對iphone6的高清視覺稿 750×1334,如果有一個區塊,在psd文件中量出:寬高750×300px的div,那麼如何轉換成rem單位呢?
公式如下:
rem = px / 基準值;
對於一個iphone6的視覺稿,它的基準值就是75。所以,在確定了視覺稿(即確定了基準值)後,通常我們會用less寫一個mixin(混合宏),像這樣:

// 例如: .px2rem(height, 80);
.px2rem(@name, @px){
    @{name}: @px / 75 * 1rem;
}

所以,對於寬高750×300px的div,我們用less就這樣寫:

.px2rem(width, 750);
.px2rem(height, 300);

轉換成css,就是這樣:

width: 10rem; // -> 750px
height: 4rem; // -> 300px

最後因爲dpr爲2,頁面scale了0.5,所以在手機屏幕上顯示的真實寬高應該是375×150px,就剛剛好(達到了一個CSS像素對應一個設備物理像素的效果)。

感覺這部分的知識太燒腦,整理了一下午,真心佩服那些大牛能夠堅持寫高質量的博文,自己也要好好加油了,多總結多思考。上面的總結如有錯誤,歡迎大家交流指正,共同學習,共同進步。
相關參考博文:
從網易與淘寶的font-size思考前端設計稿與工作流
移動端高清、多屏適配方案
使用Flexible實現手淘H5頁面的終端適配

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章