REM,你这磨人的小妖精!
前言
移動端的崛起,給了我們前端更大的舞臺,與此同時,也給我們帶來了一系列頭疼的問題,移動端適配就是其中之一,目前市面上最常用的方案即是REM適配。
為什么說她是一個磨人的小妖精?因為她確實讓人又愛又恨,靈活的自適應布局再搭配上css單位轉換工具,讓人愛不釋手;另一方面,由于移動端的機型和表現千奇百怪,想要達到完美的兼容又讓人頭疼。
即使如此,依然阻止不了筆者對于她的癡迷。本文將會圍繞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__ || 12let scale = 1 let dpr = 1 let timer = null 復制代碼- metaEl:抓取現有viewport,以支持使用者自定義頁面實際縮放比例,通過設置viewport可以實現視覺上的實際物理像素。例如initial-scale=0.5,即二倍屏,假設根節點的font-size=100px,那么0.01rem就是物理像素1px;而initial-scale=1.0,雖然在css單位中,0.01rem=1px,但我們知道,在二倍屏中,1px實際有4個物理像素。
- maxWidth:UI稿寬度,一般以iphone6為基準,即750。
- divPart:將設備寬度劃分為多少份,上述代碼中,750/15=50,意思是750寬度的屏幕,1rem=50px,劃分多少份實際上沒有固定規定,看個人習慣。
- bodySize:初始化時,設置body的字體大小。
- scale、dpr分別是頁面縮放比例、設備像素比。
初始化設置
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) || 1scale = 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有多倍屏。實際上,安卓也有多倍屏,那為什么我們不考慮呢?
- 有些安卓機的設備像素比很奇怪,比如2.5、3.8等一些奇怪的數字;
- 部分安卓機表現很奇怪,比如頁面寬度比屏幕寬度多一點,出現橫向滾動條(具體原因不詳,已排除所有css干擾),兼容起來成本太高。
核心代碼
function bodyLoaded (cb) {if (document.body) {cb && cb()} else {document.addEventListener('DOMContentLoaded', function () {cb && cb()}, false)} }// 窗口寬度改變時,刷新rem function refreshRem () {let width = docEl.clientWidthif (width / dpr > maxWidth) {width = maxWidth * dpr}// 設置根節點font-sizewindow.remUnit = width / divPartdocEl.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.remUnitif (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端打開也能看到不錯的視覺效果。
但是有一部分的安卓機,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事件,這里就沒有必要再次監聽了。
那注釋掉的這段代碼是什么意思呢?它是用來監聽瀏覽器返回,但是這段代碼在iPhone8、iPhoneX上會有問題,在返回的時候,我們拿到的document.documentElement.clientWidth是其實際的大小(沒有乘上設備像素比),因此整個頁面布局都亂了。筆者經過深思熟慮,決定刪掉這段代碼,因為在返回的時候,會保留和離開時一摸一樣的狀態,沒有必要重新再計算一遍。
工具函數
window.px2rem = function (d) {let val = parseFloat(d) / window.remUnitif (typeof d === 'string' && d.match(/px$/)) {val += 'rem'}return val }window.rem2px = function (d) {let val = parseFloat(d) * window.remUnitif (typeof d === 'string' && d.match(/rem$/)) {val += 'px'}return val } 復制代碼暴露全局函數,方便使用js來控制尺寸大小。
CSS重置樣式
篇幅所限,樣式代碼就不在這里貼了,感興趣可以在這里看:reset.css
總結
這一套rem適配代碼是筆者日常開發中總結提煉出來,不能說是100%完美,但是也足夠適配市面上的主流機型了。再配合構建工具,自動轉換為rem單位,省心又省力。
最后推薦一個好用的全局構建工具fle-cli,幫你從復雜繁瑣的構建配置中解放出來。
本文源碼地址:github.com/ansenhuang/…
總結
以上是生活随笔為你收集整理的REM,你这磨人的小妖精!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Egret 生成 自带EUI 的微信小游
- 下一篇: python requests用法总结