指明方向

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 块即可。

欢迎试用,并与我们分享您的感受和想法!