瞭解真實的『REM』手機螢幕適配
rem
作為一個低調的長度單位,由於手機端網頁的興起,在螢幕適配中得到重用。使用 rem
前端開發者可以很方便的在各種螢幕尺寸下,通過等比縮放
的方式達到設計圖要求的效果。
rem
的官方定義『The font size of the root element.』,即以根節點的字型大小作為基準值進行長度計算。一般認為網頁中的根節點是 html
元素,所以採用的方式也是通過設定 html
元素的 font-size
來做螢幕適配,但實際情況真有這麼簡單嗎?
首先我們來看看使用 rem
實現手機螢幕適配的常用方案。
以設計稿的寬度為640px
,即:designWidth = 640
,同時設定在640px屏寬下 1rem=100px
rem2px = 100
。
設定
1rem=100px
的優點不言而喻。前端開發者在切圖、重構頁面的時候,通過直接位移小數點的方式,就可以將UI圖中測量到的px
值換算成對應的rem
值,方便快捷。此外,在
head
中我們還設定了:<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0" />
viewport
的作用很重要,但不是本文的重點所以不展開,有興趣的同學可以自行搜尋。
先來看看具體方案:
下面四個方案來自同事共享,原理都是採用等比縮放的方式 —— 獲得目標螢幕寬度和設計稿寬度的比,作為
rem
的基值(縮放係數),設定為html
標籤的字型大小。不同的只是在於效能取捨和書寫習慣。
方案1
@media screen and (min-width: 320px) {html{font-size:50px;}}
@media screen and (min-width: 360px) {html{font-size:56.25px;}}
@media screen and (min-width: 375px) {html{font-size:58.59375px;} }
@media screen and (min-width: 400px) {html{font-size:62.5px;}}
@media screen and (min-width: 414px) {html{font-size:64.6875px;}}
@media screen and (min-width: 440px) {html{font-size:68.75px;}}
@media screen and (min-width: 480px) {html{font-size:75px;}}
@media screen and (min-width: 520px) {html{font-size:81.25px;}}
@media screen and (min-width: 560px) {html{font-size:87.5px;}}
@media screen and (min-width: 600px) {html{font-size:93.75px;}}
@media screen and (min-width: 640px) {html{font-size:100px;}}
@media screen and (min-width: 680px) {html{font-size:106.25px;}}
@media screen and (min-width: 720px) {html{font-size:112.5px;}}
@media screen and (min-width: 760px) {html{font-size:118.75px;}}
@media screen and (min-width: 800px) {html{font-size:125px;}}
@media screen and (min-width: 960px) {html{font-size:150px;}}
方案2
@media screen and (min-width: 320px) {html{font-size:312.5%;}}
@media screen and (min-width: 360px) {html{font-size:351.5625%;}}
@media screen and (min-width: 375px) {html{font-size:366.211%;}}
@media screen and (min-width: 400px) {html{font-size:390.625%;}}
@media screen and (min-width: 414px) {html{font-size:404.2969%;}}
@media screen and (min-width: 440px) {html{font-size:429.6875%;}}
@media screen and (min-width: 480px) {html{font-size:468.75%;}}
@media screen and (min-width: 520px) {html{font-size:507.8125%;}}
@media screen and (min-width: 560px) {html{font-size:546.875%;}}
@media screen and (min-width: 600px) {html{font-size:585.9375%;}}
@media screen and (min-width: 640px) {html{font-size:625%;}}
@media screen and (min-width: 680px) {html{font-size:664.0625%;}}
@media screen and (min-width: 720px) {html{font-size:703.125%;}}
@media screen and (min-width: 760px) {html{font-size:742.1875%;}}
@media screen and (min-width: 800px) {html{font-size:781.25%;}}
@media screen and (min-width: 960px) {html{font-size:937.5%;}}
方案3
var designWidth = 640, rem2px = 100;
document.documentElement.style.fontSize =
((window.innerWidth / designWidth) * rem2px) + 'px';
方案4
var designWidth = 640, rem2px = 100;
document.documentElement.style.fontSize =
((((window.innerWidth / designWidth) * rem2px) / 16) * 100) + '%';
為了更避免理解上的混亂,我在上面js的程式碼中加了
(
)
,實際程式碼中是不需要的。
詳細分析一下,rem
和 px
直接的轉換公式可以寫為:
1rem = 1 * htmlFontSize
htmlFontSize
為html
元素的字型大小。
首先來看方案1中,在屏寬為640px
情況下的設定:
@media screen and (min-width: 640px) {html{font-size:100px;}}
可以很明顯的表現出這一點 1rem = 1 * 100px
,同我們最初的設定。那麼我們要得到其它螢幕大小的 htmlFontSize
值要怎麼辦。很簡單如方案3,因為我們的採用等比縮放的方式適配,所以計算目標螢幕寬度和設計稿的寬度的比即可:
window.innerWidth / designWidth * rem2px + 'px'
由於瀏覽器預設字型大小為 16px
,所以當我們使用百分比作為根節點 html
的字型大小時,即html
元素的font-size
值設定為一個百分比值,rem
的計算方式就會改為:
defaultFontSize = 16px
1rem = 1 * htmlFontSize * defaultFontSize
如方案2中,在屏寬為640px
情況下的設定:
@media screen and (min-width: 640px) {html{font-size:625%;}}
應用上面的公式:
1rem = 1 * 625% * 16px
其中:625% * 16 = 6.25 * 16 = 100
所以:1rem = 1 * 100px
同樣的可以得到所有螢幕大小下,html
的 font-size
值的計算公式,即為方案4:
window.innerWidth / designWidth * rem2px / 16 * 100 + '%'
通過方案3和方案4的公式,就可以很方便的生成方案1和方案2中的css。
這裡只給出了方案3和方案4對應驗證頁面(方案1和方案2是它們的變形): scheme3.html, scheme4.html
如下面兩張圖,是在屏寬為360px下的效果,通過計算目標為:
1rem = 56.25px
。方案3設定值為:56.25px
,方案4設定值為:351.5625%
方案3 | 方案4 |
---|---|
到目前為止貌似很完美的解決了問題,實際情況當然是出現了意外。在有些 Android 手機上,瀏覽器或 webview 的預設字型是隨著系統設定的字型改變的。這樣就會導致預設字型大於或小於 16px
。
修改預設字型大小後,我們再看方案3和方案4。
同樣在屏寬為360px下,我們調大系統字型大小,如下面的效果
設定前
html
元素的字型大小的計算值
為18px
,設定後的計算值
為65px
,由於螢幕寬度沒有改變,我們的目標值,即我們在html
元素上設定的font-size
值也沒有變化任然為56.25px
,而最終計算值
出現了偏差。
方案3 | 方案4 |
---|---|
分析偏差前,先來看在360px
屏寬下,方案3和方案4的計算過程:
方案3:
document.documentElement.style.fontSize = 56.25px
htmlFontSize = 56.25px
1rem = 1 * htmlFontSize = 56.25px
實際為:
1rem = 64.6875px
方案4:
document.documentElement.style.fontSize = 351.5625%
htmlFontSize = 351.5625%
defaultFontSize = 18px
1rem = 1 * htmlFontSize * defaultFontSize = 351.5625% * 18px = 63.28125px
351.5625% * 18 = 63.28125
實際為:
1rem = 64.6875px
貌似方案4的計算結果很接近實際效果,而方案3偏差很大。再來比較方案3和方案4的計算公式:
// 方案3
document.documentElement.style.fontSize =
window.innerWidth / designWidth * rem2px + 'px';
// 方案4
document.documentElement.style.fontSize =
window.innerWidth / designWidth * rem2px / 16 * 100 + '%';
方案4較於方案3其實多除了一個16,可以推測瀏覽器在計算 rem
的具體值時,如果 html
設定的 font-size
為 px
值時會先除以 16
,然後再乘以 htmlFontSize
。
1rem = 1 * (56.25px / 16) * 18
1 * (56.25 / 16) * 18 = 63.28125
方案4存在問題,是因為系統的預設字型改為了 18px
,但是我們在計算百分比是時候,還是以 16px
為基準值進行計算,所以出現偏差(計算值和實際值之間還有一點偏差這個在後面會提到)。
而在方案3中,我們其實是不考慮瀏覽器預設字型大小的,但在實際使用的過程中,瀏覽器還是除了 16
,而此時預設字型大小為 18px
。得出如下在 html
的 fontSize
設定為 px
的情況下 rem
的計算公式為:
1rem = 1 * (htmlFontSize / 16) * defaultFontSize
在系統設定的字型大小發生改變時,defaultFontSize
會跟著改變,而 16
不會變化。所以方案3雖然表面上不考慮預設字型大小的變化,只關注螢幕與設計稿之間的寬度比,但在實際計算中還是使用到了預設字型大小,而且還有一個不變的 16
在作祟,導致方案3失敗。
所謂的「
root element
」其實不是想象的那樣,一個是16
,一個是18
,到底取的是那個root element
的字型大小。
ok,rem
的計算的時候,px
的方式會有一個16
不隨系統字型大小改變,所以我們採用百分比的方案,繞開這個問題。
採用百分比的方案4
因為在計算時寫死了預設字型大小 16px
。所以它的偏差在於沒能動態的獲取預設字型大小。更新如下:
方案4.1
var designWidth = 640, rem2px = 100;
var h = document.getElementsByTagName('html')[0];
var htmlFontSize = parseFloat(window.getComputedStyle(h, null)
.getPropertyValue('font-size'));
document.documentElement.style.fontSize =
window.innerWidth / designWidth * rem2px / htmlFontSize * 100 + '%';
效果如下圖:
16px
的圖中,設定後的html
的font-size
與1rem
的實際值有偏差,同時6.4rem
的計算值也有偏差。通過檢視程式碼發現html
的font-size
使用的是:getPropertyValue('font-size')
而1rem
使用的是getPropertyValue('width')
,偏差出在計算font-size
的時候瀏覽器進行了四捨五入。rem
定義中的另一個元素「font size
」也不能按字面意思使用,宣告失守。
18px
中的偏差,以及上文中方案4在18px
實際值和計算值出現的偏差都是同樣的問題。所以基準值還需要修改。
16px | 18px |
---|---|
在更新一版,方案4.2:
var designWidth = 640, rem2px = 100;
var d = window.document.createElement('div');
d.style.width = '1rem';
d.style.display = "none";
var head = window.document.getElementsByTagName('head')[0];
head.appendChild(d);
var defaultFontSize = parseFloat(window.getComputedStyle(d, null).getPropertyValue('width'));
d.remove();
document.documentElement.style.fontSize =
window.innerWidth / designWidth * rem2px / defaultFontSize * 100 + '%';
效果如下圖:
16px | 18px |
---|---|
到此為止,rem
在預設字型不是 16px
的情況下的處理已經解決,考慮到還有設計螢幕旋轉,最終手機端的解決方案為:
function adapt(designWidth, rem2px){
var d = window.document.createElement('div');
d.style.width = '1rem';
d.style.display = "none";
var head = window.document.getElementsByTagName('head')[0];
head.appendChild(d);
var defaultFontSize = parseFloat(window.getComputedStyle(d, null).getPropertyValue('width'));
d.remove();
// document.documentElement.style.fontSize = window.innerWidth / designWidth * rem2px / defaultFontSize * 100 + '%';
var st = document.createElement('style');
var portrait = "@media screen and (min-width: "+window.innerWidth+"px) {html{font-size:"+ ((window.innerWidth/(designWidth/rem2px)/defaultFontSize)*100) +"%;}}";
var landscape = "@media screen and (min-width: "+window.innerHeight+"px) {html{font-size:"+ ((window.innerHeight/(designWidth/rem2px)/defaultFontSize)*100) +"%;}}"
st.innerHTML = portrait + landscape;
head.appendChild(st);
return defaultFontSize
};
var defaultFontSize = adapt(640, 100);
回過頭來再看 rem
的定義,『The font size of the root element.』。我們以為的 root element
—— html
其實還有個影子在作祟,而我們以為的 font-size
其實是個近似值。
評論中提到『高清適配』的問題,解決思路是設定單獨的樣式特殊處理:其中dpr大於1的時候,1px 的問題,『Retina屏的移動裝置如何實現真正1px的線?』此文介紹的很詳細,珠玉在前,就不獻醜了。圖片高清的可以用同樣是思路,在不同dpr下載入不同解析度的圖片。
此外rem還有一個問題就是對精靈圖的適配,本人開發了一個命令列工具sprites-cli,用來處理這個問題(測試階段,可能還不是太成熟)。
一個 vue 的學習交流群:685486827
寫在最後:約定優於配置——-軟體開發的簡約原則.
——————————–(完)————————————–
更多學習資源請關注我的新浪微博….