手机端按钮包含按钮的触发问题

问题

如果给一个dom添加触摸事件以后,那这个dom里面子元素的触摸事件还可以触发么?这是最近项目中遇到的一个问题。

之后我做了个测试。

 

测试

测试环境:iphone4&ios6,lumia920&wp8,某安卓机

测试的点击方式:默认的click、MBP、Fastclick、Zepto&tap。

测试演示:http://gaofeiyu.com/demo/click/click.html

演示的基本逻辑是黄色块点击后会隐藏自己,黑色的底色块和红色块点击后会改变色块内的html并记录点击次数。

演示里面可以看出click和Fastclick的子元素和父元素都会触发,而MBP和tap事件只有当前dom会触发。

 

总结

在移动项目中还是Zepto和Fastclick混用的方案比较合适,无论点透还是这种逻辑混乱的交互都搞得通。

建议在项目中尽量用一种触摸事件触发方式,不然碰到问题改代码会很难看。我这边最开始用的是tap,之后发现各种问题搞得不得不做很多多余的操作,在优化后还是统一用了Fastclick,父子元素的问题,也只能换个方向去解决,毕竟这个问题本身的用户体验就不是很好。

 

 

我还顺便写了个连点的小样例在演示里面,有心的人可以分析一下。

 

 

关于移动设备“点透”的问题

“点透”是什么

你可能碰到过在列表页面上创建一个弹出层,弹出层有个关闭的按钮,你点了这个按钮关闭弹出层后后,这个按钮正下方的内容也会执行点击事件(或打开链接)。我暂时定义为这是一个“点透”现象。

如何产生的

一般来说我们用原生的touch事件,还有jqueryMobile和zepto的tap可能会产生上面的问题。
相关的内容可以参考http://blog.youyo.name/archives/zepto-tap-click-through-research.html

注:当你点击链接页面刷新太快也会出现点透状况,这个是要在测试中特别注意的。

来说点转载文章中没说到的

规避这个点透现象除了上面参考文章说的方法,有更简单的方法。
推荐方法是引入Fastclick.js插件,该插件重写了默认的click事件,你可以通过
FastClick.attach(document.body);

来对整个文档操作,也可以对某节点单独操作。
那么我之前写的《触屏网页防止误点击(GHOSTCLICK)》中的 MBP 也可以避免这个问题,选择你喜欢的方法去解决。

 

 

触屏网页防止误点击(ghostClick)

如果在触屏的网页中,你为一个dom元素绑定’touchstart’,’touchend’事件来实现点击互动,那么如果你拖动屏幕时触摸到这个dom也会触发这两个事件,因为move的过程中也会执行start和end,这违背了我们的交互需要。
显然,我们要想办法规避这个问题。
如果你用jqueryMobil或者zepto,只要用里面针对触屏的事件,是不会碰到这个问题的。
基本思路就是重写了触发的的方法,根据条件来判断当前事件状态,在’touchend’的时候来决定当前用户是用了点击还是用了移动。
如果不想引入类库要怎么做。

我们增加一个函数MBP(全称为Mobile Boilerplate,该点击事件函数是该项目中的一部分)。
整个函数参考《HTML5移动Web开发实践》四章9节的代码内容

但是书里的内容有些问题,我对里面的内容做了一些修改,我们看下面代码。

函数部分

