移動端頁面適配———多方案解析
在移動互聯網快速發展的今天,手機的種類和尺寸越來越多,作為前端的小伙伴們可能會越來越頭疼,但又不得不去適配一款又一款的新機型。對於移動端適配,不同的公司、不同的團隊有不同的解決方案。我在項目中也用了一部分解決方案,也看到了一些解決方案,對比下,總結一些自己的理解,希望對各位有幫助,找到最適合你們項目的適配方案。
下面是一些基礎概念的講解,幫助理解各種適配方案實現。
像素:
1、物理像素(設備像素)
屏幕的物理像素,又被稱為設備像素,他是顯示設備中一個最微小的物理部件。任何設備屏幕的物理像素出廠時就確定了,且固定不變的。
2、設備獨立像素
設備獨立像素也稱為密度無關像素,可以認為是計算機坐標系統中的一個點,這個點代表一個可以由程序使用的虛擬像素(比如說CSS像素),然后由相關系統轉換為物理像素。
3、設備像素比
設備像素比簡稱為dpr,其定義了物理像素和設備獨立像素的對應關系
設備像素比 = 物理像素 / 設備獨立像素
以iphone6為例:
iphone6的設備寬和高為375pt * 667pt,可以理解為設備的獨立像素,而其設備像素比為2.固有設備像素為750pt * 1334pt
通過:window.devicePixelRatio獲得。
設備像素比是區別是否是高清屏的標准,dpr大於1時就為高清屏,一般情況下dpr為整數,但是android有些奇葩機型不為整數。
4、css像素
在CSS、JS中使用的一個長度單位。單位px
注:在pc端1物理像素等於1px,但是移動端1物理像素不一定等於1px,1物理像素與px的關系與以下因素有關。(有些視口概念,可以把下面視口看完了再來看)
1、屏幕布局視口大小(下面會講到)
2、屏幕的分辨率(物理像素)
對於一塊屏幕,其物理像素是確定的。視覺視口尺寸是繼承的布局視口的,而視覺視口里寬度即是css的px數。故在一塊屏上物理像素與px的關系就是物理像素與布局視口的px數的關系。
比如iphone6,期物理像素為750,如果沒有設置布局視口時,viewport為980px
此時:1物理像素長度等於980/750px = 1.3067px的長度
由於像素都是點陣的,故1物理像素相當於1.3067px * 1.3067px方格。
當在meta中設置了如下配置時
<meta name="viewport" content="width=device-width">
相當於把布局視口設置為設備的寬度(即上面講到的設備獨立像素), 對於iphone6就是375px。
此時1物理像素長度等於375/750px = 0.5px的長度,故1物理像素相當於0.5px * 0.5px的方格。
視口:
1、布局視口:
在html中一般在meta中的name為viewport字段就是控制的布局視口。布局視口一般都是瀏覽器廠商給的一個值。在手機互聯網沒有普及前,網絡上絕大部分頁面都是為電腦端瀏覽而做的,根本沒有做移動端的適配。隨着移動端的發展,在手機上看電腦端的頁面已成為非常普及現象。而電腦端頁面寬度較大,移動端寬度有限,要想看到整個網頁,會有很長的滾動條,看起來非常麻煩。於是瀏覽器廠商為了讓用戶在小屏幕下網頁也能夠顯示地很好,所以把布局視口設置的很大,一般在768px ~ 1024px 之間,最常用的寬度就是 980。這樣用戶就能看到絕大部分內容,並根據具體內容選擇縮放。
故布局視口是看不見的,瀏覽器廠商設置的一個固定值,如980px,並將980px的內容縮放到手機屏內。
布局視口可以通過:
document.documentElement.clientWidth(clientHeight) // 布局視口的尺寸。
2、視覺視口:
瀏覽器可視區域的大小,即用戶看到的網頁的區域。(其寬度繼承的布局視口寬度)
window.innerWidth(innerHeight) // 視覺視口尺寸
3、理想視口:
布局視口雖然解決了移動端查看pc端網頁的問題,但是完全忽略了手機本身的尺寸。所以蘋果引入了理想視口,它對設備來說是最理想的布局視口,用戶不需要對頁面進行縮放就能完美的顯示整個頁面。最簡單的做法就是使布局視口寬度改成屏幕的寬度。
可以通過window.screen.width獲取。
<meta name="viewport" content="width=device-width">
移動端到底怎么適配不同的屏幕呢?最簡單的方法是設置如下視口:
<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
當使用以上方案定義布局視口時,即布局視口等於理想視口(屏幕寬度),屏幕沒有滾動條,不存在高清屏下,字體較小的問題。但是在不同屏幕上,其視覺寬度是不同的,不能簡單的將所有的尺寸都設置為px,可能會出現滾動條。小尺寸的可以用px,大尺寸的只能用百分比和彈性布局。
viewport縮放
對於上面的設置,再不同的屏幕上,css像素對應的物理像素具數是不一致的。
在普通屏幕下,dpr=1時,
1個css像素長度對應1個物理像素長度,1個css像素對應1個物理像素。
而在Retina屏幕下,如果dpr=2,
1個css像素長度對應2個物理像素長度,1css像素對應4個物理像素。
此時如果css中寫
border: 1px solid red; // 此時1px 對應的寬度是2物理像素的寬度。
而一般現在移動端設計稿都是基於iphone設計的,稿子一般為750px或640px,這正好是iphone6和iphone5的物理像素。在設計稿中,一般有些邊框效果,這時邊框的線寬為1px,對應的就是1物理像素。而對於iphone5和iphone6,當width=device-width時,css的1px顯示出來的是2個物理像素,所以看起來線就比較粗。怎么解決呢?1px邊框效果其實有很多hack方法,其中一種就是通過縮放viewport。
initial-scale是將布局視口進行縮放,initial-scale是相對於理想視口的,即initial-scale=1與width=device-width是一樣的效果。initial-scale=0.5等效於width= 2倍的device-width,所以設置initial-scale和width都可以改變布局視口的大小。
<meta name="viewport" content="width=device-width,initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">
對於iphone6當添加如上設置后,initial-scale=0.5,即將頁面縮小2倍后等於屏幕寬度。
布局視口width:
width / 2 = 375px; width = 750px;
所以此時布局視口為750px,此時1px等於1物理像素。
適配方案:
上面講了一些基礎概念,下面講具體適配。
對於ui設計師給的一張設計稿,怎么將其還原到頁面上?對於不同手機屏幕,其dpr不同,屏幕尺寸也不同,考慮到各種情況,有很多適配方案,所以不同的適配方案,實現方法不同,處理復雜度也不同,還原程度也不同。
方案一:
固定高度,寬度自適應。
這種方案是目前使用較多的方案,也是相對較簡單的實現方案:
該方法使用了理想視口:
<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
垂直方向使用固定的值,水平方向使用彈性布局,元素采用定值、百分比、flex布局等。這種方案相對簡單,還原度也非常低。
方案二:
固定布局視口寬度,使用viewport進行縮放
荔枝的代碼:
if(/Android (\d+\.\d+)/.test(navigator.userAgent)){
var version = parseFloat(RegExp.$1);
if(version>2.3){
var phoneScale = parseInt(window.screen.width)/640;
if(/MZ-M571C/.test(navigator.userAgent)){
document.write('<meta name="viewport" content="width=640, minimum-scale = 0.5, maximum-scale= 0.5">');
}else if(/M571C/.test(navigator.userAgent)&&/LizhiFM/.test(navigator.userAgent)){
document.write('<meta name="viewport" content="width=640, minimum-scale = 0.5, maximum-scale= 0.5">');
}else{
document.write('<meta name="viewport" content="width=640, minimum-scale = '+ phoneScale +', maximum-scale = '+ phoneScale +', target-densitydpi=device-dpi">');
}
}else{
document.write('<meta name="viewport" content="width=640, target-densitydpi=device-dpi">');
}
}else{
document.write('<meta name="viewport" content="width=640, user-scalable=no, target-densitydpi=device-dpi">');
}
網易應用:
var win = window,
width = 640,
iw = win.innerWidth || width,
ow = win.outerHeight || iw,
sw = win.screen.width || iw,
saw = win.screen.availWidth || iw,
ih = win.innerHeight || width,
oh = win.outerHeight || ih,
ish = win.screen.height || ih,
sah = win.screen.availHeight || ih,
w = Math.min(iw, ow, sw, saw, ih, oh, ish, sah),
ratio = w / width,
dpr = win.devicePixelRatio;
if (ratio = Math.min(ratio, dpr), 1 > ratio) {
var ctt = ",initial-scale=" + ratio + ",maximum-scale=" + ratio,
metas = document.getElementsByTagName("meta");ctt += "";
for (var i = 0, meta; i < metas.length; i++) meta = metas[i], "viewport" == meta.name && (meta.content += ctt)
}
固定布局視口,寬度設置固定的值,總寬度為640px,根據屏幕寬度動態生成viewport。(設計稿應該是640px的)
<meta name="viewport" content="width=640, minimum-scale = 0.5625, maximum-scale = 0.5625, target-densitydpi=device-dpi">
這種方式布局如荔枝FM的網頁寬度始終為640px。縮放比例scale為:
var scale = window.screen.width / 640
設計稿為640px時,正好可以1:1以px來寫樣式。但是1px所對應的物理像素就不一定是1了。
(window.screen.width * dpr) / 640 // 1px對應的物理像素


