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);

惊闻360综合搜索上线,前去围观了一下,果然牛逼,搜索结果页整个就是一个百度嘛,页面抄的挺像。
要做搜索,肯定是要“做”出来才行,页面抄成这样,连背景图片、CSS都直接拿来用,貌似“做”的不够道德啊。不过,抄风格这事在中国互联网界早已蔚然成风,企鹅早就驾轻就熟,因为这个就责怪360貌似有点冤,凭什么企鹅山寨就可以,我大360就不可以?
好吧,咱还是继续围观。在好奇、自恋等各种情绪的支配下,我在360的搜索框里输入了本博客的站点网址“site:www.zhujianfeng.info”。这个大家都懂的,就是看搜录了多少页面呗。搜索结果出来之后哥那个兴奋啊,360真是太给鄙人面子了,哥从来没有给360提交过url,哥的网站流量这么小,居然收录了,居然有3个页面!!!想当年哥想让百度收录,提交url之后苦等了好几个月啊!!苦等好几个月之后也才只收录了3个页面啊!!等等,怎么360收录的这3个页面跟百度一个月之前收录的那三个页面是一样的?顺序还是一样的,这让我情何以堪,合着您就是个二道贩子么?
额,这样下结论未免太早了,于是我做了一个不太艰难的决定,我决定去apache的访问日志里头找360的蜘蛛,如果没有,那就有得怀疑了。蜘蛛嘛,就是搜索引擎抓取网页的程序(详见http://baike.baidu.com/view/2755932.htm),搜索引擎给我们的结果都是蜘蛛事先从各个网站抓取过来的。那也就是说,既然我能在360综合搜索搜到我的网站,那么360的蜘蛛肯定要事前到我的网站上抓取过页面,我的apache日志里肯定就有360蜘蛛的“踪迹”。
比如,百度蜘蛛在我这里的踪迹如下:
123.125.71.113 – – [05/Jul/2012:06:59:34 +0000] “GET /wp-includes/wlwmanifest.xml HTTP/1.1” 200 1314 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.27 – – [05/Jul/2012:06:59:36 +0000] “GET /?feed=rss2 HTTP/1.1” 200 9435 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.25 – – [05/Jul/2012:06:59:38 +0000] “GET /?feed=rss2&p=18 HTTP/1.1” 200 1314 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.74 – – [05/Jul/2012:06:59:40 +0000] “GET /?p=18&replytocom=2 HTTP/1.1” 200 7297 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.48 – – [05/Jul/2012:06:59:42 +0000] “GET /?feed=rss2&page_id=2 HTTP/1.1” 200 1041 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.101 – – [05/Jul/2012:06:59:44 +0000] “GET /?feed=rss2&cat=4 HTTP/1.1” 200 4828 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.101 – – [05/Jul/2012:06:59:46 +0000] “GET /?feed=rss2&p=47 HTTP/1.1” 200 1089 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.103 – – [05/Jul/2012:06:59:48 +0000] “GET /?feed=rss2&cat=3 HTTP/1.1” 200 2864 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.53 – – [05/Jul/2012:06:59:50 +0000] “GET /?feed=rss2&p=27 HTTP/1.1” 200 1082 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.23 – – [05/Jul/2012:06:59:52 +0000] “GET /?feed=rss2&p=13 HTTP/1.1” 200 1079 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.102 – – [05/Jul/2012:06:59:54 +0000] “GET /?p=50&replytocom=3 HTTP/1.1” 200 7843 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.83 – – [05/Jul/2012:06:59:56 +0000] “GET /?feed=rss2&p=50 HTTP/1.1” 200 1209 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.113 – – [05/Jul/2012:06:59:58 +0000] “GET /?feed=rss2&cat=5 HTTP/1.1” 200 4827 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.36 – – [05/Jul/2012:07:00:00 +0000] “GET /?feed=rss2&cat=7 HTTP/1.1” 200 3824 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”
123.125.71.45 – – [05/Jul/2012:07:00:02 +0000] “GET /?feed=rss2&cat=6 HTTP/1.1” 200 2250 “-” “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)”

