【原】移動端vue頁面點透事件 - 分析與解決
近期專案遇到了vue頁面事件被帶到下一個頁面的問題,也就是我們常說的點透事件,主要表現在android機器上,花了不少時間折騰,簡單做下總結~
- vue頁面之間的切換通過Vue Router的router.push方法
- b.vue之前已經訪問過,資料通過vuex管理,從a.vue進入到b.vue不再請求資料,直接拿到b.vue資料展示頁面;
- a.vue頁面上點選最底部的賬單後,不到100ms就開啟b.vue頁面,此時最底部的賬單的觸控事件並沒有消失,a.vue的觸控事件直接平移到b.vue最底部位置,剛好最底部有個按鈕,導致直接開啟c.vue。
驗證是否因為300ms延遲導致的問題
早在2013做移動端頁面時候就聽說過點透事件,由click事件引起的300ms的延遲導致:
移動裝置上的web網頁是有300ms延遲的,往往會造成按鈕點選延遲,引起頁面點透或是點選失效。
2007年蘋果釋出首款iphone上IOS系統搭載的safari為了將適用於PC端上大螢幕的網頁能比較好的展示在手機端上,使用了雙擊縮放(double tap to zoom)的方案,比如你在手機上用瀏覽器開啟一個PC上的網頁,你可能在看到頁面內容雖然可以撐滿整個螢幕,但是字型、圖片都很小看不清,此時可以快速雙擊螢幕上的某一部分,你就能看清該部分放大後的內容,再次雙擊後能回到原始狀態。
雙擊縮放是指用手指在螢幕上快速點選兩次,iOS 自帶的 Safari 瀏覽器會將網頁縮放至原始比例。
原因就出在瀏覽器需要如何判斷快速點選上,當用戶在螢幕上單擊某一個元素時候,例如跳轉連結<a href="#"></a>,此處瀏覽器會先捕獲該次單擊,但瀏覽器不能決定使用者是單純要點選連結還是要雙擊該部分割槽域進行縮放操作,所以,捕獲第一次單擊後,瀏覽器會先Hold一段時間t,如果在t時間區間裡使用者未進行下一次點選,則瀏覽器會做單擊跳轉連結的處理,如果t時間裡使用者進行了第二次單擊操作,則瀏覽器會禁止跳轉,轉而進行對該部分割槽域頁面的縮放操作。那麼這個時間區間t有多少呢?
在IOS safari下,大概為300毫秒。這就是延遲的由來。造成的後果使用者純粹單擊頁面,頁面需要過一段時間才響應,給使用者慢體驗感覺,對於web開發者來說是,頁面js捕獲click事件的回撥函式處理,需要300ms後才生效,也就間接導致影響其他業務邏輯的處理。
事隔7年,瀏覽器廠商還沒修復這個問題麼?樓主使用iPhone X、華為、vivo、小米等主流的手機來測試。
首先我們在移動端網頁使用雙擊縮放模式(不設定viewport)
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>測試</title> <script type="text/javascript" class="library" src="https://cdn.bootcss.com/zepto/1.1.7/zepto.js"></script> <style type="text/css"> #btn{ margin: 50px 0; width: 200px; height: 40px; text-align: center; font-size: 16px; } </style> </head> <body> <button id="btn">點我檢視事件響應時間</button> <script type="text/javascript"> let startTime; let log = function (msg) { let div = $('<div></div>'); div.html((new Date().getTime()) + ': ' + (new Date().getTime() - startTime) + ': ' + msg) $('body').append(div); }; let touchStart = function () { startTime = new Date().getTime(); log('touchStart'); }; let touchEnd = function () { log('touchEnd'); }; let click = function () { log('click'); }; let mouseUp = function () { log('mouseUp'); }; let mouseDown = function () { log('mouseDown'); }; let btn = $('#btn'); btn.bind('touchstart', touchStart); btn.bind('touchend', touchEnd); btn.bind('mousedown', mouseDown); btn.bind('mouseup', mouseUp); btn.bind('click', click); </script> </html>
對應日誌如下,可以看到沒有新增viewport的情況,ios可以看到click事件300ms的延遲問題還是存在,Android表現正常。
接著,頁面新增viewport:<meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport">的情況,ios和android觸發事件後列印的日誌並沒有300ms延遲。
從上面結果看出300ms的延遲主要表現在沒有設定viewport的ios手機上,H5頁面我們一般會設定viewport:<meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport">
所以樓主認為我們正常的H5頁面中click事件是不存在300ms延遲的情況
那Android手機上點透事件到底怎麼回事呢?
為了進一步驗證不是300ms延遲引起的問題,採用網際網路常見的防點透方案來解決問題:
比較流行的解決方案有v-tap或vue2-touch-events自定義的tap事件,原理是把三個基礎觸控事件:touchstart、touchmove、touchend組合起來叫tap事件,不使用click。
然而,經過反覆測試後不能解決問題,所以樓主猜想本次點透並不是300ms引起的。
還有一種解決點透的方案是新增透明遮罩層,通常在跳轉的下一個頁面上,有一個透明遮罩層置於最頂層,從上一個頁面平移的事件不能觸發當前頁面的事件,然後過300ms後再隱藏該遮罩層,以此來阻止點透,可以暴力解決點透問題。
檢查了這次專案中程式碼的HTML結構,其中包含touchstart、touchmove、touchend 、click 4個事件,測試發現click事件的程式碼並沒有起作用,
<template> <div class="item" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" @click="clickItem" > <slot :data="item"></slot> </div> </template>
因為touchend觸發後有個路由的方法router.push讓頁面跳轉了,click事件並沒有生效,也就是touchend後頁面A把事件帶到了頁面B上,又剛好命中頁面B上的按鈕,從而導致頁面C被開啟的點透效果!
touchEnd: function (event) { this.$emit('goToDetail')//跳轉detail頁面 }
發現原因:元素touchend之後觸發瀏覽器預設行為導致點透
猜測給div標籤繫結touchend事件後,元素有了瀏覽器預設的行為,具體的預設行為是什麼呢,但它非常像click事件被帶到B頁面。為了驗證猜測,在touchEnd方法中最後一行前新增 event.preventDefault()
preventDefault是事件物件(Event)的一個方法,作用是取消瀏覽器事件的預設行為;
cancelable也是事件物件(Event)的一個方法, 表明該事件是否可以被取消預設行為,如果該事件可以用 preventDefault() 可以阻止與事件關聯的預設行為,則返回 true,否則為 false
touchEnd: function (event) { this.$emit('goToDetail') if(event.cancelable) { event.preventDefault() } }
解決方案:使用preventDefault()來阻止點透
測試發現 event.preventDefault() 能成功阻止了androids手機上vue頁面切換導致事件點透的問題(目前ios並沒有發現事件點透問題,可能在多個版本前修復了這個體驗),也驗證了猜想:div標籤繫結touchend事件後,元素有了瀏覽器預設的行為
vue頁面開發,在HTML結構上新增事件修飾符.prevent,即@touchend.prevent同樣可以呼叫 event.preventDefault()來阻止預設行為
<template> <div class="item" @touchstart="touchStart" @touchmove="touchMove" @touchend.prevent="touchEnd" > <slot :data="item"></slot> </div> </template>
使用click作為最終事件也可以防止點透
div標籤繫結touchend事件後,元素有了瀏覽器預設的行為,這個所謂的預設行為又觸發click事件,從而引起點透的問題。
那麼,如果把this.$emit('goToDetail')的方法繫結到click上,是否可以解決點透問題?
<template> <div class="item" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" @click="click" > <slot :data="item"></slot> </div> </template>
click: function () { this.$emit('goToDetail') }
經過測試,在click事件觸發this.$emit('goToDetail')方法,頁面跳轉成功後並不會引起點透的問題。
總結:
- Android裝置中,div等標籤繫結touchend事件後,元素有了瀏覽器預設的行為,比如觸發click
- 移動端vue頁面點透事件可以使用事件修飾符.prevent或event.preventDefault() 來阻止瀏覽器預設行為
最後晒上家裡2只貓,祝大家聖誕快樂~ 喵