281
“不要再问我XX的问题”系列:
一、不要再问我this的指向问题了
二、不要再问我跨域的问题了

移动端适配的问题,一般来说我们都不会去深究,因为这种东西都是配置一次就再也不用管的了,接到设计图就按照祖传套路撸就完事了。按部就班的必定只能成为活动页写手,研究透彻以后,才能成为一名专业的活动页写手嘛。

纠缠不清的关系

文章开始,我们需要来捋清楚像素、视口以及缩放之间种种藕断丝连的关系,来抽丝剥茧一波。

像素

像素我们写得多了,不就是px嘛,为什么要拿出来说呢?因为像素还不仅仅就是px。

  • 设备像素

设备像素也可以叫物理像素,由设备的屏幕决定,其实就是屏幕中控制显示的最小单位。

  • 设备独立像素

设备独立像素是一种可以被程序所控制的虚拟像素,在Web开发中对应CSS像素。

  • DPR

设备像素与设备独立像素之间的关系就是,DPR(设备像素比),设备像素比 = 设备像素 / 设备独立像素。这条公式成立的前提是,缩放比为1,原因下面讲到缩放的时候就会知道。根据这种关系,如果设备像素大于设备独立像素(DPR大于1的设备,我们常说的高清屏或者Retina屏),就会出现一个设备独立像素对应多个设备像素的情况:
图片描述

视口

遥想从前智能手机刚出的时候,很少网站去特意适配移动端,然而用户是可以直接从手机去访问PC端网站的,所以怎样显示好一个网站,无论这个网站是一个PC网站还是移动端网站,就是亟需解决的问题。所以移动端三个视口布局视口、视觉视口、理想视口横空出世,成为各种移动适配方案的基础。

  • 布局视口

布局视口是在html元素之上的容器,我们的页面就“装”在布局视口中。
想想我们常写的width:100%,这个100%是基于什么计算出来的呢?去翻资料会看到:如果某些属性被赋予一个百分值的话,它的计算值是由这个元素的包含块计算而来的。那html元素的包含块是什么呢?没错,就是我们的布局视口,它是所有CSS百分比推算的根源,如果说CSS是一支画笔,那么布局视口就是那张画布吧。这张画布有一个默认尺寸(如果没有手动去设置meta viewport),一般在768px ~ 1024px间,可以通过document.documentElement.clientWidth获取。这样一来,网页的布局就不再受限于设备的尺寸,即使是小屏幕的移动端设备中也能容得下PC网站。
布局视口

  • 视觉视口

视觉视口是指用户通过设备屏幕看到的区域,可以通过缩放来改变视觉视口的大小,并通过window.innerWidth获取。
这里有必要讲一下缩放,缩放改变的是CSS像素的大小,放大时CSS像素增大,则一个CSS像素可以跨越更多的设备像素,视觉视口会变小。什么?放大反而视觉视口变小?没错,这是因为视觉视口也是通过CSS像素度量,而放大就是使CSS像素放大,假设屏幕上本来需要200个CSS像素才能占满屏幕,由于放大,现在只需要100个CSS像素就能占满,所以视觉视口的宽就变成100px。
虽然缩放改变了CSS像素的大小,但移动端的缩放是不会改变布局视口的,所以缩放并不会影响布局,不过在PC端是会影响布局的。最直观的感受是,我们平时在移动端双指缩放网页,整个网页的布局是没有变化的,可以通过拖动来看到不同区域的东西,但是在PC端进行缩放,比如阅读时想文字大一些而对网页进行放大操作,这时字是放大了,但整个页面的布局会有所改变。那么既然与布局视口无关那还跟谁有关系呢?答案就是下面准备要讲的理想视口,它们之间的计算方式是:缩放系数 = 理想视口宽度 / 视觉视口宽度
视觉视口

  • 理想视口

