REM,你這磨人的小妖精!

前言

移動端的崛起,給了咱們前端更大的舞臺,與此同時,也給咱們帶來了一系列頭疼的問題,移動端適配就是其中之一,目前市面上最經常使用的方案便是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
複製代碼
  • 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) || 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

  • 有些安卓機的設備像素比很奇怪,好比2.五、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.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來控制尺寸大小。函數

CSS重置樣式

篇幅所限,樣式代碼就不在這裏貼了,感興趣能夠在這裏看:reset.css工具

總結

這一套rem適配代碼是筆者平常開發中總結提煉出來,不能說是100%完美,可是也足夠適配市面上的主流機型了。再配合構建工具,自動轉換爲rem單位,省心又省力。

最後推薦一個好用的全局構建工具fle-cli,幫你從複雜繁瑣的構建配置中解放出來。

本文源碼地址:github.com/ansenhuang/…

相關文章
相關標籤/搜索