本文來自尚妝前端團隊南洋
發表于尚妝github博客,歡迎訂閱。
移動端H5頁面rem縮放方案flexible.js兼容375px方案的思路
參考:
一個新的項目復用了一些老頁面,老頁面是使用375px方案進行移動端適配的,meta[viewport]使用的是
<meta name="viewport" content="width=375, user-scalabe=no">
,而新頁面使用的是flexible.js伸縮方案,動態生成meta[viewport]<meta name="viewport" content="initial-scale=[num], user-scalabe=no">
如何在老頁面使用px布局的前提下,新頁面使用rem布局,組件也使用rem布局,并且組件可以兼容老頁面和新頁面是本文的結果。
首先會介紹375px方案和rem方案的實現原理。
375px方案
<meta name="viewport" content="width=375, user-scalabe=no">
375px方案的頁面開發過程對新人非常的友好,利用頁面的布局視口(layout viewport)為固定值375px,和移動端瀏覽器窗口的自動縮放功能(視覺視口==布局視口),可以很好的在大部分移動設備上展示375px寬度的內容。
具體的開發前提是設計師給到一份750px寬的設計稿,前端同學根據ps量的像素的50%進行css書寫。若一個banner寬度量的為750px,在css中編寫為`width: 375px`。至于為什么是2倍的設計稿,可以參考[移動端高清、多屏適配方案](http://www.html-js.com/article/Mobile-terminal-H5-mobile-terminal-HD-multi-screen-adaptation-scheme%203041)這篇文章,可以找到答案。
375px方案相對于把meta[viewport]中的width屬性設置成`device-width`,然后通過媒體查詢寫幾套css規則來說已經是非常方便了。把所有不同屏幕尺寸的手機的布局視口(layout viewport)設置成一個固定的值375px,無需考慮其他屏幕尺寸的情況。
但375px方案的實現原理在某些安卓原生瀏覽器上有兼容問題,會產生一個重要bug --- 在某些安卓原生瀏覽器或webview中會出現視覺視口小于布局視口的情況。直觀的顯示就是頁面會出現左右滑動。如下圖:
而在上文也提到了375px方案得以實現就是依靠瀏覽器的原生能力 --- 迫使視覺視口等于布局視口。我們將這種情況下的document.documentElement.clientWidth(布局視口)
與window.innerWidth(視覺視口)
打印看看。
瀏覽器的縮放效果沒有實現,至于為什么,先看兩條關于縮放的總結公式/經驗。
一、meta標簽內沒有設置initial-scale的情況
瀏覽器計算出的縮放值 = layout viewport width(布局視口) / ideal viewport width(理想視口)
visual viewport width(視覺視口) = 瀏覽器計算出的縮放值 * ideal viewport width (理想視口)
===》
layout viewport width === visual viewport width // true
經過上述計算會將視覺視口會等于布局視口,布局上的所有內容都會出現在手機屏幕上。出現之前提到的bug的問題出在計算視覺視口上,瀏覽器會將所有計算出的縮放值都默認等于1,所以不管我們將布局視口設置能375還是其他任意值,視覺視口永遠會是1 * ideal viewport width (理想視口)
。ps:此款安卓機型的理想視口等于360.
二、meta標簽內設置了initial-scale的值的情況
visual viewport width(視覺視口) = initial-scale(meta 標簽內設置的初始縮放值) * ideal viewport width(理想視口又稱設備獨立像素)
layout viewport width = visual viewport width
解釋:第二條總結經驗正是rem伸縮方案flexiblejs的核心思想,設置了initial-scale后瀏覽器會計算出視覺視口,繼而將布局視口的值自動設置成視覺視口的值。達到在屏幕上完整呈現布局上的內容。
但是在同樣的安卓原生瀏覽器上,不管我們將initial-scale設置成多少,瀏覽器都默認值為1。所以視覺視口和布局視口永遠都等于1 * ideal viewport width
這個問題的hack辦法在flexible.js里也有所體現。
375px方案就解釋到這里,至于為何是375而不是其他的值比如360、320等,可以參考移動端高清、多屏適配方案以及viewport-and-flexible.js, 在后篇文章中也有介紹3種視口的一些概念。
rem方案
rem方案的目標也是用一套相同的度量標準適配所有屏幕大小的移動設備,在不同屏下進行正確的縮放。假設10rem
寬在iphone5上是屏幕寬的一半,那么10rem
在iphone6、iphone6plus、三星note等等機型上都顯示為屏幕寬的一半。
我們知道rem所對應的px值是基于html標簽上的font-size
值進行換算的。若
html {
font-size: 20px;
}
10rem === 200px //true
為了適配縮放所有設備,就要寫個腳本動態設置html的fontSize值。同時要對頁面的布局視口(layout viewport)和設計稿做一個劃分約定,這里就約定這個值為10。(理論上可以任何值)
由以上兩幅圖可以知道,設計稿的一個區塊對應1rem,布局視口的一個區塊也對應1rem。而每個機型的布局視口該如何確定,flexible.js利用了上面提到的公式:
visual viewport width(視覺視口) = initial-scale(meta 標簽內設置的初始縮放值) * ideal viewport width(理想視口又稱設備獨立像素)
瀏覽器自動將 layout viewport width = visual viewport width
之前也提到了initial-scale
不為1的情況下部分安卓機型有bug,所以這里可以將initial-scale規定設置成1。
拿iphone6為例:理想視口為375px,經計算布局視口和視覺視口都等于375,html的fontSize等于37.5px。若設計稿量的是10個區塊大小,那么在編寫css時就寫10rem,對應的width等于37。5 * 10 = 375px
正好布滿整個布局視口。
在flexible.js中對iphone設備的initial-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; // initial-scale
此處將initial-scale根據dpr動態設置是為了解決retina屏下border: 1px問題。而將安卓設備的dpr全部設置成1就是hack了之前提到的initial-scale在部分安卓機子上只能為1的bug。
回到border: 1px
問題,有些設計師在750px的設計稿中設計了一條1px的邊框線,而這個1px的邊框線在各機型上最好的效果實際是占設備物理像素的1px。
拿iphone5和初代iphone為例:若設置initial-scale=1
,那么布局視口為320px,但是iphone5的物理像素寬為640px,那就代表了1個css像素包含了4個物理像素(2x2)。而初代iphone的布局視口和物理像素都為320px如圖:
在兩者手機上顯示的效果1px是一模一樣的,但是iphone5包含著4倍的物理像素,就取上下高度而言,在iphone5上只用編寫border: 0.5px
就可以了。
但是部分機型對于0.5是不識別的,會直接賦值0。而我們想要做的就是令border的寬等于1個物理像素。我們可以這樣做,根據window.devicePixelRatio屬性動態縮放layout viewport,使之與屏幕的物理像素相同。這樣我們只用在css里編寫border: 1px
,對應的就是物理像素的1px,border:1問題完美解決。
兩種方案如何兼容
首先要考慮到組件也用rem進行布局,并且組件要在px布局和rem布局中都能兼容。那么就要全局head引入flexible.js的js腳本。
在375px布局方案里meta[viewport]已經設置,那么在flexible腳本中就要進行判斷,已經設置viewport的就沿用,不動態創建meta[viewport]。
組件的rem布局要依賴html標簽的font-size值,在新頁面rem布局中已經實現。而在375px布局中,flexiblejs根據固定layout viewport值-- 375進行計算,那么所有屏幕尺寸下的頁面html標簽font-size值都為37.5px
。
還有一個問題,在375px布局中,全局css環境中設置了
html, body {
font-size: 100%
}
而所有的瀏覽器實現默認字體大小為16px
,所以在老頁面中有些字沒有設置大小,默認是16px,引入了flexible后html上的font-size為37.5px,body標簽上的字體大小就會變成37.5 * 100% = 37.5px
,而沒有設置字體大小的字體就會變成37.5px,需要在flexible.js中設置針對這種情況。
if (doc.readyState === 'complete') {
doc.body.style.fontSize = 16 + 'px';
} else {
doc.addEventListener('DOMContentLoaded', function(e) {
doc.body.style.fontSize = 16 + 'px';
}, false);
}
最后的結果就是:
- 全局引入flexible.js文件。
- 375px布局的老頁面上html標簽的font-size固定為37.5px。body上的font-size固定為16px。
- rem布局的新頁面上html標簽的font-size隨不同機型而不同。
- 組件編寫一律按照rem布局,設計稿為750px,兼容新老頁面。
原創文章,轉載需謹慎 ~~