理想视口是指网站在移动设备中的理想大小,这个大小就是设备的屏幕大小。
为什么需要理想视口呢?首先,先来看看现在的情况是怎么的不理想。我们在浏览一个没经过移动适配的网站时,由于布局视口在768px ~ 1024px之间,整个网站就“画”在一个这么大的“画布”上,但由于手机屏幕比“画布”小,所以需要经过缩小才能塞进手机屏幕,结果我们浏览网站的时候虽然看得见全貌,但里面的东西都变得很小,需要放大一下才能看得清,就是这么不理想。如果不需要放大就可以看得清那就很理想了嘛。回想一下上面不理想的解决方案,就是将一个大画布经过缩小装进小屏幕里,假设现在画布跟屏幕一样大,就在这个画布上作画,岂不是很合适?
所以总结起来,理想视口说白了就是理想的布局视口,通过<meta name="viewport" content="width=device-width, initial-scale=1">来设置。

将它们连在一起

关联

认识Meta viewport

<meta> 元素可提供有关页面的元信息,不会显示在页面上,可以用来告诉浏览器怎样解析页面。<meta>可以设置的东西很多,但这里只讲vieport,它是所有移动适配方案的基础。
首先meta viewport的设置格式是<meta name="viewport" content="name=value,name=value",其中name的值可设为:

  1. width:将布局视口设置为固定的值,比如375px或者device-width(设备宽度)
  2. initial-scale:设置页面的初始缩放
  3. minimum-scale:设置最小的缩小程度
  4. maximum-scale:设置最大的放大程度
  5. user-scalable:设置为no时禁用缩放

虽然只有五个值,但仍有一些值得注意的点:

设置initial-scale的影响

根据公式缩放系数 = 理想视口宽度 / 视觉视口宽度 ,如果设置了initial-scale比如为0.5,那么以iPhone6为例,iPhone6的设备宽度是375px,即理想视口宽度也为375px,所以视觉视口宽度 = 375px(理想视口宽度)/ 0.5(缩放系数)。很明显设置了initial-scale就相当于初始化了视觉视口,而且会将布局视口初始化为这个视觉视口的值

width和initial-scale共存

上面说到设置了initial-scale相当于初始化了视觉视口和布局视口,但width用于指定布局视口的大小,那么一起设置的话听谁的呢?
还是以iPhone6为例,它的尺寸是667(h) * 375(w),如果设置<meta name="viewport" content="width=400, initial-scale=1">,执行一下console.log(`布局视口: ${document.documentElement.clientWidth}; 视觉视口: ${window.innerWidth}`)会得到“布局视口: 400; 视觉视口: 400”。
这时候旋转一下设备,这时尺寸变成了667(w) * 375(h),再执行一下console.log(`布局视口: ${document.documentElement.clientWidth}; 视觉视口: ${window.innerWidth}`)会得到“布局视口: 667; 视觉视口: 667”。
结论是:width与initial-scale都会初始化布局视口,但浏览器会取其最大值。

设置理想视口

这时候再看回<meta name="viewport" content="width=device-width, initial-scale=1">,明明width=device-width和initial-scale=1都是去初始化布局视口成理想的布局视口,只写其中一个不就完了嘛,为什么要两个都一起写呢?因为有的浏览器只设置其中一个,不能保证理想视口的尺寸能随着屏幕的旋转而正确改变,所以两个一起写只是为了解决兼容性问题。

舒服地还原移动端设计图

上面说了很多理论知识,其实就是为了能有一套方案舒服地还原移动端设计图,做出一个专为移动端访问的页面。

经典的问题

  • 图片

这里的图片问题是指高清/Retina屏下图片会显示得比较模糊,这是因为我们平时使用的图片大多数是png、jpg这样格式的图片,它们称作是位图图像(bitmap),是由一个个像素点构成,缩放会失真。上面讲像素的时候说过,这种高清/Retina屏DPR大于一,则一像素横跨了多个设备像素,而位图图像需要一个像素点对应一个设备像素才清晰。所以假设一张100 x 100的图片放在普通屏上看是清晰的,放到高清/Retina屏上就会显得比较模糊,那是因为本来100 x 100的图片在普通屏上图片像素与设备像素一一对应,而到了高清/Retina屏上一个图片像素却要对应多个设备像素,这样一来看起来图片就比较模糊。
图片问题
如图所示,如果一个图片像素要对应多个设备像素的话,那这些设备像素只能显示成跟这个图片像素差不多的颜色,导致看起来会模糊。
既然知道了问题产生的原因,那解决方法也很简单,位图图像需要一个像素点对应一个设备像素才清晰嘛,那就本来是100 x 100的图片在DPR为1的屏幕上显示清晰,在DPR为2的屏幕上显示模糊,那就在DPR为2的屏幕上放200 x 200的图好了,这样就一一对应了。

  • 1px边框

