适配的具体思路:等比缩放
所谓的等比缩放,就是设计师基于一个尺寸做一套设计图,开发工程师会基于设计图100%还原页面。在不同屏幕上的视觉呈现比例完全一致。以下图为例,屏幕尺寸虽有差异,但同一页面视觉效果完全一样,页面内容等比缩放。
这种方式最大的优点是简单、可控。设计师能对不同屏幕尺寸手机的展现效果做绝对的把控,测试工程师也不用担心漏测了某款机型出现样式错乱自己没有发现,开发工程师也可以一套代码满足任何场景。你好我好大家好。
当然,这种“偷懒”的适配思路也不见得完美。手机屏幕之所以越来越大,本意是想展示更多的内容,而不是让文字图片越变越大。反过来看,如果文字图片在IPhone12 Max Pro上看起来展示合理,在IPhone 6s上会小的像蚂蚁。
在接下来我会介绍等比缩放适配的具体方案:1、动态rem方案;2、vw适配方案。
前置知识
在进行介绍移动端适配之前,首先得明确下面这些基本概念(术语):
物理像素(physical pixel)
物理像素又被称为设备像素,他是显示设备中一个最微小的物理部件。每个像素可以根据操作系统设置自己的颜色和亮度。正是这些设备像素的微小距离欺骗了我们肉眼看到的图像效果。
设备独立像素(density-independent pixel)
设备独立像素也称为密度无关像素,或者叫逻辑像素。它可以被认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素(比如说CSS像素),然后由相关系统转换为物理像素。
屏幕密度
屏幕密度是指一个设备表面上存在的像素数量,它通常以每英寸有多少像素来计算(PPI)。
设备像素比(device pixel ratio)
设备像素比简称为dpr,DPR(devicePixelRatio)是默认缩放为100%的情况下,设备像素和CSS像素的比值。它的值可以按下面的公式计算得到:
设备像素比 = 物理像素 / 设备独立像素
在JavaScript中,可以通过window.devicePixelRatio获取到当前设备的dpr。而在CSS中,可以通过-webkit-device-pixel-ratio,-webkit-min-device-pixel-ratio和 -webkit-max-device-pixel-ratio进行媒体查询,对不同dpr的设备,做一些样式适配(这里只针对webkit内核的浏览器和webview)。
在早先的移动设备中,并没有DPR的概念。随着技术的发展,移动设备的屏幕像素密度越来越高。从iphone4开始,苹果公司推出了所谓的retina视网膜屏幕。之所以叫做视网膜屏幕,是因为屏幕的PPI(屏幕像素密度)太高,人的视网膜无法分辨出屏幕上的像素点。iphone4的分辨率提高了一倍,但屏幕尺寸却没有变化,这意味着同样大小的屏幕上,像素多了一倍,于是DPR = 2。
由此我们可以感受到,在移动端时代屏幕适配除了Layout之外,还要考虑到图片的适配,因为其直接影响到页面显示质量。
meta标签
<meta>标签有很多种,而这里要着重说的是viewport的meta标签,其主要用来告诉浏览器如何规范的渲染Web页面,而你则需要告诉它视窗有多大。在开发移动端页面,我们需要设置meta标签如下:
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
代码以显示网页的屏幕宽度定义了视窗宽度。网页的比例和最大比例被设置为100%。
利用rem单位进行移动端适配
什么是rem?
在W3C官网上是这样描述rem的——“font size of the root element” 换句话说,当我们指定一个元素的font-size为2rem的时候,也就说这个元素的字体大小为根元素字体大小的两倍,如果html的font-size为12px,那么这个2rem的元素font-size就是24px。只要我们根据不同屏幕设定好根元素的font-size,其他已经使用了rem单位的元素就会自适应显示相应的尺寸了。
rem方案的原理
rem方案的原理其实就是,将每一个不同的屏幕划分成相同的份数,让同一个元素在不同的屏幕上占据相同比例的空间。1rem等于此页面html的font-size,rem可以理解为每份是多少px,比如说,我们的设计稿宽度为750px,我们规定将页面划分成10份,那么rem=75px,如果有一个div宽度为75px,就刚好为1rem。即,将750px宽的屏幕划分为10份,这个div宽度占一份。那么只需要让此div在不同宽度的屏幕下宽度都占一份,就行了。那么我们只需要做一件事,就是确定不同的屏幕下一份有多宽,即不同设备下1rem等于多少px?这很容易计算,document.body.clientWidth / 10 就能算出来。
通过这个式子很清楚地看到,核心思想就是把不同的屏幕划分成相同的份数,在设计稿中占一份的,在所有设备屏幕上都应该占一份。
- viewport 配置
<meta name="viewport" content="width=device-width; initial-scale=1; maximum-scale=1; minimum-scale=1; user-scalable=no;">
上面把scale设置成固定1倍的视口的大小,也可以根据dpr的值缩放viewport,如下:
//下面是根据设备dpr设置viewport
var dpr = window.devicePixelRatio || 1
var scale = 1 / dpr
viewport.setAttribute(
"content",
"width=device-width" +
",initial-scale=" +
scale +
", maximum-scale=" +
scale +
", minimum-scale=" +
scale +
", user-scalable=no"
)
- 设置 rem 基准值,核心代码为如下
// set 1rem = 逻辑像素(设备独立像素) / 10
function setRemUnit () {
var rem = document.documentElement.clientWidth / 10
// 375/10 = 37.5
docEl.style.fontSize = rem + 'px'
}
7setRemUnit()
lib-flexible原理
flexible方案是手淘经过多年的摸索和实战,总结出的一套移动端适配方案,它就是利用rem来进行布局适配的。
这个方案实际上就是通过JS来动态改写meta标签。lib-flexible会自动在html的head中添加一个<meta name="viewport">的标签,同时会自动设置html的font-size为屏幕宽度除以10,也就是1rem等于html根节点的font-size。假如设计稿的宽度是750px,此时1rem应该等于75px。假如量的某个元素的宽度是150px,那么在css里面定义这个元素的宽度就是 width: 2rem
代码类似这样:
var metaEl = doc.createElement('meta');
var scale = isRetina ? 0.5:1;
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
document.documentElement.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
documen.write(wrap.innerHTML);
}
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}就可以了。
1px 问题的其他解决方案
直接设置0.5px
在IOS8+,苹果系列都已经支持0.5px了,可以借助媒体查询来处理。
/*这是css方式*/
.border { border: 1px solid #999 }
@media screen and (-webkit-min-device-pixel-ratio: 2) {
.border { border: 0.5px solid #999 }
}
/*ios dpr=2和dpr=3情况下border相差无几,下面代码可以省略*/
@media screen and (-webkit-min-device-pixel-ratio: 3) {
.border { border: 0.333333px solid #999 }
}
IOS7及以下和Android等其他系统里,0.5px将会被显示为0px。因此需要通过JavaScript检测浏览器能否处理0.5px的边框,如果可以,给html标签元素添加个class,这就是lib-flexible第二版的思路。
利用 css 的 伪元素::after + transfrom 进行缩放
为什么用伪元素? 因为伪元素::after或::before是独立于当前元素,可以单独对其缩放而不影响元素本身的缩放。更细致的做法是同时增加媒体查询分别对不同 DPR 的设备,进行不同比例的缩放。
.border-1px:after {
content: '';
position: absolute;
box-sizing: border-box;
top: 0;
left: 0;
width: 200%;
height: 200%;
border: 1px solid #000;
border-radius: 4px;
-webkit-transform: scale(0.5);
transform: scale(0.5);
-webkit-transform-origin: top left;
}
vant组件库和ant-design-mobile采用的都是这个思路的方案。
postcss-write-svg
PostCSS的插件postcss-write-svg来帮助我们。如果你的项目中已经有使用PostCSS,那么只需要在项目中安装这个插件。然后在你的代码中使用:
@svg 1px-border {
height: 2px;
@rect {
fill: var(--color, black);
width: 100%;
height: 50%;
}
}
.example {
border: 1px solid transparent;
border-image: svg(1px-border param(--color #00b1ff)) 2 2 stretch;
}
这样PostCSS会自动帮你把CSS编译出来:
.example {
border: 1px solid transparent;
border-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='2px'%3E%3Crect fill='%2300b1ff' width='100%25' height='50%25'/%3E%3C/svg%3E")
2 2 stretch;
}
rem的优缺点
缺点
- 在奇葩的dpr设备上表现效果不太好,比如 一些华为的高端机型 用rem布局会出现错乱。
- 设置根字体大小的方式有两种,一种是媒体查询,优点:不需要额外使用js去更改html的字体,缺点:不连续,或者说并能完全实现对所有设备的布局规范统一;另一种是js动态更改html字体,优点:连续;缺点:不如直接写媒体查询的体验好;
- 不支持css3 calc的需要大量密集的 @media hack;
- 使用iframe引用也会出现问题;
- 个人认为最重要的一点,它不是vw。为什么这么说?因为我们一直在使用Hack手段用rem模拟vw特性,而vw是css原生的支持。
优点
- 使用rem可以设置页面的最大宽度,使用vw则无法做到这点。限制了最大宽度的好处是,使得移动端页面在很宽的屏幕下,比如pc和pad上,体验和手机上相似,而不会使得元素变得特别宽而失真。这是使用lib-flexible的移动端页面在PC下的表现,它限制了最大宽度不能超过540px * dpr:限制最大宽度,而未限制最大宽度的情况下:未限制最大宽度。
关于vw
lib-flexible的GitHub已经宣布停止使用rem方案了,因为找到了新的更好的全局性参照单位:vw。 什么是 vw ?
1vw = 视窗宽度的1%
- 对于pc端:这个可视区域是
window.innerWidth/window.innerHeight的大小, 用图简单的示意如下 - 对于移动端:这个可视区域
document.documentElement.getBoundingClientRect().width/document.documentElement.getBoundingClientRect().height(mate标签的viewport中设置的width)的大小
vw的值相对于视口宽度,所以10vw是视口宽度的10%。如果更改浏览器窗口的宽度,那么元素的大小会响应更改。这个单位似乎是专门为我们适配量身打造的。首先,它本质上就是一个百分比单位;其次,它又是全局性的与屏幕宽度直接相关的单位。
如果设计稿使用750px宽度,则100vw = 750px,即1vw = 7.5px。那么我们可以根据设计图上的px值直接转换成对应的vw值。如果不想自己计算,我们可以使用PostCSS的插件postcss-px-to-viewport,让我们可以直接在代码中写px。
{
loader: 'postcss-loader',
options: {
plugins: ()=>[
require('autoprefixer')({
browsers: ['last 5 versions']
}),
require('postcss-px-to-viewport')({
viewportWidth: 375, //视口宽度(数字)
viewportHeight: 1334, //视口高度(数字)
unitPrecision: 3, //设置的保留小数位数(数字)
viewportUnit: 'vw', //设置要转换的单位(字符串)
selectorBlackList: ['.ignore', '.hairlines'], //不需要进行转换的类名(数组)
minPixelValue: 1, //设置要替换的最小像素值(数字)
mediaQuery: false//允许在媒体查询中转换px(true/false)
})
]
}
那么移动浏览器对 vw 的支持度如何呢?根据caniuse.com:
即 iOS 8+和 Android 4.4+都支持 vw ,而目前的手机应用通常支持iOS 9+和Android 5+。所以,实践中使用 vw 单位完全可行。而这也正是手机淘宝团队19年初正式转向使用 vw 单位适配的原因。
vw和Rem结合适配
- 给根元素大小设置随着视口变化而变化的vw单位,这样就可以实现动态改变其大小。
- 限制根元素字体大小的最大最小值,配合body加上最大宽度和最小宽度。
@media screen and (max-width: 320px) {
html {
font-size: 64px;
}
}
@media screen and (min-width: 540px) {
html {
font-size: 108px;
}
}
html {
font-size: 20vw;
}
.box {
max-width: 540px;
min-width: 320px;
}
这个方案适合视觉图形组件较多的,或者是组件位置有一定相对依赖关系的场景。而且可以设置最大最小宽度。京东(m.jd.com/)移动端就是采用这种适配方案。
px+百分比适配
并不是说移动端就一定要使用相对长度单位,传统的响应式布局依然是很好的选择,尤其在新闻,社区等可阅读内容较多的场景直接使用px单位可以营造更好地体验。px方案可以让大屏幕手机展示出更多的内容,更符合人们的阅读习惯。 采用这种方案的大厂有 携程(m.ctrip.com/html5/),腾讯新闻(www.qq.com/),知乎(www.zhihu.com/)等
移动端适配的重新思考
1 移动端适配就是用rem或vw ?
并不是所有场景都适合用rem或vw进行适配。
- vw和rem适配的本质是等比例缩放,让页面在不同屏幕尺寸下有类似于矢量图片缩放的效果,保证了页面元素之间的尺寸缩放比例和位置。
- 等比缩放的适配方案适合视觉组件种类比较多,视觉设计对元素位置的相对关系依赖较强的移动端页面,基本上大部分页面都可以用着两种方案进行适配。
- 但对于文本内容较多,我们希望引导用户沉浸在更多的内容而不是更大的内容的,这种等比例缩放的方案并不能满足要求,我推荐直接使用px结合flex等布局方式进行适配。
2 rem该抛弃了,使用vw不香么 ?
vw适配不是万能的,最好与rem配合使用
- 当初之所以使用rem的方案流行开来正是因为在那时viewport units的浏览器支持程度不甚理想(IOS 8+, Android 4.4+ 参见viewport units的caniuse)。而相比较之下rem就好多了(IOS 4.1+, Android 2.1+ 参见caniuse),所以对于vw,在当时的大环境下前端想说爱你不容易。
- 随着前端技术的革新,最主要是各大浏览器厂商的给力,除Opera Mini全版本和IE低版本不支持之外,其他的浏览器基本上都已经支持vw了。
- 采用vw适配后的页面效果很好,但是它是利用视口单位实现的布局,依赖视口大小而自动缩放,无论视口过大还是过小,它也随着视口过大或者过小,失去了最大最小宽度的限制。
参考资料: