接触过移动端开发的前端可能都会面临点击事件的第一个问题:click事件的300ms延迟响应。不能立即响应给体验造成了很大的困扰,因此解决这个问题就成为了必然。
这个问题的解决方案就是:zepto.js
的tap事件。tap事件可以理解为在移动端的click事件,而zepto.js因为几乎完全复制jQuery的api,因此常常被用在h5的开发上用来取代jquery.
由于模块化的原因,导致有的前端下载的zepto.js的模块并不全,造成了一大堆悲剧,以为zepto.js不支持一些方法,所以,下载的时候注意一点。
然而事情到这里并没有结束,因为tap事件解决了一个300ms延迟问题,却带来了一个新的重大bug,点击穿透。
点击穿透的意思,就是如果一个绝对定位或者固定定位元素处于页面最顶层,对这个元素绑定一个点击事件,那么你点击这个点对应的下面凡是有点击事件或者a标签都会被触发执行。这里就不贴图了,自行脑补各种弹窗,这种情况还是非常多的。
为了解决这个问题,有的人试图用touchend来搞定。touchend会在手指离开手机屏幕时触发一次。没有300ms延迟,没有点击穿透,看上去简直就是完美的解决方案。可是!
先在这里简单总结一个小知识点。
移动端有touchstart, touchmove, touchend, 以及tap
pc端有mousedown, mousemove, mouseup, click。
他们的关系和作用几乎可以对应起来,分别表示按下,滑动,松开。pc端可以用前面三个事件实现拖拽,移动端也可以用前面三个事件实现的滑动。
所以,在pc端不用mouseup来替换click,就是因为他在松开鼠标的时候就会触发,导致如果我在很远的区域滑动到目标元素,然后松开,这样的情况也会触发mouseup与touchend事件。所以这种情况下是不符合点击事件的定义的。而且如果你在某种情况下,对某一个事件需要同时绑定拖拽和点击,就更加没办法解决了。
另外还有一个很重要的原因导致touchend不能用来替换点击,是因为PC端不支持。老板们常常希望自己的页面不仅仅能够在移动端展示,因此还得想其他办法。
我知道有经验的前端读这篇文章的时候,早就在想fastclick.js
了。是的,目前来看,这是一个非常好的解决方案。为了解决300ms延迟的问题,zepto.js给出了tap事件替换的方案,而fastclick.js则是在想办法让click事件的延迟消除。因此任然是使用click事件,也就不会有点击穿透的问题。
首先想办法引入fastclick.js
如果你使用原生js开发则进行如下声明即可。
if ('addEventListener' in document) { document.addEventListener('DOMContentLoaded', function() { FastClick.attach(document.body); }, false); }
如果你想使用jquery
$(function() { FastClick.attach(document.body); });
如果你在使用CommonJS风格的框架,比如requirejs
var attachFastClick = require('fastclick'); attachFastClick(document.body);
AMD
var FastClick = require('fastclick'); FastClick.attach(document.body, options);
进行对应的声明之后,你就可以在移动端页面中放心大胆的使用click事件了。说到这里,就会有一个关于zepto.js与jquery选择的问题。说起来又可以写一大篇文章了,简单来说就是,实际开发中你就会发现zepto还是有点不太爽,虽然体积小点,既然click延迟的问题已经解决了,我还是更偏向于使用jquery.
当然,我们要踩的坑并没有结束 - -!
最近开发了一个小页面,财经日历,在日历部分,我需要同时给代表每一天的元素事件实现获取当天资讯的需求,又要给整个日历部分实现能够左右滑动来选取上一月和下一月的功能。所以我需要同时对日历部分绑定click事件和实现滑动的touchstart,touchmove,touchend事件。
这个时候问题出现了。在安卓手机上,对同一个元素,如果我绑定了click事件,然后在绑定touchstart事件,click事件会处于几乎失效的状态。就算用了fastclick事件也无法避免这个问题
错误演示大概如下
$area.on('click', '.weeknumber', function() { // 点击每一天获取当天资讯 }) // 实现左右滑动 $area.on('touchstart', function() {}) .on('touchmove', function() {}) .on('touchend', function() {})
动手能力强的前端可以去试试这个坑,这种情况下,fastfclick肯定是没办法解决的。怎么办?
我的第一次尝试,是在当滑动距离为0的时候,运行点击事件里面的内容。我们知道在实现滑动[不知道如何实现的前端,是时候关注我的公众号了,搜索isreact找到我]的时候,会计算一个滑动距离。
1 // 实现滑动的大概代码 2 3 // 滑动元素translateX的初始值 4 var iscroll = device_width, 5 6 // 用来计算的中间值 7 istarX = 0, 8 9 // 手指第一次点在屏幕上的x坐标 10 istart_pageX = 0; 11 12 // 绑定事件 13 $area.on('touchstart', touchstart) 14 .on('touchmove', touchmove) 15 .on('touchend', touchend); 16 17 function touchstart(event) { 18 event.preventDefault(); 19 istartX = iscroll; 20 istart_pageX = event.originalEvent.changedTouches[0].pageX; 21 } 22 23 function touchmove(event) { 24 25 // 滑动过程中手指位置x坐标会不停变动,这里会保存一个当前位置与初始位置的一个差值 26 var distance = event.originalEvent.changedTouches[0].pageX - istart_pageX; 27 iscroll = istartX + distance; 28 29 // 这里是我自定义的一个css方法,用来设置元素translateX的当前值 30 Utils.css(area, { translateX: iscroll }); 31 } 32 33 function touchend(event) { 34 var distance = event.originalEvent.changedTouches[0].pageX - istart_pageX; 35 $area.off('touchstart touchmove touchend'); 36 37 // 根据差值的不同,执行不同的动作 38 if (distance < -80) { 39 slideNext(function() { 40 addEventSlider($area); 41 }) 42 } 43 else if (distance > 80) { 44 slidePrev(function() { 45 addEventSlider($area); 46 }) 47 } 48 else if (distance == 0) { 49 /* 当差值为0时,我认为这是执行了一次点击 */ 50 51 addEventSlider($area); 52 } 53 else { 54 ani(area).animate(400, 'easeout', { x: -device_width }, function() { 55 iscroll = -device_width; 56 addEventSlider($area); 57 }) 58 } 59 }
上面就是我的滑动功能的实现,中间会有一些自定义的方法,因此没办法你们直接就复制过去运行。但是原理已经讲得还算明白,可以自己尝试一些简单的实现。关注我公众号会有更加详细的讲解哦!
我的这一次尝试,就是在滑动差值distance为0的时候,认为这是一次点击,因此执行点击事件应该有的动作。本来我以为这就能够解决了,测试的时候也通过了。但是 - -!qa非常有执念的在日历上点击了50多下,结果发现,多次点击之后,在点就失去效果了!!!
当发现这个bug的时候,我的内心是崩溃的。好吧,只能说,试图用touchend来代替点击事件的想法终究还是有一点不成熟。硬着头皮想办法继续解决上面的问题。
再三思考各种解决方案,最终决定借用人家封装一个tap事件。封装的tap事件的代码如下。
http://yangbo5207.github.io/static/demo/single-instance/js/dev/tap.js
这是对jquery事件的一个拓展,让jquery也能够使用tap事件。放在jquery后面引入就能够立即使用了。除此之外还扩展了longTap,swipe两个事件。
使用方式和其他一样。
$area.on('tap', function() {}); $area.tap(function() {})
当然封装的这个tap,仍然有避免不了的点击穿透bug,所以,最终的解决方案是:对于需要同时绑定点击事件和滑动事件的元素,用tap事件,其他情况都用click事件即可。需要结合这个tap.js与fastclick.js来完美解决这个问题。心累啊,终于是搞定了。
OK,最后关于移动端的点击事件总结完了,可能大家都没想到一个简单的点击事件会有那么多坑,如果大家在工作中可能会涉及到移动端开发的话,相信这篇文章还是值得你点赞和收藏的,毕竟是踩了那么多坑的经验总结。