Libon

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

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

ToC

设备类型判断

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

(() => {
  // 如果是 ssr 渲染则不 hack 事件
  if (typeof window === 'undefined') {
    return
  }

  // 如果支持这个事件类型则不做处理, 否则才需要将 mouse 事件转换为 touch
  if ('ontouchstart' in window) {
    return
  }
})();

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

mousedown to touchstart

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

// ...

// 保存事件触发时的target dom
let eventTarget

function onMouse (ev) {
  eventTarget = ev.target

  // 因为三个事件类型最终都需要转换成参数类型, 所以封装成函数方便调用
  triggerTouchEvent('touchstart', ev)
}

// 转换事件类型
function triggerTouchEvent (eventName, mouseEvent) {
  const touchEvent = new Event(eventName, { bubbles: true, cancelable: true })

  touchEvent.altKey = mouseEvent.altKey
  touchEvent.ctrlKey = mouseEvent.ctrlKey
  touchEvent.metaKey = mouseEvent.metaKey
  touchEvent.shiftKey = mouseEvent.shiftKey

  eventTarget.dispatchEvent(touchEvent)
}

// 监听全局事件从而转换事件对象
window.addEventListener('mousedown', onMouse, true)

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

mousemove|mouseup to touchmove|touchend

// ...

// 增加一个变量用于判断是否按下
// 如果被按下才触发 move 事件, 否则 mousemove 事件会一直触发
let initiated = false

// 改造 onMouse 函数
function onMouse (eventType) {
  return function (ev) {
    if (ev.type === 'mousedown') {
      initiated = true
    } else if (ev.type === 'mouseup') {
      initiated = false
    } else if (ev.type === 'mousemove' && !initiated) { // 没有按下则不触发 move 事件
      return
    }

    if (
      ev.type === 'mousedown' || // 按下时更新
      !eventTarget || // 如果事件对象不存在
      (eventTarget && !eventTarget.dispatchEvent) // 如果当前对象无法触发事件则更新
    ) {
      eventTarget = ev.target
    }

    triggerTouchEvent(eventType, ev)

    // 鼠标抬起时重置对象
    if (ev.type === 'mouseup') {
      eventTarget = null
    }
  }
}

// ...
// 增加对其他两种事件类型的支持
window.addEventListener('mousemove', onMouse('touchmove'), true)
window.addEventListener('mouseup', onMouse('touchend'), true)

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

multi point touch

// ...

function triggerTouchEvent (eventName, mouseEvent) {
  // ...
  touchEvent.touches = getActiveTouches(mouseEvent)
  touchEvent.targetTouches = getActiveTouches(mouseEvent)
  touchEvent.changedTouches = createTouchList(mouseEvent)

  eventTarget.dispatchEvent(touchEvent)
}

const Touch = function Touch (target, identifier, pos, deltaX, deltaY) {
  deltaX = deltaX || 0
  deltaY = deltaY || 0

  this.identifier = identifier
  this.target = target
  this.clientX = pos.clientX + deltaX
  this.clientY = pos.clientY + deltaY
  this.screenX = pos.screenX + deltaX
  this.screenY = pos.screenY + deltaY
  this.pageX = pos.pageX + deltaX
  this.pageY = pos.pageY + deltaY
}

function TouchList () {
  const touchList = []

  touchList.item = function (index) {
    return this[index] || null
  }

  // specified by Mozilla
  touchList.identifiedTouch = function (id) {
    return this[id + 1] || null
  }

  return touchList
}

function createTouchList (mouseEv) {
  const touchList = TouchList()

  touchList.push(new Touch(eventTarget, 1, mouseEv, 0, 0))

  return touchList
}

function getActiveTouches (mouseEvent) {
  if (mouseEvent.type === 'mouseup') {
    return TouchList()
  }

  return createTouchList(mouseEvent)
}

忽略事件

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

function onMouse (eventType) {
  // ...

  // 当父级元素上设置了 data-no-touch-simulate 属性的时候则不触发模拟事件
  if (eventTarget.closest('[data-no-touch-simulate]') == null) {
    triggerTouch(eventType, ev);
  }

  if (ev.type === 'mouseup') {
    eventTarget = null;
  }
}

完整代码

(() => {
  if (typeof window === 'undefined') {
    return
  }

  if ('ontouchstart' in window) {
    return
  }

  let eventTarget
  let initiated

  function onMouse (eventType) {
    return function (ev) {
      if (ev.type === 'mousedown') {
        initiated = true
      } else if (ev.type === 'mouseup') {
        initiated = false
      } else if (ev.type === 'mousemove' && !initiated) {
        return
      }

      if (
        ev.type === 'mousedown' || // 按下时更新
        !eventTarget || // 如果事件对象不存在
        (eventTarget && !eventTarget.dispatchEvent)
      ) {
        eventTarget = ev.target
      }

      if (eventTarget.closest('[data-no-touch-simulate]') == null) {
        triggerTouch(eventType, ev);
      }

      if (ev.type === 'mouseup') {
        eventTarget = null
      }
    }
  }

  function triggerTouchEvent (eventName, mouseEvent) {
    const touchEvent = new Event(eventName, { bubbles: true, cancelable: true })

    touchEvent.altKey = mouseEvent.altKey
    touchEvent.ctrlKey = mouseEvent.ctrlKey
    touchEvent.metaKey = mouseEvent.metaKey
    touchEvent.shiftKey = mouseEvent.shiftKey

    touchEvent.touches = getActiveTouches(mouseEvent)
    touchEvent.targetTouches = getActiveTouches(mouseEvent)
    touchEvent.changedTouches = createTouchList(mouseEvent)

    eventTarget.dispatchEvent(touchEvent)
  }

  const Touch = function Touch (target, identifier, pos, deltaX, deltaY) {
    deltaX = deltaX || 0
    deltaY = deltaY || 0

    this.identifier = identifier
    this.target = target
    this.clientX = pos.clientX + deltaX
    this.clientY = pos.clientY + deltaY
    this.screenX = pos.screenX + deltaX
    this.screenY = pos.screenY + deltaY
    this.pageX = pos.pageX + deltaX
    this.pageY = pos.pageY + deltaY
  }

  function TouchList () {
    const touchList = []

    touchList.item = function (index) {
      return this[index] || null
    }

    touchList.identifiedTouch = function (id) {
      return this[id + 1] || null
    }

    return touchList
  }

  function createTouchList (mouseEv) {
    const touchList = TouchList()

    touchList.push(new Touch(eventTarget, 1, mouseEv, 0, 0))

    return touchList
  }

  function getActiveTouches (mouseEvent) {
    if (mouseEvent.type === 'mouseup') {
      return TouchList()
    }

    return createTouchList(mouseEvent)
  }

  window.addEventListener('mousedown', onMouse('touchstart'), true)
  window.addEventListener('mousemove', onMouse('touchmove'), true)
  window.addEventListener('mouseup', onMouse('touchend'), true)
})()

CD ..