瀏覽器輸入事件:我們可以做的比點選更好麼?

曉婼的部落格發表於2015-05-24

響應使用者輸入可以說是我們介面開發的核心。為了構建響應式Web產品,理解觸控,滑鼠,指標,鍵盤如何和瀏覽器一起工作是關鍵。你很有可能已經在移動瀏覽器上經歷了300毫秒的延遲或者在糾結於觸控滾動導致頁面卡頓

在這篇文章中,我們將介紹事件級聯並且利用這些知識來實現一個支援多種輸入法的點選事件而不違反像Opera Mini這些代理瀏覽器事件規則。

概括

在當今網路互動中有三個主要方法,數字游標(滑鼠),觸覺(直接觸控或筆)和鍵盤。在JavaScript中我們必須通過觸控事件滑鼠事件指標事件鍵盤事件獲得這些。在這篇文章中,我們主要關注的是以觸控和滑鼠為基礎的互動,雖然有些事件有標準的鍵盤基礎互動。例如clicksubmit事件。

你應該已經準備好了關於mousetouch事件的事件控制程式碼。在過去不久的時候,我們有一個類似於這樣的推薦方法:

/** DO NOT EVER DO THIS! */
$('a', ('ontouchstart' in window) ? 'touchend' : 'click', handler);

微軟已經首先致力於創造更好的,更有未來的“指標事件”規範。指標事件是現在在W3C擬議的一個抽象的輸入機制。滑鼠指標事件給使用者代理很大的靈活性,它在少於一個事件的系統下可以容納大量的輸入機制。滑鼠,觸控和手寫筆是如今很容易浮現在腦海裡的例子,儘管延伸至MyoRing也是有可能實現的。而Web開發人員似乎很興奮這一點,儘管並不是所有的瀏覽器的工程師們也有同感。就像蘋果和谷歌已經決定不在這個時候落實指標事件。

谷歌的決定並不一定是最終的,但現在對指標事件並沒有積極的工作。我們通過polyfills輸入和使用指標事件和替代解決方案將成為等式的一部分,可能最終起決定作用。蘋果在2012年對指標事件做出了發言,不過我沒有看到Safari的工程師有其他更進一步的公開回應

事件級聯

當使用者在移動裝置上點選元素時,瀏覽器將啟用事件。這個動作通常會觸發一系列如下事件:touchstart → touchend →mouseover → mousemove → mousedown → mouseup → click

這是由於web的向後相容性。指標事件採取另一種方式,觸發內聯相容事件:mousemove → pointerover → mouseover→ pointerdown → mousedown → gotpointercapture → pointerup → mouseup → lostpointercapture→ pointerout → mouseout → focus → click

事件規範允許使用者代理們用不同的方式來實現相容性的事件。Patrick Lauke和Peter-Paul Koch在這個話題上有著觀法的參考資料,在這篇文章底部有連結到這些資源的連結。

下圖顯示了事件級聯的操作流程:

  • 在一個元素上最初的點選,
  • 在一個元素上第二次點選,
  • 關閉元素點選

請注意:此事件堆疊中有意的忽略了聚焦和失焦事件

事件級聯IOS

