通過前幾天寫的兩篇博客(淺談移動端三大viewport和移動端em和rem區別),我們現在來總結一下如何實現一個最佳方案。
之前在第二篇博客中提到過我們可以使用媒體查詢來針對不同設備及做適配,如下圖
但是這個可能不會太精准,比如我的設備布局viewport可能是370px,這樣只能使用320這一版本。而事實上,他們的viewport並不相同,所以他們的布局也得不一樣。也就是說我們如果用css媒體查詢只能說是可以用,但不是最佳實踐。其實通過看某些牛逼的移動端網站,可以看到他們的共同點,他們都是使用js方式來實現的。
可以看到,慕課網以及我們現在做的今日十大H5頁面,他們共同的地方就是在根html上設置data-dpr以及font-size。
為什么要設置這兩個東西呢?
我們可以回歸移動端的本質,就是根據不同的設備規划不同的布局,這里面有一點需要注意,就是任何分辨率下我們的字體要保持一致。所以data-dpr是針對字體設置的,font-size是我們不同設備上用rem的基准值。其實有一個規范的js文件可以幫我們解決以上問題,flexible.js(可以到github上copy)基本滿足我們的需求。
;(function(win, lib) { var doc = win.document; var docEl = doc.documentElement; var metaEl = doc.querySelector('meta[name="viewport"]'); var flexibleEl = doc.querySelector('meta[name="flexible"]'); var dpr = 0; var scale = 0; var tid; var flexible = lib.flexible || (lib.flexible = {}); if (metaEl) { console.warn('將根據已有的meta標簽來設置縮放比例'); var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/); 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); 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; } docEl.setAttribute('data-dpr', dpr); if (!metaEl) { 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) { width = 540 * dpr; } var rem = width / 10; 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(); 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'] = {}));
簡單的說一下這里面的重點吧,我們先設置頁面viewport中的scale,這個用來解決border 1px問題,下面是我的一個css demo
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <title>Document</title> <style> ul li { width: 100px; padding: 8px 0 8px 15px; color: #7c7c7c; position: relative; } ul li + li:before { position: absolute; top: -1px; left: 0; content: ''; width: 100%; height: 1px; border-top: 1px solid #ddd; /*transform: scaleY(0.5);*/ } </style> </head> <body> <ul> <li></li> <li></li> <li></li> </ul> </body> </html>
然后我們下面展示的樣式第一個是注釋掉 transform: scaleY(0.5)的,第二個是打開的。
為什么會這樣子呢,我在淺談移動端三大viewport說過,dpr為2的高清屏下,1個css像素=4個物理像素(1px = dpr²*dp)
所以,設計師想要的retina下border: 1px;
其實就是1物理像素寬,對於css而言,可以認為是border: 0.5px;
,這是高清屏下(dpr=2)下能顯示的最小單位,但並不是所有的瀏覽器都支持border:0.5px,所以我們需要scale(0.5),也就是上面flexible.js提到的scale,通過不同的dpr(devicePixelRatio)來設置不同的scale。
然后我們的主角rem是通過refreshRem函數中的docEl.getBoundingClientRect().width來獲取的,這個值跟document.body.clientWidth是相同的,也就是布局viewport,之后的width/10純屬是為了便於計算。等下,當我們在iphone5下查看布局viewport時,為什么是640,而不是320,
這是因為我們在前面設置了scale,在iphone5下,通過獲取dpr(devicePixelRatio)是2,然后計算出initial-scale是0.5,因為在initial-scale為1情況下,我們知道布局viewport等於設備寬度,也就是等於理想視口(screen.width),iphone5下的理想視口是320,然后縮放比是0.5,所以最終的布局viewport是640。移動端大神PPK曾經說initial-scale是理想視口與視覺視口之比,但我感覺既然設置了initial-scale,我們的視覺視口與布局視口就相等了,所以我們在計算時用的document.body.clientWidth,用window.innerWidth(視覺視口)其結果也一樣,只不過是為了給大家演示。最后根據我們計算出來的html的基值可以在css編碼中還原視覺稿的真實寬高。
比如我們拿到一個iphone6的高清設計稿(750×1334),在psd中量出一個div寬為750,如何轉換為rem單位呢?
公式—— rem = px / 基值
通過flexible.js計算根html的font-size為75px,所以我們可以這樣寫css
div {
width: 10rem;
}
轉換成html就是這樣
width: 10rem; // -> 750px
最后因為dpr為2,scale了0.5,所以手機屏幕上的真實寬度為375px,就剛剛好。試想一下,假如我們不用initial-scale=0.5,那樣我們得在這個設計稿上量完div后,再口算除以2,才會寫width=375px,想想就累。
最后說一下字體的設置吧,這個比較特殊。以下分別是我們現在做的項目(今日十大)中不同設備的字體展示,可以清楚地看到任何手機屏幕上字體大小都要統一。
當然為了方便,我們使用sass做了一次遍歷。在不同的data-dpr中遍歷font-size(這里我們就看到了之前html中data-dpr的作用了)