移动端适配

前置基础

屏幕尺寸

屏幕尺寸是以屏幕对角线的长度来计量,计量单位为英寸。

像素

像素(pixel),为组成一幅图像的全部亮度和色度的最小图像单元

一幅图像通常包含成千上万个像素,每个像素都有自己的颜色信息,它们紧密地组合在一起。

由于人眼的错觉,这些组合在一起的像素被当成一幅完整的图像。

当修改图像的某区域,实际上是在修改该区域内的像素。对这些像素修改的好与坏将决定最终图片的质量。

单位面积内的像素越多,图像的效果就越好。

彩色电视图像是由成千个像素点所组成的,而且每个像素都是由红绿蓝三种颜色并排组成的。

(注意每个像素的大小是不固定的,他是根据设备的分辨率决定的)

分辨率

屏幕分辨率是指纵横向上的像素点数,单位是px

屏幕分辨率确定计算机屏幕上显示多少信息的设置,以水平和垂直像素来衡量。

就相同大小的屏幕而言,当屏幕分辨率低时(例如 640 x 480),在屏幕上显示的像素少,单个像素尺寸比较大。

屏幕分辨率高时(例如 1600 x 1200),在屏幕上显示的像素多,单个像素尺寸比较小。

设备物理分辨率(设备像素)

相信我们所有前端开发者,都是见证了手机这个移动设备发展的过程。从蓝屏手机,到彩屏手机,到诺基亚研发出来触屏手机,再到智能手机一步步发展下来,我们的我们的手越来越清晰,越来越大,所以我们的屏幕发展也越来越迅速。

从最初的颗粒感相当大的屏幕,到720p再到1080p,甚至于现在各家旗舰手机的2k屏幕,我们的物理分辨率在变得原来越大。这样就暴露出来一个问题,我们如果手机分辨率翻倍,我们的图像不就要被缩小一倍,我们难道要在每个设备上就出个设计稿,每个设备的分辨不尽相同啊,其实你担忧的问题,我们的乔帮主在很多年前就想到了。这就是我们的逻辑分辨率。

逻辑分辨率(设备独立像素)

虽然设备物理分辨不同,但是他的这个逻辑分辨率却都差不多,这就要感谢乔帮主了

乔布斯在iPhone4的发布会上首次提出了Retina Display(视网膜屏幕)的概念,在iPhone4使用的视网膜屏幕中,把2x2个像素当1个像素使用,这样让屏幕看起来更精致,但是元素的大小却不会改变。从此以后高分辨率的设备,多了一个逻辑像素。这些设备逻辑像素的差别虽然不会跨度很大,但是仍然有点差别,于是便诞生了移动端页面需要适配这个问题,既然逻辑像素由物理像素得来,那他们就会有一个像素比值

设备像素比

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

在 CSS 中经常写的 px 就是指逻辑像素,它和物理像素并不一定是一一对应的,物理像素和逻辑像素之间的对应关系会有 DPR 决定。

1px边框问题

关于前端1px像素的问题,网上已经有很多相关的文章了,但是,关于这个问题的原因网上没有几个说到点子上的,甚至还大谈dpr。。。

试问,如果是dpr的原因,那为什么只有1px才出现视觉效果变粗的问题,而10px、20px的没有?

其实这个问题的原因和dpr没有任何关系,dpr可以用来解释不同分辨率手机呈现页面的精细度的差异,但并不能解释1px问题。

我们做移动端页面时一般都会设置meta viewport的content=“width=device-width”,这里就是把html视窗宽度大小设置等于设备宽度的大小,大多数手机的屏幕设备宽度都差不多,以iphoneX为例,屏幕宽度375px。

而UI给设计图的时候基本上都是给的二倍图甚至三倍图,假设设计图是750px的二倍图,在750px上设计了1px的边框,要拿到375px宽度的手机来显示,就相当于整体设计图缩小一倍,所以在375px手机上要以0.5px呈现才符合预期效果,然而css里最低只支持1px大小,不足1px就以1px显示,所以你看到的就显得边框较粗,实际上只是设计图整体缩小了,而1px的边框没有跟着缩小导致的。(ps:ios较新版已支持0.5px,安卓不支持,这里暂且忽略)。

简而言之就是:

多倍的设计图设计了1px的边框,在手机上缩小呈现时,由于css最低只支持显示1px大小,导致边框太粗的效果。

如何解决

解决方法有很多,根据项目环境和使用场景选择最合适的就行,下面整理了一下解决方式:

通过设置meta标签viewport

分析1px像素产生原因时,有说到meta标签设置的width=device-width,其实这也是产生1px像素问题的前提条件之一,无论你是rem适配方式还是媒体查询的响应式布局,你最终在375px的总宽度下,边框最小css单位也只能是1px,而750px的设计图里1px占1/750,375px里1px占1/375,比例大了一倍,视觉上肯定是粗了。

