深入浅出移动端适配(总结版)

故事背景

近段时间,在开发移动端项目的时候,产品提了一个“自定义页面”的需求,刚开始在DevTools上做得顺风顺水,一马平川,直到在提交到测试环境用不同的真机访问页面时,却发现了一个很严重的问题:安卓的高倍屏展示自定义页面组件间距的时候会比iPhone要大很多(组件间距由js代码控制style样式),而组件内部的样式在两者之间区别并不大(组件内部样式由css代码控制),这就神奇了! 开始爬坑。

我们移动端项目的样式适配方案使用的是lib-flexible方案,所以在用js控制style样式的时候使用了lib-flexible的属性方法px2rem,因此在处理组件间间距的时候,使用了如下的处理方式:

const { margin: { top = 0, right = 0, bottom = 0, left = 0 } } = data
// left和right表示内边距,使用padding-*进行设置;
// top和bottom表示外边距,使用margin-*或[top|bottom](吸顶|吸底布局)进行设置
const _px2rem = (pxValue) => {
  const dpr = window.lib.flexible.dpr
  return window.lib.flexible.px2rem(pxValue / 1 * dpr)
}
this.style['padding-left'] = `${_px2rem(left)}rem`
this.style['padding-right'] = `${_px2rem(right)}rem`
if (this.fixed) {
  if (this.fixed.type === 'top') {
    this.style.top = `${_px2rem(top)}rem`
  } else {
    this.style.bottom = `${_px2rem(bottom)}rem`
  }
} else {
  this.style['margin-top'] = `${_px2rem(top)}rem`
  this.style['margin-bottom'] = `${_px2rem(bottom)}rem`
}
复制代码

相信对lib-flexible源码有所了解的同学一眼就看到了问题所在:

    const dpr = window.lib.flexible.dpr
    return window.lib.flexible.px2rem(pxValue / 1 * dpr)
复制代码

lib-flexible源码中有这样一段用于获取和设置window.lib.flexible.dpr,即:

if (!dpr && !scale) {
    var isAndroid = win.navigator.appVersion.match(/android/gi);
    var isIPhone = win.navigator.appVersion.match(/iphone/gi);
    var devicePixelRatio = win.devicePixelRatio;
    if (isIPhone) {
        // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
        if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {                
            dpr = 3;
        } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
            dpr = 2;
        } else {
            dpr = 1;
        }
    } else {
        // 其他设备下,仍旧使用1倍的方案
        dpr = 1;
    }
    scale = 1 / dpr;
}
复制代码

显然,lib-flexible把我们的安卓高倍屏手机都作为了// 其他设备下,仍旧使用1倍的方案。修改方案很简单,就是在判断手机型号的时候,把安卓设备也加上即可:if (isIPhone || isAndroid) { ... },改完之后,上面的“神奇现象”立马就“恢复正常”了。

由于本人之前很长一段时间都是在开发java web项目,所以很少接触到移动端项目的开发,现在虽说已经开发移动项目有一段时间了,但仍然对移动端项目有很多不了解的地方,例如lib-flexible的这个坑,也是踩了之后才刻骨铭心。

所以,我就趁着这次踩坑的机会,下定决心去搞懂lib-flexible的实现方式以及移动端适配这个对我来说玄而又玄的神秘力量。于是在网上查阅了很多关于移动端适配的优秀文章和相关资料,花费了一大番功夫后,终于搞懂了移动端适配的基本思路和各种解决方案的实现原理,此处特别感谢@ConardLi大佬的关于移动端适配,你必须要知道的这篇文章,让我踏入了“移动端适配”的新世界的大门。

本文就是在关于移动端适配,你必须要知道的这篇文章的基础上进行了更为细致的补充和扩展,从而深入研究“移动端适配”的根本原理,并尽量理清那些看上去并无关联却有着千丝万缕关系的各种概念。

文章内容文字较多,请耐心看完,也许你有不一样的收获和精彩发现,希望本文对你能有所帮助!

一、英寸

我们一般用英寸(inch)这个物理单位来描述屏幕的物理大小,如电脑显示器的17英寸、手机显示的6.3英寸等。
需要注意的是,这个尺寸指的是屏幕对角线的长度

尺寸示意图

其中,英寸和厘米的换算单位是:1英寸 = 2.54厘米

二、分辨率

分辨率通常分为显示分辨率和图像分辨率

  1. 显示分辨率(屏幕分辨率)是屏幕图像的精密度,是指显示器所能显示的像素有多少
  2. 图片分辨率则是单位英寸内所包含的像素点数。
    ppi(像素每英寸)和dpi(点每英寸)是我们最常用的用来描述分辨率的两种单位;其实ppidpi经常都会出现混用现象。但是他们所用的领域也存在区别。从技术角度说,“像素”只存在于电脑显示领域,而“点”只出现于打印或印刷领域。
    我们这里来主要聊一下电脑显示领域的“像素”。

