移動端的崛起,給了咱們前端更大的舞臺,與此同時,也給咱們帶來了一系列頭疼的問題,移動端適配就是其中之一,目前市面上最經常使用的方案便是REM適配。css
爲何說她是一個磨人的小妖精?由於她確實讓人又愛又恨,靈活的自適應佈局再搭配上css單位轉換工具,讓人愛不釋手;另外一方面,因爲移動端的機型和表現千奇百怪,想要達到完美的兼容又讓人頭疼。html
即便如此,依然阻止不了筆者對於她的癡迷。本文將會圍繞REM適配這一話題進行討論,同時也會將筆者我的的經驗以及本身目前在用的一套代碼分享給你們。另外,現在移動端的兼容性愈來愈好,所以衍生出了一些其餘的適配方案,這點不在本文的討論範圍以內。前端
const docEl = document.documentElement const metaEl = document.querySelector('meta[name="viewport"]') const maxWidth = window.__MAX_WIDTH__ || 750 const divPart = window.__DIV_PART__ || 15 const bodySize = window.__BODY_SIZE__ || 12 let scale = 1 let dpr = 1 let timer = null
initial-scale=0.5
,即二倍屏,假設根節點的font-size=100px,那麼0.01rem就是物理像素1px;而initial-scale=1.0
,雖然在css單位中,0.01rem=1px,但咱們知道,在二倍屏中,1px實際有4個物理像素。if (metaEl) { console.warn('根據已有的meta標籤來設置縮放比例') const match = metaEl.getAttribute('content').match(/initial-scale=([\d.]+)/) if (match) { scale = parseFloat(match[1]) dpr = parseInt(1 / scale) } } else { if (window.navigator.appVersion.match(/iphone/gi)) { dpr = parseInt(window.devicePixelRatio) || 1 scale = 1 / dpr } const newMetaEl = document.createElement('meta') newMetaEl.setAttribute('name', 'viewport') newMetaEl.setAttribute('content', `width=device-width, initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}, user-scalable=no`) docEl.firstElementChild.appendChild(newMetaEl) } // 設置根節點dpr docEl.setAttribute('data-dpr', dpr)
這裏要重點將一下爲何要區分安卓和IOS設備,不少人可能會說由於IOS有多倍屏。實際上,安卓也有多倍屏,那爲何咱們不考慮呢?git
function bodyLoaded (cb) { if (document.body) { cb && cb() } else { document.addEventListener('DOMContentLoaded', function () { cb && cb() }, false) } } // 窗口寬度改變時,刷新rem function refreshRem () { let width = docEl.clientWidth if (width / dpr > maxWidth) { width = maxWidth * dpr } // 設置根節點font-size window.remUnit = width / divPart docEl.style.fontSize = window.remUnit + 'px' bodyLoaded(() => { // 測試rem的準確性,若是和預期不同,則進行縮放 let noEl = document.createElement('div') noEl.style.width = '1rem' noEl.style.height = '0' document.body.appendChild(noEl) let rate = noEl.clientWidth / window.remUnit if (Math.abs(rate - 1) >= 0.01) { docEl.style.fontSize = (window.remUnit / rate) + 'px' } document.body.removeChild(noEl) }) } // 初始化 refreshRem() bodyLoaded(() => { document.body.style.fontSize = bodySize * dpr + 'px' document.body.style.maxWidth = maxWidth * dpr + 'px' })
refreshRem
函數是整個rem適配的核心,每次須要更新都會調用此函數,咱們還限定了頁面的最大寬度,能夠保證在pc端打開也能看到不錯的視覺效果。github
可是有一部分的安卓機,1rem並不等於根節點的font-size,舉個例子:html的font-size=20px,正常狀況下1rem也應該是20px,但在部分機型中,它多是22px或18px等等(筆者懷疑上文中提到的頁面寬度溢出也是這個問題)。所以,筆者加上了bodyLoaded這段代碼,在rem設置完成後,再與實際視覺上的1rem進行比較,若誤差超過1%,則認爲須要從新定義rem,這樣就能100%保證1rem就是咱們指望的大小。瀏覽器
window.addEventListener('resize', function () { clearTimeout(timer) timer = setTimeout(refreshRem, 200) }, false) // window.addEventListener('pageshow', function (e) { // if (e.persisted) { // refreshRem() // } // }, false)
這段代碼用於監聽resize事件,以此來從新計算根節點的font-size,定時器用來防止頻繁計算(實際上在手機中,也不會有頻繁觸發resize的機會,所以定時器也能夠不加)。有些讀者可能會問題,爲何不監聽橫豎屏事件(onorientationchange),其實沒有必要,橫豎屏切換本質也是resize的一種,咱們已經監聽了resize事件,這裏就沒有必要再次監聽了。app
那註釋掉的這段代碼是什麼意思呢?它是用來監聽瀏覽器返回,可是這段代碼在iPhone八、iPhoneX上會有問題,在返回的時候,咱們拿到的document.documentElement.clientWidth
是其實際的大小(沒有乘上設備像素比),所以整個頁面佈局都亂了。筆者通過深思熟慮,決定刪掉這段代碼,由於在返回的時候,會保留和離開時一摸同樣的狀態,沒有必要從新再計算一遍。iphone
window.px2rem = function (d) { let val = parseFloat(d) / window.remUnit if (typeof d === 'string' && d.match(/px$/)) { val += 'rem' } return val } window.rem2px = function (d) { let val = parseFloat(d) * window.remUnit if (typeof d === 'string' && d.match(/rem$/)) { val += 'px' } return val }
暴露全局函數,方便使用js來控制尺寸大小。函數
篇幅所限,樣式代碼就不在這裏貼了,感興趣能夠在這裏看:reset.css工具
這一套rem適配代碼是筆者平常開發中總結提煉出來,不能說是100%完美,可是也足夠適配市面上的主流機型了。再配合構建工具,自動轉換爲rem單位,省心又省力。
最後推薦一個好用的全局構建工具fle-cli,幫你從複雜繁瑣的構建配置中解放出來。
本文源碼地址:https://github.com/ansenhuang/axe/blob/master/packages/rem-resize/README.md