(function(document){//防止误点击
window.MBP = window.MBP || {};
MBP.fastButton = function (element, handler) {//函数入口
this.element = element;
this.handler = handler;
if (element.addEventListener) {//这里对选用的触发事件做了区分,保证触屏和非触屏用的是不同的事件绑定。
if (document.hasOwnProperty("ontouchstart")) {
element.addEventListener('touchstart', this, false);
}else{
element.addEventListener('click', this, false);
}
}
};
MBP.fastButton.prototype.handleEvent = function(event) {//监听触发事件类型
switch (event.type) {
case 'touchstart': this.onTouchStart(event); break;
case 'touchmove': this.onTouchMove(event); break;
case 'touchend': this.onClick(event); break;
case 'click': this.onClick(event); break;
}
};
MBP.fastButton.prototype.onTouchStart = function(event) {
//onTouchStart方法监听touchmove和touchend事件,Stoppropagation函数用来防止事件在监听中传递,阻止冒泡,即只在当前方法体内寻找当前事件的监听器
event.stopPropagation();
this.element.addEventListener('touchend', this, false);
document.body.addEventListener('touchmove', this, false);
this.startX = event.touches[0].clientX;
this.startY = event.touches[0].clientY;
};
MBP.fastButton.prototype.onTouchMove = function(event) {//如果触摸偏移10像素以上判断为move事件
if(Math.abs(event.touches[0].clientX - this.startX) > 10 ||
Math.abs(event.touches[0].clientY - this.startY) > 10 ) {
this.reset();
}
};
MBP.fastButton.prototype.onClick = function(event) {//如果不是其他可能,执行事件
event.stopPropagation();
this.reset();
this.handler(event);
if(event.type == 'touchend') {
MBP.preventGhostClick(this.startX, this.startY);
}
};
MBP.fastButton.prototype.reset = function() {//核心方法,移除move和touched的事件,以保证做正确的事件
this.element.removeEventListener('touchend', this, false);
document.body.removeEventListener('touchmove', this, false);
};
MBP.preventGhostClick = function (x, y) {
MBP.coords.push(x, y);
window.setTimeout(function (){
MBP.coords.splice(0, 2);
}, 2500);
};
MBP.ghostClickHandler = function (event) {//这部分是对不支持touch事件设备的兼容
for(var i = 0, len = MBP.coords.length; i < len; i += 2) {
var x = MBP.coords[i];
var y = MBP.coords[i + 1];
if(Math.abs(event.clientX - x) < 25 && Math.abs(event.clientY - y) < 25) {
event.stopPropagation();
event.preventDefault();
}
}
};
if (document.addEventListener) {//为文档中其他click事件添加防止误点击操作
document.addEventListener('click', MBP.ghostClickHandler, true);
}
MBP.coords = [];
})(document);

如何使用

var btn = document.getElementById('domId');
btnTouch = new MBP.fastButton(btn,function(){alert('点击成功')});

关于触摸事件的延伸——点透问题

在移动设备中用了触摸点击事件会产生一系列的点透问题,可以转到我文章中的《关于移动设备“点透”的问题》查看解决方法。

其他补充

不要在该方法生成的事件中添加alert事件,不然在ios(我的环境是ios6)中触发后会在下一次触摸屏幕的时候触发touchstart事件,执行未被销毁的alert产生问题。

批处理压缩css和js for nodejs

同事推荐的压缩css和js的方法,是运行在nodejs的基础上的,所以你要先安装一个nodejs。

在有nodejs的基础上,进入命令行模式分别运行

npm install clean-css -g

npm install uglifyjs -g

clean-css是压缩css的,uglifyjs是压缩js的。
然后新建两个批处理文件。
@echo off
:: 设置压缩css文件的根目录,脚本会自动按树层次查找和压缩所有的css
SET OUTFOLDER= 压缩后的目标目录
echo 正在深度复制文件
xcopy 准备压缩的目录*.* "%OUTFOLDER%" /s /e && echo 复制成功 || echo 复制失败,请检查文件是否存在!
echo 复制完成
:: 设置压缩的路径
chdir /d %OUTFOLDER%
echo 正在查找CSS 文件
for /r . %%b in (*.css) do (
@echo 正在压缩 %%~b ...
cleancss -o %%~fb %%~fb
)
echo CSS压缩完成
echo 完成!

@echo off
:: 设置压缩JS文件的根目录,脚本会自动按树层次查找和压缩所有的JS
SET OUTFOLDER = 压缩后的目标目录
echo 正在深度复制文件
xcopy 准备压缩的目录*.* "%OUTFOLDER%" /s /e && echo 复制成功 || echo 复制失败,请检查文件是否存在!
echo 复制完成
:: 设置压缩的路径
chdir /d %OUTFOLDER%
echo 正在查找JS文件
for /r . %%a in (*.js) do (
@echo 正在压缩 %%~a ...
uglifyjs %%~fa -o %%~fa
)
echo JS压缩完成
echo 完成!

目录的格式应该是 C:\abc\
只要你写的路径是绝对路径批处理文件可以放在任意你喜欢的位置。
如果有需要,你也可以写到一起。

zepto兼容ie10的方法

原先的zepto,通过__proto__赋值,来使dom继承到$.fn方法,

无奈IE11之前的IE10,IE9不支持这种写法,

所以我们只能自己手动把方法添加到dom
// `$.zepto.Z` swaps out the prototype of the given `dom` array
// of nodes with `$.fn` and thus supplying all the Zepto functions
// to the array. Note that `__proto__` is not supported on Internet
// Explorer. This method can be overriden in plugins.
zepto.Z = function(dom, selector) {
dom = dom || []

// 支持ie10,主要是支持wp8
if(navigator.userAgent.indexOf(“MSIE 10″) > -1){
for(var func in $.fn){
dom[func] = $.fn[func];
}
}
else{
dom.__proto__ = $.fn
}

dom.selector = selector || ”
return dom
}

继续阅读“zepto兼容ie10的方法”

关于js中,“与”运算符“&&”和“或”运算符“||”的一些用法

在各类插件里总是能看到这样的赋值。
var a = b || {}
能看出如果b有值就赋值b,如果没有就给一个空对象。
那么下面这段呢
var a = (b==5 && 1) || (b==10 && 2) || (b==12 && 3) || (b==15 && 4) || 0;
就不是那么清晰可见了,下面引用一篇文章,详细说明一下这个问题。

原文地址:http://www.jb51.net/article/21339.htm

首先出个题:

如图:
假设对成长速度显示规定如下:
成长速度为5显示1个箭头;
成长速度为10显示2个箭头;
成长速度为12显示3个箭头;
成长速度为15显示4个箭头;
其他都显示都显示0各箭头。
用代码怎么实现?

差一点的if,else:
Js代码
var add_level = 0;
if(add_step == 5){
add_level = 1;
}
else if(add_step == 10){
add_level = 2;
}
else if(add_step == 12){
add_level = 3;
}
else if(add_step == 15){
add_level = 4;
}
else {
add_level = 0;
}

稍好些的switch:
Js代码
var add_level = 0;
switch(add_step){
case 5 : add_level = 1;
break;
case 10 : add_level = 2;
break;
case 12 : add_level = 3;
break;
case 15 : add_level = 4;
break;
default : add_level = 0;
break;

}如果需求改成:
成长速度为>12显示4个箭头;
成长速度为>10显示3个箭头;
成长速度为>5显示2个箭头;
成长速度为>0显示1个箭头;
成长速度为<=0显示0个箭头。

那么用switch实现起来也很麻烦了。

那么你有没有想过用一行就代码实现呢?
ok,让我们来看看js强大的表现力吧:
Js代码
var add_level = (add_step==5 && 1) || (add_step==10 && 2) || (add_step==12 && 3) || (add_step==15 && 4) || 0;
更强大的,也更优的:

Js代码
var add_level={'5':1,'10':2,'12':3,'15':4}[add_step] || 0;
第二个需求:
Js代码
var add_level = (add_step>12 && 4) || (add_step>10 && 3) || (add_step>5 && 2) || (add_step>0 && 1) || 0;
首先我们来梳理一下一个概念,请你一定要记住:在js逻辑运算中,0、””、null、false、undefined、NaN都会判为false,其他都为true(好像没有遗漏了吧,请各位确认下)。这个一定要记住,不然应用||和&&就会出现问题。
这里顺便提下:经常有人问我,看到很多代码if(!!attr),为什么不直接写if(attr);
其实这是一种更严谨的写法:
请测试 typeof 5和typeof !!5的区别。!!的作用是把一个其他类型的变量转成的bool类型。
下面主要讨论下逻辑运算符&&和||。
几乎所有语言中||和&&都遵循“短路”原理,如&&中第一个表达式为假就不会去处理第二个表达式,而||正好相反。
js也遵循上述原则。但是比较有意思的是它们返回的值。
代码:var attr = true && 4 && “aaa”;
那么运行的结果attr就不是简单的true或这false,而是”aaa”
再来看看||:
代码:var attr = attr || “”;这个运算经常用来判断一个变量是否已定义,如果没有定义就给他一个初始值,这在给函数的参数定义一个默认值的时候比较有用。因为js不像php可以直接在型参数上定义func($attr=5)。再次提醒你记住上面的原则:如果实参需要是0、””、null、false、undefined、NaN的时候也会当false来处理。

if(a >=5){
alert(“你好”);
}
可以写成:
a >= 5 && alert(“你好”);
这样只需一行代码就搞定。但是需要注意的一点就是:js中||和&&的特性帮我们精简了代码的同时,也带来了代码可读性的降低。这就需要我们自己来权衡了。
一方面精简js代码,能实质性的减少网络流量,尤其是大量应用的js公用库。个人比较推荐的做法是:如果是相对复杂的应用,请适当地写一些注释。这个和正在表达式一样,能够精简代码,但是可读性会降低,对读代码的人要求会高些,最好的办法就是写注释。

我们可以不使用这些技巧,但是我们一定要能看懂,因为这些技巧已经广泛应用,尤其是像JQuery等js框里的代码,不理解这些你就很难看懂别人的代码。
像var Yahoo = Yahoo || {};这种是非常广泛应用的。
ok,最后让我们来看一段jQuery中的代码吧:

Js代码
var wrap =
// option or optgroup
!tags.indexOf("<opt") &&
[ 1, "<select multiple='multiple'>", "</select>" ] ||!tags.indexOf("<leg") &&
[ 1, "<fieldset>", "</fieldset>" ] ||

tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
[ 1, "<table>", "</table>" ] ||

!tags.indexOf("<tr") &&
[ 2, "<table><tbody>", "</tbody></table>" ] ||

// <thead> matched above
(!tags.indexOf("<td") || !tags.indexOf("<th")) &&
[ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] ||

!tags.indexOf("<col") &&
[ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] ||

// IE can't serialize <link> and <script> tags normally
!jQuery.support.htmlSerialize &&
[ 1, "div<div>", "</div>" ] ||

[ 0, "", "" ];

// Go to html and back, then peel off extra wrappers
div.innerHTML = wrap[1] + elem + wrap[2];

// Move to the right depth
while ( wrap[0]-- )
div = div.lastChild;


这段代码是作者用来处理 $(html) 时,有些标签必须要约束的,如<option>必须在<select></select>之内的。
可能你也发现了作者还有一个很巧的地方就是 !tags.indexOf(“<opt”) ,作者很巧很简单的就实现了startWith的功能了,没有一点多余的代码。jquery源代码中还有很多如此精妙的代码,大家可以去学习学习。

关于js的字符串编码

escape不编码字符有69个:*,+,-,.,/,@,_,0-9,a-z,A-Z

encodeURI不编码字符有82个:!,#,$,&,’,(,),*,+,,,-,.,/,:,;,=,?,@,_,~,0-9,a-z,A-Z

encodeURIComponent不编码字符有71个:!, ‘,(,),*,-,.,_,~,0-9,a-z,A-Z

 

event.preventDefault和恢复元素默认事件

写页面事件的时候,有的时候需要用event.preventDefault取消原有的事件后进行重写,这个大家应该都知道。

那么怎么在取消默认事件后再恢复呢。

解绑我们自定义的事件就好了。

以Jquery为例

我们用$(“body”).bind(“touchmove”,function(event){event.preventDefault;//code});取消了body的拖动事件。

恢复这个拖动事件只要$(“body”).unbind(“touchmove”);

就OK了~意外地简单:3

关于判断ie版本的一段js代码


ie = (function() {
var v = 3, div = document.createElement('div'), a = div.all || [];
while (div.innerHTML = '<!--[if gt IE '+(++v)+']><br><![endif]-->', a[0]);
return v > 4 ? v : !v;
}());

这个不同于其他用ua判断浏览器版本的方法。
div.all
all方法只有ie支持。
再利用注释的兼容性方法来判断当前是哪款ie浏览器。