google的:
66.249.73.201 – – [23/Jun/2012:10:48:07 +0000] “GET /robots.txt HTTP/1.1” 404 512 “-” “Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)”
66.249.73.201 – – [23/Jun/2012:10:48:07 +0000] “GET /?p=mxvrrqtcpsp HTTP/1.1” 200 19085 “-” “Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)”
66.249.73.201 – – [23/Jun/2012:11:13:51 +0000] “GET /?p=mxvrrqtcpsp&paged=2 HTTP/1.1” 200 6873 “-” “Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)”
66.249.68.52 – – [23/Jun/2012:16:45:26 +0000] “GET /robots.txt HTTP/1.1” 404 512 “-” “Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)”
66.249.68.52 – – [23/Jun/2012:16:45:26 +0000] “GET /?p=41 HTTP/1.1” 200 4450 “-” “Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)”
66.249.68.52 – – [23/Jun/2012:19:45:24 +0000] “GET /?m=201204 HTTP/1.1” 200 8477 “-” “Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)”
66.249.67.162 – – [23/Jun/2012:22:03:28 +0000] “GET /robots.txt HTTP/1.1” 404 510 “-” “Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)”
66.249.67.162 – – [23/Jun/2012:22:03:28 +0000] “GET /?m=201206 HTTP/1.1” 200 5522 “-” “Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)”
66.249.67.162 – – [24/Jun/2012:04:03:36 +0000] “GET /wp-trackback.php?p=13 HTTP/1.1” 302 604 “-” “Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)”
66.249.67.162 – – [24/Jun/2012:04:03:37 +0000] “GET /?p=13 HTTP/1.1” 200 6593 “-” “Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)”

bing的:
157.55.18.23 – – [28/Jun/2012:08:52:12 +0000] “GET /robots.txt HTTP/1.1” 404 535 “-” “Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)”
157.55.18.23 – – [28/Jun/2012:08:52:46 +0000] “GET /?C=M;O=A HTTP/1.1” 200 9419 “-” “Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)”
157.55.17.150 – – [29/Jun/2012:06:13:25 +0000] “GET /robots.txt HTTP/1.1” 404 535 “-” “Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)”
157.55.18.23 – – [29/Jun/2012:06:56:05 +0000] “GET /?C=M;O=A HTTP/1.1” 200 9419 “-” “Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)”
157.55.16.11 – – [29/Jun/2012:08:12:05 +0000] “GET /robots.txt HTTP/1.1” 404 535 “-” “Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)”
157.55.16.11 – – [29/Jun/2012:08:23:58 +0000] “GET /phpinfo.php HTTP/1.1” 404 508 “-” “Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)”
157.55.17.150 – – [29/Jun/2012:08:55:06 +0000] “GET /wiki/index.php/%E9%A6%96%E9%A1%B5 HTTP/1.1” 404 527 “-” “Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)”
157.55.16.11 – – [29/Jun/2012:14:56:49 +0000] “GET /robots.txt HTTP/1.1” 404 535 “-” “Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)”
157.55.16.11 – – [29/Jun/2012:15:16:30 +0000] “GET / HTTP/1.1” 200 9419 “-” “Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)”
157.55.16.11 – – [23/Jun/2012:16:21:24 +0000] “GET /robots.txt HTTP/1.1” 404 535 “-” “Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)”

作为一个有身份的搜索引擎,相信360应该也是有蜘蛛的。接下来就是日志里找360蜘蛛啦。首先我们要猜,360的蜘蛛会叫啥呢?至少应该带上360或者带上qihu之类的词吧。如果真的没带,那就当我上面这些都在放P好了。在所有的日志里搜索一把“360”,大小写都算上,OH,no,这么多360浏览器的身影,去掉360EE和360SE,结果都是数字里恰好有个360。这。。。难道是真的真的真的没有360的蜘蛛来么?好吧,下面咱反过来找好了,先找所有带有bot或者spider的记录好了。正常的蜘蛛在名字里应该会有这两个单词中的一个吧,如果360的蜘蛛叫做“zhizhu”,那我无话可说,只能找块豆腐撞死算了。先查找含有bot或者spider的行,然后里面在查找里面是否有和360有关的单词。结果是。。。。。没有!!

这说明什么呢?这说明有很大的可能360的蜘蛛从来没有来过我的网站,但是他的结果页却有我的网站。套用小沈阳的一句名言:这是为什么捏?
为了避免是因为我的文本查找能力太差导致冤枉了360,我把日志放出来好了,大家帮我找找有360蜘蛛来过没,如果谁找到了请告诉我一声,我好好准备点苍蝇虫子啥的迎接360蜘蛛的到来。
日志下载地址:http://www.zhujianfeng.info/temp/access.tar.gz

作为一个菜鸟,最近学习了一下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代码等其他元素。