事件級聯在IOS裝置上點選元素兩次然後失去事件(圖片:Stephen Davis)(檢視大圖

事件級聯Android

事件級聯在Android4.4以上裝置上點選元素兩次然後失去事件(圖片:Stephen Davis)(檢視大圖

事件級聯IE

事件級聯在IE11(相容觸控事件實施之前)上點選元素兩次然後失去事件(圖片:Stephen Davis)(檢視大圖

運用事件級聯

因為瀏覽器工程師的工作,如今建立的大多數桌面Web網站“只是可以執行”。儘管級聯看起來有點粗糙,但建立滑鼠事件是我們以前通常工作的保守做法。

當然,這有一個陷阱。臭名昭著的300毫秒延遲是非常有名的,但是滾動之間的相互作用,touchmovepointermove事件,瀏覽器的渲染這些額外的問題。避免300毫秒的延遲是很容易的,如果:

  • 我們只優化Android和桌面上的使用啟發式現代瀏覽器,比如通過<meta name="viewport" content="width=device-width">來禁用延遲。
  • 我們只優化適用於IOS,而且使用者也明確按下而不是一個快速點選或者長按——這知識一個良好的、正常的、明確的元素(哦,那也取決於它是否是在一個UIWebView或者一個WKWebView上面-可以看一下FastClick的話題)。

如果我們的目標是建立一個在使用者體驗上可以和本地平臺競爭Web產品,那麼我們就需要減少互動所帶來的延遲。要做到這一點,我們必須建立原始事件(down,moveup),並建立自己的複合事件(clickdouble-click)。當然,我麼仍然需要包括本機備用處理程式來得到廣泛支援和輔助。

這樣做需要大量的程式碼和知識。為了避免300毫秒(或任何長度)跨瀏覽器延遲,我麼需要自己處理生命週期的全部互動。對於給定的{type}down事件,我麼需要繫結所有事件,為完成該操作這是必要的。當互動完成後,我們會在需要自己清理除了其實事件的所有被接觸繫結的事件。

網站開發人員,你是唯一一個知道頁面是否應該放大或另一個雙擊事件是否必須等待的人。如果只有你需要回撥推遲你應該允許一個預定的動作來延遲。

在下面的連結裡,你會發現一個小的,無依賴點選案例來說明建立一個多輸入、低延遲的點選事件所需的工作量。Polymer-gestures是一個為tap點選和其他事件生產開發的庫。儘管是這個名字有Polymer,但是它是不依賴Polymer庫而且很容易被隔離。

明確的說,實施這個從一開始來說就是一個壞主意,以下這些應該僅僅用於教育而不是用於生產環境。用於生產環境的庫已經存在,例如:FastClickpolymer-gesturesHammer.js

重要部分

在所有開始的地方繫結你的初始事件,這下面處理多輸入的模式是被認為一種保險的方式。

/**
 + If there are pointer events, let the platform handle the input
 + mechanism abstraction. If not, then it’s on you to handle
 + between mouse and touch events.
 */
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事件需要和渲染效能妥協,即使它們沒有做任何事,但為了減少這種影響,通常推薦在處理程式開始時候繫結跟蹤事件。別忘了在完成你的事件處理後要清理自己的環境和解綁跟蹤事件。

/**
 + On tapStart we want to bind our move and end events to detect
 + whether this is a “tap” action.
 + @param {Event} event the browser event object
 */
function tapStart(event) {
  // bind tracking events. “bindEventsFor” is a helper that automatically
  // binds the appropriate pointer, touch or mouse events based on our
  // current event type. Additionally, it saves the event target to give
  // us similar behavior to pointer events’ “setPointerCapture” method.
  bindEventsFor(event.type, event.target);
  if (typeof event.setPointerCapture === 'function') {
    event.currentTarget.setPointerCapture(event.pointerId);
  }
  // prevent the cascade
  event.preventDefault();
  // start our profiler to track time between events
  set(event, 'tapStart', Date.now());
}
/**
 + tapEnd. Our work here is done. Let’s clean up our tracking events.
 + @param {Element} target the html element
 + @param {Event} event the browser event object
 */
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,jQuery polyfill的Pointer Events或者polymer-gestures

總結

一些曾經很清楚的事件現在卻是有歧義的,以前click事件用來指有且只有一件事,但是現在在觸控式螢幕上面需要辨別是雙擊、滾動或者其他作業系統的手勢。

好訊息是,我們現在明白了很多使用者的操作習慣和瀏覽器的響應之間的事件級聯和相互作用,通過在工作中認識的原語,我們自己能夠在我們的專案中為我們的使用者和Web的未來做出更好的決策。

你在構建多裝置的網站時,有遇到什麼意想不到的問題?你採取什麼樣的方法來解決Web上眾多的互動模式?

擴充套件閱讀

相關文章