<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Computer, Electron and Technology &#187; flickr</title>
	<atom:link href="http://www.donevii.com/post/tag/flickr/feed" rel="self" type="application/rss+xml" />
	<link>http://www.donevii.com</link>
	<description>关注技术、移动互联网以及一切 GEEK &#38; NERD 的事情</description>
	<lastBuildDate>Wed, 21 Dec 2011 10:49:54 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>flickr对javascript干的好事</title>
		<link>http://www.donevii.com/post/9.html</link>
		<comments>http://www.donevii.com/post/9.html#comments</comments>
		<pubDate>Thu, 17 Aug 2006 03:26:15 +0000</pubDate>
		<dc:creator>dengwei</dc:creator>
				<category><![CDATA[web]]></category>
		<category><![CDATA[blog]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[flickr]]></category>
		<category><![CDATA[html]]></category>
		<category><![CDATA[java]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[unix]]></category>
		<category><![CDATA[windows]]></category>
		<category><![CDATA[优化]]></category>
		<category><![CDATA[开发]]></category>
		<category><![CDATA[技巧]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[类]]></category>
		<category><![CDATA[缓存]]></category>
		<category><![CDATA[网站]]></category>
		<category><![CDATA[翻译]]></category>

		<guid isPermaLink="false">http://www.donevii.com/?p=9</guid>
		<description><![CDATA[在一个讨论web技术的网站vitamin上发现这篇《Serving JavaScript Fast》，读过之后大有收获，茅塞顿开。于是就有了翻译过来的念头——我这人有个毛病，看到有意思的英文文章，就想自己翻过来（... ]]></description>
			<content:encoded><![CDATA[<p>在一个讨论web技术的网站<a href="http://www.thinkvitamin.com/"><font color="#0066dd">vitamin</font></a>上发现这篇<a href="http://www.thinkvitamin.com/features/webapps/serving-javascript-fast"><font color="#0066dd">《Serving JavaScript Fast》</font></a>，读过之后大有收获，茅塞顿开。于是就有了翻译过来的念头——我这人有个毛病，看到有意思的英文文章，就想自己翻过来（虽然英文水平很烂）。先在网上查了查，已经有blog谈到这篇文章（我算是后知后觉了），有总结要点的<a href="http://www.dbanotes.net/web/flickr_web_tech.html"><font color="#0066dd">《Flickr 的开发者的 Web 应用优化技巧》</font></a>，也有延伸开来的<a href="http://blog.94smart.com/2006/05/26/744.html" class="broken_link"><font color="#0066dd">《接着讲Flickr的八卦》</font></a>，但似乎没有全文翻译的（这下就好，不会忙了半天发现是无用功）。之后，就写信问作者可不可以，作者一口答应：“sure &#8211; i’d love you to translate it”，只是要求我翻好之后给他一个链接地址。得到准许，心里就有底了。</p>
<p>先介绍一下作者。<a href="http://www.iamcal.com/"><font color="#0066dd">Cal Henderson</font></a>，伦敦人，现居加利福尼亚的旧金山。PHP，MySQL和Perl专家，现任<a href="http://www.flickr.com/"><font color="#0066dd">flickr</font></a>架构师（flickr被收购后就在<a href="http://www.yahoo.com/"><font color="#3366cc">yahoo</font></a>了），同时也是vitamin的<a href="http://www.thinkvitamin.com/advisors/cal_henderson.php" class="broken_link"><font color="#0066dd">特聘顾问</font></a>（写些技术性文章）。</p>
<p>既然他是架构师，flickr用的应该就是文中谈到的这些技术，于是参照文章，再对比网站，种种迹象表明确实如此。虽然在中国访问flickr速度不敢恭维，加速效果不得而知，但其用了n多css和javascript资源却似乎从没出过什么问题，也从侧面印证了这些技术的有效性。</p>
<p>仔细的看完文章，还有个强烈的感觉：这老兄也太能卖关子了，一句话非分成三句说，摆事实讲道理是够透彻，就是有点太@#$%了…… 算了，他怎么说我怎么翻吧，忠实于原著嘛，要不就成篡改了。经过几天努力，加上同事<a href="http://my.donews.com/thincat/" class="broken_link"><font color="#0066dd">thincat</font></a>兄倾力援手（小弟不胜感激啊），终于完工（@_@ 真是苦力活啊，我再也不想干了～）。</p>
<p>全文翻译如下：</p>
<h2>让javascript跑得更快</h2>
<p>作者：Cal Henderson</p>
<p><cite><font color="#666666">下一代web应用让javascript和css得堪大用。我们会告诉你怎样使这些应用又快又灵。</font></cite></p>
<p>建立了号称“Web 2.0”的应用，也实现了富内容（rich content）和交互，我们期待着css和javascript扮演更加重要的角色。为使应用干净利落，我们需要完善那些渲染页面的文件，优化其大小和形态，以确保提供最好的用户体验——在实践中，这就意味着一种结合：使内容尽可能小、下载尽可能快，同时避免对未改动资源不必要的重新获取。</p>
<p>由于css和js文件的形态，情况有点复杂。跟图片相比，其源代码很有可能频繁改动。而一旦改动，就需要客户端重新下载，使本地缓存无效（保存在其他缓存里的版本也是如此）。在这篇文章里，我们将着重探讨怎样使用户体验最快：包括初始页面的下载，随后页面的下载，以及随着应用渐进、内容变化而进行的资源下载。</p>
<p>我始终坚信这一点：对开发者来说，应该尽可能让事情变得简单。所以我们青睐于那些能让系统自动处理优化难题的方法。只需少许工作量，我们就能建立一举多得的环境：它使开发变得简单，有极佳的终端性能，也不会改变现有的工作方式。</p>
<h3>好大一沱</h3>
<p>老的思路是，为优化性能，可以把多个css和js文件合并成极少数大文件。跟十个5k的js文件相比，合并成一个50k的文件更好。虽然代码总字节数没变，却避免了多个HTTP请求造成的开销。每个请求都会在客户端和服务器两边有个建立和消除的过程，导致请求和响应header带来开销，还有服务器端更多的进程和线程资源消耗（可能还有为压缩内容耗费的cpu时间）。</p>
<p>（除了HTTP请求，）并发问题也很重要。默认情况下，在使用持久连接（persistent connections）时，ie和firefox在同一域名内只会同时下载<a href="http://blogs.msdn.com/ie/archive/2005/04/11/407189.aspx"><font color="#0066dd">两个资源</font></a>（在<a href="http://www.ietf.org/rfc/rfc2616.txt"><font color="#0066dd">HTTP 1.1规格书</font></a>中第8.1.4节的建议）<cite><font color="#666666">（htmlor注：可以通过修改注册表等方法改变这一默认配置）</font></cite>。这就意味着，在我们等待下载2个js文件的同时，将无法下载图片资源。也就是说，这段时间内用户在页面上看不到图片。</p>
<p>（虽然合并文件能解决以上两个问题，）可是，这个方法有两个缺点。第一，把所有资源一起打包，将强制用户一次下载完所有资源。如果（不这么做，而是）把大块内容变成多个文件，下载开销就分散到了多个页面，同时缓解了会话中的速度压力（或完全避免了某些开销，这取决于用户选择的路径）。如果为了随后页面下载得更快而让初始页面下载得很慢，我们将发现更多用户根本不会傻等着再去打开下一个页面。</p>
<p>第二（这个影响更大，一直以来却没怎么被考虑过），在一个文件改动很频繁的环境里，如果采用单文件系统，那么每次改动文件都需要客户端把所有css和js重新下载一遍。假如我们的应用有个100k的合成的js大文件，任何微小的改动都将强制客户端把这100k再消化一遍。</p>
<h3>分解之道</h3>
<p>（看来合并成大文件不太合适。）替代方案是个折中的办法：把css和js资源分散成多个子文件，按功能划分、保持文件个数尽可能少。这个方案也是有代价的，虽说开发时代码分散成逻辑块（logical chunks）能提高效率，可在下载时为提高性能还得合并文件。不过，只要给build系统（把开发代码变成产品代码的工具集，是为部署准备的）加点东西，就没什么问题了。</p>
<p>对于有着不同开发和产品环境的应用来说，用些简单的技术可以让代码更好管理。在开发环境下，为使条理清晰，代码可以分散为多个逻辑部分（logical components）。可以在<a href="http://smarty.php.net/"><font color="#0066dd">Smarty</font></a>（一种php模板语言）里建立一个简单的函数来管理javascript的下载：</p>
<pre>SMARTY:{insert_js files="foo.js,bar.js,baz.js"}PHP:function smarty_insert_js($args){  foreach (explode(',', $args['files']) as $file){    echo "&lt;script type=\"text/<a href="http://www.donevii.com/post/tag/javascript" class="st_tag internal_tag" rel="tag" title="Posts tagged with javascript">javascript</a>\" SOURCE=\"/<a href="http://www.donevii.com/post/tag/javascript" class="st_tag internal_tag" rel="tag" title="Posts tagged with javascript">javascript</a>/$file\"&gt;&lt;/script&gt;\n";  }}OUTPUT:&lt;script type="text/<a href="http://www.donevii.com/post/tag/javascript" class="st_tag internal_tag" rel="tag" title="Posts tagged with javascript">javascript</a>" SOURCE="/<a href="http://www.donevii.com/post/tag/javascript" class="st_tag internal_tag" rel="tag" title="Posts tagged with javascript">javascript</a>/foo.js"&gt;&lt;/script&gt;&lt;script type="text/<a href="http://www.donevii.com/post/tag/javascript" class="st_tag internal_tag" rel="tag" title="Posts tagged with javascript">javascript</a>" SOURCE="/<a href="http://www.donevii.com/post/tag/javascript" class="st_tag internal_tag" rel="tag" title="Posts tagged with javascript">javascript</a>/bar.js"&gt;&lt;/script&gt;&lt;script type="text/<a href="http://www.donevii.com/post/tag/javascript" class="st_tag internal_tag" rel="tag" title="Posts tagged with javascript">javascript</a>" SOURCE="/<a href="http://www.donevii.com/post/tag/javascript" class="st_tag internal_tag" rel="tag" title="Posts tagged with javascript">javascript</a>/baz.js"&gt;&lt;/script&gt;</pre>
<p><cite><font color="#666666">（htmlor注：wordpress中会把“src”替换成不知所谓的字符，因此这里只有写成“SOURCE”，使用代码时请注意替换，下同）</font></cite></p>
<p>就这么简单。然后我们就命令build过程（build process）去把确定的文件合并起来。这个例子里，合并的是foo.js和bar.js，因为它们几乎总是一起下载。我们能让应用配置记住这一点，并修改模板函数去使用它。（代码如下：）</p>
<pre>SMARTY:{insert_js files="foo.js,bar.js,baz.js"}PHP:# 源文件映射图。在build过程合并文件之后用这个图找到js的源文件。$GLOBALS['config']['js_source_map'] = array(  'foo.js'	=&gt; 'foobar.js',  'bar.js'	=&gt; 'foobar.js',  'baz.js'	=&gt; '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 "&lt;script type=\"text/javascript\" SOURCE=\"/javascript/$file\"&gt;&lt;/script&gt;\n";  }}OUTPUT:&lt;script type="text/javascript" SOURCE="/javascript/foobar.js"&gt;&lt;/script&gt;&lt;script type="text/javascript" SOURCE="/javascript/baz.js"&gt;&lt;/script&gt;</pre>
<p>模板里的源代码没必要为了分别适应开发和产品阶段而改动，它帮助我们在开发时保持文件分散，发布成产品时把文件合并。想更进一步的话，可以把合并过程（merge process）写在php里，然后使用同一个（合并文件的）配置去执行。这样就只有一个配置文件，避免了同步问题。为了做的更加完美，我们还可以分析css和js文件在页面中同时出现的几率，以此决定合并哪些文件最合理（几乎总是同时出现的文件是合并的首选）。</p>
<p>对css来说，可以先建立一个主从关系的模型，它很有用。一个主样式表控制应用的所有样式表，多个子样式表控制不同的应用区域。采用这个方法，大多数页面只需下载两个css文件，而其中一个（指主样式表）在页面第一次请求时就会缓存。</p>
<p>对没有太多css和js资源的应用来说，这个方法在第一次请求时可能比单个大文件慢，但如果保持文件数量很少的话，你会发现其实它更快，因为每个页面的数据量更小。让人头疼的下载花销被分散到不同的应用区域，因此并发下载数保持在一个最小值，同时也使得页面的平均下载数据量很小。</p>
<h3>压缩</h3>
<p>谈到资源压缩，大多数人马上会想到<a href="http://sourceforge.net/projects/mod-gzip/"><font color="#0066dd">mod_gzip</font></a>（但要当心，mod_gzip实际上是个魔鬼，至少能让人做恶梦）。它的原理很简单：浏览器请求资源时，会发送一个header表明自己能接受的内容编码。就像这样：</p>
<pre>Accept-Encoding: gzip,deflate</pre>
<p>服务器遇到这样的header请求时，就用gzip或deflate压缩内容发往客户端，然后客户端解压缩。这过程减少了数据传输量，同时消耗了客户端和服务器的cpu时间。也算差强人意。但是，mod_gzip的工作方式是这样的：先在磁盘上创建一个临时文件，然后发送（给客户端），最后删除这个文件。在高容量的系统中，由于磁盘io问题，很快就会达到极限。要避免这种情况，可以改用<a href="http://httpd.apache.org/docs/2.0/mod/mod_deflate.html"><font color="#0066dd">mod_deflate</font></a>（apache 2才支持）。它采用更合理的方式：在内存里做压缩。对于apache 1的用户来说，可以建立一块ram磁盘，让mod_gzip在它上面写临时文件。虽然没有纯内存方式快，但也不会比往磁盘上写文件慢。</p>
<p>话虽如此，其实还是有办法完全避免压缩开销的，那就是预压缩相关静态资源，下载时由mod_gzip提供合适的压缩版本。如果把压缩添加在build过程，它就很透明了。需要压缩的文件通常很少（用不着压缩图片，因为并不能减小更多体积），只有css和js文件（和其他未压缩的静态内容）。</p>
<p>配置选项会告诉mod_gzip去哪里找到预压缩过的文件。</p>
<pre>mod_gzip_can_negotiate	Yesmod_gzip_static_suffix	.gzAddEncoding	gzip	.gz</pre>
<p>新一点的mod_gzip版本（从1.3.26.1a开始）添加一个额外的配置选项后，就能自动预压缩文件。不过在此之前，必须确认apache有正确的权限去创建和覆盖压缩文件。</p>
<pre>mod_gzip_update_static	Yes</pre>
<p>可惜，事情没那么简单。某些Netscape 4的版本（尤其是4.06-4.08）认为自己能够解释压缩内容（它们发送一个header这么说来着），但其实它们不能正确的解压缩。大多数其他版本的Netscape 4在下载压缩内容时也有各种各样的问题。所以要在服务器端探测代理类型，（如果是Netscape 4，就要）让它们得到未压缩的版本。这还算简单的。ie（版本4-6）有些更有意思的问题：当下载压缩的javascript时，有时候ie会<a href="http://support.microsoft.com/default.aspx?scid=kb;en-us;823386&amp;Product=ie600"><font color="#0066dd">不正确的解压缩文件</font></a>，或者解压缩到一半中断，然后把这半个文件显示在客户端。如果你的应用对javascript的依赖比较大<cite><font color="#666666">（htmlor注：比如ajax应用）</font></cite>，那么就得避免发送压缩文件给ie。在某些情况下，一些更老的5.x版本的ie倒是能正确的收到压缩的javascript，可它们会忽略这个文件的etag header，不缓存它。<cite><font color="#666666">（thincat友情提示：尽管压缩存在一些浏览器不兼容的现象，由于这些不能很好的支持压缩的浏览器数量现在已经非常少了，我认为这种由于浏览器导致的压缩不正常的情况可以忽略不计。这些过时的浏览器还能不能在现在流行的windows或unix环境下面安装都存在不小的问题）</font></cite></p>
<p>既然gzip压缩有这么多问题，我们不妨把注意力转到另一边：不改变文件格式的压缩。现在有很多这样的javascript压缩脚本可用，大多数都用一个正则表达式驱动的语句集来减小源代码的体积。它们做的不外乎几件事：去掉注释，压缩空格，缩短私有变量名和去掉可省略的语法。</p>
<p>不幸的是，大多数脚本效果并不理想，要么压缩率相当低，要么某种情形下会把代码搞得一团糟（或者两者兼而有之）。由于对解析树的理解不完整，压缩器很难区分一句注释和一句看似注释的引用字符串。因为闭合结构的混合使用，要用正则表达式发现哪些变量是私有的并不容易，因此一些缩短变量名的技术会打乱某些闭合代码。</p>
<p>还好有个压缩器能避免这些问题：<a href="http://dojotoolkit.org/docs/compressor_system.html" class="broken_link"><font color="#0066dd">dojo压缩器</font></a>（现成的版本在<a href="http://alex.dojotoolkit.org/shrinksafe/" class="broken_link"><font color="#0066dd">这里</font></a>）。它使用rhino（mozilla的javascript引擎，是用java实现的）建立一个解析树，然后将其提交给文件。它能很好的减小代码体积，仅用很小的成本：因为只在build时压缩一次。由于压缩是在build过程中实现的，所以一清二楚。（既然压缩没有问题了，）我们可以在源代码里随心所欲的添加空格和注释，而不必担心影响到产品代码。</p>
<p>与javascript相比，css文件的压缩相对简单一些。由于css语法里不会有太多引用字符串（通常是url路径跟字体名），我们可以用正则表达式大刀阔斧的干掉空格<cite><font color="#666666">（htmlor注：这句翻的最爽，哈哈）</font></cite>。如果确实有引用字符串的话，我们总可以把一串空格合成一个（因为不需要在url路径和字体名里查找多个空格和tab）。这样的话，一个简单的perl脚本就够了：</p>
<pre>#!/usr/bin/perlmy $data = '';open F, $ARGV[0] or die "Can't open source file: $!";$data .= $_ while &lt;F&gt;;close F;$data =~ s!/*(.*?)*/!!g;  # 去掉注释$data =~ s!s+! !g;           # 压缩空格$data =~ s!} !}\n!g;         # 在结束大括号后添加换行$data =~ s!\n$!!;             # 删除最后一个换行$data =~ s! { ! {!g;         # 去除开始大括号后的空格$data =~ s!; }!}!g;          # 去除结束大括号前的空格print $data;</pre>
<p>然后，就可以把单个的css文件传给脚本去压缩了。命令如下：</p>
<pre>perl compress.pl site.source.css &gt; site.compress.css</pre>
<p>做完这些简单的纯文本优化工作后，我们就能减少数据传输量多达50%了（这个量取决于你的代码格式，可能更多）。这带来了更快的用户体验。不过我们真正想做的是，尽可能避免用户请求的发生——除非确实有必要。这下HTTP缓存知识派上用场了。</p>
<h3>缓存是好东西</h3>
<p>当用户代理（如浏览器）向服务器请求一个资源时，第一次请求过后它就会缓存服务器的响应，以避免重复之后的相同请求。缓存时间的长短取决于两个因素：代理的配置和服务器的缓存控制header。所有浏览器都有不同的配置选项和处理方式，但大多数都会把一个资源至少缓存到会话结束（除非被明确告知）。</p>
<p>为了不让浏览器缓存改动频繁的页面，你很可能已经发送过header不缓存动态内容。在php中，以下两行命令可以做到：</p>
<pre>&lt;?phpheader("<a href="http://www.donevii.com/post/tag/cache" class="st_tag internal_tag" rel="tag" title="Posts tagged with cache">Cache</a>-Control: private");header("<a href="http://www.donevii.com/post/tag/cache" class="st_tag internal_tag" rel="tag" title="Posts tagged with cache">Cache</a>-Control: no-<a href="http://www.donevii.com/post/tag/cache" class="st_tag internal_tag" rel="tag" title="Posts tagged with cache">cache</a>", false);?&gt;</pre>
<p>听起来太简单了？确实如此——因为有些代理（浏览器）在某些环境下将忽略这些header。要确保浏览器不缓存文档，应该更强硬一些：</p>
<pre>&lt;?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.1header("Cache-Control: no-store, no-cache, must-revalidate");header("Cache-Control: post-check=0, pre-check=0", false);# HTTP/1.0header("Pragma: no-cache");?&gt;</pre>
<p>这样，对于我们不想缓存的内容来说已经行了。但对于那些不会每次请求时都有改动的内容，应该鼓励浏览器更霸道的缓存它。“If-Modified-Since”请求header能够做到这点。如果客户端在请求中发送一个“If-Modified-Since”header，apache（或其他服务器）会以状态代码304（没改过）响应，告诉浏览器缓存已经是最新的。使用这个机制，能够避免重复发送文件给浏览器，不过仍然导致了一个HTTP请求的消耗。嗯，再想想。</p>
<p>与If-Modified-Since机制类似的是实体标记（entity tags）。在apache环境下，每个对静态文件的响应都会发出一个“ETag”header，它包含了一个由文件修改时间、文件大小和inode号生成的校验和（checksum）。在下载文件之前，浏览器会发送一个HEAD请求去检查文件的etag。可ETag跟If-Modified-Since有同样的问题：客户端仍旧需要执行HTTP请求来验证本地缓存是否有效。</p>
<p>此外，如果你使用多台服务器提供内容，得小心使用if-modified-since和etags。在两台负载平衡的服务器环境下，对一个代理（浏览器）来说，一个资源可以这次从A服务器得到，下次从B服务器得到<cite><font color="#666666">（htmlor注：lvs负载平衡系统就是个典型的例子）</font></cite>。这很好，也是采用平衡负载的原因。可是，如果两台服务器给同一个文件生成了不同的etag或者文件修改日期，浏览器就无所适从了（每次都会重新下载）。默认情况下，etag是由文件的inode号生成的，而多台服务器之间文件的inode号是不同的。可以使用apache的配置选项关掉它：</p>
<pre>FileETag MTime Size</pre>
<p>使用这个选项，apache将只用文件修改日期和文件大小来决定etag。很不幸，这导致了另一个问题（一样能影响if-modified-since）。既然etag依赖于修改时间，就得让时间同步。可往多台服务器上传文件时，上传时间差个一到两秒是常有的事。这样一来，两台服务器生成的etag还是不一样。当然，我们还可以改变配置，让etag的生成只取决于文件大小，但这就意味着如果文件内容变了而大小没变，etag也不会变。这可不行。</p>
<h3>缓存真是个好东西</h3>
<p>看来我们正从错误的方向入手解决问题。（现在的问题是，）这些可能的缓存策略导致了一件事情反复发生，那就是：客户端向服务器查询本地缓存是否最新。假如服务器在改动文件的时候通知客户端，客户端不就知道它的缓存是最新的了（直到接到下一次通知）？可惜天公不做美——（事实）是客户端向服务器发出请求。</p>
<p>其实，也不尽然。在获取js或css文件之前，客户端会用&lt;script&gt;或&lt;link&gt;标记向服务器发送一个请求，说明哪个页面要加载这些文件。这时候就可以用服务器的响应来通知客户端这些文件有了改动。有点含糊，说得再详细点就是：如果改变css和js文件内容的同时，也改变它们的文件名，就可以告诉客户端对url全都永久缓存——因为每个url都是唯一的。</p>
<p>假如能确定一个资源永不更改，我们就可以发出一些霸气十足的缓存header<cite><font color="#666666">（htmlor注：这句也很有气势吧）</font></cite>。在php里，两行就好：</p>
<pre>&lt;?phpheader("Expires: ".gmdate("D, d M Y H:i:s", time()+315360000)." GMT");header("Cache-Control: max-age=315360000");?&gt;</pre>
<p>我们告诉浏览器这个内容在10年后（10年大概会有315,360,000秒，或多或少）过期，浏览器将会保留它10年。当然，很有可能不用php输出css和js文件（因此就不能发出header），这种情况将在稍后说明。</p>
<h3>人力有时而穷</h3>
<p>当文件内容更改时，手动去改文件名是很危险的。假如你改了文件名，模板却没有指向它？假如你改了一些模板另一些却没改？假如你改了模板却没改文件名？还有最糟的，假如你改动了文件却忘了改名或者忘了改变对它的引用？最好的结果，是用户看到老的而看不到新的内容。最坏的结果，是找不到文件，网站没法运转了。听起来这（指改动文件内容时修改url）似乎是个馊主意。</p>
<p>幸运的是，计算机做这类事情——当某种变化发生，需要相当准确地完成的、重复重复再重复的<cite><font color="#666666">（htmlor注：番茄鸡蛋伺候～）</font></cite>、枯燥乏味的工作——总是十分在行。</p>
<p>这个过程（改变文件的url）没那么痛苦，因为我们根本不需要改文件名。资源的url和磁盘上文件的位置也没必要保持一致。使用apache的<a href="http://httpd.apache.org/docs/2.0/mod/mod_rewrite.html"><font color="#0066dd">mod_rewrite</font></a>模块，可以建立简单的规则，让确定的url重定向到确定的文件。</p>
<pre>RewriteEngine onRewriteRule ^/(.*.)v[0-9.]+.(css|js|gif|png|jpg)$	/$1$2	[L]</pre>
<p>这条规则匹配任何带有指定扩展名同时含有“版本”信息（version nugget）的url，它会把这些url重定向到一个不含版本信息的路径。如下所示：</p>
<pre>URL			   Path/images/foo.v2.gif	-&gt; /images/foo.gif/css/main.v1.27.css	-&gt; /css/main.css/javascript/md5.v6.js	-&gt; /javascript/md5.js</pre>
<p>使用这条规则，就可以做到不改变文件路径而更改url（因为版本号变了）。由于url变了，浏览器就认为它是另一个资源（会重新下载）。想更进一步的话，可以把我们之前说的脚本编组函数结合起来，根据需要生成一个带有版本号的&lt;script&gt;标记列表。</p>
<p>说到这里，你可能会问我，为什么不在url结尾加一个查询字符串（query string）呢（如/css/main.css?v=4）？根据HTTP缓存规格书所说，用户代理对含有查询字符串的url永不缓存。虽然ie跟firefox忽略了这点，opera和safari却没有——为了确保所有浏览器都缓存你的资源，还是不要在url里用查询字符串的好。</p>
<p>现在不移动文件就能更改url了，如果能让url自动更新就更好了。在小型的产品环境下（如果有大型的产品环境，就是开发环境了），使用模板功能可以很轻易的实现这点。这里用的是smarty，用其他模板引擎也行。</p>
<pre>SMARTY:&lt;link xhref="{version xsrc='/css/group.css'}" rel="stylesheet" type="text/css" /&gt;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:&lt;link xhref="/css/group.v1234567890.css" rel="stylesheet" type="text/css" /&gt;</pre>
<p>对每个链接到的资源文件，我们得到它在磁盘上的路径，检查它的mtime（文件最后修改的日期和时间），然后把这个时间当作版本号插入到url中。对于低流量的站点（它们的stat操作开销不大）或者开发环境来说，这个方案不错，但对于高容量的环境就不适用了——因为每次stat操作都要磁盘读取（导致服务器负载升高）。</p>
<p>解决方案相当简单。在大型系统中每个资源都已经有了一个版本号，就是版本控制的修订号（你们应该使用了版本控制，对吧？）。当我们建立站点准备部署的时候，可以轻易的查到每个文件的修订号，写在一个静态配置文件里。</p>
<pre>&lt;?php$GLOBALS['config']['resource_versions'] = array(  '/images/foo.gif'    =&gt; '2.1',  '/css/main.css'      =&gt; '1.27',  '/javascript/md5.js' =&gt; '6.1.4',);?&gt;</pre>
<p>当我们发布产品时，可以修改模板函数来使用版本号。</p>
<pre>&lt;?phpfunction 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']);}?&gt;</pre>
<p>就这样，不需要改文件名，也不需要记住改了哪些文件——当文件有新版本发布时它的url就会自动更新——有意思吧？我们就快搞定了。</p>
<h3>只欠东风</h3>
<p>之前谈到为静态文件发送超长周期（very-long-period）的缓存header时曾说过，如果不用php输出，就不能轻易的发送缓存header。很显然，有两个办法可以解决：用php输出，或者让apache来做。</p>
<p>php出马，手到擒来。我们要做的仅仅是改变rewrite规则，把静态文件指向php脚本，用php在输出文件内容之前发送header。</p>
<pre>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;}</pre>
<p>这个方案有效，但并不出色。（因为）跟apache相比，php需要更多内存和执行时间。另外，我们还得小心防止可能由path参数传递伪造值引起的exploits。为避免这些问题，应该用apache直接发送header。rewrite规则语句允许当规则匹配时设置环境变量（environment variable），当给定的环境变量设置后，Header命令就可以添加header。结合以下两条语句，我们就把rewrite规则和header设置绑定在了一起：</p>
<pre>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</pre>
<p>考虑到apache的执行顺序，应该把rewrite规则加在主配置文件（httpd.conf）而不是目录配置文件（.htaccess）中。否则在环境变量设置之前，header行会先执行（就那没意义了）。至于header行，则可以放在两文件任何一个当中，没什么区别。</p>
<h3>眼观六路</h3>
<p><cite><font color="#666666">（htmlor注：多谢</font><a href="http://tchaikov.blogsome.com/"><font color="#0066dd">tchaikov</font></a><font color="#666666">告知“skinning rabbits”的含义，但我不想翻的太正式，眼下的这个应该不算太离谱吧。）</font></cite></p>
<p>通过结合使用以上技术，我们可以建立一个灵活的开发环境和一个快速又高性能的产品环境。当然，这离终极目标“速度”还有一段距离。有许多更深层的技术（比如分离伺服静态内容，用多域名提升并发量等）值得我们关注，包括与我们谈到的方法（建立apache过滤器，修改资源url，加上版本信息）殊途同归的其他路子。你可以留下评论，告诉我们那些你正在使用的卓有成效的技术和方法。</p>
<p>（完）</p>
]]></content:encoded>
			<wfw:commentRss>http://www.donevii.com/post/9.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

