2007年2月25日星期日

排序

sun的官方实现:
private static void sort1(int x[], int off, int len) {
// Insertion sort on smallest arrays
if (len < i="off;" for="" int="" j="i;">off &&amp; x[j-1]>x[j]; j--)
swap(x, j, j-1);
return;
}

// Choose a partition element, v
int m = off + (len >> 1); // Small arrays, middle element
if (len > 7) {
int l = off;
int n = off + len - 1;
if (len > 40) { // Big arrays, pseudomedian of 9
int s = len/8;
l = med3(x, l, l+s, l+2*s);
m = med3(x, m-s, m, m+s);
n = med3(x, n-2*s, n-s, n);
}
m = med3(x, l, m, n); // Mid-size, med of 3
}
int v = x[m];

// Establish Invariant: v* (v)* v*
int a = off, b = a, c = off + len - 1, d = c;
while(true) {
while (b <= c && x[b] <= v) { if (x[b] == v) swap(x, a++, b); b++; } while (c >= b && x[c] >= v) {
if (x[c] == v)
swap(x, c, d--);
c--;
}
if (b > c)
break;
swap(x, b++, c--);
}

// Swap partition elements back to middle
int s, n = off + len;
s = Math.min(a-off, b-a ); vecswap(x, off, b-s, s);
s = Math.min(d-c, n-d-1); vecswap(x, b, n-s, s);

// Recursively sort non-partition-elements
if ((s = b-a) > 1)
sort1(x, off, s);
if ((s = d-c) > 1)
sort1(x, n-s, s);
}

2007年2月1日星期四

丢谁的人

Google又惹事了。这一次是一个叫佛祖也淫笑的热血愤青惊异地发现,在GoogleEarth里面沈阳的地名变成了日文,而且标注的都是伪满时期使用的地名。一石激起千层粪,这个消息引来了网上又一波对小日本的鄙视和攻击,同时也引发了又一轮gfans和百度拥趸的对战。

其实愤青们应该理性地想想这事,究竟是谁在丢人,丢谁的人。

我 们知道,googleearthcommunity是GE里面一个开放编辑的图层,任何人都有权限进行任何标注,从网易的报道来看,显然,记者对这种编辑 方式是颇有微词的:“想标啥就标啥,没人监管”“自由标注,没有限制”“日本人还在不断标准沈阳地图”“Google事先未审核”。言下之意,这种开放编 辑是不负责任的,GE的运行方没有能够有效的限制使用者的编辑,没有尽到事先审查的责任,所以这次沈阳标注日文地名事件,应该由捣乱的日本网友和未尽审查 义务的google共同承担责任。

毫无疑问,网易的这位编辑仍然生活在我贴你看的门户网站时代,他并不能理解,开放式编辑的网站不过是网民表演的一个舞台,是闹剧,悲剧还是喜剧,抑或是荒诞剧,这一切完全取决于网民个人。

维基百科里, 我们见过太多乐于自取其辱的白痴了,曾经有一个天文学爱好者,他不能忍受其他任何人修改他撰写的内容,在与其他网友以及管理员发生多次冲突之后,此人启动 了自编的机器人程序,对维基百科发动了灌水式攻击,我们知道了,这是一个极端自负自私的人;另有一个网友,剽窃了他人拍摄的照片,放在自己的用户页,并且 留言“谁删我跟谁急”,于是我们知道了,这是一个漠视著作权的人;还有一个网友,在与中文维基百科几乎所有的管理员都发生过争执之后,他跑去英文维基百科 告状了,他说中文维基百科的所有管理员被某党收买,死心塌地的做了某党的走狗,还好外国人没有他想象的那么愚蠢,英文的管理员直接封掉了他的帐号,于是我 们知道了,这孙子是一个卑鄙无耻的人。

太多的经验告诉我们,越是开放编辑的网站,越不怕无聊无知无耻的小白来捣乱,小白越捣乱,现得眼越大。

回 到GE,我们以最坏的恶意去揣测这些为沈阳标注伪满时代地名的日本人:他们是一群不折不扣的日本右翼分子,对中国怀有最大的敌意,以这种意淫的方式来恢复 日本对中国东北的殖民统治。那么他们这么做,究竟是在丢谁的人,现谁的眼?丢人现眼的恰恰是这些没有教养的日本人自己,google earth 为全世界网民提供了一个开放的空间,人家都在好好利用这个空间,做自己该做的事,只有这伙无聊的日本人,以伤害别国人民的感情为乐,把自己那见不得人的意 淫展示在全天下网民的面前。试想全世界的网民看到这伙日本人的表演之后,真的会以为,今日的沈阳就是当年的奉天吗?绝对不会,每一个正直的人,都会由衷地 鄙视这些捣乱的日本小白,他们充分地表演,侮辱的是只是他们自己,丢他们自己的人,现日本人的眼!既然他们愿意在大庭广众之下裸身卖弄,我们又着什么急 呢?让他们充分表演去吧

可是我们的愤青们的表现,实在太令人失望了,在牛博有个傻逼说:

Re: Google Earth竟用伪满洲国地图?——傻逼FQ又有攻击


囸 @ 2007-1-31 14:28:06 引用
keyhole不过是个社区。

而且自定义共享地标是个非默认图层。。

和google有JB关系啊!!!

不服气你们也去改日本的地标啊。。。我就把日本的靖国神厕标注为厕所。。也显示的啊。。人家日本就没有要求google什么的道歉

怎么对自己民族那么不自信。。。也太脆弱了吧。。。坚强点

乖乖,您还是歇歇吧,比那帮小日本儿还丢人!

转载自 相空间

2007年1月19日星期五

rails1.2正式发布

rails1.2正式发布了,restful、更好的utf8支持,一定要抽时间好好研究一下了。争取找时间做点东西出来玩玩。

官方发布消息:这里

prototype1.5.0正式发布