2.1 像素

像素,即一个小方块,它具有位置和颜色两个属性
电子屏幕(手机、电脑)就是由多个具有特定位置和颜色的方块拼接而成。


图中的图片是经过放大后的图片,其中每一个小方块就是我们说的像素

2.2 物理像素

  1. 物理像素等同于设备像素(dp:device pixel),顾名思义,显示屏是由一个个物理像素点组成的,通过控制每个像素点的颜色使屏幕显示出不同的图像,屏幕从出厂那天起,它上面的物理像素点就固定不变了,单位pt
  2. 其中,我们通常说的显示器分辨率,其实是指桌面设定的分辨率,而不是显示器的物理分辨率

2.3 设备独立像素

在智能机出现伊始,移动设备的屏幕像素密度都很低,比如iPhone3,它的分辨率是320x480,在iPhone3的屏幕上,我们看到的内容都是“真实”的,1px = 1pxiPhone3屏幕上最多只能展示一个320px宽的元素。但如果仔细观察,你会在iPhone3的屏幕上发现有很多细小的发光点,也即是大家常说的“颗粒感”,这一颗一颗的发光点就是我们上面说到的真实的像素物理单元,也就是物理像素。
随着技术的快速发展,屏幕的变化也日新月异。很快,更高分辨率的屏幕诞生了,乔布斯在2010年6月8日发布了iPhone4,它的分辨率是640*960,正好是iPhone3的两倍。然而iPhone4iPhone3的尺寸是一样的,都是3.5英寸,那么在物理尺寸不变的情况下,iPhone4是怎么做到把两倍的分辨率使用在这块3.5英寸的屏幕上的呢?

iPhone3和iPhone4对比

理论上来讲,在白色手机上相同大小的图片和文字,在黑色手机上会被缩放一倍,因为它的分辨率提高了一倍。这样,岂不是后面出现更高分辨率的手机,页面元素会变得越来越小吗?
然而,事实并不是这样的,我们现在使用的智能手机,不管分辨率多高,他们所展示的界面比例都是基本类似的。乔布斯在iPhone4的发布会上首次提出了Retina Display(视网膜屏幕)的概念,它正是解决了上面的问题,这也使它成为一款跨时代的手机。


iPhone4使用的视网膜屏幕中,把2x2个像素当1个像素使用,这样让屏幕看起来更精致,但是元素的大小却不会改变。


如果黑色手机使用了视网膜屏幕的技术,那么显示结果应该是下面的情况,比如列表的宽度为300个像素,那么在一条水平线上,白色手机会用300个物理像素去渲染它,而黑色手机实际上会用600个物理像素去渲染它。 我们必须用一种单位来同时告诉不同分辨率的手机,它们在界面上显示元素的大小是多少,这个单位就是设备独立像素(Device Independent Pixels)简称DIPDP。上面我们说,列表的宽度为300个像素,实际上我们可以说:列表的宽度为300个设备独立像素。


打开chrome的开发者工具,我们可以模拟各个手机型号的显示情况,每种型号上面会显示一个尺寸,比如iPhone X显示的尺寸是375x812,实际iPhone X的分辨率会比这高很多,这里显示的就是设备独立像素

2.3 设备像素比(dpr)

设备像素比device pixel ratio简称dpr,即物理像素和设备独立像素的比值。

在web中,浏览器为我们提供了window.devicePixelRatio来帮助我们获取dpr。 在css中,可以使用媒体查询min-device-pixel-ratio,区分dpr

@media (-webkit-min-device-pixel-ratio: 2),(min-device-pixel-ratio: 2){ }
复制代码

实际上,从苹果提出视网膜屏幕开始,才出现设备像素比这个概念,因为在这之前,移动设备都是直接使用物理像素来进行展示。 紧接着,Android同样使用了其他的技术方案来实现DPR大于1的屏幕,不过原理是类似的。由于Android屏幕尺寸非常多、分辨率高低跨度非常大,不像苹果只有它自己的几款固定设备、尺寸。所以,为了保证各种设备的显示效果,Android按照设备的像素密度将设备分成了几个区间:

当然,所有的Android设备不一定严格按照上面的分辨率,每个类型可能对应几种不同分辨率,所以,每个Android手机都能根据给定的区间范围,确定自己的DPR,从而拥有类似的显示。当然,仅仅是类似,由于各个设备的尺寸、分辨率上的差异,设备独立像素也不会完全相等,所以各种Android设备仍然不能做到在展示上完全相等。

2.4 css像素(PX)

