指明方向

Sérgio Gomes

指向网页上的内容过去很简单。您有一个鼠标,会四处移动,有时甚至会按下按钮,就此完成了。所有不是鼠标的操作都可以模拟为一个对象,因此开发者确切地知道应该依赖哪些工具。

不过,简单并不一定就是好事。随着时间的推移,并非所有东西都是(或假装成)鼠标,而是变得越来越重要:你可以使用压力感应和倾斜感知的笔,实现惊人的创作自由度;你可以使用手指,因此只需设备和手即可;嘿,使用鼠标时为什么不用一根多根手指呢?

为了帮助您实现这一目标,我们已经有一段时间使用了触摸事件,但它们是专门用于触摸的完全独立的 API,因此,如果您想同时支持鼠标和触摸,就必须编写两个单独的事件模型。Chrome 55 附带更新的标准,统一了这两种模型:指针事件。

单个事件模型

指针事件统一了浏览器的指针输入模型,将触摸、钢笔和鼠标整合到一组事件中。例如:

document.addEventListener('pointermove',
    ev => console.log('The pointer moved.'));
foo.addEventListener('pointerover',
    ev => console.log('The pointer is now over foo.'));

下面列出了所有可用事件,如果您熟悉鼠标事件,应该已经非常熟悉这些事件了:

pointerover 指针已进入元素的边界框。 对于支持悬停功能的设备,系统会立即执行此操作;对于不支持悬停的设备,则会在 pointerdown 事件之前触发。
pointerenter pointerover 类似,但不会以不同的方式冒泡和处理后代。 规范详细信息
pointerdown 指针已进入有效按钮状态,要么按下按钮,要么联系,具体取决于输入设备的语义。
pointermove 指针的位置发生了变化。
pointerup 指针已退出活动按钮状态。
pointercancel 出现了问题,这意味着指针不太可能发出更多事件。这意味着您应该取消所有正在进行的操作,并返回到中性输入状态。
pointerout 指针离开了元素或屏幕的边界框。同样是在 pointerup 之后(如果设备不支持悬停功能)。
pointerleave pointerout 类似,但不会以不同的方式冒泡和处理后代。 规范详细信息
gotpointercapture 元素已收到指针捕获
lostpointercapture 正在捕获的指针已释放。

不同的输入类型

通常,指针事件允许您以与输入无关的方式编写代码,而无需为不同输入设备注册单独的事件处理脚本。当然,您仍然需要注意输入类型之间的差异,例如悬停的概念是否适用。如果您确实想要区分不同的输入设备类型(或许是为不同的输入提供单独的代码/功能),则可以使用 PointerEvent 接口的 pointerType 属性在同一事件处理脚本中执行此操作。例如,如果您要对侧边抽屉式导航栏进行编码,则可以对 pointermove 事件使用以下逻辑:

switch(ev.pointerType) {
    case 'mouse':
    // Do nothing.
    break;
    case 'touch':
    // Allow drag gesture.
    break;
    case 'pen':
    // Also allow drag gesture.
    break;
    default:
    // Getting an empty string means the browser doesn't know
    // what device type it is. Let's assume mouse and do nothing.
    break;
}

默认操作

在支持触控的浏览器中,系统会使用某些手势来滚动、缩放或刷新网页。 对于触摸事件,当这些默认操作发生时,您仍然会收到事件,例如,当用户滚动时,touchmove 仍会被触发。

借助指针事件,每当触发滚动或缩放等默认操作时,您都会获得 pointercancel 事件,告知您浏览器已控制指针。例如:

document.addEventListener('pointercancel',
    ev => console.log('Go home, the browser is in charge now.'));

内置速度:默认情况下,与触摸事件相比,此模型可提供更高的性能;在触摸事件中,您需要使用被动事件监听器来实现相同级别的响应速度。

您可以使用 touch-action CSS 属性阻止浏览器获得控制权。在某个元素上将其设置为 none 会停用针对该元素启动的所有浏览器定义的操作。不过,还有许多其他值可用于实现更精细的控制,例如 pan-x,可让浏览器对 x 轴(而非 y 轴)上的移动做出响应。Chrome 55 支持以下值:

auto 默认;浏览器可以执行任何默认操作。
none 浏览器不得执行任何默认操作。
pan-x 浏览器只能执行水平滚动默认操作。
pan-y 浏览器只能执行垂直滚动默认操作。
pan-left 浏览器只能执行水平滚动默认操作,并且只能向左平移页面。
pan-right 浏览器只能执行水平滚动默认操作,并且只能向右平移页面。
pan-up 浏览器只能执行垂直滚动默认操作,且只能向上平移页面。
pan-down 浏览器只能执行垂直滚动默认操作,且只能向下平移页面。
manipulation 浏览器只能执行滚动和缩放操作。

指针捕获

您是否曾花了一个令人沮丧的小时来调试一个损坏的 mouseup 事件,直到您意识到这是因为用户在点击目标之外松开了按钮?不是吗?好吧,那可能只有我一个人。

但直到现在,还没有一个很好的方法解决这个问题。当然,您可以在文档上设置 mouseup 处理程序,并在应用中保存某种状态以跟踪内容。但这并不是最简洁的解决方案,尤其是在您构建 Web 组件并试图让一切保持良好和隔离的情况下。

使用指针事件可以提供更好的解决方案:您可以捕获指针,从而确保获取该 pointerup 事件(或其他任何难以捉摸的事件)。

const foo = document.querySelector('#foo');
foo.addEventListener('pointerdown', ev => {
    console.log('Button down, capturing!');
    // Every pointer has an ID, which you can read from the event.
    foo.setPointerCapture(ev.pointerId);
});

foo.addEventListener('pointerup', 
    ev => console.log('Button up. Every time!'));

浏览器支持

在撰写本文时,Internet Explorer 11、Microsoft Edge、Chrome 和 Opera 支持指针事件,在 Firefox 中部分支持。您可以在 caniuse.com 上查看最新列表

您可以使用指针事件 polyfill 来填补这些空白。或者,在运行时检查浏览器是否支持相应操作也非常简单:

if (window.PointerEvent) {
    // Yay, we can use pointer events!
} else {
    // Back to mouse and touch events, I guess.
}

指针事件非常适合进行渐进式增强:只需修改初始化方法以进行上述检查,在 if 块中添加指针事件处理脚本,然后将鼠标/触摸事件处理脚本移至 else 块即可。

赶快试试吧,然后告诉我们你的想法!