谈: 移动端适配Rem到底是怎么回事?

1,911 阅读5分钟

前言

最近几年都是基于PC在开发,突然要搞移动端有点不适应,主要还是处理各终端的适配.我们通常开发的页面在PC浏览器上显示正常,但是如果放在窄屏设备(移动设备)上,会在比屏幕宽的虚拟窗口或者视口中呈现页面,以便让用户可以一次性看到所有内容,用户可以通过平移缩放查看不同区域,这是没有进行移动优化的页面,它的体验是很不好的,或者说看起来很差.

后来,使用媒体查询,进行一些优化.但仍然存在问题,比如无法或者说很难做到更细的窗口变化体验.只能是一个区间来控制.

meta和viewport

简单的说viewport就是浏览器的窗口宽度高度.但是在移动端就变得复杂了,移动端的viewport宽度高度都变小,做布局需要用到两个viewport: viewportvisualviewport(虚拟的viewport)和viewportlayoutviewport(布局的viewport).

我们在针对移动设备优化的网站需要有以下内容:

<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />

这里的width与height来控制视口的大小.可以将它设置为特定的像素(例如width = 666).这边设置为device-width(也有device-height),表示CSS像素的屏幕宽度,缩放比例为100%.initial-scale控制页面首次加载是的缩放级别,maximum-scale,minimum-scale和user-scalable属性控制用户如何/是否允许缩放页面。

比如我们平时开发UI给的设计图一般按照iphone6的尺寸也就是 375.这时候我们可以设置width=375, initial-scale=1.这样子可以精确的适应这个尺寸的设备.但是如果有其他的尺寸的话,就会出现问题...这里是各种设备的尺寸参考

rem or em

em是一个长度单位.表示相对于父级文本的字体尺寸.如果父级字体没有设置尺寸,那就相对于浏览器默认尺寸font-size: 16px.它的值不是固定的,可以继承父级字体尺寸.

元素的width/height/line-height/padding/margin用em为单位都是是相对于该元素的font-size

.div1 {font-size: 16px;}
.div2 {font-size: 2em;height: 2em;}
.div3 {font-size: 2em;}

<div class="div1">   // font-size: 16px;
    <div class="div2">1</div> // font-size: 32px;height: 64px;
    <div class="div3">1</div> // font-size: 32px;
</div>

rem是css3的相对单位.它设置的字体元素相对的是HTML根元素.也就是说我们设置html页面跟元素的font-size, 那项目里的其他节点使用rem这个单位去换算,都会依照这个根元素的值.

其实它就是基于宽度的等比缩放.比如我们将屏幕平分100份,那我们如果知道屏幕宽度,然后去换算出来1份的宽度.然后我们开发中每次设置元素的大小都基于这个比例,那我们只需要关注屏幕宽度就可以了.换句话说,我们通过js去拿到屏幕宽度,然后去换算出每一份的值, 把这个值作为根节点的font-size,不就解决问题了.

实际的是,浏览器字体最小是12px, 如果100份,那换算出来只有个位数大小.浏览器不支持了,我们一般都换算10份.

这里还涉及一个概念,就是设备像素比(device pixel ratio),它是用物理像素/设备独立像素得到的.在Javascript中可以通过window.devicePixelRatio获取当前设备的dpr.我们可以通过dpr设置body的font-size来支持项目中的字体适配em这个单位.

css:
html {
    font-size: 37.5px; /* 375/10 */
}
body {
    font-size: 24px; /* 12 * dpr 修正字体大小 */
    width: 10rem;
}
.p1 {
    width: 5rem;
    height: 5rem;
    font-size: 1.2em; /* 字体使用em */
}
html:
...

<body>
    <div class="p1">
        <div class="s1"></div>
    </div>
</body>
...
js:
var doc = document.documentElement;

function callback() {
    var rem = docEl.clientWidth / 10
    docEl.style.fontSize = rem + 'px'
    if (document.body) {
      // 设置全局body为当前dpr * 12px
      document.body.style.fontSize = (12 * dpr) + 'px'
    }
}

document.addEventListener('DOMContentLoaded', callback);
window.addEventListener('resize', callback)

上面的例子可以使用rem做移动端优化,但是字体使用的是em.原因就是设置根节点字体大小,会影响所以字体大小,字体大小会继承.总不能每个字体都设置字体大小...还有就是大屏用户可以选择要更大的字体还是更多的内容,如果使用rem,那就限制了用户的选择.所以在开发中,图片盒子宽度这些使用rem, 字体大小使用em.

vue项目如何使用

在vue项目中其实有比较成熟的方案了.lib-flexiblePostcss(pxtorem).前者所做的事情就是去获取屏幕并换算出合适的根font-size值,后者顾名思义,就是把px转为rem,方便我们在项目中可以放心使用px这个单位开发.这里是源码位置:lib-flexible,Postcss(pxtorem)

使用起来很简单只需要:

npm i amfe-flexible --save-dev
npm i postcss postcss-pxtorem --save-dev

然后在项目中分别引入:

main.js

import "amfe-flexible";

vue.config.js
css: {
    loaderOptions: {
      postcss: {
        plugins: [
          require("postcss-pxtorem")({
            rootValue: 37.5,
            propList: ["*"], //作用于哪些标签
          })
        ]
      }
    }
}

这样就可以了.至于amfe-flexiblepxtorem具体原理,可以去看源码.下面贴一部分amfe-flexible的代码,其实很简单:

  var docEl = document.documentElement
  var dpr = window.devicePixelRatio || 1

  // adjust body font size
  function setBodyFontSize () {
    if (document.body) {
      // 设置全局body为当前dpr * 12px
      document.body.style.fontSize = (12 * dpr) + 'px'
    }
    else {
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
  }
  setBodyFontSize();

  // set 1rem = viewWidth / 10
  function setRemUnit () {
    var rem = docEl.clientWidth / 10
    docEl.style.fontSize = rem + 'px'
  }

  setRemUnit()

  // reset rem unit on page resize
  window.addEventListener('resize', setRemUnit)
  window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
      setRemUnit()
    }
  })
  
  if (dpr >= 2) {
    var fakeBody = document.createElement('body')
    var testElement = document.createElement('div')
    testElement.style.border = '.5px solid transparent'
    fakeBody.appendChild(testElement)
    docEl.appendChild(fakeBody)
    if (testElement.offsetHeight === 1) {
      docEl.classList.add('hairlines')
    }
    docEl.removeChild(fakeBody)
}

对于setBodyFontSize这个方法,官方的解释在这里.,个人觉得用来设置字体的单位em倒是也可以.

pxtorem作为一个plugin,可想而知它做的事情就是基于webpack而完成的,对代码解析,然后把项目开发时的px转换为rem.

总结

一个项目做完,总得有所收获.移动端适配也算是web开发的一个经典难题,从开始到解决这个过程也是挺艰辛的,而我们现在能站在巨人的肩膀上去做事,能做的就是了解这个历程,这样能让我们有很多收获与新的感悟.

尽管目前来说flexible不更新维护了,这边的目的在于搞清楚它到底做了什么.毕竟现在浏览器对rem是挺友好的.等后续vw/vh可以完美支持了,可以修改方案.

加油.