css像素是一种虚拟像素,可以理解为“直觉”像素,CSS和JS使用的抽象单位,浏览器内的一切长度都是以CSS像素为单位的,CSS像素的单位是px

在CSS规范中,长度单位可以分为两类,绝对(absolute)单位以及相对(relative)单位。px是一个相对单位,相对的是设备像素(device pixel)。

  1. 在同样一个设备上,每1个CSS像素所代表的物理像素是可以变化的(即CSS像素的第一方面的相对性);
  2. 在不同的设备之间,每1个CSS像素所代表的物理像素是可以变化的(即CSS像素的第二方面的相对性);

在上一小节中,我们已经知道了在移动端浏览器中以及某些桌面浏览器中,window对象有一个devicePixelRatio(设备像素比)属性,而它的官方的定义为:设备物理像素和设备独立像素的比例,从而我们可以得到一个公式: dpr = dp / dips

  1. dp(device pixels):设备物理像素;
  2. dips(device-independent pixels):设备独立像素,dips = css像素 / scale(缩放比例)

那么在没有缩放的情况下,1个css像素等同于一个设备独立像素。

CSS像素在视觉上是很容易改变大小的,比如缩放浏览器页面,就是改变的CSS像素,当放大一倍,那么一个CSS像素在横向或者纵向上会覆盖两个设备独立像素。例如宽度100px,当页面放大一倍,它会在横向上由原本占据100个设备独立像素,变成占据200个设备独立像素;如果缩小,则恰好相反,只能占据50个设备独立像素。


图示说明:

  1. 设备独立像素(深蓝色背景)、CSS像素(半透明背景)。
  2. 左图表示当用户进行缩小操作时,一个设备独立像素覆盖了多个CSS像素。
  3. 右图表示当用户进行放大操作时,一个CSS像素覆盖了多个设备独立像素。

无论CSS像素是缩小还是放大,它是像素数目是不变的,比如100px,无论缩放,它依然是100px,只不过它占据的设备独立像素发生了变化(体积发生了变化,视觉大小上发生了变化而已)。

三、视口

Viewport翻译成中文可以叫“视窗”或者是“视口”,它表示的是用户网页的可视范围(指页面能够被浏览的范围)

在移动设备上进行网页的重构或开发,首先得搞明白的就是移动设备上的viewport了,只有明白了viewport的概念以及弄清楚了跟viewport有关的meta标签的使用,才能更好地让我们的网页适配或响应各种不同分辨率的移动设备。

3.1 三个viewport

ppk大神对于移动设备上的viewport有着非常多的研究(A tale of two viewports — part oneA tale of two viewports — part twoMeta viewport),ppk认为,移动设备上有三个viewport

首先,移动设备上的浏览器认为自己必须能让所有的网站都正常显示,即使是那些不是为移动设备设计的网站。
但如果以浏览器的可视区域作为viewport的话,因为移动设备的屏幕都不是很宽,所以那些为桌面浏览器设计的网站放到移动设备上显示时,必然会因为移动设备的viewport太窄,而挤作一团,甚至布局什么的都会乱掉。

如果把移动设备上浏览器的可视区域设为viewport的话,某些网站就会因为viewport太窄而显示错乱,所以这些浏览器就决定默认情况下把viewport设为一个较宽的值,比如980px,这样的话即使是那些为桌面设计的网站也能在移动浏览器上正常显示了。ppk把这个浏览器默认的viewport叫做layout viewport(布局视口)。
layout viewport的宽度可以通过 document.documentElement.clientWidth 来获取。

然而,layout viewport 的宽度是大于浏览器可视区域的宽度的,所以我们还需要一个viewport来代表 浏览器可视区域的大小,ppk把这个viewport叫做 visual viewport(视觉视口)。
visual viewport的宽度可以通过window.innerWidth 来获取。

George Cummins在stack overflow上解释了基本的概念:

layout viewport想象为一个不会改变大小或形状的大图像。现在想象你有一个较小的框架,通过它你可以看到这个大图片。小框架由不透明的材料构成,通过它你只能看到大图像的一部分,这一部分就叫做 visual viewport .你可以拿着这个框架和图像拉开一定的距离(zoom out)看到整个图像,你也可以让自己离图像更近(zoom in)看到其中的一部分。你也可以旋转这个框架的方向,但是这个图片(layout viewport)的大小和形状永远不会改变。

ps: visual viewport的宽度指的是浏览器可视区域的宽度

现在我们已经有两个viewport了:layout viewportvisual viewport

