作为接口开发者,对用户输入的响应可以说是我们工作的核心。为了搭建响应式的网络应用,理解 touch、mouse、pointer 和 keyboard 动作与浏览器之间的关系是关键。你很有可能经历过移动浏览器的三百毫秒延迟或者在触摸移动中挣扎以及与滚动争斗。
在本文中我们将会介绍事件级联,然后实现一个tap事件的demo,让它支持多种输入方式,同时又不会在代理浏览器(如Opera Mini)中崩溃。
概览
如今,有三种主要的输入方式用于与网页交互:数字游标(鼠标),触碰(直接触摸或用触摸笔)和键盘。在javascript中,我们通过touch events, mouse events, pointer events 和 keyboard events去使用它们。本篇文章里,虽然一些事件有标准的基于键盘的交互,比如我们主要关注基于touch(touch-based)和基于mouse的(mouse-based)交互,比如click和submit事件。
你很可能已经实现过touch和mouse事件的事件处理器。在不久之前有过这么一段时间,类似于这种写法受到推崇:
1 2 |
/** 永远不要这么做!*/ $('a', ('ontouchstart' in window) ? 'touchend' : 'click', handler); |
微软首开先河,通过“指针事件(Pointer Events)”规范创造了一个更好的面向未来的事件模型。指针事件是一个目前W3C推荐的抽象输入机制。它向用户提供用户代理(user agent)灵活性去覆盖在一个事件系统中无数的输入机制。鼠标、触摸以及触控笔是我们目前很容易联想到的例子,虽然Myo(手势控制臂环)和Ring(智能指环)这样的延伸非常的有想象力。Web开发者们似乎对此很兴奋,但并不是所有浏览器工程师都有同样的感觉。因为Apple和Google已经同时决定不去实现指针事件了。
Google目前的决定并不一定是不变的,但是他们并没有在指针事件上有任何活跃的动作。通过polyfill和其他的解决方案,我们的输入以及对指针事件的使用最终将会是令天平倾斜的因素之一。Apple在2012年对指针事件发表过声明,至今我也没有从其他Safari的工程师那听到任何公开回应。
The Event Cascade 事件级联
当用户在移动设备上轻敲一个元素时,浏览器触发了一系列的事件。这一系列事件通常是这样的:
1 |
touchstart → touchend → mouseover →mousemove → mousedown → mouseup → click |
由于web的向后兼容性。指针事件使用另一种方式触发兼容事件:
1 |
mousemove → pointerover → mouseover → pointerdown →mousedown → gotpointercapture → pointerup → mouseup → lostpointercapture → pointerout→ mouseout → focus → click |
事件规范允许UA在兼容事件的实现上有所不同。Patrick Lauke和Peter-Paul Koch维护着很多关于这个话题的很多参考资料,在文章最后可以找到这些资源的链接。
下面的图片展示了下列动作的事件级联:
- 开始点击一个元素一次,
- 再次点击一个元素,
- 移开元素
请注意:该事件栈有意忽略掉focus 和 blur。
iOS设备上的“点击一个元素两次并移开”事件级联(图片: Stephen Davis) (大图)
安卓4.4设备上的“点击一个元素两次并移开”事件级联。 (图片: Stephen Davis) (大图)
IE 11浏览器(在兼容触碰事件实现之前的版本)上的“点击一个元素两次并移开”事件级联。 (图片:Stephen Davis) (大图)
事件级联的应用
现如今,由于浏览器工程师并不是很努力,大部分桌面网站仅仅只是“能运行”而已。尽管该级联看起来很粗糙,但我们之前建立的鼠标事件的保守方法通常还是有效的。
当然了,这里还是有问题。声名狼藉的300秒延迟是其中最出名的问题,但是scrolling, touchmove 和 pointermove事件之间的互相影响以及浏览器绘图也很让人头疼。
- 我们优化了在Android和桌面上的新版的Chrome,使用到了启发式方法,例如用
<meta name="viewport" content="width=device-width”>
去消除延迟; - 我们还为iOS做了单独的优化,用户可以做明确的点击,既不是快速点击也不是长按——只是一个普通而清晰的对元素的点击(对了,这还取决于这是发生在
UIWebView
还是WKWebView
上——看看FastClick’s issue on the topic好好哭一场吧)
如果我们的目的是建立一个基于本地平台的用户体验和优化的网络应用,那么我们需要降低交互响应的延迟。为了做到这一点,我们需要使用原始事件(down, move 和up)并且建立我们自己的组合事件(click, double-click)。当然了,我们仍然需要将回退处理程序包含在内,以便我们的应用有更大的兼容性和可用性。
实现这些需要不少的代码和知识。为了避免浏览器遇到300毫秒(或其他的时长)延迟,我们需要自己去处理整个交互的生命周期。对于给定的{type}down事件,我们需要将完成该动作的所需的所有事件都绑定起来。当交互完成,我们还得去做善后工作,将所有开始的事件解绑。
作为网站开发者的你,是唯一知道页面是该缩放还是需要等待其他双击事件的人。当且仅当你需要推迟一个回调时,你才会去允许一个有意的延迟。
在下面的链接中,你会看到一个无依赖的点击的小demo,用于展示如何去实现一个多输入,低延迟的点击事件。
明确地说,利用这个简陋的demo去实现你的方案是个坏主意。下面的代码实现仅仅用于教学目的,不应该在实际应用中使用。产品级的解决方案是存在的,比如:FastClick, polymer-gestures 和 Hammer.js.
- Demo: The tap event
- Code: taps.js
重要的部分
一切都要从绑定你初始的事件处理器开始。下面的模式被认为是一种万无一失的处理多设备输入的处理方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/** * * 如果有指针事件,让平台去处理输入(机制的抽象)???。 * 如果没有的话,那么就该你自己去处理mouse和touch事件。 * */ if (hasPointer) { tappable.addEventListener(POINTER_DOWN, tapStart, false); clickable.addEventListener(POINTER_DOWN, clickStart, false); } else { tappable.addEventListener('mousedown', tapStart, false); clickable.addEventListener('mousedown', clickStart, false); if (hasTouch) { tappable.addEventListener('touchstart', tapStart, false); clickable.addEventListener('touchstart', clickStart, false); } } clickable.addEventListener('click', clickEnd, false); |
绑定touch事件处理器可能会导致绘图性能下降,即使它们不做任何事情。为了降低这种影响,我们常常推荐在开始的事件处理器中绑定跟踪事件。不要忘记了做好事后的清理,在你的完成动作的处理器中解绑跟踪事件。
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 |
/** * 在tapStart中,我们想要绑定我们的move和end事件去探测 * 这是否是一个“tap”动作。 * @param {Event} 浏览器事件对象 */ function tapStart(event) { // 绑定跟踪事件。“bindEventsFor”可以根据我们目前的 // 事件类型去绑定合适的pointer, touch或mouse事件。 // 另外,它省了事件目标,让我们对指针事件的 // “setPointerCapture”方法有相似的行为。 bindEventsFor(event.type, event.target); if (typeof event.setPointerCapture === 'function') { event.currentTarget.setPointerCapture(event.pointerId); } // 防止级联 event.preventDefault(); // 启动分析器去跟踪事件之间的时间。 set(event, 'tapStart', Date.now()); } /** * * 我们的工作在这结束。让我们清理我们的跟踪事件。 * @param {Element} 指定html元素 * @param {Event} 浏览器事件对象 */ function tapEnd(target, event) { unbindEventsFor(event.type, target); var _id = idFor(event); log('Tap', diff(get(_id, 'tapStart'), Date.now())); setTimeout(function() { delete events[_id]; }); } |
剩下的代码不言自明。实际上,有很多东西需要补充。实现定制的手势需要你深度利用浏览器的事件系统。为了让你远离烦恼,请不要在你的代码库中重复做这些事,而应该建立或使用一个强有力的库,比如Hammer.js, Pointer Events(一个jQuery polyfill) 或polymer-gestures。
总结
一些曾经定义非常清晰的事件现在充满了歧义。click事件以前只有一个定义,但是触摸屏的出现将它复杂化了。现在还需要关注动作是否是双击,滚动事件或其他系统级的手势。
好消息是我们现在理解了事件级联和用户行为和浏览器响应之间的互相作用。有了对原始事件的理解,我们可以在自己的项目中为用户以及网站的未来做出更好的选择。
在建立支持多种设备的网站时,你遇到了哪些始料不及的问题?为了应对你的网站中无数的交互模型,你又使用了哪些手段呢?
附加资源
- “Pointer Events Finalized, But Apple’s Lack of Support Still a Deal Breaker,” Peter Bright
- “Getting Touchy: An Introduction to Touch and Pointer Events,” including slides and talk, Patrick E. Lauke
- “Apple’s Web?” by Tim Kadlec
- “Avoiding the 300ms Click Delay, Accessibly,” Tim Kadlec
- “Touch Table,” Peter-Paul Koch
- “Making the Web ‘Just Work’ With Any Input: Mouse, Touch, and Pointer Events,” Jacob Rossi
- FastClick library
- Hammer.js
- polymer-gestures
- PointerEvents jQuery polyfill
- “Implement Custom Gestures,” Google Developers