之前一直使用一个叫做新浪连接的插件来同步我的博客内容到新浪微博上。最近今天发现,总是同步失败,发布内容时页面报500错误。仔细查了下,是新浪强制使用了Oauth2的认证方式,插件使用的老认证方式已经不被支持了。到插件官网去看,作者貌似没有更新插件的意思,只能自己再想他法鸟。

找了一下,发现微博通提供api可以调用,同时提供了一个现成的wordpress插件,只需要注册微博通,然后在里面绑定微博账号,当然,可以绑定新浪、人人、腾讯等各种微博,一发全发,这个是之前那个插件没有的优势。不过也有令人不爽的地方,首先是这个插件功能比较弱,只能发文章标题,不能发摘要,不能发图片。参考新浪连接,我就把代码改了下,现在可以发标题摘要也能发图片了。代码如下:

<?php
/*
Plugin Name: 微博通同步发布
Plugin URI: http://www.wbto.cn
Description: 自动把你的博客文章同步到微博通,微博通将同步至你所绑定的各个平台。
Version: 1.0
Author: yige <abcwuwuwu@qq.com>
Author URI: http://t.qq.com/abcwuwuwu

Date: 2011年3月17日 23:07:30
Modified by :zhujianfeng <http://weibo.com/fdjianfeng> 2012-07-27
*/

function wbto_install() {
	global $wpdb;
	$table_name = $wpdb->prefix."wbto";
	if($wpdb->get_var("show tables like '$table_name'") != $table_name) {
		$sql = "CREATE TABLE " . $table_name . " (id mediumint(9) NOT NULL AUTO_INCREMENT, wbto_username VARCHAR(100) NOT NULL, wbto_password VARCHAR(100) NOT NULL, );";
	}
	require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
	dbDelta($sql);
}

function send_to_wbto($post_ID) {
	$username = get_option('wbto_username');
	$password = get_option('wbto_password');
	$posted = get_post($post_ID);

	$post_title = $posted->post_title;
	$post_content = get_post_excerpt($posted);

	$title_len = mb_strlen($post_title,'UTF-8');
	$content_len = mb_strlen($post_content,'UTF-8');
	$rest_len = 110;

	if($title_len + $content_len> $rest_len) {
		$post_content = mb_substr($post_content,0,$rest_len-$title_len).'... ';
	}
	$status = '【'.$post_title.'】 '.$post_content;

	$pic = get_post_first_image($posted->post_content);

	$fields = array();
	$fields['source'] = 'wordpress';
	$fields['content'] = urlencode($status.' '.$posted->guid);
	$wbto_url = "http://wbto.cn/api/update.json";
	if ($pic){
		$wbto_url = "http://wbto.cn/api/upload.json";
		$fields["imgurl"] = $pic;
	}

	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $wbto_url);
	curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
	curl_setopt($ch, CURLOPT_FAILONERROR, TRUE);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER,TRUE);
	curl_setopt($ch, CURLOPT_TIMEOUT, 10);
	curl_setopt($ch, CURLOPT_POST, TRUE);
	curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
	$result = curl_exec($ch);
	curl_close($ch);

}