但浏览器觉得还不够,因为现在越来越多的网站都会为移动设备进行单独的设计,所以必须还要有一个能完美适配移动设备的viewport
所谓的完美适配指的是:

  1. 首先不需要用户缩放和横向滚动条就能正常的查看网站的所有内容;
  2. 显示的文字的大小是合适,比如一段14px大小的文字,不会因为在一个高密度像素的屏幕里显示得太小而无法看清,理想的情况是这段14px的文字无论是在何种密度屏幕,何种分辨率下,显示出来的大小都是差不多的。当然,不只是文字,其他元素像图片什么的也是这个道理。

ppk把这个viewport叫做 ideal viewport(理想视口),也就是第三个viewport——移动设备的理想viewport
ideal viewport 可通过window.screen.width获取。

ideal viewport 并没有一个固定的尺寸,不同的设备拥有有不同的 ideal viewport

只要在css中把某一元素的宽度设为ideal viewport的宽度(单位用px),那么这个元素的宽度就是设备屏幕的宽度了,也就是宽度为100%的效果

deal viewport 的意义在于,无论在何种分辨率的屏幕下,那些针对ideal viewport 而设计的网站,不需要用户手动缩放,也不需要出现横向滚动条,都可以完美的呈现给用户。

3.2 关于layout viewport的一些细节

  1. css布局,尤其是百分比宽度,是相对于layout viewport的宽度计算的(即页面最顶级元素html的自适应宽度(或width: 100%)的参考系就是layout viewport的宽度),它要比visual viewport宽得多
  2. layout viewportvisual viewport都是由css像素测量的,但是visual viewport的尺寸会随着缩放变化(如果你进行放大操作,那么屏幕上将展示更少的css像素),而layout viewport的尺寸将保持不变。(否则,你的页面将会持续地按百分比回流和重新计算,这是我们不想看到的)
  3. 大部分手机浏览器会初始展示整个页面在一屏之内(完全的缩小模式下),也就意味着这些浏览器会默认使用layout viewport的尺寸,这样它就可以在完全缩小的模式下完全覆盖屏幕,因此layout viewportvisual viewport的尺寸相同
  4. layout viewport的宽度和高度等于在最大缩小模式下的屏幕上显示内容的宽度和高度,当用户放大缩小时,layout viewport的尺寸保持不变
  5. 安卓的webkit内核和IE的最小视口大小是320px,因此当你给layout viewport设置小于320的值时,浏览器会自动重置为ideal viewport的宽度
  6. layout viewport的最大宽度是10000px
  7. layout viewport的最小宽度大约为ideal viewport的宽度的十分之一

3.3 Meta Viewport

<meta> 元素表示那些不能由其它HTML元相关元素之一表示的任何元数据信息,它可以告诉浏览器如何解析页面, 我们可以借助<meta>元素的viewport来帮助我们设置视口、缩放等,从而让移动端得到更好的展示效果。

3.3.1 指令

<meta>元素可以通过设置其nameviewport,然后对其content设置不同的指令,从而可以达到控viewport的目的。

content设置指令的语法格式如下:
<meta name="viewport" content="name=value,name=value">

其中每一对键值对都是一个指令,Meta Viewport共有6组指令:

  1. width,设置layout viewport的宽度,为一个正整数,或关键词device-width
  2. initial-scale,设置页面的初始缩放值,为一个大于0的小数
  3. minimum-scale,允许用户的最小缩放值,为一个大于0的小数
  4. maximum-scale,允许用户的最大缩放值,为一个大于0的小数
  5. height,设置layout viewport的高度,有这个属性,但是好像并没有人支持
  6. user-scalable,是否允许用户进行缩放,值为yesno

这些属性可以同时使用,也可以单独使用或混合使用,多个属性同时使用时用逗号隔开就行了。

3.3.2 和ideal viewport不得不说的事情

layout viewport可以设置成为ideal viewport,通过设置width=device-width 这组指令就可以了(但要注意的是,在iphoneipad上,无论是竖屏还是横屏,device-width都是竖屏时ideal viewport的宽度。)

initial-scale=1 同样也可以把当前的layout viewport变为ideal viewport,这是为什么呢?

要想清楚这件事情,首先你得弄明白这个缩放是相对于什么来缩放的,因为这里的缩放值是1,也就是没缩放,但却达到了 ideal viewport 的效果,所以,那答案就只有一个了,缩放是相对于 ideal viewport来进行缩放的,当对ideal viewport进行100%的缩放,也就是缩放值为1的时候,不就得到了 ideal viewport吗?事实证明,的确是这样的:


测试结果表明 initial-scale=1 也能把当前的viewport宽度变成 ideal viewport 的宽度,但这次轮到了windows phone 上的IE 无论是竖屏还是横屏都把宽度设为竖屏时ideal viewport的宽度。

但如果 widthinitial-scale=1 同时出现,并且还出现了冲突呢?

比如:<meta name="viewport" content="width=400, initial-scale=1">

