手淘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是目前用到的比較成熟的適配方案,所以,讓我們一起繼續探索吧~