所以,如果设置content的width就等于设计图大小750px,然后通过动态设置initial-scale值让网页整体缩放,就能实现效果了,这也是适配移动端不同屏幕大小的一种思路。

以iphoneX为例,

1
<meta name="viewport" content="width=750,initial-scale=0.5,user-scalable=no">

这样就能让iphoneX完美还原750px的设计图了,initial-scale的值动态设置为window.screen.width / 750,iphoneX下就是0.5。

不过这样设置后,在和其他content属性width值不同的页面间来回切换会出现横向滚动条,不推荐使用(这里介绍这种方法也是帮助更好的理解1px像素问题)。

通过transform: scale()缩放(推荐)

单边 以下边框为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
.div::after {
content: '';
box-sizing: border-box;
position: absolute;
z-index: 1;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
border-bottom: 1px solid #bfbfbf;
transform: scaleY(1/2);
transform-origin: left bottom;
}

如果还需要添加其他边框,可以用before伪元素再加,例如再加个右边框:

1
2
3
4
5
6
7
8
9
10
11
12
13
.div::before {
content: '';
box-sizing: border-box;
position: absolute;
z-index: 1;
right: 0;
bottom: 0;
width: 2px;
height: 100%;
border-right: 1px solid #bfbfbf;
transform: scaleX(1/2);
transform-origin: right bottom;
}

所有边 此种方式可以添加边框圆角:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 .div::after {
content: '';
box-sizing: border-box;
position: absolute;
z-index: 1;
left: 0;
top: 0;
width: 300%;
height: 300%;
border: 1.5px solid #bfbfbf;
transform: scale(1/1.5/2);
transform-origin: left top;
border-radius: 6px;
}

备注:

注意需要先给目标div元素添加非static的position属性,因为以上方法都是利用伪元素定位实现的。

具体的scale缩放比例取决于你是几倍设计图尺寸下的1px,例如二倍图(750px)尺寸下,缩放比例就是1/2(敲黑板:scale缩放比例和dpr无关)。

网上的常规代码会有个别安卓机型边框不显示的问题,我自己的代码已经做了各种兼容处理:单边方案就给伪元素1px的内容区域高度或宽度,加上边框就是2px;所有边的话就基于1.5px边框宽度来进行缩放。

viewport

视口(viewport)代表当前可见的计算机图形区域。在Web浏览器术语中,通常与浏览器窗口相同,但不包括浏览器的UI, 菜单栏等——即指你正在浏览的文档的那一部分。

而移动端则较为复杂,它涉及到三个视口:布局视口(Layout Viewport)、视觉视口(Visual Viewport)和理想视口(Ideal Viewport)。

布局视口(layout viewport)

一般移动设备的浏览器都默认设置了一个 viewport 元标签,定义一个虚拟的布局视口(layout viewport),用于解决早期的页面在手机上显示的问题。iOS, Android 基本都将这个视口分辨率设置为 980px,所以 PC 上的网页基本能在手机上呈现,只不过元素看上去很小,一般默认可以通过手动缩放网页。

布局视口的宽度/高度可以通过 document.documentElement.clientWidth / Height 获取。

可以看到,默认的布局视口宽度为 980px。如果要显式设置布局视口,可以使用 HTML 中的 meta 标签:

1
<meta name="viewport" content="width=400">

布局视口使视口与移动端浏览器屏幕宽度完全独立开。CSS 布局将会根据它来进行计算,并被它约束。

视觉视口(visual viewport)

视觉视口是用户当前看到的区域,用户可以通过缩放操作视觉视口,同时不会影响布局视口。

视觉视口和缩放比例的关系为:

1
当前缩放值 = 理想视口宽度  / 视觉视口宽度

所以,当用户放大时,视觉视口将会变小,CSS 像素将跨越更多的物理像素。

理想视口(ideal viewport)

布局视口的默认宽度并不是一个理想的宽度,于是 Apple 和其他浏览器厂商引入了理想视口的概念,它对设备而言是最理想的布局视口尺寸。显示在理想视口中的网站具有最理想的宽度,用户无需进行缩放。

理想视口的值其实就是屏幕分辨率的值,它对应的像素叫做设备逻辑像素(device independent pixel, dip)。dip 和设备的物理像素无关,一个 dip 在任意像素密度的设备屏幕上都占据相同的空间。如果用户没有进行缩放,那么一个 CSS 像素就等于一个 dip。

用下面的方法可以使布局视口与理想视口的宽度一致:

1
<meta name="viewport" content="width=device-width">

实际上,这就是响应式布局的基础。

视口的设置

