移动端兼容
# 移动端兼容
# 浏览器事件触发的顺序
touchstart --> mouseover(有的浏览器没有实现) --> mousemove(一次) -->mousedown --> mouseup --> click -->touchend
# 移动端布局
# 移动端布局发展流程
- 静态布局:过
<meta>
标签中的 applicable-device 应用设备标识识别移动设备
即<meta name = 'applicable-device' content = 'mobile'>
- 流式布局:使用百分比%定义宽度,高度使用 px 固定,根据可视区域大小实时进行尺寸调整,通常使用 max-width/min-width 控制尺寸范围过大或者过小
缺点:在大屏手机或横竖屏切换场景下可能会导致页面元素被拉伸变形,字体大小无法随屏幕大小发生变化。
- 弹性布局:em/rem
包裹文字的元素的尺寸采用 em/rem 为单位,页面主要划分区域的尺寸依据情况使用 px、百分数或者 em/rem
# 视口
viewport:浏览器上(也可能是一个 app 中的 webview)用来显示网页的那部分区域
<meta>
标签中定义了一些元数据信息,通过设置<meta name = "viewport">
,提供有关 视口初始大小 的信息,供移动设备 使用。
属性值为:
属性 | 属性值 | 描述 |
---|---|---|
width | 数值 / device-width | 视口宽度 |
height | 数值 / device-height | 视口高度 |
initial-scale | 0.0 ~ 10.0 | 设备宽度与视口大小之间的缩放比率 |
maximum-scale | 0.0 ~ 10.0 | 缩放最大值 |
minimum-scale | 0.0 ~ 10.0 | 缩放最小值 |
user-scalable | 布尔值 | 默认 yes,为 no 时用户不能缩放网页 |
移动端分为:布局视口(Layout Viewport)、视觉视口(Visual ViewPort)和理想视口(Ideal ViewPort)。
布局视口:是指用视口元标签(viewport meta)来进行布局视口设置,css 布局是相对于布局视口计算,layout viewport 的宽度可以通过 document.documentElement.clientWidth 来获取,,layout viewport 的宽度是大于浏览器可视区域的宽度的
视觉视口:是指用户当前看到的区域,可以通过 window.innerWidth 来获取
理想视口:是屏幕分辨率的值,ideal viewport 的宽度等于移动设备的屏幕宽度,ideal viewport 的意义在于,无论在何种分辨率的屏幕下,那些针对 ideal viewport 而设计的网站,不需要用户手动缩放,也不需要出现横向滚动条,都可以完美的呈现给用户。
用下面的方法可以使布局视口与理想视口的宽度一致:
通过设置 <meta name = "viewport" content = "width = device-width, initial-scale = 1.0">
实现
# 像素
像素包含 2 种像素:物理像素和 css 像素
- 物理像素
物理像素又称设备像素,任何设备的物理像素的数量是固定不变的,单位是 pt。
所谓的一倍屏、二倍屏(Retina)、三倍屏,指的是设备以多少物理像素来显示一个 CSS 像素。
- CSS 像素
CSS 像素就是我们写 CSS 时所用的像素,是一个抽像的单位,在不同的设备或者不同的环境中,css 中的 1px 所代表的设备物理像素是不同的。比如早期的 iphone3 的分辨率是 320px480px,1css 像素=1 物理像;iphoen4 开始分辨率提高成了 640px960px,但屏幕尺寸没变,意味着同样大小的屏幕上,像素多了一倍,此时 1css 像素 = 2 物理像素.
# 像素其它概念:
- 设备像素比简称为 dpr
DPR (devicePixelRatio):设备物理像素和设备独立像素比、DPR = 物理像素 / css像素
是指在理想布局宽度,使用多少个物理像素来渲染一个 css 像素
js 中通过 window.devicePixelRatio 获取
css 中通过-webkit-device-pixel-ratio
,-webkit-min-device-pixel-ratio
,-webkit-max-device-pixel-ratio
进行媒体查询
# 移动端布局适配方案
三种兼容性适配方案:
百分比方案
rem 方案
vh/vw 方案
rem+vw/vh 方案
# 百分比方案
使用 百分比% 定义 宽度,高度 用 px 固定,根据可视区域实时尺寸进行调整,尽可能适应各种分辨率,通常使用 max-width/min-width 控制尺寸范围过大或者过小。
属性 | 设置参考 |
---|---|
height/width | 基于子元素的直接父元素,width 相对于父元素的 width,height 相对于父元素的 height |
top/bottom 和 left/right | 相对于直接非 static 定位的父元素的 height/width |
padding/margin | 不论是垂直方向或者是水平方向,都相对于直接父亲元素的 width,与父元素的 height 无关。 |
border-radius | 相对于自身的宽度 |
优点:原理简单,不存在兼容性问题
缺点:
如果屏幕尺度跨度太大,相对设计稿过大或者过小的屏幕不能正常显示,在大屏手机或横竖屏切换场景下可能会导致页面元素被拉伸变形,字体大小无法随屏幕大小发生变化。
设置盒模型的不同属性时,其百分比设置的参考元素不唯一,容易使布局问题变得复杂
# rem 方案
案例:lib-flexible+postcss-pxtorem
rem 是相对长度单位,rem 方案中的样式设计为相对于根元素 font-size 计算值的倍数。根据 屏幕宽度 设置 html 标签的 font-size,在布局时使用 rem 单位布局,达到自适应的目的,是弹性布局的一种实现方式。
实现过程: 首先获取文档根元素和设备 dpr,设置 rem,在 html 文档加载和解析完成后调整 body 字体大小; 在页面缩放 / 回退 / 前进的时候, 获取元素的内部宽度 (不包括垂直滚动条,边框和外边距),重新调整 rem 大小。
实现方法:用 css 处理器或 npm 包将页面 css 样式中的 px 自动转换成 rem。在整个 flexible 适配方案中,文本使用 px 作为单位,使用[data-dpr]属性来区分不同 dpr 下的文本字号。由于手机浏览器对字体显示最小是 8px,因此对于小尺寸文字需要采用 px 为单位,防止通过 rem 转化后出现显示问题。手机淘宝 中的字体使用 px 为单位,腾讯新闻中的字体使用 rem 为单位。
优点:
- 兼容性好
(1) ios: 6.1 系统以上都支持
(2) android: 2.1 系统以上都支持
(3) 大部分主流浏览器都支持
- 相较于之前的静态布局和百分比方案,页面不会因为伸缩发生变形,自适应效果更佳。
缺点:
不是纯 css 移动适配方案,需要引入 js 脚本 在头部内嵌一段 js 脚本 监听分辨率的变化来动态改变根元素的字体大小,css 样式和 js 代码有一定 耦合性,并且必须将改变 font-size 的代码放在 css 样式之前。
小数像素问题:浏览器渲染最小的单位是像素,元素根据屏幕宽度自适应,通过 rem 计算后可能会出现小数像素,浏览器会对这部分小数四舍五入,按照整数渲染。浏览器在渲染时所做的摄入处理只是应用在元素的尺寸渲染上,其真实占据的空间依旧是原始大小。也就是说如果一个元素尺寸是 0.625px,那么其渲染尺寸应该是 1px,空出的 0.375px 空间由其临近的元素填充;同样道理,如果一个元素尺寸是 0.375px,其渲染尺寸就应该是 0,但是其会占据临近元素 0.375px 的空间。会导致:缩放到低于 1px 的元素时隐时现(解决办法:指定最小转换像素,对于比较小的像素,不转换为 rem 或 vw);两个同样宽度的元素因为各自周围的元素宽度不同,导致两元素相差 1px;宽高相同的正方形,长宽不等了;border-radius: 50% 画的圆不圆。
Android 浏览器下 line-height 垂直居中偏离的问题。常用的垂直居中方式就是使用 line-height,这种方法在 Android 设备下并不能完全居中。
cursor: pointer 元素点击背景变色的问题,对添加了 cursor:pointer 属性的元素,在移动端点击时,背景会高亮。为元素添加 tag-highlight-color:transparent 属性可以隐藏背景高亮。
# vh/vw 方案
vw 是视口单位,视口单位中的“视口”,
在桌面端指的是浏览器的可视区域;在移动端指的就是 Viewport 中的 Layout Viewport(布局适口)
案例:post-css-to-viewport https://www.cnblogs.com/zhangnan35/p/12682925.html (opens new window)
post-css-to-viewport 原理:
vw : 1vw 等于 视口宽度 的 1%
vh : 1vh 等于 视口高度 的 1%
vmin : 选取 vw 和 vh 中 最小 的那个
vmax : 选取 vw 和 vh 中 最大 的那个
使用 css 预处理器把设计稿尺寸转换为 vw 单位,包括 文本,布局高宽,间距 等,使得这些元素能够随视口大小自适应调整。
以 1080px 设计稿为基准,转化的计算表示为:
// 以1080px作为设计稿基准
$vw_base: 1080 @function vw($px) {
@return ($px / 1080) * 100vw;
}
2
3
4
优点:
纯 css 移动端适配方案,不存在脚本依赖问题
相对于 rem 以根元素字体大小的倍数 定义 元素大小,逻辑清晰简单,视口单位依赖于视口的尺寸 "1vw = 1/100 viewport width",根据 视口尺寸的百分比 来定义 元素宽度
缺点:存在一些兼容性问题,Android4.4 以下不支持
参考:https://juejin.cn/post/6916473795490349063 (opens new window)
# rem+vw/vh 方案
原理:vw/vh 方案能够实现宽度和高度的自适应,并且逻辑清晰,由于其被支持得较晚,所以存在一定的兼容性问题。将 vw/vh 方案与 rem 方案相结合,给根元素设置随视口变化的 vw 单位,可以通过 postcss-plugin-vwtorem 将其转换。
对于 1080px 宽的设计稿,设置默认根字号的大小为 100px,那么设计稿中 1px 对应的是 100vw/1080 = 0.0925926vw,并且 1rem = 100px,也就可以得到 1rem = 9.256926vw
同时可以使用媒体查询限制根元素的最大最小值,实现对页面的最大最小宽度限制,对用户的视觉体验更好。
实用性:
rem 弹性布局方式作为移动端 web 页面适配方法,后期从 rem 过渡到 vw ,只需要通过 改变根元素大小的计算方式 不需要其他处理。vw 将会成为一种更好的适配方式,目前由于兼容性的原因得不到广泛应用。rem+vw/vh 不存在 vw/vh 的兼容性问题,可以成为由 rem 向 vw/vh 转变的一种过渡方案。
# 基于媒体查询的响应式设计
响应式设计 使得一个网站同时适配 多种设备 和 多个屏幕,让网站的布局和功能随用户的使用环境(屏幕大小、输出方式、设备/浏览器能力而变化),使其视觉合理,交互方式符合习惯。如使得内容区块可伸缩与自由排布,边距适应页面尺寸,图片适应比例变化,能够自动隐藏/部分显示内容,能自动折叠导航和菜单。
原理:主要实现是通过媒体查询,通过给不同分辨率的设备编写不同的样式实现响应式布局,用于解决不同设备不同分辨率之间兼容问题,一般是指 PC、平板、手机设备之间较大的分辨率差异。实现上不局限于具体的方案,通常结合了 流式布局 + 弹性布局 方案。比如给小屏幕手机设置@2x 图,为大屏手机设置@3x 图
优点:能够使网页在不同设备、不同分辨率屏幕上呈现合理布局,不仅仅是样式伸缩变换
缺点:
要匹配足够多的设备与屏幕,一个 web 页面需要多个设计方案,工作量比较大
通过媒体查询技术需要设置一定量的断点,到达某个断点前后的页面发生显著变化,用户体验不太友好
# 移动端 1px 像素问题
问题描述:如果你对边框设置 border:1px,会发现,边框在某些手机机型上面显示的 1px 比实际感觉会变粗,这也就是 1 像素问题
# 原因
一般来说,在桌面的浏览器中,设备像素比(dpr)等于 1,一个 css 像素就是代表的一个物理像素,而在移动端,大多数机型都不是为 1,其中 iphone 的 dpr 普遍是 2 和 3,那么一个 css 像素不再是对应一个物理像素,而是 2 个和 3 个物理像素。即我们通常在 css 中设置的 width:1px,对应的便是物理像素中的 2px。
# 解决办法
- 设置 viewport 的 scale 值
优点:全机型兼容,直接写 1px 不能再方便
缺点:适用于新的项目,老项目可能改动大
<html>
<head>
<title>1px question</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta
name="viewport"
id="WebViewport"
content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<style>
html {
font-size: 1px;
}
* {
padding: 0;
margin: 0;
}
.top_b {
border-bottom: 1px solid #e5e5e5;
}
.a,
.b {
box-sizing: border-box;
margin-top: 1rem;
padding: 1rem;
font-size: 1.4rem;
}
.a {
width: 100%;
}
.b {
background: #f5f5f5;
width: 100%;
}
</style>
<script>
var viewport = document.querySelector("meta[name=viewport]");
//下面是根据设备像素设置viewport
if (window.devicePixelRatio == 1) {
viewport.setAttribute(
"content",
"width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
);
}
if (window.devicePixelRatio == 2) {
viewport.setAttribute(
"content",
"width=device-width,initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no"
);
}
if (window.devicePixelRatio == 3) {
viewport.setAttribute(
"content",
"width=device-width,initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no"
);
}
var docEl = document.documentElement;
var fontsize = 32 * (docEl.clientWidth / 750) + "px";
docEl.style.fontSize = fontsize;
</script>
</head>
<body>
<div class="top_b a">下面的底边宽度是虚拟1像素的</div>
<div class="b">上面的边框宽度是虚拟1像素的</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
- 使用伪元素+transform
原理:把原先元素的 border 去掉,然后利用:before 或者:after 重做 border,并 transform 的 scale 缩小一半,原先的元素相对定位,新做的 border 绝对定位。
优点:全机型兼容,实现了真正的 1px,而且可以圆角。
缺点:暂用了 after 伪元素,可能影响清除浮动。
.setOnePx {
position: relative;
&::after {
position: absolute;
content: "";
background-color: #e5e5e5;
display: block;
width: 100%;
height: 1px; /*no*/
transform: scale(1, 0.5);
top: 0;
left: 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- 使用 box-shadow 实现
box-shadow: 0 -1px 1px -1px #e5e5e5, //上边线
1px 0 1px -1px #e5e5e5, //右边线
0 1px 1px -1px #e5e5e5, //下边线
-1px 0 1px -1px #e5e5e5; //左边线
2
3
4
5
优点:使用简单,圆角也可以实现
缺点:模拟的实现方法,仔细看谁看不出来这是阴影不是边框。
- 使用边框图片
border: 1px solid transparent;
border-image: url("./../../image/96.jpg") 2 repeat;
2
优点:没有副作用
缺点:修改颜色麻烦, 需要替换图片;圆角需要特殊处理,并且边缘会模糊
- 使用 0.5px
border:0.5px solid #E5E5E5
IOS7 及以下和 Android 等其他系统里,0.5px 将会被显示为 0px
优点:简单,没有副作用
缺点:支持 iOS 8+,不支持安卓。
# 总结
对于老项目,建议采用 transform+伪类。
新项目可以设置 viewport 的 scale 值,这个方法兼容性好。
# 移动端 300ms 延时
# 原因
由于用户可以进行双击缩放或者单击跳转的操作,当用户一次点击屏幕之后,浏览器并不能立刻判断用户是确实要打开这个链接,还是想要进行双击操作。因此,iOS Safari 就等待 300 毫秒,以判断用户是否再次点击了屏幕。
# 解决办法
- faskclick
原理: 在检测到 touchend 事件的时候,会通过 DOM 自定义事件立即出发模拟一个 click 事件,并把浏览器在 300ms 之后真正的 click 事件阻止掉
- 更改默认的视口窗口
我们可以通过以下标签来设置视口宽度为设备宽度:
<meta name="viewport" content="width=device-width">
原理:如果设置了上述 meta 标签,那浏览器就可以认为该网站已经对移动端做过了适配和优化,就无需双击缩放操作了。
- 禁用缩放
<meta name="viewport" content="user-scalable=no" />
<meta name="viewport" content="initial-scale=1,maximum-scale=1" />
2
缺点:完全禁用了缩放(我们只是想禁掉默认的双击缩放),页面不能通过双指缩放来进行缩放操作,比如放大一张图片,放大一段很小的文字。
- 指针事件的 polyfill
指针事件的 polyfill 比较多,以下列出比较流行的几个。
Google 的 Polymer
微软的 HandJS
@Rich-Harris 的 Points
缺点:引入一整套指针事件
# 为什么不监听 touchstart 事件
第一:touchstart 是手指触摸屏幕就触发,有时候用户只是想滑动屏幕,却触发了 touchstart 事件,这不是我们想要的结果;
第二:使用 touchstart 事件在某些场景下可能会出现点击穿透的现象
# 点击穿透
假如页面上有两个元素 A 和 B。B 元素在 A 元素之上。我们在 B 元素的 touchstart 事件上注册了一个回调函数,该回调函数的作用是隐藏 B 元素。我们发现,当我们点击 B 元素,B 元素被隐藏了,随后,A 元素触发了 click 事件。
这是因为在移动端浏览器,事件执行的顺序是 touchstart > touchend > click。而 click 事件有 300ms 的延迟,当 touchstart 事件把 B 元素隐藏之后,隔了 300ms,浏览器触发了 click 事件,但是此时 B 元素不见了,所以该事件被派发到了 A 元素身上。如果 A 元素是一个链接,那此时页面就会意外地跳转。
点击穿透现象 3 种情况:
(1)点击穿透问题:点击蒙层(mask)上的关闭按钮,蒙层消失后发现触发了按钮下面元素的 click 事件。
(2)跨页面点击穿透问题:如果按钮下面恰好是一个有 href 属性的 a 标签,那么页面就会发生跳转因为 a 标签跳转默认是 click 事件触发 ,所以原理和上面的完全相同
(3)点击穿透问题:这次没有 mask 了,直接点击页内按钮跳转至新页,然后发现新页面中对应位置元素的 click 事件被触发了。
解决方案:
2 种思路:
(1)不要混用 touch 和 click。既然 touch 之后 300ms 会触发 click,只用 touch 或者只用 click 就自然不会存在问题了。
(2)用掉(或者说是消费掉)touch 之后的 click。依旧用 tap,只是在可能发生点击穿透的情形做额外的处理,拿个东西来挡住、或者 tap 后延迟 350 毫秒再隐藏 mask、pointer-events、在下面元素的事件处理器里做检测(配合全局 flag)
详细方案:
(1)只用 touch
最简单的解决方案,完美解决点击穿透问题。
把页面内所有 click 全部换成 touch 事件 touchstart 、’touchend’、’tap’, 需要特别注意 a 标签,a 标签的 href 也是 click,需要去掉换成 js 控制的跳转,或者直接改成 span + tap 控制跳转。
(2)只用 click
下下策 ,因为会带来 300ms 延迟,页面内任何一个自定义交互都将增加 300 毫秒延迟,想想都慢。不用 touch 就不会存在 touch 之后 300ms 触发 click 的问题。
(3)拿个东西挡住
比较笨的方法, 千万不要用。更多信息请查看 【移动端兼容问题研究】javascript 事件机制详解(涉及移动兼容)
(4)tap 后延迟 350ms 再隐藏 mask
改动最小,缺点是隐藏 mask 变慢了,350ms 还是能感觉到慢的。
(5)pointer-events
比较麻烦且有缺陷, 不建议使用。mask 隐藏后,给按钮下面元素添上 pointer-events: none; 样式,让 click 穿过去,350ms 后去掉这个样式,恢复响应。缺陷是 mask 消失后的的 350ms 内,用户可以看到按钮下面的元素点着没反应,如果用户手速很快的话一定会发现。
(6)在下面元素的事件处理器里做检测(配合全局 flag)
比较麻烦, 不建议使用。全局 flag 记录按钮点击的位置(坐标点),在下面元素的事件处理器里判断 event 的坐标点,如果相同则是那个可恶的 click,拒绝响应。
(7)fastclick
好用的解决方案,不介意多加载几 KB 的话, 不建议使用 ,因为有人遇到了 bug,更多信息请查看: Fastclick 导致 click 事件触发两次的问题。
首先引入 fastclick 库,再把页面内所有 touch 事件都换成 click,其实稍微有点麻烦,建议引入这几 KB 就为了解决点透问题不值得,不如用第一种方法呢。