深刻感觉到facebook的一个缺陷,就是慢。用IE7在facebook上看youtube视频,只需要来回移动鼠标就可以让视频的图像停播(声音还会继续),直到鼠标不动了,视频才会恢复正常。看了一下源码,一上来光css和js就load了几十行,还都是不同的文件;onloadRegister调了几十次。整个源码里充斥着js代码,页面也是'heavily-ajaxed'难怪这么慢呢。我知道facebook雇的人肯定都是高手,不知道这样写是出于什么目的。一上来load那些css,一看就是程序自己生成的,那为什么不一口气生产一个,而要分别生产这么多呢。要知道这就是几十次request。我下了一个css看了看,短得很。当然fb有钱,server多,从哪儿连都很快,这咱没脾气。听说为了刚上线不久的web chat,fb新花了100个million搞了很多shiny new servers。
说到他们这个chat,我最近也研究了一下。架构是mochiweb,是erlang写的一个框架。所谓erlang就是爱立信语言的缩写,起初是爱立信搞来弄啥的我也记不清了,反正好处就是highly scalable而且耗资源少。fb用mochiweb来实现comet的功能。原理也不是很复杂,就是页面初始化的时候,client做一个request,然后server那边收到以后阻塞住,不返回。这样这个连接就被保持住了,形成了一个client和server间的通道。如果不加任何处理,你在浏览器端会发现这个页面永远也load不完。这个机制就实现了server push。与传统的browser/server间request/response相异的是,这个机制可以让browser像一个桌面程序一样运作;browser不需要向server发请求,server可以直接push data到browser。比如web chat,传统的技术只能这样做:server端准备一个消息队列,然后browser每隔一段时间去查一下,看看有没有新消息,如果有,拿回来,没有的话,这次request就白request了。显而易见,这样做很傻,给服务器增加了很多无谓的压力和网络流量。而comet不需要这样做,可以minimize网络流量,同时保持最好的即时性,一收到消息,立刻可以直接push到browser端。所以可以comet说是一个颠覆web的技术。保持连接,就需要阻塞request;阻塞request最简单的办法就是server端while true的无限循环,当然显而易见这不是个很好的办法,因为server端负担很大,一个线程handle一个连接,像facebook这样可能会有6、7位数的用户同时在线的网站,这个办法肯定是行不通的。mochiweb我没仔细看,不过应该是一个event-driven的框架,套在整个应用的外面,用来handle web chat相关的事情,包括保持连接,以及从server端向browser通过mochiweb push数据。所以在线人数加了,服务器吃紧了以后,很简单,代码都不用动,直接加一个server就好。comet现在有很多框架,cometd、mochiweb、orbited之类的;dojo.io.bind和jquery的comet插件好像也可以做这个用,我没有研究过。实现comet的另一个tricky part,上面说到了,就是浏览器会像是永远下不完页面。不同浏览器表现形式不同,ie表现为有个圈圈在那儿一直一直的转,which can provide a very bad user experience。所以要把它搞掉。Gmail的team在做gmail的web chat的时候发明了一个很优雅的方法,用一个从很早版本的IE就支持的东西解决了这个问题。具体可以参见这里。这些代码其实已经被各个框架总结归纳了,所以除非你觉得你可以写得更好,否则也没必要自己写,看看怎么回事儿直接拿来用就可以了。我最近费了很大的劲实现了一个web chat(主要是调研了很久很久),具体架构什么的保密(其实看看源码就知道了,我一向觉得web开发本没什么秘密可言),一个比较牛逼的feature是可以和手机之间互相发instant message,比gmail和fb的web chat又进了一步吧算是。不过目前还有小bug,正式上线了我会做广告的,敬请关注……
另外还想说个小技巧。我看很多页面都用到如下的代码:
function preloadImages() { //v3.0
var d=document; if(d.images){ if(!d.p) d.p=new Array();
var i,j=d.p.length,a=preloadImages.arguments; for(i=0; i<a.length; i++)
if (a[i].indexOf("#")!=0){ d.p[j]=new Image; d.p[j++].src=a[i];}}
}
function swapImgRestore() { //v3.0
var i,x,a=document.sr; for(i=0;a&&i<a.length&&(x=a[i])&&x.oSrc;i++) x.src=x.oSrc;
}
function findObj(n, d) { //v4.01
var p,i,x; if(!d) d=document; if((p=n.indexOf("?"))>0&&parent.frames.length) {
d=parent.frames[n.substring(p+1)].document; n=n.substring(0,p);}
if(!(x=d[n])&&d.all) x=d.all[n]; for (i=0;!x&&i<d.forms.length;i++) x=d.forms[i][n];
for(i=0;!x&&d.layers&&i<d.layers.length;i++) x=findObj(n,d.layers[i].document);
if(!x && d.getElementById) x=d.getElementById(n); return x;
}
function swapImage() { //v3.0
var i,j=0,x,a=swapImage.arguments; document.sr=new Array; for(i=0;i<(a.length-2);i+=3)
if ((x=findObj(a[i]))!=null){document.sr[j++]=x; if(!x.oSrc) x.oSrc=x.src; x.src=a[i+2];}
}
大约是为各种东西提供hover效果吧。这套代码是我见过次数最多的一段,在数不清的地方都遇到过。其实很早我就在疑惑,为什么要额外的弄这么一套出来,<a><input>什么的不都可以在css里规定hover效果嘛。后来自己实际做了一下发现,:hover里规定的背景图片在页面load的时候是不会load进来的。鼠标移上去以后的效果就是,之前的图片先消失了,然后浏览器的小圈圈转两圈儿,load进来那个hover效果的。所以UE是很差的。上面这个代码的preloadImages就是做这个用的,先一股脑load进来,然后hover的时候就不会有那个空白期。不过我觉得有一个简单很多的办法。
比方说,你有个60*20的button,背景图片是1.png;鼠标移上去以后,你希望它chuachua冒光,就需要另外一张60*20的图片,2.png。但是2.png在页面load的时候不会被load进来。解决的方法很简单,就是做一个60*40的图片,然后把1.png和2.png竖着搁在一起,做成一张图,比如还叫1.png。然后css里面这样写就好了:
.btn{ width: 60px; height: 20px; background: url(images/1.png) no-repeat top left; }
.btn:hover{ width: 60px; height: 20px; background: url(images/1.png) no-repeat bottom left; }
可以看出,我们用的还是一张图,1.png,hover的时候跟平时唯一的区别就是背景图片的align改成bottom了,而原来是top。由于我们规定了长和宽,所以不会有多余的地方露出来,相当于原来显示的是图片的上半部分,hover的时候直接把图片往上挪一下,显示下半部分。而由于是同一张图片,在页面初始化的时候,它就已经被load进来了,不会出现那个空白期。所以问题解决了,不需要js不需要函数调用,不需要给你的button加onmouseover的listener,不需要自己去填你要preload的图片文件名,代码量、维护难度和资源开销都非常小,而且少一张图片,少一个request。所以我觉得比上面那段js要好很多。不过一般也是在比较业余的作品里见到这段代码。唯一一次不业余的appearance就是在mig33上,admire mig33。