方案三:
根據不同屏幕動態寫入font-size,以rem作為寬度單位,固定布局視口。
如網易新聞:
<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
以640px設計稿和750px的視覺稿,網易這樣處理的:
var width = document.documentElement.clientWidth; // 屏幕的布局視口寬度
var rem = width / 7.5; // 750px設計稿將布局視口分為7.5份
var rem = width / 6.4; // 640px設計稿將布局視口分為6.4份
這樣不管是750px設計稿還是640px設計稿,1rem 等於設計稿上的100px。故px轉換rem時:
rem = px * 0.01;
在750px設計稿上:
75px 對應 0.75rem, 距離占設計稿的10%;
在ipone6上:
width = document.documentElement.clientWidth = 375px;
rem = 375px / 7.5 = 50px;
0.75rem = 37.5px; (37.5/375=10%;占屏幕10%)
在ipone5上:
width = document.documentElement.clientWidth = 320px;
rem = 320px / 7.5 = 42.667px;
0.75rem = 32px; (32/320=10%;占屏幕10%)
故對於設計稿上任何一個尺寸換成rem后,在任何屏下對應的尺寸占屏幕寬度的百分比相同。故這種布局可以百分比還原設計圖。


方案四:
以rem作為寬度單位,動態寫入viewport和font-size進行縮放。
根據設置的dpr設置font-size。如:
document.documentElement.style.fontSize = 50 * dpr;
// dpr 為設置的設備像素比。(注意不是設備自身的設備像素比,而是認為設置的dpr)
這種情況下,dpr = 1時,1rem = 50px;
dpr = 2時, 1rem = 100px;
當設計以iphone6為標准,出750px的設計稿時,此時dpr=2,故1rem 等於100px,將圖上的尺寸轉換為rem非常方便,除以100就行。代碼如下:
var scale = 1.0;
var dpr = 1;
var isAndroid = window.navigator.appVersion.match(/android/gi);
var isIPhone = window.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = window.devicePixelRatio;
// 此處只簡單對ios做了伸縮處理,安卓沒有做伸縮處理,統一dpr = 1
if ( isIPhone ) {
scale /= devicePixelRatio;
dpr *= devicePixelRatio;
}
var viewport = document.getElementById('viewport');
var content = 'initial-scale=' + scale + ', maximum-scale=' + scale + ',minimum-scale=' + scale + ', width=device-width, user-scalable=no';
viewport.setAttribute( 'content', content );
document.documentElement.style.fontSize = 50 * dpr + 'px';
document.documentElement.setAttribute('data-dpr', dpr);
對於該方案,
假設肉眼看到的寬度(視覺寬度):visualWidth,令dpr=1時,其1rem對應的寬度為50.
dpr = 1 時, 1rem = 50px, initial-scale=1, 縮放為1。
visualWidth = 50 * 1 = 50;
dpr = 2 時, 1rem = 100px, initial-scale=0.5, 縮放為0.5。
visualWidth = 100 * 0.5 = 50;
dpr = 3 時, 1rem = 150px, initial-scale=0.3333, 縮放為0.3333。
visualWidth = 150 * 0.3333 = 50;
所以該方案,1rem在所有屏幕上對應的肉眼距離相同,故不同屏幕下,總的rem數不同,大屏下總的rem數大於小屏下,如iphone6下,總寬度為7.5rem,iphone5下,總寬度為6.4rem。故此方案不能百分比還原設計稿,故寫樣式時,對於大塊元素應該用百分比,flex等布局,不能直接用rem。
關於這個方案的詳細教程請參考這篇文章傳送門