function wbto_menu() {
add_options_page('微博通同步设置', '微博通同步', 8, __FILE__, 'wbto_options');
}
if(!function_exists('get_post_excerpt')){
	function get_post_excerpt($post){
		$post_excerpt = strip_tags($post->post_excerpt); 
		if(!$post_excerpt){
			###第一种情况,以<p>开始,</p>结束来取第一段 Windows live writer
			if(preg_match('/<p>(.*)<\/p>/iU',trim(strip_tags($post->post_content,"<p>")),$result)){ 
				$post_content = $result['1'];
			} else {
			###第二种情况,以换行符(\n)来取第一段   
				$post_content_r = explode("\n",trim(strip_tags($post->post_content))); 
				$post_content = $post_content_r['0'];
			}
			$post_excerpt = explode("\n",trim(strip_tags($post->post_content))); 
   			$post_excerpt = $post_excerpt['0'];	
		}
		$post_excerpt = trim(strip_tags($post_excerpt));
		$post_excerpt = str_replace('"', '', $post_excerpt);	
		// replace newlines on mac / windows?
		$post_excerpt = str_replace("\r\n", ' ', $post_excerpt);
		// maybe linux uses this alone
		$post_excerpt = str_replace("\n", ' ', $post_excerpt);
		$post_excerpt = mb_substr($post_excerpt,0,120);

		return $post_excerpt;
	}
}
if(!function_exists('get_post_first_image')){

	function get_post_first_image($post_content){
		preg_match_all('|<img.*?src=[\'"](.*?)[\'"].*?>|i', $post_content, $matches);
		if($matches){		
			return $matches[1][0];
		}else{
			return false;
		}
	}
}
function wbto_options() {
	echo '<div class="wrap">';
	echo '<h2>微博通同步</h2>';

	echo '<form method="post" action="options.php">';
	echo wp_nonce_field('update-options');

	echo '<table class="form-table">';

	echo '<tr valign="top">';
	echo '<th scope="row">用户名 <a href="http://www.wbto.cn/?app=wp">注册</a></th>';
	echo '<td><input type="text" name="wbto_username" value="'.get_option('wbto_username').'" /></td>';
	echo '</tr>';

	echo '<tr valign="top">';
	echo '<th scope="row">密码</th>';
	echo '<td><input type="password" name="wbto_password" value="'.get_option('wbto_password').'" /></td>';
	echo '</tr>';

	echo '</table>';

	echo '<input type="hidden" name="action" value="update" />';
	echo '<input type="hidden" name="page_options" value="wbto_username,wbto_password" />';

	echo '<p class="submit">';
	echo '<input type="submit" name="submit" id="submit" class="button-primary" value="保存更改" />';
	echo '</p>';

	echo '</form>';
	echo '</div>';

}

add_action('admin_menu', 'wbto_menu');
add_action('publish_post', 'send_to_wbto');
?>

还有另外一点比较不爽的就是,发送频率有限制,具体限制多久不知道,但是根据我的测试,5分钟内发两次是不行的。
参考的文章如下:
http://fairyfish.net/project/sina-connect/
http://www.jsxubar.info/wordpress-use-wp_wbto-plugin-sync-post-to-weibo.html

Posted in PHP.

工作中有个功能想用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,那么为什么当时把他设计出来了呢?

围脖上看到有人说AppJS,去官网围观了下,果然NB,这东西还真就能用JS写图形化本地程序。

传统的图形化本地程序(或者说客户端程序)一般是一个可执行文件,在windows上一般是个exe,执行之后有个图形界面供用户交互。能写图形化本地程序的语言比较多,C/C++、java、C#、python等等,可能有些程序需要图形库支持,比如QT、GTK之类。JS写图形化本地程序之前我还没有听说过,今天这个算是看了眼界了。

简单来说,AppJS把Chromium和Node.js结合在一起,使用了Node.js的本地化能力来进行本地逻辑处理,同时使用Chromium的图形化能力来展现图形化界面,把这两者串起来的就是js了。Node果然无比强大。

使用AppJS来写本地程序有以下几个好处:

1、像写网页一样写本地程序。因为AppJS的图形化用的是Chromium,本质上还是使用浏览器来展现,所以写这样的本地程序跟写网页区别不大。
2、你可以使用Chromium提供的很多牛X哄哄的特性:HTML5, CSS3, SVG, WebGL等
3、你可以使用Node提供的与系统有关的特性:文件系统访问、网络访问、进程管理等,你甚至可是让你的程序是个http服务器。
4、跨平台,目前AppJS支持Windows、Linux和Mac。

来看一下AppJS自带的一个例子吧,界面如图:

这样的界面是怎么做出来的呢,还是看看代码好了

<!doctype html>
<html>
<head>
<title>Hello World!</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>

仅仅是一个页面而已,当然还用了点CSS3

html{ 
width: 100%; 
height: 100%; 
overflow: hidden; 
background-image: url(rainbowsky.jpg); 
background-size: cover;
-webkit-box-sizing: border-box; 
}
body{ 
margin: 0; 
height: 100%; 
font-family: 
FixedWidth; 
color: #fff 
}
h1{ 
text-align: center; 
font-weight: 900; 
font-size: 120px; 
line-height: 1; 
text-shadow: 6px 3px 3px rgba(0,0,0,.7), 0 0 30px #aaccff;
position: absolute; 
margin: -1em 0 0 0; 
top: 50%; 
width: 100%; 
-webkit-text-stroke-width: 2px; 
-webkit-text-stroke-color: #000; 
}

逻辑部分,代码就是Node了,不过例子里也没什么复杂逻辑,就是把界面显示出来而已