1px边框
“你看看设计图这根线是很细的,为什么你实现出来那么粗,看起来很劣质的感觉。”
没道理呀,设计图量的是1px,css写的也是1px,怎么会粗了呢?一般设计师出图的时候,都会按照一个尺寸作为标准来出图,比如按照iPhone6的尺寸出图,就是一张750px宽的设计图,这个750px其实就是iPhone6的设备像素,在测量设计图时量到的1px其实是1设备像素,而当我们设置<meta name="viewport" content="width=device-width, initial-scale=1">时,布局视口等于理想视口等于375px,并且由于iPhone6的DPR为2,写css时的1px对应的是2设备像素,所以看起来会粗一点。
那么只要写0.5px就是对应1设备像素了嘛。是的,道理是这么说,但是很多浏览器并不支持0.5px的写法,导致显示不出来,但不要紧,网上很多方法解决这个问题的方法就不细说了,这里只是讲清楚1px边框问题产生的原因。

还原设计图

因为PC端屏幕一般都会比设计图尺寸要大,所以只需要居中固定一个内容区用于显示设计图的内容,其余多出的地方留白即可。而移动端屏幕有大有小,设计图一般会以一款机型为标准来出图,比如说iPhone6的尺寸,如果不经处理直接量设计图就开干会出现什么问题呢?
对比
(从左到右为iPhone4、iPhone6、iPhone plus)
可以看到以iPhone6为标准出的设计图测量出来350px x 350px的元素在iPhone6上写width: 350px;height: 350px;是刚刚好的,左右的间隙各有10px,但小一点的屏幕iPhone4横向滚动条都出来了,而plus左右间隙明显比10px大很多,这样一来不同尺寸的屏幕出来的效果跟设计图的效果就会有不同程度的出入,这并不是我们想要的,我们想要的是不同尺寸的屏幕显示的效果与设计图比例是一致的。
既然想要的是不同屏幕尺寸显示的比例与设计图一致,那么显然适配方案就是等比缩放
(以下代码都是为了讲述原理,没有过多的细节考虑与测试,不能用于生产环境)

  • viewport方案

说到缩放,首先想到的当然是initial-scale。回想一下initial-scale的作用:设置了initial-scale就相当于初始化了视觉视口,而且会将布局视口初始化为这个视觉视口的值。那么我们是不是可以以设计图为基准等比缩放布局视口从而适配呢?

<script>
    const scale = window.screen.width / 750
    document.write(`<meta name="viewport" content="initial-scale=${scale}">`)
</script>

这种方式进行适配优点是简单粗暴,缺点是太简单粗暴了,因为viewport的设置是影响全局的,这样一来虽然可以直接将设计图量得的尺寸写到css上,但如果有一些需要地方不需要等比缩放而需要设置固定尺寸,比如要求在不同尺寸屏幕上显示固定大小的文字,或者你引进了一个库,里面的有样式你也不知道人家是按照怎样的适配方案进行适配的,那么到了你的项目里由于全局的viewport缩放,可能会影响到这个库的显示效果。

  • rem方案

