posts - 35,  comments - 4,  trackbacks - 1
公告
  2008年9月29日

转载自:http://book.csdn.net/bookfiles/362/10036213882.shtml

随需下载的JavaScript

On-Demand JavaScript

●●○

行为,启动(Bootstrap),跨域(CrossDomain),动态,JavaScript,延迟加载(LazyLoading),随需应变(OnDemand),ScriptTag

图6-13:随需下载的Ja vaScript

目标故事

Bill正登录到他的银行网站,登录表单立刻出现,并且随着他键入用户名称和密码,浏览器安静地下载应用其余部分所需要的JavaScript。

问题

怎样部署大量的JavaScript程序代码?

先决条件

l   Ajax应用大量使用JavaScript。越丰富的浏览器行为就意味着越多的JavaScript需要下载。

l   下载JavaScript对性能有冲击。直到全部初始化JavaScript被下载,交互才能完全开始。

l   大量的JavaScript内容会造成带宽的问题。往往,并非所有下载的JavaScript实际上都能被用到,从而导致带宽的浪费。

方案

下载并执行JavaScript片段。初始页面下载的JavaScript代码包含能进一步下载其他 JavaScript 的启动代码(bootstrapping code)。有两种有效技术:Script Tag Creation(Script 标签建立)和 Service Eval(服务评估)。我们将分别描述这两种技术,然后介绍这个模式的 3 个主要应用。

采用Script Tag Creation技术,使用DOM操作把新的脚本(script)元素注入到页面。这种方式也许让人吃惊,但是这种效果的确完全等同于一开始就已经存在<script>标签:引用到的JavaScript将被下载并自动执行。标签可以被附加在头部消息(head)或主体部分(body)上,然而,前者较常见,因为那通常是你发现script标签的位置:

var head = document.getElementsByTagName("head")[0];

script = document.createElement('script');

script.id = 'importedScriptId';

script.type = 'text/javascript';

script.src = "http://path.to.javascript/file.js";

head.appendChild(script);

