移动端-004-移动端常见问题

移动端常见问题

300ms延迟

代码

1
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!--
<meta name="viewport" content="width=device-width">
-->
</head>
<body>
<div id="delay">click有延迟 </div>
<div id="no-delay">touchstart无延迟</div>
<div> <a id="link1" href="#1">链接1</a> <a id="link2" href="#2">链接1</a></div>
<div id="log"></div>

<style>
body {
font-size: 60px;
}
</style>

<script>
const $ = s => document.querySelector(s)
const log = str => $('#log').innerText = str

let t1, t2

$('#delay').ontouchstart = e => {
t1 = Date.now()
}

$('#delay').onclick = e => {
log(Date.now() - t1)
}

$('#no-delay').ontouchstart = e => {
e.preventDefault()
log('touchstart无延迟')
}

$('#link1').ontouchstart = e => {
t2 = Date.now()
}

$('#link2').ontouchstart = e => {
t2 = Date.now()
}

window.onhashchange = () => {
log(`link: ${Date.now() - t2}ms`)
}
</script>
</body>
</html>

延迟的原因

当时的网站都是为大屏幕设备所设计的(没有设置<meta name="viewport">),当时的iphone手机有一个双击放大功能。手指在屏幕上快速点击两次,iOS 自带的 Safari 浏览器会将网页缩放至原始比例。

那如何判断用户快速点击两次呢?

假定用户点击后300ms内如果有第二次点击,就认为用户的目的是双击缩放而不是普通的点击页面元素。

所以用户第一次点击一个链接后,必须等300ms,浏览器才能决定是否是跳转还是缩放页面。 后来全触屏手机流行后,其他设备浏览器也开始效仿。

如何解决这个延迟问题

  • 方法1、设置meta
    <meta name="viewport" content="width=device-width">
  • 其他方法
    • faskclick库
    • 不用click用 touchstart

fastclick的原理和简单实现

什么时候用

  • 一些历史原因,没有使用 <meta name="viewport" content="width=device-width"> 的时候, 如 <meta name="viewport" content="width=750"> 这种写具体数值的时候在某些机型会有300ms延迟
  • 2017年以前使用,现在一般不用。

如何使用

1
2
3
4
5
// 引入 faskclick.js 
// 加入如下 内容
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body)
}, false)

fastclick 原理

在检测到touchend事件的时候,会通过DOM自定义事件立即出发模拟一个click事件,并把浏览器在300ms之后真正的click事件阻止掉

实现一个简单的fastclick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const FastClick = (function(){

function attach(root) {
let targetElement = null
root.addEventListener('touchstart', function () {
targetElement = event.target
})
root.addEventListener('touchend', function (event) {
event.preventDefault()
let touch = event.changedTouches[0]
let clickEvent = document.createEvent('MouseEvents')
clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null)
clickEvent.forwardedTouchEvent = true
targetElement.dispatchEvent(clickEvent)
})
}

return { attach }
})()

FastClick.attach(document.body)

点击穿透和规避

什么是点击穿透

移动端弹窗有个浮层,假设关闭按钮在左上角,关闭按钮后面刚好是一个链接,关闭浮层的时候会触发链接跳转。 这就是点击穿透

点击穿透现象:请在手机看,或者调试为手机模式

1
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
margin: 0;
padding: 30px;
font-size: 60px;
}
.mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.8);
color: #fff;
padding: 30px;

}
.close {
background: red;
padding: 10px;
}
</style>
</head>
<body>
<span class="bg">aaa</span>
<p class="log">0</p>
<div class="mask">
<span class="close">X</span>
</div>

<script>
const $ = s => document.querySelector(s)
const log = str => $('.log').innerText = str

let i = 1

$('.close').ontouchstart = (e) => {
$('.mask').style.display = 'none'
log('touched ' + (++i))
}

$('.bg').onclick = () => {
log('点击了背景')
}
</script>
</body>
</html>

原因分析

上古时代触屏设备为了区分用户双击缩放,对click做了300ms延迟触发,因此用户在移动端触屏设备的操作流程以及事件触发为:

1、手指触摸屏幕到屏幕,触发 touchstart

2、手指在屏幕短暂停留(如果是移动,触发 touchmove)

3、手指离开屏幕, 触发 touchend

4、等待约300ms,看用户在此时间内是否再次触摸屏幕。如果没有

5、300ms后 在用户手指离开的位置触发 click事件

击穿的根源在第4步。当用户用户点击关闭让遮罩隐藏后,浏览器在300ms后在原来用户手指离开的位置触发click事件,此时遮罩不存在了,自然“点击”到后面的元素。

解决方案

方案1

设置<meta name="viewport" content="device-width">。表面上看起来解决了300ms延时,就能解决穿透。实际上这种方法不可行。因为click事件是在touchend事件之后,更晚于touchstart(间隔了60~100ms),如果在touchstart 里绑定关闭了浮层,约100ms后 click事件触发的时候仍然会引发问题。

方法2

关闭浮层用click,不用touchstart。 可行。因为浮层关闭监听的是click,当浮层消失后不会再在原来的位置再冒出个click