请注意,这才是真正的正式发布。官方网站也搬家了:http://www.prototypejs.org

看ajaxian上很多人都评价说"Too little too late",这段时间prototype丢了很多人气啊。

2007年1月17日星期三

domQuery VS. jQuery VS. prototype

由于jQuery1.1的发布,我才注意到jQuery和yui-ext/domQuery的作者针对 它们提供的dom选择器的性能爆发了一场论战。具体情况看他们的blog:这里这里
他们的dom选择器功能都很全面,提供基于css selector和部分xsl的选择器。由于看不起prototype这部分功能的不完善(可怜啊),根本没有把它加入测试。所以我干脆自己测了一下,直接说结果:
性能排列 domQuery0.40>jQuery1.1>prototype1.5.0RC2>jQuery1.04
domQuery的性能确实很好,在很多测试项上速度确实达到了新版jQuery的3倍甚至更多。jQuery的新版本也确实在很多项目上速度比老版本提高了数倍。让我觉得不可思议的是prototype,性能比老版本的jQuery也仅是高一点点而已。

结论:prototype的$$功能不全、性能不佳,要慎用。如需复杂的dom查询,domQuery(性能极好,文档也不错)与jQuery1.1(文档极好,性能也可以接受)都是很好的选择。

这里是测试页面

用greasemonkey搞定google快照

曾经为了快照选百度而弃谷歌,不过自从发现了这个办法,我又重回谷歌怀抱了。
第一步,请确定你用的是firefox浏览器。
第二步,安装greasemonkey插件(非常有用的插件,具体情况看介绍)。
第三步,安装这段用户脚本
重启,大功告成。

2007年1月16日星期二

jQuery1.1发布

jQuery1.1发布了,本来我是很不喜欢这套js framework的,原因还是那两点:性能和态度(整天跟prototype比大小,却不用同样的标准)。不过这次的新版本看来在这两点上改变了很多,可以抽时间来研究一下了,毕竟它的易用性确实很好。


官方发布页:http://jquery.com/blog/2007/01/14/jquery-birthday-11-new-site-new-docs/

2007年1月15日星期一

“取消”和“停止”浏览器事件

这种做法是从Bret Taylor的blog上面抄来的,原文地址在这里(好像是,不是也别怪我。)。对于避免ajax应用中不同层的事件干扰很有用的。


先说一下“取消(cancel)”和“停止(stop)”的区别:说白了,停止就是当事件发生后,阻止它自动冒泡到父容器内;而取消则是当冒泡发生到父容器之后停止,也就是不在当前元素中响应。天哪,这东西说起来话长了,代码说话:


function stopEvent(e) {
if (!e) e = window.event;
if (e.stopPropagation) {
e.stopPropagation();
} else {
e.cancelBubble = true;
}
}

var link = document.getElementById("link");
link.onclick = stopEvent;

以上这一段是阻止父容器事件的


function cancelEvent(e) {
if (!e) e = window.event;
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
}

var link = document.getElementById("link");
link.onclick = function(e) {
cancelEvent(e);
stopEvent(e);
}

以上这一段是阻止子元素事件的


例子地址在这里:demo

2007年1月14日星期日

月光博客也出事了???

起因是下午用google reader的时候,突然不能访问了,google的所有服务也几乎都无法访问。百度一下才知道,很可能是由于google reader中订阅的某个feed的原因。顺藤摸瓜找下去,发现月光博客已无法访问,联想最近的某些事情,恐怕某些不愿看到的事情发生了。


年前果然是多事之秋啊。。。。。。



更新:猜错了,原因未知。用https可以访问google reader

越来越喜欢google

这么久以来我写blog都是断断续续的,主要原因是自己太懒,不过一直找不到一个好的BSP也是一个重要原因。这次我直到blogspot解封之后把blog搬到这里才发现(懒,动作实在很慢),一个好的BSP对blogger的促进是不可言喻的。新版Blogger更好用了,我尤其喜欢它提供给用户充分的自主权,国内的BSP相比之下就。。。。。。


说回主题,为什么说我越来越喜欢google呢?除了换了BSP,我这次还换了rss订阅的服务,从抓虾换到google reader了。线上的 RSS 阅读器我最初用的是bloglines,可惜身处伟大的GFW之后,再加上网络条件的问题,所以刚知道抓虾就马上搬过去了。期间也曾经考虑过google reader,可惜那时的google reader不愧为最差劲的google产品。不过最近抓虾开始不断抽风,重新跑回google reader一看:天哪,这不就是我想要的吗?漂亮的聚合式浏览,完美的Start 和 Tag,再加上阅读趋势、批量订阅处理和酷酷的滚动事件触发(这个要研究一下,ajax应用是我最关心的)。二话不说,俺搬家了。


说了这么多,其实google reader还是有些缺点的:中文tag的支持好像还没有(应该不是大问题,相信很快会加上的),速度(这个就没话说了,谁让咱生在这个伟大的国度呢?忍了吧。)。


顺便再说一句,针对firefox3.0的google toolbar好像也可以用了。

2007年1月13日星期六

wordpress实在是很有意思

wordpress是一个php的blog程序,支持插件方式扩展,功能相当完善,比起java世界中那些无比复杂的blog程序不知道好多少倍。

很早就听说了这东西,不过由于不懂php一直也没有去碰,直到昨天。昨天为了找个好点的bsp重写blog折腾了一晚上,今天一怒之下直接自己装WP开始玩。果然一玩就上瘾了,模版、插件,好玩的东西真不少。今天已经写了一个模版出来,我的xhtml/css/js终于有用武之地了,再不用像在公司还要顾虑那么多。

爽!

蝙蝠侠无所不能



相信如今的人们再也不会怀疑火箭年初的这笔交易了。

2007年1月12日星期五

好消息,textmate的windows版本Intype快要出来了

以下转自cnbeta.com