var app = module.exports = require('appjs');

app.serveFilesFrom(__dirname + '/content');

var window = app.createWindow({
  width  : 640,
  height : 460,
  icons  : __dirname + '/content/icons',
/***************************** defaults ********************************
* url            : 'http://appjs', // serve static file root and routers
* autoResize     : false,          // resizes in response to html content
* showChrome     : true,           // show border and title bar
* resizable      : false,          // control if users can resize window
* disableSecurity: true,           // allow cross origin requests
* opacity        : 1,              // flat percent opacity for window
* alpha          : false,          // per-pixel alpha blended (Win & Mac)
* fullscreen     : false,          // client area covers whole screen
* left           : -1,             // centered by default
* top            : -1,             // centered by default
*************************************************************************/
});

window.on('create', function(){
  console.log("Window Created");
  this.frame.show();
  this.frame.center();
});

window.on('ready', function(){
  console.log("Window Ready");
  this.require = require;
  this.process = process;
  this.module = module;
  this.console.log('process', process);
});

window.on('close', function(){
  console.log("Window Closed");
});

AppJS托管在github上,有兴趣的去围观吧 https://github.com/appjs/appjs

已经把这事搁置很久了,前一段时间主要是毕业的事情比较多,现在终于能有一点时间继续写了。好了,进入正题,本节的重点在于WebSocket。
由于各大浏览器对WebSocket的支持各不相同,所以直接使用浏览器提供的API会比较复杂。Socket.IO对这些API做了封装,并且还使用其他方式对不支持WebSocket的浏览器做了支持,使用Socket.IO会减轻很多工作量。
使用Socket.IO只需要在express里安装Socket.IO模块,同时在页面中加入/socket.io/socket.io.js的引用,服务端包含socket.io包即可。服务端代码如下:

var express = require('express')
  , routes = require('./routes');

var app = module.exports = express.createServer();
var io = require('socket.io').listen(app);
// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'ejs');
  app.set('view options',{layout:false});
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

app.configure('production', function(){
  app.use(express.errorHandler());
});

// Routes

app.get('/', routes.index);
var conns = {};
var userNames = {};
var sids = {};
var onLineList = [];
io.sockets.on('connection',function(socket){
	var sid=socket.id;
	conns[sid]=socket;
    socket.on('submitUserName',function(userName, fn) {
        if (userNames[userName]) {
            fn("have");
        } else {
            for (var i = onLineList.length - 1; i >= 0; i--) {
                var tmpSid = userNames[onLineList[i]]["sid"];
                conns[tmpSid].emit('addNewUserToList', userName);
            }
            var userInfo = {};
            userInfo["sid"] = sid;
            userNames[userName] = userInfo;
            sids[sid] = userName;
            onLineList.push(userName);
            fn("not_have");

        }
    });
    socket.on('getOnlineList', function(userName, fn){
        fn(onLineList);
    });
    socket.on('setCompetitor', function(competitor, fn){
        var myName = sids[socket.id];
        for (var i = onLineList.length -1; i >= 0; i--) {
            if (onLineList[i] == myName) {
                onLineList.splice(i, 1);
            }
        }
        for (var i = onLineList.length -1; i >= 0; i--) {
            if (onLineList[i] == competitor) {
                onLineList.splice(i, 1);
            }
        }
        var deletedUser = [myName, competitor];
        for (var i = onLineList.length -1; i >= 0; i--) {
            var tmpSid = userNames[onLineList[i]]["sid"];
            conns[tmpSid].emit('deleteUserFromList', deletedUser);
        }
        if (!userNames[myName]["competitor"] && !userNames[competitor]["competitor"]) {
            userNames[myName]["competitor"] = competitor;
            userNames[competitor]["competitor"] = myName;
            var comSid = userNames[competitor]["sid"];
            conns[comSid].emit('beginChess', 'begin');
            fn("success");
            socket.on('goChess', function(data){
				conns[comSid].emit('comGoChess', data);
            });
            conns[comSid].on('goChess', function(data){
                socket.emit('comGoChess', data);
            });
        } else {
            fn("fail");
        }
    });
});
app.listen(3000, function(){
  console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
});

代码前半部分都是Express框架的设置部分,然后io.sockets.on();部分开始监听来自浏览器端的WebSocket连接。连接建立之后,会将该连接存起来,并在该连接上设置一些可能的监听事件。

socket.on('submitUserName',function(userName, fn){});
//用户提交自己的名字时触发的事件

