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

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

纠缠不清的关系

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

像素

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

  • 设备像素

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

  • 设备独立像素

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

  • DPR

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

视口

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

  • 布局视口

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

  • 视觉视口

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

  • 理想视口

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

将它们连在一起

移动适配探讨 - 图4

认识 Meta viewport

  • width:将布局视口设置为固定的值,比如 375px 或者 device-width(设备宽度)

  • initial-scale:设置页面的初始缩放

  • minimum-scale:设置最小的缩小程度

  • maximum-scale:设置最大的放大程度

  • 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 都会初始化布局视口,但浏览器会取其最大值。

设置理想视口

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

舒服地还原移动端设计图

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

经典的问题

  • 图片

这里的图片问题是指高清 / Retina 屏下图片会显示得比较模糊,这是因为我们平时使用的图片大多数是 png、jpg 这样格式的图片,它们称作是位图图像(bitmap),是由一个个像素点构成,缩放会失真。上面讲像素的时候说过,这种高清 / Retina 屏 DPR 大于一,则一像素横跨了多个设备像素,而位图图像需要一个像素点对应一个设备像素才清晰。所以假设一张 100 x 100 的图片放在普通屏上看是清晰的,放到高清 / Retina 屏上就会显得比较模糊,那是因为本来 100 x 100 的图片在普通屏上图片像素与设备像素一一对应,而到了高清 / Retina 屏上一个图片像素却要对应多个设备像素,这样一来看起来图片就比较模糊。
移动适配探讨 - 图5

如图所示,如果一个图片像素要对应多个设备像素的话,那这些设备像素只能显示成跟这个图片像素差不多的颜色,导致看起来会模糊。
既然知道了问题产生的原因,那解决方法也很简单,位图图像需要一个像素点对应一个设备像素才清晰嘛,那就本来是 100 x 100 的图片在 DPR 为 1 的屏幕上显示清晰,在 DPR 为 2 的屏幕上显示模糊,那就在 DPR 为 2 的屏幕上放 200 x 200 的图好了,这样就一一对应了。

  • 1px 边框

移动适配探讨 - 图6

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

还原设计图

因为 PC 端屏幕一般都会比设计图尺寸要大,所以只需要居中固定一个内容区用于显示设计图的内容,其余多出的地方留白即可。而移动端屏幕有大有小,设计图一般会以一款机型为标准来出图,比如说 iPhone6 的尺寸,如果不经处理直接量设计图就开干会出现什么问题呢?
移动适配探讨 - 图7

(从左到右为 iPhone4、iPhone6、iPhone plus)
可以看到以 iPhone6 为标准出的设计图测量出来 350px x 350px 的元素在 iPhone6 上写width: 350px;height: 350px;是刚刚好的,左右的间隙各有 10px,但小一点的屏幕 iPhone4 横向滚动条都出来了,而 plus 左右间隙明显比 10px 大很多,这样一来不同尺寸的屏幕出来的效果跟设计图的效果就会有不同程度的出入,这并不是我们想要的,我们想要的是不同尺寸的屏幕显示的效果与设计图比例是一致的。
既然想要的是不同屏幕尺寸显示的比例与设计图一致,那么显然适配方案就是等比缩放
(以下代码都是为了讲述原理,没有过多的细节考虑与测试,不能用于生产环境)

  • viewport 方案

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

  1. <script>
  2. const scale = window.screen.width / 750
  3. document.write(`<meta name="viewport" content="initial-scale=${scale}">`)
  4. </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 的方式肯定内有乾坤:

  1. if (!dpr && !scale) {
  2. var isAndroid = win.navigator.appVersion.match(/android/gi);
  3. var isIPhone = win.navigator.appVersion.match(/iphone/gi);
  4. var devicePixelRatio = win.devicePixelRatio;
  5. if (isIPhone) {
  6. if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
  7. dpr = 3;
  8. } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
  9. dpr = 2;
  10. } else {
  11. dpr = 1;
  12. }
  13. } else {
  14. // 其他设备下,仍旧使用1倍的方案
  15. dpr = 1;
  16. }
  17. scale = 1 / dpr;
  18. }

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

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

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

  1. .test {
  2. width: 1rem;
  3. height: 2rem;
  4. font-size: 12px;
  5. }
  6. [data-dpr="2"] .test {
  7. font-size: 13px;
  8. }
  9. [data-dpr="3"] .test {
  10. font-size: 14px;
  11. }
  • 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 方案直白又优雅不试试看吗?方案没有优劣之分只有合适与否。
最后,如果有说得不对的地方,还望指正。
https://segmentfault.com/a/1190000017784801