方案五:
根據不同屏幕動態寫入font-size和viewport,以rem作為寬度單位
將屏幕分為固定的塊數10:
var width = document.documentElement.clientWidth; // 屏幕的布局視口寬度
var rem = width / 10; // 將布局視口分為10份
這樣在任何屏幕下,總長度都為10rem。1rem對應的值也不固定,與屏幕的布局視口寬度有關。
對於動態生成viewport,他們原理差不多,根據dpr來設置縮放。看看淘寶的:
var devicePixelRatio = window.devicePixelRatio;
var isIPhone = window.navigator.appVersion.match(/iphone/gi);
var dpr,scale;
if (isIPhone) {
if (devicePixelRatio >=3) {
dpr = 3;
} else if (devicePixelRatio >=2) {
dpr = 2;
} else {
dpr = 1;
}
} else {
dpr = 1;
}
scale = 1 / dpr;
淘寶只對iphone做了縮放處理,對於android所有dpr=1,scale=1即沒有縮放處理。
此方案與方案三相似,只是做了viewport縮放,能百分比還原設計稿。


適配中要解決的問題 :
移動端適配最主要的是使在不同屏幕下不用縮放頁面就能正常顯示整個頁面。以上方案都完成了這一需求。其次有幾個需求:
1、解決高清屏下1px的問題,其實有很多hack方法,這里只講了縮放視口。先將布局視口設置為高清屏的物理像素。這樣css中1px就是1個物理像素,這樣看到的線條才是真正的1px。但是此時視口寬度大於設備的寬度,就會出現滾動條。故對視口進行縮放,使視口寬度縮放到設備寬度。
淘寶團隊在處理安卓端的縮放存在很多問題,所以dpr都做1處理,所以安卓端就沒有解決1px的問題。
2、在大屏手機中一行看到的段落文字應該比小屏手機的多。
由於淘寶和網易新聞rem都是百分比,故如果用rem一行顯示的文字個數應該是相同的。故對於段落文本不能用rem作為單位,應該用px處理,對於不同的dpr下設置不同的字體。
.selector {
color: red;
font-size: 14px;
}
[data-dpr="2"] .selector {
font-size: 28px; // 14 * 2
}
[data-dpr="3"] .selector {
font-size: 42px; // 14 * 3
}
對於方案四,不管什么情況下,1rem對應的視覺上的寬度都是一樣的,而對應的大屏、小屏手機其視覺寬度當然不同,故字體設置為rem單位時,也能滿足大屏手機一行顯示的字體較多這個需求。
五種方案對比:
上面四種方案對設計稿還原程度是有差別的。
除了方案一和方案四以外,其他方案都是百分比還原設計稿,大屏下元素的尺寸就大。
方案一還原設計稿程度較低,這里不做說明。
方案二做了百分比適配,部分1px適配,沒有字體適配。
方案三做了百分比適配,沒有1px適配,有字體大小適配。
方案四沒有做百分百適配,布局要用百分百和flex布局,做了1px的適配,並且對於段落文字直接可以用rem做單位,不需要做適配。
方案五做了百分比適配,有1px適配,有字體大小適配。
項目中遇到的問題:
在我們項目中方案四和方案五都用過。
方案五在使用中沒有遇到什么問題,就是剛開始沒有做字體適配都是用的rem,后面加入了字體適配,這種方案設計師相對輕松些,不用考慮在大小屏幕下的布局效果。
方案四時沒有跟ui設計師溝通清楚,導致設計師在設計圖上一行排了很多交互元素,在小屏下放不下去,又不能簡單放百分比(元素里的文字放不下)。所以還是要做動態判斷大小屏,做出相應適配。這個方案可能設計師需要考慮的多些,盡量減少一行內的交互元素,當一行交互元素多時要考慮小屏手機怎么適配。
其實對於1px的適配在蘋果端很好,在android端各個廠商手機差別太大,適配有很多問題。這是為什么絕大多數方案里都放棄了android端1px適配。不過最近看到很多網站都用了densitydpi=device-dpi這個安卓的私有屬性來兼容部分安卓機型,這個屬性在新的webkit已經被移除了,使用它主要為了兼容低版本的android系統。
這里大漠老師針對flexible方案進行了改版,兼容了更多的android機型的1px效果。文章傳送門
他給了個壓縮版的方案,我看了下源碼,把它寫了一遍,不知道有沒有問題,效果是一樣的。
var dpr, scale, timer, rem;
var style = document.createElement('style');
dpr = window.devicePixelRatio || 1;
scale = 1 / dpr;
document.documentElement.setAttribute('data-dpr', dpr);
var metaEl = document.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'target-densitydpi=device-dpi, initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
document.documentElement.firstElementChild.appendChild(metaEl);
document.documentElement.firstElementChild.appendChild(style);
if (980 === document.documentElement.clientWidth) {
metaEl.setAttribute('content', 'target-densitydpi=device-dpi,width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1');
}
function refreshRem () {
var c = '}';
var width = document.documentElement.clientWidth;
var isPhone = window.navigator.userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile/i);
if (!isPhone && width > 1024) {
width = 640;
c = 'max-width:' + width + 'px;margin-right:auto!important;margin-left:auto!important;}';
}
window.rem = rem = width / 16;
style.innerHTML = 'html{font-size:' + rem + 'px!important;}body{font-size:' + parseInt(12 * (width / 320)) + 'px;' + c;;
}
refreshRem();
window.addEventListener('resize', function () {
clearTimeout(timer);
timer = setTimeout(refreshRem, 300);
}, false);
window.addEventListener('pageshow', function (e) {
if (e.persisted) {
clearTimeout(timer);
timer = setTimeout(refreshRem, 300);
}
}, false);
這些方案只是針對絕大部分機型,項目中可能有些特殊機型有特殊問題,需要特殊對待。比如在這篇文章中作者使用flexible在小米max和榮耀8中有問題,需要特殊hack。傳送門,我沒有這種手機,也沒有對此做驗證。
對於上面的五種方案,方案五看似是適配最好的,但是當項目中引入第三方插件時可能要一一適配,比如:引入一個富文本,里面設置字體大小的一般都是px,你需要將其一一轉換成rem。而對於方案二,可以直接用px做單位來百分百還原設計稿,引入的插件時也不用適配。所以說,具體項目中用哪個方案,其實不光是前端的選擇,還要跟設計師討論下,滿足設計需求,選擇最適合項目的方案。
以上是個人對於移動端適配的一些理解,如有不對歡迎指正。
參考文章: