REM,你這磨人的小妖精!

連城發表於2018-06-19

前言

移動端的崛起,給了我們前端更大的舞臺,與此同時,也給我們帶來了一系列頭疼的問題,移動端適配就是其中之一,目前市面上最常用的方案即是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__ || 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有多倍屏。實際上,安卓也有多倍屏,那為什麼我們不考慮呢?

  • 有些安卓機的裝置畫素比很奇怪,比如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.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端開啟也能看到不錯的視覺效果。

但是有一部分的安卓機,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.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/…

相關文章