引用
Intype alpha 0.2.x 提供下载

软件新闻leonardo投递:
有些人可能没有听过这个名字,不过说起他的兄弟 TextMate 或许有 Apple 经验的人就不陌生了吧.这个 Intype 就是他的 Windows 版本的实现.以下为邮件原文:

Hello,
we are proud to announce our first Intype Alpha release.

Download link is available at:
http://intype.info/home/

Support forums are available at:
http://intype.info/forums/

Your e-mail address has been deleted from our database and
no further e-mails will be sent to you.

Next releases will be announced via Intype Blog and
Intype Forums.

Thank you for your interest,
Intype Team

(转贴)架构css

跟上一篇是同一位作者,翻译的这两篇文章真不错啊。正好google又见鬼的不能访问了,直接促使我下定决心重开blog。废话不说了。

大约一个月之前,看到了Garrett Dimon的这篇《Architecting CSS》,不禁动了翻过来的念头。联系作者后他满口答应,我也准备3天之内完工。只可惜国庆假期琐事繁多,一直腾不出手来开工。拖啊拖拖啊拖,一直到今天才得完成。这效率……唉,真是愧对作者,希望他别见怪~

废话不说了,回到主题。关于这篇文章,我有两个声明:1.不是css用法指南,而是宏观上的组织架构方法;2.没有提出绝对正确的某种方案,而是列出多种方案以及利弊让你根据具体情况选择。

全文如下:

架构css

作者:Garrett Dimon
翻译:htmlor

在当前浏览器普遍支持的前提下,css被我们赋予了前所未有的使命。然而依赖css越多,样式表文件就会变得越大越复杂。与此同时,文件维护和组织的考验也随之而来。

(曾几何时)只要一个css文件就够了——所有规则(rule)汇聚一堂,增删改都很方便——可这种日子早已远去。(现在)建立新网站时,必须花点时间好好筹划怎么组织和架构css。

文件的组织

构建css系统的第一步是大纲的拟定。(我认为)css组织规划的重要性堪比网站目录结构。(htmlor注:用词夸张一点,让你加深记忆) 没有哪种方案放之四海而皆准,因此我们会讨论一些基本的组织方案,以及它们各自的利弊。

主css文件

通常可以使用一个主css文件,来放置所有页面共享的规则。这个文件会包含默认的字体、链接、页眉和其他等样式。有了主css文件之后,我们开始探讨高级组织策略。

方法一:基于原型

最基本的策略是基于原型页面(archetype page)分离css文件。假如一个网站的首页、子页面和组合页设计不同,就可以采用基于原型的策略。(这种策略下)每个页面都会有专属的css文件。

在原型数量不多的情况下,这个方法简单明了、行之有效。然而,当页面元素并不按部就班的位于各个原型页时,问题就出现了。如果子页面和组合页共享某些元素,而首页却没有,我们应该怎么做呢?

  1. 把共享元素放入主css文件。这虽不是最纯正的解决办法,却适用于某些具体情况。可是如果网站庞大,(这样做的话)主css文件会迅速膨胀——这就违背了分离文件的初衷:避免导入不必要的大文件。
  2. 在组合页和子页面的css文件里各放一份样式代码。(这么做)就意味着要维护冗余代码,很显然我们不想这样。
  3. 创建一个新的文件,由这两种页面共享。这听起来不错。不过假如只有10行代码,我们创建这个文件仅仅是为了共享这10行代码?(htmlor注:杀鸡用牛刀?) 这方法很纯粹,但如果网站庞大有很多对页面共享很少量元素时(htmlor注:比如30对页面分别共享10行代码),就显得很笨重了。
  4. 创建一个单独的css文件,包含所有共享元素的样式。这方法可能比较简单,却要取决于网站的大小和共享元素的多少。有种情况会很烦:导入了一个很大的css文件,但页面只用到一小部分样式——还是那句话,这违背了分离文件的初衷。

这就是我所说的重叠的两难(overlap dilemma)。零碎css规则的重叠不一而足,并没有一个完全清晰无误的方案来组织它们。

方法二:基于页面元素/块

如果网站使用服务器端include,这个方法会很不错。举例说明,如果使用页眉include,它会有自己相应的css文件。页脚或者其他部分的include可以如法炮制,只须导入自己的css文件。这个方法简单干净,不过可能会产生很多小css文件。

举例来说,假如页脚的样式只需要20行css代码,单独创建一个文件就划不来了。而且这个方法会导致每个页面都包含一堆css文件——因为有多少include,就得有多少css文件。

方法三:基于标记

这个方案直观实际,与前一个类似。如果网站共有30个页面,其中10个含有form,那么可以创建一个css文件专门处理form的样式,只在这10个页面导入它。如果另外10个页面含有table,就创建一个文件专门处理table样式……诸如此类。

另外的组织技巧

除了用主观的方法组织文件,我们还要考虑如打印、手持设备和屏幕等多种媒体类型。这虽然已经很清楚的定义过,可依旧是建立文件结构时应该考虑的一个因素。一旦必须支持多种媒体类型,主css文件里的某些规则可能就得重新考虑。

另外,品牌联合也可能是一个重要因素。(htmlor注:如googlenike联手推出的joga) 如果涉及品牌联合,你就得考虑哪些元素应该调整以适应另一品牌。比如分别使用不同的css文件等。

还有一个常被忽略的技巧:使用嵌套的@import语句。只包含一连串@import语句,或者再加几句css规则,就能创建一个css文件。用这个方法完全可以创建网站的主css文件(用@import导入各部分的样式文件)。假如网站的每个页面都导入了4到5个不同的css文件,无疑你应该考虑使用这个技巧。

规则和选择器的组织

谈完了文件组织,接着讨论一下怎么组织文件里的东西吧。很自然,我们希望在文件里畅通无阻的浏览,迅速找到要编辑的选择器(selector)或规则。

冗余 vs. 附属

正如Dave Shea在其文章《冗余 vs. 附属》(Redundancy vs. Dependency)里所说的,你必须不断了解级联(cascade)。你要决定是对选择器编组(意味着附属),还是把它们分离(意味着冗余)。编组可 以保持代码简洁扼要,可是会建立附属关系,导致维护开销增加。如果不编组,就会增加文件大小,让相似的选择器保持一致变得困难。只有做好这种权衡、取舍, 才能每次都作出正确的决定。

按相互关系/上下文编组

既然文件组织可以是主观的,那么显然,按照规则和选择器与其他部分的相互关系来进行编组是最好的方法。举例说明,假设你用容器、页眉和页脚来完成布局,就应该把它们编成一组。

这似乎很简单,其实不然。比如,把页眉中的导航加入css时,是将它跟父元素编组还是独立编组?这种情况下,要视乎规则的上下文。通常,页眉与页面布局相关,应该与其他布局元素一起编组。而导航是页眉的一块,应该和页眉的其他块编组,而不是页眉本身。

使用注释

跟大多数代码类似,注释是组织良好与否的关键。应该根据css的控制范围,清楚的标注每节(section)。最好确保注释视觉突出,以便在内容滚动、一目十行时快速定位。

Doug Bowman在其文章《css组织技巧之一:标记》(CSS Organization Tip #1: Flags)里把css注释玩得高明之极。他详细说明了在节名之前加上等号,以便使用文本编辑器的查找功能迅速跳到某节。

别忘了

你应该细致认真的了解了特异性、级联和继承,并善用它们。它们之中的每一项都可以是你最可怕的敌人,也可以是你最友善的朋友。当建立庞大的网站时,是否理解这些细微精妙之处,决定了你所构建的是坚如磐石的系统,还是经不起风雨的豆腐渣工程。(htmlor注:这句完全意译,比较爽)

属性的组织

现在我们了解了文件的组织,也知道了文件内规则的组织,但还有一个重要的组织环节(没有提到),那就是属性(attribute)。虽然属性比前两个概念更简单,可是还有一些非常好的、能够保持规则整洁的方法很值得一提。

按字母排序

提到属性,如果说需要遵循什么原则的话,那就是:按-字-母-排-序。其实这招对于属性浏览帮助不大,不过可以防止属性值覆盖这种偶然事件的发生。

举个例子吧,已经数不清有多少次,我为某个选择器定义过了margin值,之后的某天无意间又加了一个(或前或后)。(这种情况下)后一个属性自然会起作用。假设不知道第二个属性存在,不管我怎么调整第一个属性值、刷新浏览器,也看不到页面变化。(htmlor注:这个问题我深有体会) 如果按照字母顺序排列,你就会发现margin被定义了两次(因为它们挨在一起),这个问题自然可以避免。

优先项

当网站复杂、牵涉太多css文件时,会建立大量的附属关系。一旦需要定制某个元素特有的样式,!important选项似乎是最佳选择。没错,!important是能解一时之需,但最好搞清楚导致问题的根源,然后根据级联关系决定是否真的需要用它。

如果你对上文提到的特异性、级联和继承很熟悉,大可不必抱着!important一颗树不放。(htmlor注:整片森林等着你~) 当然它还是会派上用场,不过使用之前要对具体情况了然于胸。千万不要因为不知问题的症结所在而把!important当作捷径或是补救方案。

小结

当我们变得依赖css而使样式表日渐复杂时,就需要正确的计划来避免犯错,并使代码易于维护。既然完美无缺的方案并不存在,那么了解css的工作方式以及文件、选择器和属性的多种组织方案,无疑有助于我们写出优质的代码,经受住时间考验。

(完)

原文链接 : http://blog.htmlor.com/2006/10/31/architecting_css/

(转贴)flickr对javascript干的好事

在一个讨论web技术的网站vitamin上发现这篇《Serving JavaScript Fast》,读过之后大有收获,茅塞顿开。于是就有了翻译过来的念头——我这人有个毛病,看到有意思的英文文章,就想自己翻过来(虽然英文水平很烂)。先在网上查了查,已经有blog谈到这篇文章(我算是后知后觉了),有总结要点的《Flickr 的开发者的 Web 应用优化技巧》,也有延伸开来的《接着讲Flickr的八卦》,但似乎没有全文翻译的(这下就好,不会忙了半天发现是无用功)。之后,就写信问作者可不可以,作者一口答应:“sure - i’d love you to translate it”,只是要求我翻好之后给他一个链接地址。得到准许,心里就有底了。

先介绍一下作者。Cal Henderson,伦敦人,现居加利福尼亚的旧金山。PHP,MySQL和Perl专家,现任flickr架构师(flickr被收购后就在yahoo了),同时也是vitamin的特聘顾问(写些技术性文章)。

既然他是架构师,flickr用的应该就是文中谈到的这些技术,于是参照文章,再对比网站,种种迹象表明确实如此。虽然在中国访问flickr速度 不敢恭维,加速效果不得而知,但其用了n多css和javascript资源却似乎从没出过什么问题,也从侧面印证了这些技术的有效性。

仔细的看完文章,还有个强烈的感觉:这老兄也太能卖关子了,一句话非分成三句说,摆事实讲道理是够透彻,就是有点太@#$%了…… 算了,他怎么说我怎么翻吧,忠实于原著嘛,要不就成篡改了。经过几天努力,加上同事thincat兄倾力援手(小弟不胜感激啊),终于完工(@_@ 真是苦力活啊,我再也不想干了~)。

全文翻译如下:

让javascript跑得更快

作者:Cal Henderson

下一代web应用让javascript和css得堪大用。我们会告诉你怎样使这些应用又快又灵。

建立了号称“Web 2.0”的应用,也实现了富内容(rich content)和交互,我们期待着css和javascript扮演更加重要的角色。为使应用干净利落,我们需要完善那些渲染页面的文件,优化其大小和 形态,以确保提供最好的用户体验——在实践中,这就意味着一种结合:使内容尽可能小、下载尽可能快,同时避免对未改动资源不必要的重新获取。

由于css和js文件的形态,情况有点复杂。跟图片相比,其源代码很有可能频繁改动。而一旦改动,就需要客户端重新下载,使本地缓存无效(保存在其 他缓存里的版本也是如此)。在这篇文章里,我们将着重探讨怎样使用户体验最快:包括初始页面的下载,随后页面的下载,以及随着应用渐进、内容变化而进行的 资源下载。

我始终坚信这一点:对开发者来说,应该尽可能让事情变得简单。所以我们青睐于那些能让系统自动处理优化难题的方法。只需少许工作量,我们就能建立一举多得的环境:它使开发变得简单,有极佳的终端性能,也不会改变现有的工作方式。

好大一沱

老的思路是,为优化性能,可以把多个css和js文件合并成极少数大文件。跟十个5k的js文件相比,合并成一个50k的文件更好。虽然代码总字节 数没变,却避免了多个HTTP请求造成的开销。每个请求都会在客户端和服务器两边有个建立和消除的过程,导致请求和响应header带来开销,还有服务器 端更多的进程和线程资源消耗(可能还有为压缩内容耗费的cpu时间)。

(除了HTTP请求,)并发问题也很重要。默认情况下,在使用持久连接(persistent connections)时,ie和firefox在同一域名内只会同时下载两个资源(在HTTP 1.1规格书中第8.1.4节的建议)(htmlor注:可以通过修改注册表等方法改变这一默认配置)。这就意味着,在我们等待下载2个js文件的同时,将无法下载图片资源。也就是说,这段时间内用户在页面上看不到图片。

(虽然合并文件能解决以上两个问题,)可是,这个方法有两个缺点。第一,把所有资源一起打包,将强制用户一次下载完所有资源。如果(不这么做,而 是)把大块内容变成多个文件,下载开销就分散到了多个页面,同时缓解了会话中的速度压力(或完全避免了某些开销,这取决于用户选择的路径)。如果为了随后 页面下载得更快而让初始页面下载得很慢,我们将发现更多用户根本不会傻等着再去打开下一个页面。

第二(这个影响更大,一直以来却没怎么被考虑过),在一个文件改动很频繁的环境里,如果采用单文件系统,那么每次改动文件都需要客户端把所有css和js重新下载一遍。假如我们的应用有个100k的合成的js大文件,任何微小的改动都将强制客户端把这100k再消化一遍。

分解之道

(看来合并成大文件不太合适。)替代方案是个折中的办法:把css和js资源分散成多个子文件,按功能划分、保持文件个数尽可能少。这个方案也是有 代价的,虽说开发时代码分散成逻辑块(logical chunks)能提高效率,可在下载时为提高性能还得合并文件。不过,只要给build系统(把开发代码变成产品代码的工具集,是为部署准备的)加点东 西,就没什么问题了。

对于有着不同开发和产品环境的应用来说,用些简单的技术可以让代码更好管理。在开发环境下,为使条理清晰,代码可以分散为多个逻辑部分(logical components)。可以在Smarty(一种php模板语言)里建立一个简单的函数来管理javascript的下载:

SMARTY:
{insert_js files="foo.js,bar.js,baz.js"}

PHP:
function smarty_insert_js($args){
foreach (explode(',', $args['files']) as $file){
echo "<script type='\"text' />
}
}

OUTPUT:
<script type='text/javascript' source='/javascript/foo.js'<>/script>
<script type='text/javascript' source='/javascript/bar.js'></script>
<script type='text/javascript' source='/javascript/baz.js'></script>

(htmlor注:wordpress中会把“src”替换成不知所谓的字符,因此这里只有写成“SOURCE”,使用代码时请注意替换,下同)

就这么简单。然后我们就命令build过程(build process)去把确定的文件合并起来。这个例子里,合并的是foo.js和bar.js,因为它们几乎总是一起下载。我们能让应用配置记住这一点,并修改模板函数去使用它。(代码如下:)

SMARTY:
{insert_js files="foo.js,bar.js,baz.js"}

PHP:
# 源文件映射图。在build过程合并文件之后用这个图找到js的源文件。

$GLOBALS['config']['js_source_map'] = array(
'foo.js' => 'foobar.js',
'bar.js' => 'foobar.js',
'baz.js' => 'baz.js',
);

function smarty_insert_js($args){
if ($GLOBALS['config']['is_dev_site']){
$files = explode(',', $args['files']);
}else{
$files = array();
foreach (explode(',', $args['files']) as $file){
$files[$GLOBALS['config']['js_source_map'][$file]]++;
}
$files = array_keys($files);
}

foreach ($files as $file){
echo "<script type='\"text' />
}
}

OUTPUT:
<script type='text/javascript' source='/javascript/foobar.js'></script>
<script type='text/javascript' source='/javascript/baz.js'></script>

模板里的源代码没必要为了分别适应开发和产品阶段而改动,它帮助我们在开发时保持文件分散,发布成产品时把文件合并。想更进一步的话,可以把合并过 程(merge process)写在php里,然后使用同一个(合并文件的)配置去执行。这样就只有一个配置文件,避免了同步问题。为了做的更加完美,我们还可以分析 css和js文件在页面中同时出现的几率,以此决定合并哪些文件最合理(几乎总是同时出现的文件是合并的首选)。

对css来说,可以先建立一个主从关系的模型,它很有用。一个主样式表控制应用的所有样式表,多个子样式表控制不同的应用区域。采用这个方法,大多数页面只需下载两个css文件,而其中一个(指主样式表)在页面第一次请求时就会缓存。

对没有太多css和js资源的应用来说,这个方法在第一次请求时可能比单个大文件慢,但如果保持文件数量很少的话,你会发现其实它更快,因为每个页 面的数据量更小。让人头疼的下载花销被分散到不同的应用区域,因此并发下载数保持在一个最小值,同时也使得页面的平均下载数据量很小。

压缩

谈到资源压缩,大多数人马上会想到mod_gzip(但要当心,mod_gzip实际上是个魔鬼,至少能让人做恶梦)。它的原理很简单:浏览器请求资源时,会发送一个header表明自己能接受的内容编码。就像这样:

Accept-Encoding: gzip,deflate

服务器遇到这样的header请求时,就用gzip或deflate压缩内容发往客户端,然后客户端解压缩。这过程减少了数据传输量,同时消耗了客 户端和服务器的cpu时间。也算差强人意。但是,mod_gzip的工作方式是这样的:先在磁盘上创建一个临时文件,然后发送(给客户端),最后删除这个 文件。在高容量的系统中,由于磁盘io问题,很快就会达到极限。要避免这种情况,可以改用mod_deflate(apache 2才支持)。它采用更合理的方式:在内存里做压缩。对于apache 1的用户来说,可以建立一块ram磁盘,让mod_gzip在它上面写临时文件。虽然没有纯内存方式快,但也不会比往磁盘上写文件慢。

话虽如此,其实还是有办法完全避免压缩开销的,那就是预压缩相关静态资源,下载时由mod_gzip提供合适的压缩版本。如果把压缩添加在 build过程,它就很透明了。需要压缩的文件通常很少(用不着压缩图片,因为并不能减小更多体积),只有css和js文件(和其他未压缩的静态内容)。

配置选项会告诉mod_gzip去哪里找到预压缩过的文件。

mod_gzip_can_negotiate Yes
mod_gzip_static_suffix .gz
AddEncoding gzip .gz

新一点的mod_gzip版本(从1.3.26.1a开始)添加一个额外的配置选项后,就能自动预压缩文件。不过在此之前,必须确认apache有正确的权限去创建和覆盖压缩文件。

mod_gzip_update_static Yes

可惜,事情没那么简单。某些Netscape 4的版本(尤其是4.06-4.08)认为自己能够解释压缩内容(它们发送一个header这么说来着),但其实它们不能正确的解压缩。大多数其他版本的 Netscape 4在下载压缩内容时也有各种各样的问题。所以要在服务器端探测代理类型,(如果是Netscape 4,就要)让它们得到未压缩的版本。这还算简单的。ie(版本4-6)有些更有意思的问题:当下载压缩的javascript时,有时候ie会不正确的解压缩文件, 或者解压缩到一半中断,然后把这半个文件显示在客户端。如果你的应用对javascript的依赖比较大(htmlor注:比如ajax应用),那么就得 避免发送压缩文件给ie。在某些情况下,一些更老的5.x版本的ie倒是能正确的收到压缩的javascript,可它们会忽略这个文件的etag header,不缓存它。(thincat友情提示:尽管压缩存在一些浏览器不兼容的现象,由于这些不能很好的支持压缩的浏览器数量现在已经非常少了,我 认为这种由于浏览器导致的压缩不正常的情况可以忽略不计。这些过时的浏览器还能不能在现在流行的windows或unix环境下面安装都存在不小的问题)

既然gzip压缩有这么多问题,我们不妨把注意力转到另一边:不改变文件格式的压缩。现在有很多这样的javascript压缩脚本可用,大多数都 用一个正则表达式驱动的语句集来减小源代码的体积。它们做的不外乎几件事:去掉注释,压缩空格,缩短私有变量名和去掉可省略的语法。

不幸的是,大多数脚本效果并不理想,要么压缩率相当低,要么某种情形下会把代码搞得一团糟(或者两者兼而有之)。由于对解析树的理解不完整,压缩器 很难区分一句注释和一句看似注释的引用字符串。因为闭合结构的混合使用,要用正则表达式发现哪些变量是私有的并不容易,因此一些缩短变量名的技术会打乱某 些闭合代码。

还好有个压缩器能避免这些问题:dojo压缩器(现成的版本在这里)。 它使用rhino(mozilla的javascript引擎,是用java实现的)建立一个解析树,然后将其提交给文件。它能很好的减小代码体积,仅用 很小的成本:因为只在build时压缩一次。由于压缩是在build过程中实现的,所以一清二楚。(既然压缩没有问题了,)我们可以在源代码里随心所欲的 添加空格和注释,而不必担心影响到产品代码。

与javascript相比,css文件的压缩相对简单一些。由于css语法里不会有太多引用字符串(通常是url路径跟字体名),我们可以用正则 表达式大刀阔斧的干掉空格(htmlor注:这句翻的最爽,哈哈)。如果确实有引用字符串的话,我们总可以把一串空格合成一个(因为不需要在url路径和 字体名里查找多个空格和tab)。这样的话,一个简单的perl脚本就够了:

#!/usr/bin/perl

my $data = '';
open F, $ARGV[0] or die "Can't open source file: $!";
$data .= $_ while ;
close F;

$data =~ s!/*(.*?)*/!!g; # 去掉注释
$data =~ s!s+! !g; # 压缩空格
$data =~ s!} !}\n!g; # 在结束大括号后添加换行
$data =~ s!\n$!!; # 删除最后一个换行
$data =~ s! { ! {!g; # 去除开始大括号后的空格
$data =~ s!; }!}!g; # 去除结束大括号前的空格