width=400表示把layout viewport的宽度设为400pxinitial-scale=1则表示把layout viewport的宽度设为ideal viewport的宽度(不一定是400px),那么浏览器到底该服从哪个命令呢?

layour viewport的宽度值会取width指定的宽度值和ideal viewport宽度值经过(initial-scale指定的值的倍数)缩放后的宽度值的最大值!

例如,在ideal viewport的宽度为320的前提下,当width=400initial-scale=1时,此时layout viewport会取400px(因为400 > 320 / 1)作为它的宽度值;当width=400initial-scale=0.5时,此时layout viewport会取640px(因为320 / 0.5 > 400320 / 0.5 = 640)作为它的宽度值。

如图中所示,在iPhoneX中,ideal viewport的宽度和device-width都是375px,此时设置initial-scale0.5,则此时的visual viewport的宽度为 375 / 0.5 = 750。所以,从图上也可以看到visual viewportwindow.innerWidth)是750px,同时layout viewportdocument.documentElement.clientWidth)会取375px750px中的最大值,即750px作为宽度值。

最后,总结一下,要把当前的 viewport宽度设为ideal viewport的宽度,既可以设置 width=device-width,也可以设置 initial-scale=1,但这两者各有一个小缺陷,就是iphone、ipad以及IE 会横竖屏不分,通通以竖屏的ideal viewport宽度为准。所以,最完美的写法应该是两者都写上去,这样 :initial-scale=1 解决了 iphone、ipad的毛病,width=device-width则解决了IE的毛病。

3.3.3 关于缩放以及initial-scale的默认值

前面已经提到过,缩放是相对于ideal viewport来缩放的,缩放值越大,当前viewport的宽度就会越小,反之亦然。

例如在iphone4中,ideal viewport的宽度是320px,如果我们设置 initial-scale=2 ,此时viewport的宽度会变为只有160px了,这也好理解,放大了一倍嘛,就是原来1px的东西变成2px了,但是1px变为2px并不是把原来的320px变为640px,而是在实际宽度不变的情况下,1px变得跟原来的2px的长度一样了,所以放大2倍后原来需要320px才能填满的宽度现在只需要160px就做到了。因此,我们可以得出一个公式:

visual viewport宽度 = ideal viewport宽度 / 当前缩放值

当前缩放值 = ideal viewport宽度 / visual viewport宽度

现在再来说下initial-scale的默认值问题,就是不写这个属性的时候,它的默认值会是多少呢?

很显然不会是1,因为当 initial-scale = 1 时,当前的layout viewport宽度会被设为 ideal viewport的宽度,但前面说了,各浏览器默认的 layout viewport宽度一般都是980px1024px800px等等这些值,没有一开始就是 ideal viewport的宽度的,所以 initial-scale的默认值肯定不是1。安卓设备上的initial-scale默认值好像没有方法能够得到,或者就是干脆它就没有默认值,一定要你显示的写出来这个东西才会起作用,我们不管它了,这里我们重点说一下iphone和ipad上的initial-scale默认值。

根据测试,我们可以在iphone和ipad上得到一个结论:

在iphone和ipad上,无论你给viewport设的宽的是多少,如果没有指定默认的缩放值,则iphone和ipad会自动计算这个缩放值,以达到当前页面不会出现横向滚动条(或者说viewport的宽度就是屏幕的宽度)的目的。

3.4 视口小结

首先如果不设置meta viewport标签,那么移动设备上浏览器默认的宽度值为800px980px1024px等这些,总之是大于屏幕宽度的。这里的宽度所用的单位px都是指css中的px,它跟代表实际屏幕物理像素的px不是一回事。

第二、每个移动设备浏览器中都有一个理想的宽度,这个理想的宽度是指css中的宽度,跟设备的物理宽度没有关系,在css中,这个宽度就相当于100%所代表的那个宽度。我们可以用meta标签把viewport的宽度设为那个理想的宽度,如果不知道这个设备的理想宽度是多少,那么用device-width这个特殊值就行了,同时initial-scale=1也有把viewport的宽度设为理想宽度的作用。所以,我们可以使用 <meta name="viewport" content="width=device-width, initial-scale=1"> 来得到一个理想的viewport(也就是前面说的ideal viewport)。

四、移动端适配

尽管我们可以使用设备独立像素来保证各个设备在不同手机上显示的效果类似,但这并不能保证它们显示完全一致,我们需要一种方案来让设计稿得到更完美的适配

4.1 flexible方案

flexible方案是阿里早期开源的一个移动端适配解决方案,引用flexible后,我们在页面上统一使用rem来布局。

它的核心代码非常简单:

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

上面的代码中,将html节点的font-size设置为页面clientWidth(布局视口)的1/10,即1rem就等于页面布局视口的1/10这就意味着我们后面使用的rem都是按照页面比例来计算的