如何知道脚本已经下载完毕?在这里,不同浏览器似乎有不同的行为,有些是同步下载脚本,有些则不是。在IE 里,你可能被一种类似于XMLHttpRequest(http://www.xml.com/ lpt/a/2005/11/09/fixing-ajax-xmlhttprequest-considered-harmful.htmlonreadystatechange)的机制所通知。其他浏览器里,你可能需要持续询问某种关于下载的指示(这里的指示取决于脚本的意图)。如果你碰巧能控制服务器脚本,可以实现一个通知机制;即通过回

调侦听函数(如果存在的话),完成该脚本的下载。实际上,有一个大有可为的建议,JSONP(http://bob.pythonmac.org/archives/2005/12/05/remote-json-jsonp/),其目标是完成一个简单(像这样具有灵活性的机制)的标准,适用于整个业界。

Script Tag Creation不让你获取任何旧文本;它必须是完整有效的JavaScript,而且你的脚本不能直接读取它,因为浏览器只用它做结果评估(evaluation)。那么,你怎样获得来自外部服务器的数据?对远程JavaScript最常见的方式,是为变量分配需要的数据:

var characters = new Array("Mario", "Sonic", "Lara");

内容通常是数据结构(data structure),例如,是标准的JSON消息,但是它也需要指定(assignment),以便被代码引用到。另外,它也可以是一个返回需要值的函数。两种方式的任何一种都不尽理想,因为浏览器代码必须使用在脚本中里提到的名称。再一次,像J这种具有灵活性的脚本机制的想法值得考虑,因为它让调用者自行决定变量的名称。

Service Eval是另一种On-Demand JavaScript技术,虽然不如Script Tag Creation卓越(其原因我们稍后将讨论)。在Service Eval里,以标准的XMLHttpRequest Call调用Web服务,输出一些JavaScript作为响应内容,这些JavaScript接着以eval()调用来执行。检查XMLHttpRequest Call的responseText属性,在评估(evaluating)之前,我们能够操作它,因此,它的body内容不必是完整、有效的JavaScript(与Script Tag Creation不同)。

任何不在函数内部的代码将立刻被执行。为了增加新函数以供稍后使用,JavaScript 能够直接添加到当前的窗口,或者现存的已知对象。例如,下面内容可以被发送:

self.changePassword = function(oldPassword, newpassword) {

  ...

}

XMLHttpRequest回调函数只需要把响应当作纯文本,并且把它传给eval()。再一次提醒:这里,异步状况要特别小心处理。你不能假设新的代码在请求之后立即生效,因此,别这么做:

if (!self.changePassword) {

  requestPasswordModuleFromServer();

}

changePassword(old, new) // 第一次将无效,因为 changePassword 尚未加载

相反地,你不需要对服务器产生同步调用,增加循环持续检查新函数,或者在响应处理函数中明确的发起调用。

On-Demand JavaScript有3个不同的应用:

Lazy Loading(延迟载入)

将大量JavaScript代码的加载往后推迟。以其中一种On-Demand JavaScript技术(Service EvalScript Tag Creation)运行。

Behavior Message(行为消息)

让服务器以一种“行为消息”(Behavior Message)的形式响应,行为消息指定浏览器的下一个动作。以其中一种On-Demand JavaScript技术(Service EvalScript Tag Creation)运行。

Cross-Domain Scripting(跨域 Scripting)

使用Script Tag Creation,绕过标准“同源”(same-origin)策略(这通常使 Cross- Domain Proxy成为必要),只能使用Script Tag Creation

先来看看Lazy Loading。传统上的最佳实践是避免不唐突地通过一或多个script标签包含(include)JavaScript。

<html>

  <head>

    <script type="text/javascript" src="search.js"></script>

    <script type="text/javascript" src="validation.js"></script>

    <script type="text/javascript" src="visuals"></script>

  </head>

  ...

</html>

Lazy Loading则建议在初始HTML里,只加载最小的初始化模块(initialization module):

<html>

  <head>

    <script type="text/javascript" src="init.js"></script>

  </head>

  ...

</html>

初始化模块声明页面启动的任何必要动作,或许足以覆盖该页面的一般使用。另外,它必须执行一个初始启动的函数(bootstrapping function),再根据需要下载JavaScript。

第2个应用,Behavior Message,是HTML Message模式的变形。Lazy Loading建立代码库提供持续性的使用,而Behavior Message 接受暂时性的代码并且立刻以eval()执行——这段脚本没有封装在函数中(虽然可以定义一些调用的函数)。实际上,这意味着浏览器正在询问服务器下一步做什么。

Cross-Domain Scripting是此模式的第3种应用。script 标签一直都能包含来自外部域的JavaScript。这个规则不仅适用于静态的<script>标签,对动态建立的script标签(如Script Tag Creation技术)也适用。因此,不同于XMLHttpRequest和IFrame,你的 script 能以这种方式直接访问外部内容。并且,因为src属性可以是任何的URL,你能够传递参数作为CGI变量。这种想法正变得相当受欢迎,有些公司(像是Yahoo!)特别为这种方法(见稍后的“真实世界的实例”)提供JavaScript API。

当你信任这种方式,并且理想地控制这种方式时,执行从外部域来的script是很有用的。其他时候,它肯定是安全性的风险。Douglas Crockford,JSON的创造者,警告外部 script能够造成一定的破坏(http://www.mindsack.com/uxe/dynodes/):

这种脚本能够交付数据,但是它以基础页面(base page)上的脚本的相同授权执行,因此,它能偷取cookie或者滥用服务器用户的授权。恶意的脚本能对用户和基础服务器之间的关系进行破坏….这个不受限制的script标签妙招是浏览器内最后的大安全漏洞。它无法轻易被修复,因为整个广告基础设施依赖此漏洞。要非常小心。

决策

使用Service Eval,还是使用Script Tag Creation?

两者之间的选择取决于几项因素。Service Eval包含一些胜过Script Tag Creation的好处:

l   以XMLHttpRequest为基础,当脚本准备好时,有标准机制被通知,因此没有调用尚未存在的函数的风险。

l   能够访问未经加工的脚本代码。

l   在脚本格式上具有较多灵活性:例如,你能够发送一些在不同XML节点里的JavaScript片段,并且让浏览器脚本将它们提取出来。

Script Tag Creation有两个胜过Service Eval的好处:

l   你能够从外部域载入JavaScript。这是两种风格之间唯一重大的功能性差别。

l   当第一次遇到<script>标签时,JavaScript将自动被评估(evaluated)—— 以大致相同于静态HTML所链接的JavaScript被评估的方式。因此,你不必为了稍后的使用,特别为文档添加变量和函数;只要正常声明它们即可。

使用 Lazy Loading,你将怎样分解模块?

你需要决定怎样切割你的JavaScript。软件开发的标准原则:模块应该职责明确(well- focused),模块间依赖应尽可能避免。另外,还存在一些Web方面的特定考虑:

l   理想的,任何模块不是根本没被用到,就是充分被使用。你不想要5 000行的模块被下载,却只为了获取一个3行的函数,这是带宽的浪费,这一点优于On-Demand JavaScript 的主要目的。

l   你需要一个或多个模块在启动时就存在。至少需要一个,用于开启更进一步的下载。

l   记住,代码可能在本地端被缓存。Alexander Kirk已经对On-Demand JavaScript的缓存做了一些实验(http://alexander.kirk.at/2005/10/11/caching-of-downloaded-code-testing- results/),结果是所有主流浏览器将缓存以Script Tag Generation创建的代码。通常情况下,你能够确保来自XMLHttpRequest的响应也被缓存。

使用Lazy Loading,脚本将在什么阶段下载JavaScript?

在必须使用JavaScript之前下载它是最容易的,但是这种方式未必总是最好的。下载 JavaScript 的过程中,将有一些延迟,这表示,如果你在最后的可能时刻抓取它,用户将等待片刻。当用户的行为或者系统状态,显示某些JavaScript很快将需要被用到时,考虑立刻下载它。

预测 JavaScript 是否将被用到是Predictive Fetch(第13章)的范例——抓取某些直觉上可能需要内容。你必须在使用的可能性和延迟加载可能引起的麻烦之间,有所取舍。例如,想象你有一些需要验证完成表单的JavaScript,如果你等到最后再下载,确实不会浪费带宽,但是代价是用户的满意度。当有一个或两个字段要进行验证,或者在这个表单最初被加载时,你可能就会下载它。每增加一次浪费下载的机会,也就会增加验证程序更平顺的机会。

真实世界的实例

MapBuilder

MapBuilder(http://mapbuilder.sourceforge.net)是一个地图网站(图6-14)框架。它使用 On-Demand JavaScript,降低代码被下载的数量。这个应用基于MVC模型(Model-View- Controller),Model信息被声明在XML文档中,随同需要用于下载相应小部件(widget)的JavaScript。当页面启动时,只有必要的JavaScript被下载,而不是全部代码。因此,这个范例是这个模式的部分实例;它确保只有最小的JavaScript子集被用到,但是不是以延迟方式载入(lazy),或者随需应变(on-demand)的风格。

Delicious/Yahoo! APIs

社交性书签(social bookmarking)网站,Delicious(http://del.icio.us),以及网站的拥有者,Yahoo!,两者皆提供以JSON为基础的API,具有合适的hook,允许你通过Script Tag Creation直接从浏览器访问。Delicious API(http://del.icio.us/help/json)将以此结果建立新对象(如果已经存在,就为它填值)。Yahoo! API(http://developer.yahoo.net/ common/json.html)让你在脚本里指定回调函数,JSON 将被传递给该函数。

图6-14:Map Builder

Dojo打包(packaging)框架

Dojo(http://dojotoolkit.org/download)是针对简化JavaScript开发的一种全面性框架。就其本身而言,它提供许多脚本,个别项目可能只需要使用当中的一个子集。为了管理脚本,有类似Java的包(package)系统,让你根据需要获取新的JavaScript(http://dojo.jot.com/ WikiHome/Documents/DojoPackageSystem)。你只需要直接包含单一的 JavaScript 文档:

<script type="text/javascript" src="/dojo/dojo.js"></script>

接着使用Dojo API根据需要获取包:

dojo.hostenv.moduleLoaded("dojo.aDojoPackage.*");

运行以上命令将引起Dojo自动下载在dojo.aDojoPackage下的模块。

JSAN导入系统

JSAN(JavaScript Archive Network,http://openjsan.org)是一个在线的脚本知识库。此外,它包含一个用来导入 JavaScript 模块的代码库,使用的约定类似 Java。下列的调用:

JSAN.use('Module.To.Include');

映射到Module/To/Include.js。JSAN.includePath定义此路径可能存在的全部顶层目录。如果includePath是 ["/","/js"],JSAN将查找/Module/To/Include和/js/Module/ To/Include。

代码范例:AjaxPatterns On-Demand JavaScript Wiki

将On-Demand JavaScript引入到Wiki Demo

在Basic Wiki Demo(http://ajaxify.com/run/wiki)里,全部JavaScript被一次性下载。但是很多时候,用户仅仅从wiki读取信息——为什么要下载写入的代码呢?因此,这个demo分三阶段重构成On-Demand JavaScript:

1. uploadMessage函数被提取到第2个JavaScript文件,upload.js。还没有用到On- Demand JavaScript,因为两个文件都被包含了进来。

2. 倘若发生上传操作,通过确保只有upload.js文件被下载,进一步的重构引进On-Demand JavaScript。这个版本使用Script Tag Creation

3. 在更进一步的重构里,Script Tag Creation技术被替换成Service Eval

分离JavaScript:提取出upload.js

在第一个重构(http://ajaxlocal/run/wiki/separateJS/)中,上传函数仅仅被移到一个分离的 JavaScript文件。初始HTML包含新文件:

<script type="text/javascript" src="wiki.js"></script>

<script type="text/javascript" src="upload.js">

新的upload.js现在包含uploadMessage函数。为反映出分离,引入了两个参数,帮助从主wiki.js中解耦出这个函数:

function uploadMessages(pendingMessages, messageResponseHandler) {

  ...

}

调用代码几乎和以前一样:

uploadMessages(pendingMessages, onMessagesLoaded);

到目前为止,我们获得了一点模块化,但是对提升性能仍然没有帮助,因为这两个文件必须在一开始就被下载。

Script Tag Creation

使用On-Demand JavaScript,upload.js不再于启动时就需要,因此,它的引用不再出现于初始的HTML,只留下主要模块,wiki.js

<script type="text/javascript" src="wiki.js"></script>

增加一个新函数用于下载脚本。为避免多次下载,一个保护条件检查uploadMessages是否已经存在,如果存在,立即返回。遵循Script Tag Creation技术,它为文件头部添加脚本元素(使用upload.js的URL初始化),以及标准JavaScript的type属性:

function ensureUploadScriptIsLoaded() {

  if (self.uploadMessages) { // 已经存在

    return;

  }

  var head = document.getElementsByTagName("head")[0];

  script = document.createElement('script');

  script.id = 'uploadScript';

  script.type = 'text/javascript';

  script.src = "upload.js";

  head.appendChild(script);

}

调用的脚本必须调用这个函数。不过,正如前文“方案”中所述的,可能异步下载,因此,这里也必须作检查:

ensureUploadScriptIsLoaded();

if (self.uploadMessages) { // 如果还没有下载,等待下一次同步

  uploadMessages(pendingMessages, onMessagesLoaded);

  ....

如果脚本被浏览器异步下载,这个测试实际上在第一次将失败,因为在脚本被下载之前,下载函数将返回。但是在这个特定的应用中,实际上无关紧要——不论如何,整个同步程序每隔5秒被执行一次。如果上传程序还不存在,5秒之内,它应该会存在,这一点符合我们的目的。

Service Eval

Script Tag Creation代码改用Service Eval重构代替,在此,XMLHttpRequest Call取回 upload.js并且以eval()处理。初始的脚本——wiki.js只在取回JavaScript的实现上有所不同。文字响应被传送给eval:

function ensureUploadScriptIsLoaded() {

  if (self.uploadMessages) { // 已存在

    return;

  }

  ajaxCaller.getPlainText("upload.js", function(jsText) { eval(jsText); });

}

JavaScript响应也必须改变。如果它只是定义一个类似之前的全局函数,即:

function uploadMessages(pendingMessages, messageResponseHandler) {

  ...

}

当评估(evaluation)完成时,函数接着结束。相反地,我们能够通过声明函数如下,达到相同效果(这将把函数关联到当前的窗口上,因此在脚本被评估(evaluation)之后,它将继续存活)。

uploadMessages = function(pendingMessages, messageResponseHandler) {

  ...

}

相关模式

HTML Message

这个模式的Behavior Message用法是HTML Message(第9章)的对等物,并遵循一种类似以服务器为中心的哲学,在那里,服务器动态控制浏览器活动。

Predictive Fetch

当你预期JavaScript很快将需要用到时,通过先行下载它,将Predictive Fetch(第13章)应用到On-Demand JavaScript。

Multi-Stage Download

这个模式的Lazy Loading应用类似于Multi-Stage Download(多阶段下载),它也是递延下载。Multi-Stage Download的重点是下载的语义及显示内容,而不是下载JavaScript。另外,这个模式更多的是关于根据预定计划的顺序下载,而不是根据需要下载。

更多信息

l   Thomas Brattli提供一份教学课程,“Dynamic Data Using the DOM and Remote Scripting”(http://www.dhtmlcentral.com/tutorials/ tutorials.asp?id=11)。

posted @ 2008-09-29 12:04 啊凡 阅读(146) 评论(1) 编辑

转载自:http://www.cckee.com/edu/3/29/3555_4.html

在一个讨论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/javascript\" SOURCE=\"/javascript/$file\"></script>\n";
}
}

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/javascript\" SOURCE=\"/javascript/$file\"></script>\n";
}
}

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 Yesmod_gzip_static_suffix .gzAddEncoding 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 op

 

en source file: $!";
$data .= $_ while <F>;
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中,以下两行命令可以做到:

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

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

<?php
# 让它在过去就“失效”
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>或<link>标记向服务器发送一个请求,说明哪个页面要加载这些文件。这时候就可以用服务器的响应来通知客户端这些文件有了改动。有点含糊,说得再详细点就是:如果改变css和js文件内容的同时,也改变它们的文件名,就可以告诉客户端对url全都永久缓存—— 因为每个url都是唯一的。
假如能确定一个资源永不更改,我们就可以发出一些霸气十足的缓存header(htmlor注:这句也很有气势吧)。在php里,两行就好:

<?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 onRewriteRule ^/(.*.)v[0-9.]+.(css|js|gif|png|jpg)$ /$1

6;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:
<link xhref="{version xsrc='/css/group.css'}" rel="stylesheet" type="text/css" />

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:
<link xhref="/css/group.v1234567890.css" mce_href="/css/group.v1234567890.css" rel="stylesheet" type="text/css" />

对每个链接到的资源文件,我们得到它在磁盘上的路径,检查它的mtime(文件最后修改的日期和时间),然后把这个时间当作版本号插入到url中。对于低流量的站点(它们的stat操作开销不大)或者开发环境来说,这个方案不错,但对于高容量的环境就不适用了——因为每次stat操作都要磁盘读取(导致服务器负载升高)。
解决方案相当简单。在大型系统中每个资源都已经有了一个版本号,就是版本控制的修订号(你们应该使用了版本控制,对吧?)。当我们建立站点准备部署的时候,可以轻易的查到每个文件的修订号,写在一个静态配置文件里。

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

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

<?php
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 onRewriteRule ^/(.*.)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_FILEHeader add "Cache-Control" "max-age=315360000" env=VERSIONED_FILE
考虑到apache的执行顺序,应该把rewrite规则加在主配置文件(httpd.conf)而不是目录配置文件(.htaccess)中。否则在环境变量设置之前, header行会先执行(就那没意义了)。至于header行,则可以放在两文件任何一个当中,没什么区别。
眼观六路

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

posted @ 2008-09-29 11:40 啊凡 阅读(59) 评论(0) 编辑