print $data;

然后,就可以把单个的css文件传给脚本去压缩了。命令如下:

perl compress.pl site.source.css > site.compress.css

做完这些简单的纯文本优化工作后,我们就能减少数据传输量多达50%了(这个量取决于你的代码格式,可能更多)。这带来了更快的用户体验。不过我们真正想做的是,尽可能避免用户请求的发生——除非确实有必要。这下HTTP缓存知识派上用场了。

缓存是好东西

当用户代理(如浏览器)向服务器请求一个资源时,第一次请求过后它就会缓存服务器的响应,以避免重复之后的相同请求。缓存时间的长短取决于两个因 素:代理的配置和服务器的缓存控制header。所有浏览器都有不同的配置选项和处理方式,但大多数都会把一个资源至少缓存到会话结束(除非被明确告 知)。

为了不让浏览器缓存改动频繁的页面,你很可能已经发送过header不缓存动态内容。在php中,以下两行命令可以做到:

header("Cache-Control: private");
header("Cache-Control: no-cache", false);
?>

听起来太简单了?确实如此——因为有些代理(浏览器)在某些环境下将忽略这些header。要确保浏览器不缓存文档,应该更强硬一些:

# 让它在过去就“失效”
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");

# 永远是改动过的
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");

# HTTP/1.1
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);

# HTTP/1.0
header("Pragma: no-cache");
?>

这样,对于我们不想缓存的内容来说已经行了。但对于那些不会每次请求时都有改动的内容,应该鼓励浏览器更霸道的缓存它。“If-Modified- Since”请求header能够做到这点。如果客户端在请求中发送一个“If-Modified-Since”header,apache(或其他服务 器)会以状态代码304(没改过)响应,告诉浏览器缓存已经是最新的。使用这个机制,能够避免重复发送文件给浏览器,不过仍然导致了一个HTTP请求的消 耗。嗯,再想想。

与If-Modified-Since机制类似的是实体标记(entity tags)。在apache环境下,每个对静态文件的响应都会发出一个“ETag”header,它包含了一个由文件修改时间、文件大小和inode号生 成的校验和(checksum)。在下载文件之前,浏览器会发送一个HEAD请求去检查文件的etag。可ETag跟If-Modified-Since 有同样的问题:客户端仍旧需要执行HTTP请求来验证本地缓存是否有效。

此外,如果你使用多台服务器提供内容,得小心使用if-modified-since和etags。在两台负载平衡的服务器环境下,对一个代理(浏 览器)来说,一个资源可以这次从A服务器得到,下次从B服务器得到(htmlor注:lvs负载平衡系统就是个典型的例子)。这很好,也是采用平衡负载的 原因。可是,如果两台服务器给同一个文件生成了不同的etag或者文件修改日期,浏览器就无所适从了(每次都会重新下载)。默认情况下,etag是由文件 的inode号生成的,而多台服务器之间文件的inode号是不同的。可以使用apache的配置选项关掉它:

FileETag MTime Size

使用这个选项,apache将只用文件修改日期和文件大小来决定etag。很不幸,这导致了另一个问题(一样能影响if-modified- since)。既然etag依赖于修改时间,就得让时间同步。可往多台服务器上传文件时,上传时间差个一到两秒是常有的事。这样一来,两台服务器生成的 etag还是不一样。当然,我们还可以改变配置,让etag的生成只取决于文件大小,但这就意味着如果文件内容变了而大小没变,etag也不会变。这可不 行。

缓存真是个好东西

看来我们正从错误的方向入手解决问题。(现在的问题是,)这些可能的缓存策略导致了一件事情反复发生,那就是:客户端向服务器查询本地缓存是否最 新。假如服务器在改动文件的时候通知客户端,客户端不就知道它的缓存是最新的了(直到接到下一次通知)?可惜天公不做美——(事实)是客户端向服务器发出 请求。

其实,也不尽然。在获取js或css文件之前,客户端会用 script 或标记向服务器发送一个请求,说明 哪个页面要加载这些文件。这时候就可以用服务器的响应来通知客户端这些文件有了改动。有点含糊,说得再详细点就是:如果改变css和js文件内容的同时, 也改变它们的文件名,就可以告诉客户端对url全都永久缓存——因为每个url都是唯一的。

假如能确定一个资源永不更改,我们就可以发出一些霸气十足的缓存header(htmlor注:这句也很有气势吧)。在php里,两行就好:

header("Expires: ".gmdate("D, d M Y H:i:s", time()+315360000)." GMT");
header("Cache-Control: max-age=315360000");
?>

我们告诉浏览器这个内容在10年后(10年大概会有315,360,000秒,或多或少)过期,浏览器将会保留它10年。当然,很有可能不用php输出css和js文件(因此就不能发出header),这种情况将在稍后说明。

人力有时而穷

当文件内容更改时,手动去改文件名是很危险的。假如你改了文件名,模板却没有指向它?假如你改了一些模板另一些却没改?假如你改了模板却没改文件 名?还有最糟的,假如你改动了文件却忘了改名或者忘了改变对它的引用?最好的结果,是用户看到老的而看不到新的内容。最坏的结果,是找不到文件,网站没法 运转了。听起来这(指改动文件内容时修改url)似乎是个馊主意。

幸运的是,计算机做这类事情——当某种变化发生,需要相当准确地完成的、重复重复再重复的(htmlor注:番茄鸡蛋伺候~)、枯燥乏味的工作——总是十分在行。

这个过程(改变文件的url)没那么痛苦,因为我们根本不需要改文件名。资源的url和磁盘上文件的位置也没必要保持一致。使用apache的mod_rewrite模块,可以建立简单的规则,让确定的url重定向到确定的文件。

RewriteEngine on
RewriteRule ^/(.*.)v[0-9.]+.(css|js|gif|png|jpg)$ /$1$2 [L]

这条规则匹配任何带有指定扩展名同时含有“版本”信息(version nugget)的url,它会把这些url重定向到一个不含版本信息的路径。如下所示:

URL      Path
/images/foo.v2.gif -> /images/foo.gif
/css/main.v1.27.css -> /css/main.css
/javascript/md5.v6.js -> /javascript/md5.js

