Libon

创建兼容 PC 和移动端事件的应用

6 分钟 #JavaScript
学习如何在 PC 端兼容移动端的 touch 事件

ToC

设备类型判断

首先我们要做的事是判断当前的设备类型,如果本身就是移动端,那我们就不需要做任何处理,使用因为我们本身就是监听的 touch 事件。判断的方式也很简单,只需要 'ontouchstart' in window 就能知道是移动端(或者 Pad 端)还是 PC 端:

1
(() => {
2
// 如果是 ssr 渲染则不 hack 事件
3
if (typeof window === 'undefined') {
4
return
5
}
6
7
// 如果支持这个事件类型则不做处理, 否则才需要将 mouse 事件转换为 touch
8
if ('ontouchstart' in window) {
9
return
2 collapsed lines
10
}
11
})();

需要我们做处理的事件类型其实只有 mousedown mousemove mouseup,而 mouseenter mouseleave mouseover mouseout 是不需要处理的,因为这四个类型根本没法模拟,所以我们主要专注于点击类的事件即可。我们先从 mousedown 开始

mousedown to touchstart

首先我们需要监听全局的 mouse 事件,然后需要把 mouse 事件对象参数转换为 touch 事件的对象参数类型,最后再触发 touch 事件:

1
// ...
2
3
// 保存事件触发时的target dom
4
let eventTarget
5
6
function onMouse (ev) {
7
eventTarget = ev.target
8
9
// 因为三个事件类型最终都需要转换成参数类型, 所以封装成函数方便调用
17 collapsed lines
10
triggerTouchEvent('touchstart', ev)
11
}
12
13
// 转换事件类型
14
function triggerTouchEvent (eventName, mouseEvent) {
15
const touchEvent = new Event(eventName, { bubbles: true, cancelable: true })
16
17
touchEvent.altKey = mouseEvent.altKey
18
touchEvent.ctrlKey = mouseEvent.ctrlKey
19
touchEvent.metaKey = mouseEvent.metaKey
20
touchEvent.shiftKey = mouseEvent.shiftKey
21
22
eventTarget.dispatchEvent(touchEvent)
23
}
24
25
// 监听全局事件从而转换事件对象
26
window.addEventListener('mousedown', onMouse, true)

这样就支持了将 mousedown 事件转换为 touchstart 事件,现在已经可以在 PC 上已经使用简易的 touchstart 事件类型来作为 mousedown 的替代了,但这样还不够,我们还需要处理 mousemove mouseup 类型,在上述代码中我们将时间类型写死了,所以接下来我们对其做一点优化,比如把 onMouse 转换为高阶函数,同时增加对其他时间类型的处理代码。

mousemove|mouseup to touchmove|touchend

1
// ...
2
3
// 增加一个变量用于判断是否按下
4
// 如果被按下才触发 move 事件, 否则 mousemove 事件会一直触发
5
let initiated = false
6
7
// 改造 onMouse 函数
8
function onMouse (eventType) {
9
return function (ev) {
29 collapsed lines
10
if (ev.type === 'mousedown') {
11
initiated = true
12
} else if (ev.type === 'mouseup') {
13
initiated = false
14
} else if (ev.type === 'mousemove' && !initiated) { // 没有按下则不触发 move 事件
15
return
16
}
17
18
if (
19
ev.type === 'mousedown' || // 按下时更新
20
!eventTarget || // 如果事件对象不存在
21
(eventTarget && !eventTarget.dispatchEvent) // 如果当前对象无法触发事件则更新
22
) {
23
eventTarget = ev.target
24
}
25
26
triggerTouchEvent(eventType, ev)
27
28
// 鼠标抬起时重置对象
29
if (ev.type === 'mouseup') {
30
eventTarget = null
31
}
32
}
33
}
34
35
// ...
36
// 增加对其他两种事件类型的支持
37
window.addEventListener('mousemove', onMouse('touchmove'), true)
38
window.addEventListener('mouseup', onMouse('touchend'), true)

这样就算支持了三种事件类型,但是这还不够好,因为移动端可能会有多点触控,这个能力是我们现在还不具备的,所以我们还需要 hack 一下多触控点:

multi point touch

