概念
對於移動端開發來說,無可避免的就是直面各種設備不同分辨率和不同DPR(設備像素比)的問題,在此忽略其他兼容性問題的探討。
移動端像素
-
設備像素(dp),也叫物理像素。指設備能控制顯示的最小物理單位,意指顯示器上一個個的點。從屏幕在工廠生產出的那天起,它上面設備像素點就固定不變了。
-
分辨率,屏幕上物理像素的數量。
-
設備獨立像素(dip),又稱密度無關像素。可以認為是計算機坐標系統中的一個點,這個點代表一個可以由程序使用並控制的虛擬像素。由相關系統轉化為物理像素在設備上體現。
-
css像素,web編程中的概念,屬於設備獨立像素中的一種,獨立於設備,屬於邏輯上衡量像素的單位。
-
設備像素比(dpr) = 設備像素值(dps) / 設備獨立像素值(dips),代表系統轉化時一個css像素占有多少個物理像素。
-
像素密度(ppi),設備(屏幕)每英寸內有多少個像素點。
移動端三個視口
移動端視口 viewport(div100%時的css大小):移動設備上的 viewport 就是設備的屏幕上能用來顯示我們的網頁的那一塊區域,可能與瀏覽器的可視區域不同。默認比瀏覽器可視區域要大(980px),這也是為什么一般的PC端網頁放在移動端會出現橫向滾動條的原因。
移動端中的三個不同的可視區域大小,來自於ppk關於移動設備的viewport研究:
-
布局視口(layout viewport),瀏覽器默認的viewport,一般比瀏覽器可視區域大。
-
視覺視口(visual viewport),瀏覽器的可視區域大小(瀏覽器的可見區域css像素值)
-
理想視口(ideal viewport),設備的實際物理寬度(device-width),是一種與ppi無關的設備原始的寬度(英寸),例如320px和660px下的iphone的理想視口都是320px。
位圖像素
一個位圖像素是柵格圖像(如:png, jpg, gif等)最小的數據單元。每一個位圖像素都包含着一些自身的顯示信息(如:顯示位置,顏色值,透明度等)。
理論上,1個位圖像素對應於1個物理像素,圖片才能得到完美清晰的展示。當遇上對應的位圖像素與物理像素不統一的時候。
-
位圖像素 < 物理像素。 1個位圖像素對應於多個物理像素,由於單個位圖像素不可以再進一步分割,所以只能就近取色,從而導致圖片模糊。(具體取決於設備系統的圖像算法,並不是簡單的切割圖片)(圖片拉伸)
-
位圖像素 > 物理像素。1個物理像素對應多個位圖像素,所以它的取色也只能通過一定的算法(顯示結果就是一張位圖像素只有原圖像素總數四分之一的圖片),肉眼看上去雖然圖片不會模糊,但是會覺得圖片缺少一些銳利度,或者是有點色差(但還是可以接受的)(圖片擠壓)
rem適配
什么是rem
即以根節點(html)的字體大小作為基准值進行長度計算。
假定 html 的 fontSize 為 16px,則 1rem = 16px
如果我們更改 html 的 fontSize,rem 也會更新,總是保持 1rem = 1 fontSize (html)
為什么使用rem
開發過移動端項目的同學應該都知道,不同手機設備的大小是不一樣的,在進行移動端開發時,我們通常會為 html 加上 viewport meta
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
這里得結合上面的移動端像素和移動端視口進行分析,width=device-width
將此時的頁面寬度設置為設備寬度(理想視口),所以此時頁面寬度等於設備寬度,不同手機的設備寬度是不同的所以頁面寬度也不同
iPhone4 頁面寬度 = window.innerWidth = 設備寬度 = 320px
iPhone6 頁面寬度 = window.innerWidth = 設備寬度 = 375px
所以為了適配不同的設備寬度,我們通常不直接用px來寫css代碼,因為在不同手機中頁面寬度不同,此時px的相對大小也是不同的。如果我們把一個元素設置為375px來達到100%寬度效果的話,在320設備寬度的手機就出問題了。
由此我們引入了 rem 來做適配,在 css 中直接使用 rem 作為計量單位,如果不做些什么的話,1rem = 16px(瀏覽器默認字體大小),在不同手機上都是一樣,還是無法適配,所以要點在於如何根據設備寬度在做轉化
// 假定設計稿寬度750px
const designWidth = 750;
// 通過設備寬度(window.innerWidth)和設計稿寬度(designWidth)的比例來設置 html fontSize
document.documentElement.style.fontSize = (window.innerWidth / designWidth) + 'px';
通過上面代碼的設置,我們就可以很輕松的適配移動端項目了,假定設計稿上一個元素寬度750px,那我們就在css定義750rem
在設備寬度為320px的手機上
750rem = 750 * 1rem = 750 * (window.innerWidth / designWidth) px = 750 * (320 / 750) px = 320px
同理,在設備寬度為375px的手機上
750rem = 750 * 1rem = 750 * (window.innerWidth / designWidth) px = 750 * (375 / 750) px = 375px
可能還有個問題,為什么不直接用百分比來適配?因為百分比在很多情況下是除不盡或者帶有小數的,顯然帶有小數點的px會帶來各種各樣的誤差
高清適配
如果你覺得移動端適配像上面一樣簡單轉化下就行,那就 too young too sample
1px問題
什么是 1px 問題?
以 iphone6 為例,大家應該聽過啥視網膜像素之類的,2倍屏之類的吧。其實也就是此時 設備像素比(dpr) = 設備像素值(dps) / 設備獨立像素值(dips),即一個css像素對應兩個物理像素,也就是你在css中寫的1px其實在設備顯示的是兩個像素,當你設置 border = 1px
時看起來就沒有那種1px的纖細效果,總感覺不盡如人意,差那么一點點味道。
你以為的1px
用戶看到的1px(請忽略顏色不同)
追求用戶體驗的公司通常是不能容忍 1px 問題的
圖片的模糊問題
同樣的以 iphone6 為例,我們如果定義一張圖片寬度為375px,如果圖片的像素(位圖像素),此時一個像素的圖片會對應兩個物理像素(參考上面的位圖像素),就會造成圖片模糊的問題了。你可能會問?那我直接加載750px像素的圖片不就好了(位圖像素大於物理像素時很多人是看不出失真的)。
答案當然是可以的,但你覺得追求用戶體驗的公司能容忍無故的流量耗費和性能浪費么?當然不能
解決方案
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
前面也有介紹過這部分代碼,但是沒有說明 initial-scale=1
的作用,initial-scale
定義了頁面的初始縮放,1代表不縮放。initial-scale
的值也會影響頁面寬度,即此時的css像素。
前面我們說過,在 viewport meta 的約束下
頁面寬度 = window.innerWidth = 設備寬度
,但其實正確的是 頁面寬度 = window.innerWidth = 設備寬度 / scale
,為什么是除呢?大家可以想象一下,當頁面縮放時(例如scale=0.5),是不是會導致更多的內容內容展示在當前可見區域中,css像素(頁面)是變大了。
以 iphone6 為例,當我們設置
<meta name="viewport" content="width=device-width, initial-scale=0.5, user-scalable=no">
此時頁面寬度 = window.innerWidth = 設備寬度 / scale = 375 / 0.5 = 750px
,也就是說現在頁面寬度(對應css像素)和物理像素是相等的,所以我們設置的 1px 在手機中將真正顯示 1pt(1個物理像素),也就解決了1px的問題。
所以解決方法如下
// 獲取設備dpr
const dpr = window.devicePixelRatio;
// 計算縮放比例
const scale = 1 / dpr;
// 動態設置meta
const metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'width=device-width,initial-scale=' + scale + ', user-scalable=no');
對應圖片而言,要想達到最清晰的顯示狀態則要使圖片的位圖像素與設備的物理像素對應,所以可以對圖片做如下適配
[dpr=1] img {
width: 200rem;
background: '@1x.png';
}
[dpr=2] img {
width: 200rem;
background: '@2x.png';
}
此方案的原理就是利用meta來更過css像素(因為css像素是虛擬像素由計算機定義的,見上文),以此達到一個css像素對應一個物理像素的效果,1px == 1pt
rem高清適配
利用上文提供的rem移動端適配思路,加上現在的高清適配思路,就可以完成移動端高清適配啦
直接貼代碼,來自前端:『REM』手機屏幕高清適配方案
(function(designWidth, rem2px) {
var win = window;
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var dpr = 0;
var scale = 0;
var tid;
if (!dpr && !scale) {
var devicePixelRatio = win.devicePixelRatio;
if (win.navigator.appVersion.match(/iphone/gi)) {
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
dpr = 1;
}
scale = 1 / dpr;
}
docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'width=device-width,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);
}
} else {
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'width=device-width,initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
}
// 以上代碼是對 dpr 和 viewport 的處理,代碼來自 lib-flexible。
// 一下代碼是處理 rem,來自上篇文章。不同的是獲取屏幕寬度使用的是
// document.documentElement.getBoundingClientRect
// 也是來自 lib-flexible ,tb的技術還是很強啊。
function refreshRem(_designWidth, _rem2px){
// 修改viewport后,對網頁寬度的影響,會立刻反應到
// document.documentElement.getBoundingClientRect().width
// 而這個改變反應到 window.innerWidth ,需要等較長的時間
// 相應的對高度的反應,
// document.documentElement.getBoundingClientRect().height
// 要稍微慢點,沒有准確的數據,應該會受到機器的影響。
var width = docEl.getBoundingClientRect().width;
var d = window.document.createElement('div');
d.style.width = '1rem';
d.style.display = "none";
docEl.firstElementChild.appendChild(d);
var defaultFontSize = parseFloat(window.getComputedStyle(d, null).getPropertyValue('width'));
// d.remove();
var portrait = "@media screen and (width: "+ width +"px) {html{font-size:"+ ((width/(_designWidth/_rem2px)/defaultFontSize)*100) +"%;}}";
var dpStyleEl = doc.getElementById('dpAdapt');
if(!dpStyleEl) {
dpStyleEl = document.createElement('style');
dpStyleEl.id = 'dpAdapt';
dpStyleEl.innerHTML = portrait;
docEl.firstElementChild.appendChild(dpStyleEl);
} else {
dpStyleEl.innerHTML = portrait;
}
// 由於 height 的響應速度比較慢,所以在加個延時處理橫屏的情況。
setTimeout(function(){
var height = docEl.getBoundingClientRect().height;
var landscape = "@media screen and (width: "+ height +"px) {html{font-size:"+ ((height/(_designWidth/_rem2px)/defaultFontSize)*100) +"%;}}"
var dlStyleEl = doc.getElementById('dlAdapt');
if(!dlStyleEl) {
dlStyleEl = document.createElement('style');
dlStyleEl.id = 'dlAdapt'
dlStyleEl.innerHTML = landscape;
docEl.firstElementChild.appendChild(dlStyleEl);
} else {
dlStyleEl.innerHTML = landscape;
}
},500);
}
// 延時,讓瀏覽器處理完viewport造成的影響,然后再計算root font-size。
setTimeout(function(){
refreshRem(designWidth, rem2px);
}, 1);
})(750, 100);
代碼比較多,有興趣的可以直接上github上找到源代碼(https://github.com/hbxeagle/rem/blob/master/HD_ADAPTER.md)
后記
這是一篇很早之前寫的總結了,今天又復習修改了一下,寫的有錯誤或者寫的不清楚的地方請大家多多指正。
這么多年過去,其實現在已經逐漸流行直接使用 vw vh 來做移動端適配了,因為隨着設備的更新兼容性的問題已經大大減少。但使用 rem 模式還是有一定需求的,畢竟vw還沒有全部兼容,可以參考vw兼容性。還有就是有pc瀏覽器打開並限制最大寬度的需求使用vw就不可以了。
后面有時間將寫寫利用 vw vh
來進行移動端適配的總結,會比這個簡單。
參考
歡迎到前端學習打卡群一起學習~516913974