socket.on('getOnlineList', function(userName, fn){});
//用户获取在线等待配对用户列表时触发的事件

socket.on('setCompetitor', function(competitor, fn){});
//用户选中一个对手之后设置对手的事件

浏览器端在触发事件时会发送一些数据给服务端,比如socket.on(‘submitUserName’,function(userName, fn){})中,userName是浏览器端发送过来的用户名,而fn则是浏览器端定义的一个函数回调函数,当服务端完成某些操作后,会调用fn函数,相当于浏览器端得到了服务端的响应。当然,这里也可以不设置fn,只发送数据。关于socket.io的具体情况可以查看官方文档,地址:http://socket.io/#how-to-use
浏览器端会根据用户的一些操作触发一些服务端事件,部分代码如下:

$(function(){
    $("#chessCanvas").hide();
    $("#PiecesCanvas").hide();
    $("#onlinelist").hide();
    socket = io.connect();
    window.submitUserName = function(){
        var userName = $("#username").val();
        if (userName) {
            socket.emit('submitUserName', userName, function(data){
                if (data == "have") {
                    alert("用户名已经被使用,请重新取一个更酷的名字吧!");
                    $("#username").val("");
                    $("#username").focus();
                } else {
                    socket.emit('getOnlineList', userName, function(list){
                        $.each(list, function(i, item){
                            if (item == userName){
                                return true;
                            }
                            var str = '<div id="' + item + '"><input type="radio" name="onlineuser" value="'
                                + item + '" /><span>' + item + '</span></div>';
                            $("#namelist").append(str);
                        });
                        $("#login").hide();
                        $("#onlinelist").show();
                    });
                }
            });
            socket.on('addNewUserToList', function(data){
                var str = '<div id="' + data + '"><input type="radio" name="onlineuser" value="'
                    + data + '" /><span>' + data + '</span></div>';
                $("#namelist").append(str);
            });
            socket.on('deleteUserFromList', function(data) {
                $.each(data, function(i, item) {
                    $("#namelist > #" + item).remove();
                });
            });
            socket.on('beginChess', function(data) {
                showChess('red', 'wait', socket);
            });
        } else {
            alert("请输入用户名!");
        }

    };
    window.beginChess = function(){
        var competitor = $("input[name='onlineuser']:checked").val();
        if (competitor) {
            socket.emit('setCompetitor', competitor, function(data){
                if (data == "success") {
                    showChess('black', 'idle', socket);
                } else {
                    //未配对成功的情况
                }
            });
        } else {
            alert("请选择一个对手!");
        }
    };
    function showChess(ourColor, status, sock) {
        $("#onlinelist").hide();
        $("#chessCanvas").show();
        $("#PiecesCanvas").show();
        window.chss = chess("chessCanvas", "PiecesCanvas", sock);
        window.chss.ourColor = ourColor;
        window.chss.status = status;
        window.chss.initPieces();
    }

});

当用户输入完用户名之后,点击登录,就会使用socket.emit(‘submitUserName’, userName, function(data){})触发服务端响应。同时,浏览器端也会监听一些服务端事件,例如socket.on(‘beginChess’, function(data) {})就监听了服务端发起的棋局开始事件。这就是WebSocket的强大之处,浏览器可以被动接收服务端发送的消息,换个角度说,这一刻的浏览器就是C/S结构中的服务端,而服务器则是C/S结构中的客户端。当棋局开始后,浏览器使用如下代码监听服务器发送过来的对方走子情况,并在本地执行。

//监听服务端发送过来的对方棋子移动的事件
me.socket.on('comGoChess', function(movement) {
    var lineX = 10 - movement.toLineX;
    var lineY = 11 - movement.toLineY;
    var comPiece = movement.piece;
    me.pieceMoveTo('opposite', comPiece, lineX, lineY);
    me.status = 'idle';
});

当然,在本地走子之后,也会将走子信息发送给服务器,服务器再发送到对方。本子走子信息通过如下代码向服务端发送。

var movement = {};
movement["piece"] = me.activePiece;
movement["toLineX"] = lineX;
movement["toLineY"] = lineY;
me.socket.emit('goChess', movement);

到这里,仅仅实现了配对,同步走子的功能。判断输赢和结束棋局重来棋局等功能还未实现。最后,再放截图一张。

每年这个时候,总会有各种各样的离别故事。回想三年前,我也是刚刚经历了一番离别,然后来到上海。看到微博上各种离别状态,人人上各种散伙饭照片,不免也会被这些伤感的气息感染。毕业了,难免会对学校有或多或少的留恋,毕竟在这里待了三年。其实,与其说是对学校的留恋,倒不如说是对学校里曾经朝夕相处的人的留恋。因为有了你们,我的这三年才那么的多姿多彩。除了想和你们道别,更多的还是想对你们说声谢谢。
404的哥们,谢谢你们。谢谢你们的夜夜相伴,谢谢你们的次次卧谈。虽然每次斗嘴我都斗不过天梅,但是以后连在一起斗嘴的机会也少了。很怀念每次站在天梅后面看电影的场景,你不愧是各种大片喂出来的,对电影的品味也那么独特,口味奇特无人能及。我们寝室的电费每次都是中杰去交,我们寝室的水喝完了也是中杰去换,饮水机的各种毛病你也都能搞定,果然不愧是家里蹲大学饮水系毕业。我觉得每次打电话回家的时刻就是最不公平的时刻,你们两个说啥我都听不懂,但是我打电话说的每一句话你们都能听的懂。以后天南海北,不知何时才能相聚,希望你们两个高富帅别忘记我这个还在魔都混的IT屌丝男。
软工一起入学的兄弟们,谢谢你们,依然记得刚来复旦的时候我们一起住宾馆的日子。我到上海第一个联系的同学是肥兔子,谢谢你安排我到宾馆,到东京之后记得扬我国威。当然还有小苗,谢谢你到公交站台去帮我拎行李,七年的同学,缘分啊。谢谢邱哥、谢谢flyshow、谢谢小明。阿杜,干的不错,争取以后要当个好的家庭煮夫。欢欢也加油,争取早日毕业哈。Tancy一直是高富帅的代表,除了羡慕还是羡慕。
软工的学长学姐学弟学妹,谢谢你们。陈宇,谢谢你送的O1,不过要令你失望了,我到现在为止都没有开发出一款Ophone应用来,你的投资要打水漂了。水哥,谢谢,和你一起出差的日子很快乐,尤其是当你迷路的时候,能给我带来更多快乐。赵哥,谢谢,跟着你做过一段时间的bpel,只是最后我去搞其他的了,感觉很遗憾没能一干到底。伟哥,谢谢,谢谢你每次请我们到你寝室吃东西,我真心觉得你做的饭很好吃,博士里的大厨,大厨中的博士。刚哥,谢谢,从你那我学到了很多东西,以后有空还要多想你请教。董哥,谢谢,能有你这样的博士学长,我感觉很幸运。惠姐,谢谢,感谢你给我们做大餐吃,你那爽朗的笑声我将永远记住。邱诚、潘森、陈宵、宋扬、徐天伦、王永峰,谢谢你们,你们都很牛,从你们那里我也学到了很多。林日昶,加油,网管不好当,但是我觉得你比我牛,你可以的。还有林博和钱博,两位博士要多为实验室发paper,软工实验室的繁荣就看你们的了。实验室所有的同学们,谢谢你们,你们才是我对实验室记忆的主体,谢谢你们陪伴我走过了这么长时间。
小团队的小朋友们,谢谢你们。因为有了你们,我的实验室生活才更加欢乐。丁贝贝,好基友一被子,谢谢去年夏天你收留我到你寝室去吹空调。作为你个认不全颜色的前端,你做的很成功,希望你以后继续加油。Jofanie,谢谢你,接了我那么多工作,你干的很出色,另外,作为一个自动提款机,你做的也很出色。漂亮阿姨,谢谢你,是你在我最缺钱的时候接济了我,作为一个美丽善良的白富美,你做的很好很好。诸姣,谢谢,谢谢你帮我拿过多次快递帮我打印过多次东西还当过我写的程序的小白鼠。旭旭,谢谢,智慧与美貌并存说的估计就是你了。挺妹,谢谢,听说你在毕业聚餐上吻了7个男人,那么为什么你没有吻我?岚姨,谢谢,到了米国要记得帮我要一张taylor的签名照。
还有08SS的小朋友们,谢谢你们,第一次当TA,希望你们能多多包涵。喜之郎和pandalove,你们一直是模范小夫妻,好好读书好好幸福下去。
最后,要谢谢实验室的老师们。谢谢你们这三年来的培养和教诲,毕业之后我也是软工实验室的学生。