使用这条规则,就可以做到不改变文件路径而更改url(因为版本号变了)。由于url变了,浏览器就认为它是另一个资源(会重新下载)。想更进一步的话,可以把我们之前说的脚本编组函数结合起来,根据需要生成一个带有版本号的 script标记列表。

说到这里,你可能会问我,为什么不在url结尾加一个查询字符串(query string)呢(如/css/main.css?v=4)?根据HTTP缓存规格书所说,用户代理对含有查询字符串的url永不缓存。虽然ie跟 firefox忽略了这点,opera和safari却没有——为了确保所有浏览器都缓存你的资源,还是不要在url里用查询字符串的好。

现在不移动文件就能更改url了,如果能让url自动更新就更好了。在小型的产品环境下(如果有大型的产品环境,就是开发环境了),使用模板功能可以很轻易的实现这点。这里用的是smarty,用其他模板引擎也行。

SMARTY:


PHP:
function smarty_version($args){
$stat = stat($GLOBALS['config']['site_root'].$args['src']);
$version = $stat['mtime'];

echo preg_replace('!.([a-z]+?)$!', ".v$version.$1", $args['src']);
}

OUTPUT:

对每个链接到的资源文件,我们得到它在磁盘上的路径,检查它的mtime(文件最后修改的日期和时间),然后把这个时间当作版本号插入到url中。 对于低流量的站点(它们的stat操作开销不大)或者开发环境来说,这个方案不错,但对于高容量的环境就不适用了——因为每次stat操作都要磁盘读取 (导致服务器负载升高)。

解决方案相当简单。在大型系统中每个资源都已经有了一个版本号,就是版本控制的修订号(你们应该使用了版本控制,对吧?)。当我们建立站点准备部署的时候,可以轻易的查到每个文件的修订号,写在一个静态配置文件里。

$GLOBALS['config']['resource_versions'] = array(
'/images/foo.gif' => '2.1',
'/css/main.css' => '1.27',
'/javascript/md5.js' => '6.1.4',
);
?>

当我们发布产品时,可以修改模板函数来使用版本号。

function smarty_version($args){
if ($GLOBALS['config']['is_dev_site']){
$stat = stat($GLOBALS['config']['site_root'].$args['src']);
$version = $stat['mtime'];
}else{
$version = $GLOBALS['config']['resource_versions'][$args['src']];
}

echo preg_replace('!.([a-z]+?)$!', ".v$version.$1", $args['src']);
}
?>

就这样,不需要改文件名,也不需要记住改了哪些文件——当文件有新版本发布时它的url就会自动更新——有意思吧?我们就快搞定了。

只欠东风

之前谈到为静态文件发送超长周期(very-long-period)的缓存header时曾说过,如果不用php输出,就不能轻易的发送缓存header。很显然,有两个办法可以解决:用php输出,或者让apache来做。

php出马,手到擒来。我们要做的仅仅是改变rewrite规则,把静态文件指向php脚本,用php在输出文件内容之前发送header。

Apache:
RewriteRule ^/(.*.)v[0-9.]+.(css|js|gif|png|jpg)$ /redir.php?path=$1$2 [L]

PHP:
header("Expires: ".gmdate("D, d M Y H:i:s", time()+315360000)." GMT");
header("Cache-Control: max-age=315360000");

# 忽略带有“..”的路径
if (preg_match('!..!', $_GET[path])){ go_404(); }

# 保证路径开头是确定的目录
if (!preg_match('!^(javascript|css|images)!', $_GET[path])){ go_404(); }

# 文件不存在?
if (!file_exists($_GET[path])){ go_404(); }

# 发出一个文件类型header
$ext = array_pop(explode('.', $_GET[path]));
switch ($ext){
case 'css':
header("Content-type: text/css");
break;
case 'js' :
header("Content-type: text/javascript");
break;
case 'gif':
header("Content-type: image/gif");
break;
case 'jpg':
header("Content-type: image/jpeg");
break;
case 'png':
header("Content-type: image/png");
break;
default:
header("Content-type: text/plain");
}

# 输出文件内容
echo implode('', file($_GET[path]));

function go_404(){
header("HTTP/1.0 404 File not found");
exit;
}

这个方案有效,但并不出色。(因为)跟apache相比,php需要更多内存和执行时间。另外,我们还得小心防止可能由path参数传递伪造值引起 的exploits。为避免这些问题,应该用apache直接发送header。rewrite规则语句允许当规则匹配时设置环境变量 (environment variable),当给定的环境变量设置后,Header命令就可以添加header。结合以下两条语句,我们就把rewrite规则和header设 置绑定在了一起:

RewriteEngine on
RewriteRule ^/(.*.)v[0-9.]+.(css|js|gif|png|jpg)$ /$1$2 [L,E=VERSIONED_FILE:1]

Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT" env=VERSIONED_FILE
Header add "Cache-Control" "max-age=315360000" env=VERSIONED_FILE

考虑到apache的执行顺序,应该把rewrite规则加在主配置文件(httpd.conf)而不是目录配置文件(.htaccess)中。否 则在环境变量设置之前,header行会先执行(就那没意义了)。至于header行,则可以放在两文件任何一个当中,没什么区别。

眼观六路

(htmlor注:多谢tchaikov告知“skinning rabbits”的含义,但我不想翻的太正式,眼下的这个应该不算太离谱吧。)

通过结合使用以上技术,我们可以建立一个灵活的开发环境和一个快速又高性能的产品环境。当然,这离终极目标“速度”还有一段距离。有许多更深层的技 术(比如分离伺服静态内容,用多域名提升并发量等)值得我们关注,包括与我们谈到的方法(建立apache过滤器,修改资源url,加上版本信息)殊途同 归的其他路子。你可以留下评论,告诉我们那些你正在使用的卓有成效的技术和方法。

(完)

原文地址:http://blog.htmlor.com/2006/08/03/serving_javascript_fast_chinese/