1
// ...
2
3
function triggerTouchEvent (eventName, mouseEvent) {
4
// ...
5
touchEvent.touches = getActiveTouches(mouseEvent)
6
touchEvent.targetTouches = getActiveTouches(mouseEvent)
7
touchEvent.changedTouches = createTouchList(mouseEvent)
8
9
eventTarget.dispatchEvent(touchEvent)
46 collapsed lines
10
}
11
12
const Touch = function Touch (target, identifier, pos, deltaX, deltaY) {
13
deltaX = deltaX || 0
14
deltaY = deltaY || 0
15
16
this.identifier = identifier
17
this.target = target
18
this.clientX = pos.clientX + deltaX
19
this.clientY = pos.clientY + deltaY
20
this.screenX = pos.screenX + deltaX
21
this.screenY = pos.screenY + deltaY
22
this.pageX = pos.pageX + deltaX
23
this.pageY = pos.pageY + deltaY
24
}
25
26
function TouchList () {
27
const touchList = []
28
29
touchList.item = function (index) {
30
return this[index] || null
31
}
32
33
// specified by Mozilla
34
touchList.identifiedTouch = function (id) {
35
return this[id + 1] || null
36
}
37
38
return touchList
39
}
40
41
function createTouchList (mouseEv) {
42
const touchList = TouchList()
43
44
touchList.push(new Touch(eventTarget, 1, mouseEv, 0, 0))
45
46
return touchList
47
}
48
49
function getActiveTouches (mouseEvent) {
50
if (mouseEvent.type === 'mouseup') {
51
return TouchList()
52
}
53
54
return createTouchList(mouseEvent)
55
}

忽略事件

现在事件已经比较完善了,但毕竟是模拟的,我们可能在某些场景下会不希望模拟的事件触发,那么我们可以增加一个选项:

1
function onMouse (eventType) {
2
// ...
3
4
// 当父级元素上设置了 data-no-touch-simulate 属性的时候则不触发模拟事件
5
if (eventTarget.closest('[data-no-touch-simulate]') == null) {
6
triggerTouch(eventType, ev);
7
}
8
9
if (ev.type === 'mouseup') {
3 collapsed lines
10
eventTarget = null;
11
}
12
}

完整代码

1
(() => {
2
if (typeof window === 'undefined') {
3
return
4
}
5
6
if ('ontouchstart' in window) {
7
return
8
}
9
94 collapsed lines
10
let eventTarget
11
let initiated
12
13
function onMouse (eventType) {
14
return function (ev) {
15
if (ev.type === 'mousedown') {
16
initiated = true
17
} else if (ev.type === 'mouseup') {
18
initiated = false
19
} else if (ev.type === 'mousemove' && !initiated) {
20
return
21
}
22
23
if (
24
ev.type === 'mousedown' || // 按下时更新
25
!eventTarget || // 如果事件对象不存在
26
(eventTarget && !eventTarget.dispatchEvent)
27
) {
28
eventTarget = ev.target
29
}
30
31
if (eventTarget.closest('[data-no-touch-simulate]') == null) {
32
triggerTouch(eventType, ev);
33
}
34
35
if (ev.type === 'mouseup') {
36
eventTarget = null
37
}
38
}
39
}
40
41
function triggerTouchEvent (eventName, mouseEvent) {
42
const touchEvent = new Event(eventName, { bubbles: true, cancelable: true })
43
44
touchEvent.altKey = mouseEvent.altKey
45
touchEvent.ctrlKey = mouseEvent.ctrlKey
46
touchEvent.metaKey = mouseEvent.metaKey
47
touchEvent.shiftKey = mouseEvent.shiftKey
48
49
touchEvent.touches = getActiveTouches(mouseEvent)
50
touchEvent.targetTouches = getActiveTouches(mouseEvent)
51
touchEvent.changedTouches = createTouchList(mouseEvent)
52
53
eventTarget.dispatchEvent(touchEvent)
54
}
55
56
const Touch = function Touch (target, identifier, pos, deltaX, deltaY) {
57
deltaX = deltaX || 0
58
deltaY = deltaY || 0
59
60
this.identifier = identifier
61
this.target = target
62
this.clientX = pos.clientX + deltaX
63
this.clientY = pos.clientY + deltaY
64
this.screenX = pos.screenX + deltaX
65
this.screenY = pos.screenY + deltaY
66
this.pageX = pos.pageX + deltaX
67
this.pageY = pos.pageY + deltaY
68
}
69
70
function TouchList () {
71
const touchList = []
72
73
touchList.item = function (index) {
74
return this[index] || null
75
}
76
77
touchList.identifiedTouch = function (id) {
78
return this[id + 1] || null
79
}
80
81
return touchList
82
}
83
84
function createTouchList (mouseEv) {
85
const touchList = TouchList()
86
87
touchList.push(new Touch(eventTarget, 1, mouseEv, 0, 0))
88
89
return touchList
90
}
91
92
function getActiveTouches (mouseEvent) {
93
if (mouseEvent.type === 'mouseup') {
94
return TouchList()
95
}
96
97
return createTouchList(mouseEvent)
98
}
99
100
window.addEventListener('mousedown', onMouse('touchstart'), true)
101
window.addEventListener('mousemove', onMouse('touchmove'), true)
102
window.addEventListener('mouseup', onMouse('touchend'), true)
103
})()

CD ..