谈谈rem与vw — rem
写这篇文章的原因,源于我在头条的面试。面试官问到了关于手机端适配rem的问题,这个问题非常有意思,想和大家分享一下:
Q:能谈谈rem的作用吗,与em有什么区别?
A: rem 是html 的font-size大小,一般为 16px, em 是父节点的 font-size 大小
Q: 我们为什么要用rem去做手机的适配?如果rem只是根节点字体大小的话,那么rem 其实和px 、em没有区别,rem解决了什么问题?
我之前确实做过几个简单的手机H5页面,只是在使用淘宝的lib-flexible方案,只觉得rem是解决方案的结果,但是从没想过为什么要使用rem来做手机端的适配。
当我再次去看淘宝的lib-flexible的时候,发现此方案已经不被推荐了,取而代之的是《如何在Vue项目中使用vw实现移动端适配》,于是想借此机会将 rem 与 vw 一起总结一下。
手机端适配模式
一些基本概念
具体内容可以查看《使用Flexible实现手淘H5页面的最终适配》,在这里,我只对其中的一些概念谈一谈自己的理解。
viewport
简单来说,viewport严格等于浏览器视窗大小(不包含滚动条),但是在移动设备上有些复杂,分为 visualviewport 与 layoutviewport。这里不做详细解释,可以查看《viewports剖析》来了解详细内容,看完这篇文章,你会明白为什么在html头部 emmet 会自动帮你生成
<meta name="viewport" content="width=device-width, initial-scale=1.0">
设备独立像素(虚拟像素)
设备独立像素是软件概念上的一个像素,一个设备独立像素可以对应多个物理像素,对应关系由相关系统控制(我们不用管),CSS像素(即写在css中的1px)就是一种设备独立像素。
例如苹果是Retina屏,1px(虚拟像素) = 4px(物理像素,dpr=2)
盗一张手淘的图:
dpr
dpr实际上是一个比值,标识一个虚拟像素对应几个物理像素。正常屏幕1个 CSS 像素对应1个物理像素,dpr=1,Retina 屏幕1个 CSS 像素对应 n^2 个物理像素,dpr 为 n。例如上面那张图 dpr 就是2,因为一个 CSS 像素对应 4 个物理像素, 长对应2个,宽对应2个。
rem
rem 简单来说是 html 的 font-size 大小,一般默认为 16px,与 em 的父节点 font-size 大小不同。一般 rem 方案解决小屏幕适配问题就是通过 JavaScript 动态改变 html 元素的 font-size 大小进行适配(组件使用 rem 作为度量)
lib-flexible
lib-flexible是淘宝团队推出的一种 rem 适配方案,它帮我们解决了不同屏幕如何设置 html 的 font-size 大小与 dpr 的问题。
可以看到 lib-flexible 里面有两个版本, master 里面用正则匹配 IOS 端和 Android 端,而Branch 2.0 版本则取消了对于 dpr 的适配,具体我会在下面进行介绍。
rem 解决了什么问题
首先要明确的一点是 data-dpr 和 rem 解决的是两个维度的问题。 Rem 解决的是移动端适配的问题, data-dpr 是在解决了移动端适配问题的基础上用于提升视觉效果。
在 Web App 上面,我们需要禁止页面缩放。
可以试着打开一下手机淘宝页面,你会发现你无法将这个页面放大。
考虑一个问题,你需要为一个 375px 大小的屏幕做页面,页面充满整个屏幕, 里面有一个 200px 大小的弹窗,你需要如何去实现它?
.wrapper {
width: 375px;
.dialog {
width: 200px;
}
}
如果这么做就 GG 了,当这个页面放到大一些的屏幕上(例如 414px的屏幕),那么显示出来的效果会出现留白。 414 是 iPhone * Plus 的尺寸,如果放到 ipad 上面那么留白会更大。
我们如何解决这个问题呢?
如果能够有一种动态的修改元素的长度就好了。
rem 就是这样一种原理,我们的组件使用 rem 进行编写,然后使用 JavaScript 动态设置 rem 到 px 的映射关系,这样就可以实现对于不同手机页面的适配了。
上 lib-flexible 源码
// 经过精简
var docEl = document.documentElement;
function setRemUnit () {
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'
}
具体 rem 的计算我会在下面介绍,核心代码实际上就是为 html 元素设置一个 font-size 而已。
lib-flexible 设置 dpr 又是为了什么?
为了解决“极致的1px”问题。
再次把这张图放一下:
在 Retina 屏幕上面,1个 CSS 像素实际上对应着4个物理像素(dpr=2的情况)。当我们在屏幕上画1px 的线时,Retina 屏幕实际上是2个物理像素宽。我们要实现“极致的1px”就是希望在Retina屏幕上画出一条1个物理像素宽的细线。
原理其实也很简单,对于 dpr=2的设备,设置 rem 的值为正常值的 2 倍,将需要画出“极致的线”的地方使用1px表示,再将页面缩小2倍,这样1px的线就变成了“0.5px”,实际上为1个物理像素来表示。
同理,对于 dpr=n 的设备,将 rem 值设为正常的 n 倍,在对页面缩放 n 倍。
现在的 lib-flexible 有两个版本,master 版本和 2.0 版本。在 master 版本中使用 正则来判断系统种类(IOS/Andorid),但是只对 IOS 系统做了 dpr 的适配,对于 Android 手机,统一设置 dpr =1;
实际上使用 rem 配合 dpr 缩放的方式有非常多的问题。最明显的例子就是安卓机 dpr 的混乱,例如 VIVO 的某款手机甚至出现了 dpr 为小数的情况(上文我们介绍到,1个 CSS 像素对应多少个物理像素 dpr 就是几,显然不可能出现小数位的情况),所以使用缩放来实现“极致的1px”兼容性并不是很好。
有团队的做法是将 dpr 设置有问题的机型进行上报,然后收集成一个白名单,再根据白名单在 lib-flexible 对于有问题的机型进行单独适配,然而这个工作量一般的小团队没有精力去做……
所以我们可以看到 2.0 版本已经舍弃了这种做法。
// 进行了精简
// detect 0.5px supports
var docEl = document.documentElement;
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)
}
可以看到这个版本对于设备进行了一个测试,如果该设备支持 0.5px 的书写方式,那么就在 html
元素上面添加一个 hairlines
的类,我们在使用的时候只需要在header
中添加.hirlines div {border-width: 0.5px}
就可以了。
例如:
// some device support 0.5 px
<!DOCTYPE html>
<html lang="en" class="hairlines">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
div {
boder: 1px solid #000;
}
.hairlines div {
border: 0.5px solid #000;
}
</style>
</head>
<body>
<div>我是极致的1px</div>
</body>
</html>
这样,如果支持 0.5 px,就会出现一条非常细的线,否则就按照正常的显示方式。
Rem的映射方式
这个问题其实很简单,首先我们设定一个基于设计稿的 rem 基准值,然后在真实设备上乘一个设计稿对真实设备的比值:
而 lib-flexible 则默认规定了 rem 的取值为屏幕宽度的十分之一
Rem的一些问题
使用 rem 方式布局其实有很多问题。我们使用 rem 转换出来的 px 往往有很多小数点,所以最终的展示效果的 CSS 长宽实际上是经过四舍五入的。因此往往我们需要手动去调整“最后的1px”,这给我们带来了很多的麻烦。
因此 rem 确实是对移动端适配的一种解决方案,但并非无懈可击。