我们可以使用视口元标签(viewport meta 标签)来进行布局视口的设置。

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

下面是每个属性的详细说明:

属性名取值描述
width正整数或device-width定义视口的宽度,单位为像素
height正整数或device-height定义视口的高度,单位为像素,一般不用
initial-scale[0.0-10.0]定义初始缩放值
minimum-scale[0.0-10.0]定义放大最大比例,它必须小于或等于maximum-scale设置
maximum-scale[0.0-10.0]定义缩小最小比例,它必须大于或等于minimum-scale设置
user-scalableyes / no定义是否允许用户手动缩放页面,默认值 yes

有几点值得注意:

  • viewport 标签只对移动端浏览器有效,对 PC 端浏览器是无效的
  • 当缩放比例为 100% 时,dip 宽度 = CSS 像素宽度 = 理想视口的宽度 = 布局视口的宽度
  • 单独设置 initial-scale 或 width 都会有兼容性问题,所以设置布局视口为理想视口的最佳方法是同时设置这两个属性
  • 即使设置了 user-scalable = no,在 Android Chrome 浏览器中也可以强制启用手动缩放

解决适配方法

rem适配

em:在 font-size 中使用是相对于父元素的字体大小,在其他属性中使用是相对于自身的字体大小,如 width。

rem是CSS3新增的一个相对单位,这个单位引起了广泛关注。这个单位与em有什么区别呢?

区别在于使用rem为元素设定字体大小时,仍然是相对大小,但相对的只是HTML根元素

这个单位可谓集相对大小和绝对大小的优点于一身,通过它既可以做到只修改根元素就成比例地调整所有字体大小,又可以避免字体大小逐层复合的连锁反应。

目前,除了IE8及更早版本外,所有浏览器均已支持rem。对于不支持它的浏览器,应对方法也很简单,就是多写一个绝对单位的声明。这些浏览器会忽略用rem设定的字体大小。

举个例子:

1
2
3
4
5
6
7
8
9
//假设我给根元素的大小设置为14px
html{
font-size14px
}
//那么我底下的p标签如果想要也是14像素
p{
font-size:1rem
}
//如此即可

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

他的原理非常简单

1
2
3
4
5
6
// set 1rem = viewWidth / 10
function setRemUnit () {
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'
}
setRemUnit();

rem 是相对于html节点的font-size来做计算的。所以在页面初始话的时候给根元素设置一个font-size,接下来的元素就根据rem来布局,这样就可以保证在页面大小变化时,布局可以自适应,

如此我们只需要给设计稿的px转换成对应的rem单位即可

当然,这个方案只是个过渡方案,为什么说是过渡方案

因为当年viewport在低版本安卓设备上还有兼容问题,而vw,vh还没能实现所有浏览器兼容,所以flexible方案用rem来模拟vmin来实现在不同设备等比缩放的“通用”方案,之所以说是通用方案,是因为他这个方案是根据设备大小去判断页面的展示空间大小即屏幕大小,然后根据屏幕大小去百分百还原设计稿,从而让人看到的效果(展示范围)是一样的,这样一来,苹果5 和苹果6p屏幕如果你按照设计稿还原的话,字体大小实际上不一样,而人们在一样的距离上希望看到的大小其实是一样的,本质上,用户使用更大的屏幕,是想看到更多的内容,而不是更大的字

so,这个用缩放来解决问题的方案是个过渡方案,注定时代所淘汰

vw,vh布局

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

vwvh,比较容易混淆的一个单位是%,不过百分比宽泛的讲是相对于父元素:

对于普通定位元素就是我们理解的父元素

  • 对于position: absolute;的元素是相对于已定位的父元素
  • 对于position: fixed;的元素是相对于 ViewPort(可视窗口)

视口单位主要包括以下4个:

  • vw:1vw等于视口宽度的1%。
  • vh:1vh等于视口高度的1%。
  • vmin:选取vw和vh中最小的那个。
  • vmax:选取vw和vh中最大的那个。

缺点:用户失去了放缩任何使用vw单位的元素的能力。

vh和vw方案和rem类似也是相当麻烦需要做单位转化,而且px转换成vw不一定能完全整除,因此有一定的像素差。

不过在工程化的今天,webpack解析css 的时候用postcss-loader 有个postcss-px-to-viewport能自动实现px到vw的转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
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)
})
]
}

px为主,vx和vxxx(vw/vh/vmax/vmin)为辅,搭配一些flex(推荐)

移动端适配流程

1. 在head 设置width=device-width的viewport

2. 在css中使用px

3. 在适当的场景使用flex布局,或者配合vw进行自适应

4. 在跨设备类型的时候(pc <-> 手机 <-> 平板)使用媒体查询

5. 在跨设备类型如果交互差异太大的情况,考虑分开项目开发