不同于px是固定尺寸单位,rem是相对单位,相对于html标签字体大小的单位。比如html标签的font-size为100px,那么1rem就等于100px。借助rem这个相对单位我们同样可以达到等比缩放的效果。

  • 这个方案不需要对viewport进行缩放,所以首先按照惯例我们让布局视口等于理想视口:<meta name="viewport" content="width=device-width, initial-scale=1">
  • 还是以iPhone6的设备像素为标准的设计图,宽是750px,假设以设计图为标准的html标签的font-size为100px,所以1rem = 100px,那么这个设计图总宽就有7.5rem
  • 以总宽是7.5rem的设计图为标准,则不同屏幕尺寸的总宽应该也是7.5rem,由于上面设置了布局视口等于理想视口,所以以iPhone6为例,iPhone6的布局视口等于理想视口,则它的布局视口为375px(也就是总宽7.5rem),现在只需要解决在布局视口为375px的情况下,html的font-size需要设置多少。很简单,html font-size * 7.5 = 375,那么font-size为50px。
  • 拓展到其他屏幕document.documentElement.style.fontSize = `${document.documentElement.clientWidth / 7.5}px`
  • 现在我们只需要测量设计图,比如设计图有一个300px的元素,那我们写css的时候就写成3rem(由于以1rem = 100px为基准,所以这里300px / 100即可)

使用这个方案,我们只对需要等比缩放的元素使用rem,而要求固定尺寸的地方使用px即可,这样一来相对于viewport方案来说就比较灵活,可以按需使用而不是一刀切。不过这种方案写css的时候可能会没那么直观,成本可能会高一点点,但是借助构建工具或者less/sass可以解决,毕竟现在应该很少项目不使用这些工具的了吧。

  • 加强版rem方案

这里所说的加强版rem方案其实就是手淘的Flexible方案(也类似移动端高清、多屏适配方案),究竟加强了什么呢?那就是,通过设置viewport进而全局解决1px边框问题。
既然要通过设置viewport来解决1px边框问题,那设置这个viewport的方式肯定内有乾坤:

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) {
        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;
}

得出的scale用于设置viewport的缩放document.write(`<meta name="viewport" content="initial-scale=${scale}">`),这样一来,对于Retina屏将viewport缩放为1 / dpr最终产生的效果是,1px css像素严格等于1px 设备像素,由此解决了1px边框问题。那为什么只对iPhone进行缩放呢?请看大漠老师的文章再谈Retina下1px的解决方案
其他与rem相关的配置与上面的rem方案类似,这里就不再展开说了。
这个加强版rem方案最大的优势是解决了1px边框问题,但由此也进行了viewport的缩放,仍然会面临着上面说的viewport方案涉及到的一些影响,为此该方案会通过给html设置data-dpr

document.documentElement.setAttribute('data-dpr', dpr)

从而写css的时候可以针对不同的dpr固定设置尺寸:

.test {
    width: 1rem; 
    height: 2rem;
    font-size: 12px; 
}
[data-dpr="2"] .test {
    font-size: 13px;
}
[data-dpr="3"] .test {
    font-size: 14px;
}
  • vw方案

vw也是一个相对单位,它相对的是布局视口,1vw就是1%的布局视口宽度。其实rem方案就是在模拟vw,来看看使用vw怎么做。

  1. 还是熟悉的iPhone6标准设计图,宽750px。那么1vw = 1%视口宽度的话,按设计图来说就是100vw = 750px,则1vw = 7.5px。
  2. 设计图量得一个元素是100px,css需要写成 Xvw * 7.5 = 100,所以X就等于13.3vw。
  3. 计算的话还是交给构建工具即可,详细请看再聊移动端页面的适配

rem方案有的优势vw也有,而且也不会像rem那么绕,但就是兼容性不够rem好,长远来看vw最后会接棒rem作为移动适配的主力,因为它生来就干这个事情呢。

终于结束了

没有银弹。
全局viewport缩放方案很粗暴?但对于要求不高也不需要兼顾固定尺寸的页面,上来就全局缩放,拿起设计稿就可以写代码了。要求高又想灵活,还会怕构建的那一点点麻烦吗?rem方案走起。兼容性不需要考虑,那vw方案直白又优雅不试试看吗?方案没有优劣之分只有合适与否。
最后,如果有说得不对的地方,还望指正。


写Bug
5.4k 声望2.2k 粉丝