rem 是相对于html节点的font-size来做计算的。

我们通过设置document.documentElement.style.fontSize就可以统一整个页面的布局标准。

以iPhone6为例:布局视口为375px,则1rem = 37.5px,这时UI给定一个元素的宽为75px(设备独立像素),我们只需要将它设置为75 / 37.5 = 2rem。 当然,每个布局都要计算非常繁琐,我们可以借助PostCSSpx2rem插件来帮助我们完成这个过程。

让我们仔细阅读lib-flexible中的源码,我们会发现有这样一段是用于设置viewport的:

var metaEL= doc.querySelector('meta[name="viewport"]');
var dpr = window.devicePixelRatio;
var scale = 1 / dpr
metaEl.setAttribute('content', 'width=device-width, initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
复制代码

代码很简单易懂,但是你知道为什么会这段代码呢?结合我们上面介绍到的像素和视口相关的知识点,你读懂了这段代码的实现原理了吗?

让我们一步步深入分析下这段代码这样写的原因:

移动端浏览器在我们不对<meta name="viewport"/>进行设置的场景下,它会默认在完全的缩小模式下将页面内容完整的展示在屏幕内,此时的layout viewportvisual viewport尺寸相等(一般是980px1024px等,每个浏览器的默认行为不尽相同)。因此,我们需要对<meta name="viewport"/>进行设置,改变viewport这个默认宽度,从而可以对不同设备的屏幕设置不同的layout viewportvisual viewport尺寸。

其中width=device-width指令使得layout viewport的宽度等于设备宽度(即ideal viewport的宽度),然而我们前面有提到过,如果在viewport的设置中同时出现了width=xxxinitial-scale=xx,浏览器会取其中的最大值作为layout viewport的宽度。

我们回到上面3.3.3小结《关于缩放以及initial-scale的默认值》再捋一遍,我们会发现一个公式:

visual viewport宽度 = ideal viewport宽度 / 当前缩放值

也就意味着如果我们在dpr大于1的设备上设置了initial-scale的值是小于1的小数,那么visual viewport的宽度就会变大(相比于ideal viewport的宽度),此时,layout viewport会在device-widthvisual viewport的宽度值中取一个最大值作为其宽度值,显然是visual viewport的宽度值更大!

同时也产生了另外一个现象:visual viewport的宽度值会和layout viewport的宽度一直保持一致,即我们的屏幕窗口上看到的内容一定是完整的layout viewport上的内容!,这就可以解释为什么我们可以在不出现横向滚动条的情况下看到整个页面的内容了。

到这里就完了吗?如果此时你使用真机访问该页面,你会发现该页面在不同dpr的设备上已经实现了样式的自适应,即我们写的80px160px“正确的”反应在了真机的显示屏幕上,例如我们在代码里写的在dpr为1的安卓机上是80px,而iPhone6上是160px,在iPhoneX上是240px(这些知道按比例增大是rem的功劳,咱们暂不讨论),重点是屏幕上展示出来的元素都差不多大。

意思就是我们已经达到屏幕适配的目的了!我们已经成功了!,但是如果让我们思考一下下面这个问题,你就会发现事情到这儿并没有结束:

我们在开发移动端页面时,一般会按照iPhone6的尺寸设计稿进行还原开发(即设计稿的宽度为750px),而其中750px中的px对应的是设备像素,即物理像素,但是我们代码中的px(无论是css还是js)控制的都是css像素,即逻辑像素。

也就是说,我们的代码中块级元素的最大宽度值只能是375px(以iPhone6为例),否则大于375px就会出现横向滚动条(因为iPhone6的设备独立像素是375),吓得我赶紧去试了下,发现页面在不同dpr的设备上依然正常展示,都是宽度占满,并没有出现横向滚动条!这是为什么呢? 从理论上说不通啊。

结合上文中的《2.4 css像素》小结中的在没有缩放的情况下,1个css像素等同于一个设备独立像素dpr = dp / dips,我们终于知道了答案:

其实,initial-scale设置为小于1的小数还有一个作用,就是缩小css像素和dips之间的比例关系,即经过缩小操作,1个设备独立像素对应了多个css像素,因此反应在物理像素上也是按比例的缩小了n倍尺寸,最终达到了“扩大视口的同时使得1物理像素 = 1css像素”的目的和效果。(依据:dips = css像素 / scaledpr = dp / dips

4.2 vh、vw方案

vh、vw方案即将视觉视口宽度 window.innerWidth和视觉视口高度 window.innerHeight 等分为 100 份

  • vw(Viewport's width)1vw等于视觉视口的1%
  • vh(Viewport's height) :1vh 为视觉视口高度的1%
  • vmin : vwvh 中的较小值
  • vmax : 选取 vwvh 中的最大值

如果视觉视口为375px,那么1vw = 3.75px,这时UI给定一个元素的宽为75px(设备独立像素),我们只需要将它设置为75 / 3.75 = 20vw

这里的比例关系我们也不用自己换算,我们可以使用PostCSS的 postcss-px-to-viewport 插件帮我们完成这个过程。
写代码时,我们只需要根据UI给的设计图写px单位即可。

当然,没有一种方案是十全十美的,vw同样有一定的缺陷:

  • px转换成vw不一定能完全整除,因此有一定的像素差;
  • 当容器使用vwmargin采用px时,很容易造成整体宽度超过100vw,从而影响布局效果。当然我们也是可以避免的,例如使用padding代替margin,结合calc()函数使用等等...

五、安全区域

iPhoneX发布后,许多厂商相继推出了具有边缘屏幕的手机。

这些手机和普通手机在外观上无外乎做了三个改动:圆角(corners)、刘海(sensor housing)和小黑条(Home Indicator)。为了适配这些手机,安全区域这个概念变诞生了:安全区域就是一个不受上面三个效果的可视窗口范围。 为了保证页面的显示效果,我们必须把页面限制在安全范围内,但是不影响整体效果

5.1 viewport-fit

viewport-fit是专门为了适配iPhoneX而诞生的一个属性,它用于限制网页如何在安全区域内进行展示。

contain:可视窗口完全包含网页内容

cover:网页内容完全覆盖可视窗口

默认情况下或者设置为autocontain效果相同。

5.2 env、constant

我们需要将顶部和底部合理的摆放在安全区域内,iOS11新增了两个CSS函数envconstant,用于设定安全区域与边界的距离。 函数内部可以是四个常量:

  • safe-area-inset-left:安全区域距离左边边界距离
  • safe-area-inset-right:安全区域距离右边边界距离
  • safe-area-inset-top:安全区域距离顶部边界距离
  • safe-area-inset-bottom:安全区域距离底部边界距离

注意:我们必须指定viweport-fit后才能使用这两个函数:

<meta name="viewport" content="viewport-fit=cover">
复制代码

constantiOS < 11.2的版本中生效,enviOS >= 11.2的版本中生效,这意味着我们往往要同时设置他们,将页面限制在安全区域内:

body {
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
}
复制代码

六、常见问题

文章开头,我们提到过一些在移动端开发过程中常见的一些问题,如1px问题,UI完美适配各类尺寸的屏幕,高清屏图片模糊等样式适配问题。

关于“UI完美适配各类尺寸的屏幕”,我们可以使用lib-flexible或者vw、vh的方案去解决。

我们在这里主要讲解下1px问题和“高清屏图片展示模糊”的问题。

6.1 1px问题

为了适配各种屏幕,我们写代码时一般使用设备独立像素来对页面进行布局。

而在设备像素比大于1的屏幕上,我们写的1px实际上是被多个物理像素渲染,这就会出现1px在有些屏幕上看起来很粗的现象。

解决方案:

  1. 媒体查询利用设备像素比缩放,设置小数像素;

优点:简单,好理解
缺点:兼容性差,目前之余IOS8+才支持,在IOS7及其以下、安卓系统都是显示0px

  • IOS8+下已经支持带小数的px值,media query 对应 devicePixelRatio 有个查询值 -webkit-min-device-pixel-ratio
.border { border: 1px solid #999 }
@media screen and (-webkit-min-device-pixel-ratio: 2) {
    .border { border: 0.5px solid #999 }
}
@media screen and (-webkit-min-device-pixel-ratio: 3) {
    .border { border: 0.333333px solid #999 }
}
复制代码
  1. border-image方案

缺点:需要制作图片,圆角可能出现模糊

.border-image-1px {
    border-width: 1px 0px;
    -webkit-border-image: url("border.png") 2 0 stretch;
    border-image: url("border.png") 2 0 stretch;
}
复制代码
  • border-width:指定边框的宽度,可以设定四个值,分别为上右下左 border-width: top right bottom left;
  • border-image:该例意为:距离图片上方2px(属性值上没有单位)裁剪边框图片作为上边框,下方2px裁剪作为下边框。距离左右0像素裁剪图片即没有边框,以拉伸方式展示。
  1. background-image方案
.border_1px{
        @media only screen and (-webkit-min-device-pixel-ratio:2){
            .border_1px{
                background: url(../img/1pxline.png) repeat-x left bottom;
                background-size: 100% 1px;
            }
        }
复制代码

缺点:需要制作图片,圆角可能出现模糊

  1. box-shadow方案
    利用阴影也可以实现,优点是没有圆角问题,缺点是颜色不好控制
div {
    -webkit-box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.5);
}
复制代码
  • box-shadow属性的用法:box-shadow: h-shadow v-shadow [blur] [spread] [color] [inset]
  • 参数分别表示: 水平阴影位置,垂直阴影位置,模糊距离, 阴影尺寸,阴影颜色,将外部阴影改为内部阴影,后四个可选;
  • 该例中为何将阴影尺寸设置为负数?设置成-1px 是为了让阴影尺寸稍小于div元素尺寸,这样左右两边的阴影就不会暴露出来,实现只有底部一边有阴影的效果。从而实现分割线效果(单边边框)。
  1. viewport + rem

通过设置缩放,让CSS像素等于真正的物理像素。
例如:当设备像素比为3时,我们将页面缩放1/3倍,这时1px等于一个真正的屏幕像素。

const scale = 1 / window.devicePixelRatio;
    const viewport = document.querySelector('meta[name="viewport"]');
    if (!viewport) {
        viewport = document.createElement('meta');
        viewport.setAttribute('name', 'viewport');
        window.document.head.appendChild(viewport);
    }
    viewport.setAttribute('content', 'width=device-width,user-scalable=no,initial-scale=' + scale + ',maximum-scale=' + scale + ',minimum-scale=' + scale);
复制代码

实际上,上面这种方案是早先flexible采用的方案。

  1. 伪类 + transform方案
.border_1px:before{
          content: '';
          position: absolute;
          top: 0;
          height: 1px;
          width: 100%;
          background-color: #000;
          transform-origin: 50% 0%;
        }
        @media only screen and (-webkit-min-device-pixel-ratio:2){
            .border_1px:before{
                transform: scaleY(0.5);
            }
        }
        @media only screen and (-webkit-min-device-pixel-ratio:3){
            .border_1px:before{
                transform: scaleY(0.33);
            }
        }
复制代码

这种方式可以满足各种场景,如果需要满足圆角,只需要给伪类也加上border-radius即可

  1. svg

上面我们border-imagebackground-image都可以模拟1px边框,但是使用的都是位图,还需要外部引入。 借助PostCSSpostcss-write-svg我们能直接使用border-imagebackground-image创建svg的1px边框:

@svg border_1px { 
  height: 2px; 
  @rect { 
    fill: var(--color, black); 
    width: 100%; 
    height: 50%; 
    } 
  } 
.example { border: 1px solid transparent; border-image: svg(border_1px param(--color #00b1ff)) 2 2 stretch; }
复制代码

编译后:

.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; }
复制代码

6.2 图片模糊问题

6.2.1 产生原因

我们平时使用的图片大多数都属于位图(png、jpg...),位图由一个个像素点构成的,每个像素都具有特定的位置和颜色值:

理论上,位图的每个像素对应在屏幕上使用一个物理像素来渲染,才能达到最佳的显示效果。

而在dpr > 1的屏幕上,位图的一个像素可能由多个物理像素来渲染,然而这些物理像素点并不能被准确的分配上对应位图像素的颜色,只能取近似值,所以相同的图片在dpr > 1的屏幕上就会模糊:

6.2.2 解决方案

为了保证图片质量,我们应该尽可能让一个屏幕像素来渲染一个图片像素,所以,针对不同DPR的屏幕,我们需要展示不同分辨率的图片。

如:在dpr=2的屏幕上展示两倍图(@2x),在dpr=3的屏幕上展示三倍图(@3x)

  1. media查询
.avatar{
            background-image: url(conardLi_1x.png);
        }
        @media only screen and (-webkit-min-device-pixel-ratio:2){
            .avatar{
                background-image: url(conardLi_2x.png);
            }
        }
        @media only screen and (-webkit-min-device-pixel-ratio:3){
            .avatar{
                background-image: url(conardLi_3x.png);
            }
        }
复制代码

只适用于背景图

  1. image-set
.avatar {
    background-image: -webkit-image-set( "conardLi_1x.png" 1x, "conardLi_2x.png" 2x );
}
复制代码

只适用于背景图

  1. srcset 使用img标签的srcset属性,浏览器会自动根据像素密度匹配最佳显示图片:
<img src="conardLi_1x.png"
     srcset=" conardLi_2x.png 2x, conardLi_3x.png 3x">
复制代码
  1. 使用svg
    SVG的全称是可缩放矢量图(Scalable Vector Graphics)。不同于位图的基于像素,SVG 则是属于对图像的形状描述,所以它本质上是文本文件,体积较小,且不管放大多少倍都不会失真。
<img src="conardLi.svg">

<img src="data:image/svg+xml;base64,[data]">

.avatar {
  background: url(conardLi.svg);
}
复制代码

参考

分类:
前端
标签: