# 👉 移动适配方案问题总结

# 移动端适配原理

移动端多屏幕适配是为了保证各个屏幕上的布局视口都是等于屏幕(视觉上的)宽度。即width(layoutVieport)= device-width

理想的网页展示应该是用户进入页面不需要手动缩放就能够看清页面的内容也没有横向滚动条。

要如何去实现移动端网页展示达到理想效果涉及到很多因素,但主要包括三个方面:像素(pixels)、屏幕(screen)、视口(viewport)、缩放(scale)。

首先熟悉以下常见的几个概念。

# 像素

# 物理像素 (physical pixel) / 设备像素

设备屏幕实际拥有的像素点,屏幕的基本单元,是有实体的。iphone6/7/8 物理像素为 750 * 1334,总共有 750 * 1334 个像素点。

# 设备像素独立像素 DIP (device-independent pixel) / CSS 像素 / 逻辑像素

反映在 CSS/JS 程序里面的像素点, css 的 px 是逻辑像素的一种。

iphone6/7/8 物理像素为 750 * 1334,逻辑像素是 375 * 667

CSS 像素面积随着浏览器的缩放比例而同步缩放。

缩放比为 100%时,一个 CSS 像素占多少个设备像素是由设备像素比(DPR)决定的。

DPR 为 2,则占两个,DPR 为 3 则占三个。即 1 个 css 像素对应(覆盖)的物理像素个数。

# 设备像素比 DPR (device pixel ratio)

设备物理像素和设备独立像素的比例,也就是 DevicePixelRatio = 物理像素 / 独立像素

iphone6/7/8 物理像素为 750 * 1334,逻辑像素是 375 * 667。那设备像素比是 2(750 / 375)。

CSS 中的 px 就可以看做是设备的独立像素,所以通过 devicePixelRatio,我们可以知道该设备上一个 CSS 像素代表多少个物理像素。CSS 中的像素只是一个抽象的单位,在不同的设备中 1px 所代表的设备物理像素是不同的。

UI 设计稿 2 倍图、3 倍图,这个倍就是指设备像素比。例如设计稿是 2 倍图,里面的字体是 24 px,那我们用 24 / 2 就可以得出开发要用的像素为 12 px。

  • DPR 取值可以通过window.devicePixelRatio获取。

  • 媒体查询时:

    @media only screen and (-webkit-min-device-pixel-ratio: 1),
        only screen and (min-device-pixel-ratio: 1) {
    }
    

# 位图像素

一个位图像素是栅格图像(如:png, jpg, gif 等)最小的数据单元。每一个位图像素都包含着一些自身的显示信息(如:显示位置,颜色值,透明度等)。

对于 DPR=2 的 retina 屏幕而言,1 个位图像素对应于 4 个物理像素,由于单个位图像素不可以再进一步分割,所以只能就近取色(导致使用一倍图图片会模糊原因)。所以,对于图片高清问题,比较好的方案就是两倍图片(@2x)。如:200×300(css pixel)img 标签,就需要提供 400×600 的图片。如此一来,位图像素点个数就是原来的 4 倍,在 retina 屏幕下,位图像素点个数就可以跟物理像素点个数形成  1 : 1 的比例,图片自然就清晰了(这也解释了之前留下的一个问题,为啥视觉稿的画布大小要 ×2?)。

# 屏幕

屏幕分辨率指一个屏幕具体由多少个像素点组成(设备像素)。iPhone 6 的屏幕分辨率为750x1334。这表示手机分别在水平和垂直上所具有的像素点数。

屏幕的完整大小用设备像素来衡量,单位是物理属性的单位,做开发的时候也不需要太过关注,我们关注的是 CSS 像素。

一般就可以理解为这个单位和 CSS 像素的大小在缩放比为 100%时是一样大的。我们可以在 chrome 这类下载浏览器中能够通过 screen.width/height

当然分辨率高不代表屏幕就清晰,屏幕的清晰程度还与尺寸有关。

# PPI(Pixel Per Inch)

每英寸包括的像素数,屏幕像素密度。 PPI 可以用于描述屏幕的清晰度以及一张图片的质量。PPI 越高质量越高。一般来说,高像素密度的屏,屏幕分辨率也越大。

# DPI(Dot Per Inch)

即每英寸包括的点数。使用 DPI 来描述图片和屏幕,这时的 DPI 应该和 PPI 是等价的,DPI 最常用的是用于描述打印机,表示打印机每英寸可以打印的点数。

在显示器中,dpi=ppi。dpi 强调的是每英寸多少点。同时,屏幕像素密度=分辨率/屏幕尺寸

# viewport 视口

先附上一张关于浏览器视口的图:

视口

PC 浏览器的 viewport 视口指可视区域,移动端的 viewport 指布局视口,即 layout viewport,默认的情况下,布局视口的宽度是要远远大于浏览器的宽度的。

# layout viewport

布局视口,meta-viewport 设置的 width 就是 layoutviewport。

在 PC 端上,布局视口等于浏览器窗口的宽度。

而在移动端上,由于要使为 PC 端浏览器设计的网站能够完全显示在移动端的小屏幕里,浏览器厂商一般将其设为 980px 然后进行缩放,让网页在窄屏幕上能够全部显示。此时的布局视口会远大于移动设备的屏幕,就会出现滚动条。

  • 布局视口尺寸可以通过 document.documentElement.clientWidth(不包括了滚动条)

  • 同时我们做的媒体查询中判断的 max-width 也是判断布局视口的宽度。

  • 虽然 document.documentElement 就是 html 元素,但是取值却不是取的 html 元素的宽度。

  • 取值 max(width(viewport设置的width), ideaViewport宽度/scale)

# visual viewport

可见视口,会根据屏幕大小缩放变化;

也可以理解成是用户正在看到的网页的区域。用户可以通过缩放来查看网站的内容。如果用户缩小网站,我们看到的网站区域将变大,此时视觉视口也变大了,同理,用户放大网站,我们能看到的网站区域将缩小,此时视觉视口也变小了。不管用户如何缩放,都不会影响到布局视口的宽度。

  • 取值  visual viewport = idea viewpoint / scale = ideal viewport * dpr
  • 通过  window.innerWidth(包括滚动条)可获取;

# idea viewport

理想视口,布局视口的一个理想尺寸,只有当布局视口的尺寸等于设备屏幕的尺寸时,才是理想视口。

为浏览器定义的可完美适配移动端的理想 viewport,固定不变,可以认为是设备视口宽度。
比如:iphone 6/7/8 为 375px, iphone 7p 为 414px。

那么怎么才能得到 ideal viewport 呢?这就该轮到 meta 标签出场了。

# meta - viewport

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">

该 meta 标签的作用是把当前的 viewport 宽度设置为 ideal viewport 的宽度。

# initial-scale

缩放的基准值,因此 initial-scale=1 和 width=device-width 效果一样。

// 其实这两句话和上面达到的效果都是一样的,都可以把当前的的viewport变为ideal
viewport
// 指定布局视口宽度=设备屏幕宽度,指定了布局视口就是理想视口
<meta name="viewport" content="width=device-width" />

// 这样设置跟上面的width=device-width设置产生的效果是一样的。
<meta name="viewport" content="initial-scale=1" />

其中:
1)width:设置的是 layout viewport 宽度。
meta viewport 中把 width 设为 device-width,就将 layout viewport 的宽度设为移动设备的屏幕宽度(也就是 ideal viewport)。

2)initial-scale:设置初始缩放值。
这个缩放值相对于 ideal viewport 来缩放的(并不是说改变理想视口大小);
缩放值越大(没有设置 width=device-width 的前提下),当前布局视口document.documentHtml.clientWidth跟可视视口window.clientWidth的宽度都会越小,反之亦然。

缩放值为 1 的时候就表示当前 viewport 的宽度设为 ideal viewport 的宽度,这个时候,逻辑像素 = CSS 像素宽度 = 理想视口的宽度 = 布局视口的宽度。

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

单独设置 initial-scale 或 width 都会有兼容性问题,所以设置布局视口为理想视口的最佳方法是同时设置这两个属性。如果两个值有冲突,layout viewport 则取两值中的最大值。

# Rem 原理

Rem 相对于文档的根元素 html 的 front-size;em 相对于父元素的front-size

# flexible.js

  • flexible.js 主要就是利用 Rem 对页面元素进行适配,其中:
  1. 把宽高是 px 的转换成 rem
    为了保证在不同宽度尺寸的设备中能够保证布局的等比例缩放。

  2. 字体使用 px 而不使用 rem
    因为使用 rem 后由于不同的尺寸,换算之后出现各种奇奇怪怪的数值,最为明显的就是更多的小数位,比如 13.755px 之类的数值,这些时候会被浏览器四舍五入而字体大小导致不精确。
    由于 Retina 屏幕下 DPR 各不相同,我们又想显示的字体一样大,于是就给字体再增大 DPR 的倍数,这样当缩小 DPR 倍时,那么字体也就和设计稿所示的大小一样大了,在不同的手机中显示的大小也是一致的。

其中 flexible 仅仅只是针对 iPhone 进行适配,而默认所有的安卓设备都强制性设置 DPR 为 1。

  • flexible.js 主要原理:
  1. 根据 dpr 值来修改根 html 元素的 font-size,模拟 vw 将页面分成十分,然后使用 rem 实现等比缩放,适配布局。

一般 1rem 通常会被设置为设备可视区域/10。

现在设计稿上有一块区域宽 B,那它是不是等比放到设备可视区域的宽度为 B/A rem。即 B 在设计稿上占 B/A 份,那在设备可视区域上也要占 B/A 份,所以宽是 B/A rem。

比如:设备可视区域为 375 像素,1rem = 37.5rem,一个 div 元素为宽高 75px 的正方形,那 div 的 widht/height 都应该被设置为 2rem。同理,这份代码放到 750 像素的设备里面,1rem 会被重新计算为 75,因为这个正方形 div 被设置成了 widht/height=2rem,因此也会自动被等比例放大两倍,变成了一个 150px 的正方形,以此来实现等比例缩放的效果。

// * dpr 是因为initial-scale值有可能被进行了 1/dpr 的缩放,所以需要乘上这个dpr值来保证等比例缩放,不缩放时默认为1
rem = (document.documentElement.clientWidth * dpr) / 10;
  1. 根据 dpr 值来修改 viewport,实现 1px 线(压缩 px,使 1px=1 个物理像素)

对于 dpr=2 的屏幕,1px=2 个物理像素,会让 1px 的线看起来比设计稿更粗,因此需要让 1px = 1 个物理像素。

这个时候可以通过将缩放比 initial-scale 设置为 0.5 (1/2) 来实现。以此类推 dpr=3 的屏幕可以将 initial-scale 设置为 0.33=1/3 来实现。

通过借助 JS 的 window.devicePixelRatio 去动态获取设备的 DPR,然后设置 initial-scale = 1/dpr

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');

# vw/vh 原理

基于 css3 中 Viewport 相关采用 vw、vh、vmin 和 vmax 单位。

vw – 视区宽度百分值
vh – 视区高度百分值
vmin – vw或vh,取小的那个
vmax – vw或vh,取大的那个

1vw = window.innerWidth * 1%;
1vh = window.innerHeight * 1%;

即 1vw 在可视视口宽度为 400px 宽时是 4px,在可视视口宽度为 1000px 时,就变成了 10px,自动会进行等比例缩放。

使用postcss-px-to-viewport可实现编译时候的自动帮你转换为 vw,只需要在配置时指定设计图宽度就可以了。

# 1px 方案

# 产生原因

这个 1px 是指 1 个 CSS 像素,在有些设备上(比如 dpr=3),就是用了 3 个物理像素矩阵(即:3x3=9 个 CSS 像素)来显示这 1px,导致在这些设备上,这条线看上去非常粗。

但设计师想要的 retina 下 border: 1px;,其实就是 1 物理像素宽,对于 css 而言,应该是 1/3px 显示这条线。

# 解决方案

# flexible 动态设置initial-scale值为 1/dpr

<meta name="viewport" content="initial-scale=1/dpr" />

# 伪元素 + transform scale

/* 1px线/单边边框 - 上边框*/
.setOnePx {
    position: relative;
    &::after {
        content: "";
        position: absolute;
        width: 100%;
        height: 1px;
        top: 0;
        left: 0;
        transform: scale(1, 0.5);
        background: red;
    }
}

/* 4条边 边框的圆角 */
.contain::after {
    content: " ";
    position: absolute;
    top: 0;
    left: 0;
    width: 200%;
    height: 200%;
    transform: scale(0.5);
    transform-origin: left top;
    box-sizing: border-box;
    border: 1px solid #e5e5e5;
    border-radius: 4px;
}

通过 less 抽离的方案:

.border(
    @borderWidth: 1px;
    @borderStyle: solid;
    @borderColor: @lignt-gray-color;
    @borderRadius: 0) {
    position: relative;
    &::after {
        content: "";
        position: absolute;
        width: 100%;
        height: 1px;
        top: 0;
        left: 0;
        transform-origin: left top;
        -webkit-transform-origin: left top;
        box-sizing: border-box;
        pointer-events: none;
    }
    @media (-webkit-min-device-pixel-ratio: 2) {
        &::after {
            width: 200%;
            height: 200%;
            -webkit-transform: scale(0.5);
        }
    }
    @media (-webkit-min-device-pixel-ratio: 2.5) {
        &::after {
            width: 250%;
            height: 250%;
            -webkit-transform: scale(0.4);
        }
    }
    @media (-webkit-min-device-pixel-ratio: 2.75) {
        &::after {
            width: 275%;
            height: 275%;
            -webkit-transform: scale(1 / 2.75);
        }
    }

    @media (-webkit-min-device-pixel-ratio: 3) {
        &::after {
            width: 300%;
            height: 300%;
            transform: scale(1 / 3);
            -webkit-transform: scale(1 / 3);
        }
    }
    .border-radius(@borderRadius);

    &:before {
        border-width: @borderWidth;
        border-style: @borderStyle;
        border-color: @borderColor;
    }
}

.border-all(
	@borderWidth: 1px;
	@borderStyle: solid;
	@borderColor: @lignt-gray-color;
	@borderRadius: 0) {
    .border(@borderWidth; @borderStyle; @borderColor; @borderRadius);
}

# box-shadow

box-shadow:  x偏移量 | y偏移量 | 阴影模糊半径 | 阴影扩散半径 | 阴影颜色

0 -1px 1px -1px #e5e5e5, //上边线

0 1px 1px -1px #e5e5e5, //下边线

1px 0 1px -1px #e5e5e5, //右边线

-1px 0 1px -1px #e5e5e5; //左边线

# background-image