1
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>tap击穿解决1</title>
<meta name="viewport" content="width=device-width">

<style>
body {
margin: 0;
padding: 30px;
font-size: 60px;
}
.mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.8);
color: #fff;
padding: 30px;

}
.close {
background: red;
padding: 10px;
}
</style>
</head>
<body>
<span class="bg">aaa</span>
<p class="log">0</p>
<div class="mask">
<span class="close">X</span>
</div>

<script>
const $ = s => document.querySelector(s)
const log = str => $('.log').innerText = str

let i = 1

$('.close').onclick = (e) => {
$('.mask').style.display = 'none'
log('touched ' + (++i))
}

$('.bg').onclick = () => {
log('点击了背景')
}
</script>

</body>
</html>

方法3

浮层关闭的时间延长。可行。当用户触发touchstart关闭浮层时,浮层可以使用渐变消失(可设置浮层关闭动画在300ms以上),此刻click事件在原来的位置触发时点击的还是浮层,而不是浮层后面的链接

1
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
70
71
72
73
74
75
76
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
margin: 0;
padding: 30px;
font-size: 60px;
}
.mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.8);
color: #fff;
padding: 30px;

}
.close {
background: red;
padding: 10px;
}

.fade {
animation: fade .3s;
animation-fill-mode: forwards;
}

.hide {
display: none
}
@keyframes fade {
0% {
opacity: 100%;
}
100% {
opacity: 0;
}
}
</style>
</head>
<body>
<a href="https://baidu.com">百度</a>
<span class="bg">百度</span>
<p class="log">0</p>
<div class="mask">
<span class="close">X</span>
</div>

<script>
const $ = s => document.querySelector(s)
const log = str => $('.log').innerText = str

let i = 1

$('.close').ontouchstart = (e) => {

log('touched ' + (++i))
}
$('.close').ontouchend = e => {
//e.preventDefault()
//$('.mask').style.display = 'none'
$('.mask').classList.add('fade')
setTimeout(() => $('.mask').classList.add('hide'), 300)
}

$('.bg').onclick = () => {
log('bg click')
}
</script>

</body>
</html>

方法4

在ontouchstart 里阻止默认事件。可行。 这样可以阻止click的触发

1
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>

<style>
body {
margin: 0;
padding: 30px;
font-size: 60px;
}
.mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.8);
color: #fff;
padding: 30px;

}
.close {
background: red;
padding: 10px;
}
</style>
</head>
<body>
<span class="bg">背景</span>
<p class="log">0</p>
<div class="mask">
<span class="close">X</span>
</div>

<script>
const $ = s => document.querySelector(s)
const log = str => $('.log').innerText = str

let i = 1

$('.close').ontouchstart = (e) => {
e.preventDefault()
$('.mask').style.display = 'none'
log('touched ' + (++i))
}

$('.bg').onclick = () => {
log('bg click')
}
</script>

</body>
</html>

1px实现

为什么要实现1px

当我们在css里写 border: 1px solid #000的时候,用户会觉得边线依然很粗不美观。如何实现比1px更细的边线呢?下面列举了几种方案。

1
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>1px</title>
<meta name="viewport" content="width=device-width">
</head>
<body>
<div class="item item-1px">列表1</div>
<div class="item item-0_5px">列表2</div>
<div class="item item-css3transform">列表3</div>
<div class="item item-shadow">列表4</div>
<div class="item item-background">列表5</div>
<div class="item item-4-border">列表6</div>


<style>

.item {
padding: 10px 0
}

.item-1px {
border-bottom: 1px solid #ccc;
}

.item-0_5px {
border-bottom: .5px solid #ccc;
/*ios有效 android无效*/
}

.item-css3transform::after {
content: '';
display: block;
height: 1px;
transform: scaleY(0.5);
background: #ccc;
position: relative;
top: 10px;
}

.item-shadow {
box-shadow: 0 0.5px 0 0 #ccc;
}

.item-background::after {
display: block;
content: '';
height: 1px;
background: linear-gradient(0deg, #ccc 50%, transparent 50%);
position: relative;
top: 10px;
}

.item-4-border {
margin: 10px 0;
padding: 10px;
background: linear-gradient(to bottom, #ccc .5px, transparent 0), linear-gradient(to left, #ccc .5px, transparent 0), linear-gradient(to right, #ccc .5px, transparent 0), linear-gradient(to top, #ccc .5px, transparent 0);
}
</style>
</body>
</html>

方案总结

方案1, viewport缩放

前面讲过viewport适配的方案,适配的原理是整个页面放大缩小,当然也包括边线

方案2, 设置0.5px

ios有效,部分安卓机型无效。 慢慢等吧,总有一天(可能一两年后)安卓会生效

方案3,使用CSS3 transform,给伪元素缩小尺寸

都有效,缺点是一条边框还好,如果是4条边框两个伪元素不够得新增标签

方案4,使用背景线性渐变

方便,兼容性好。

其他方案

背景图片(麻烦、兼容性不好),svg背景图片(麻烦)

要避免的问题

  • 1px像素缩小要避免和圆角一起使用