随着前端开发在 Web 开发中的地位日渐重要,前端相关的资源部署也成了各大网站开始重视的一步。
对于一个访问量相当大的网站来说,从前端方面考虑,除了使用 CDN 向客户端分发 JavaScript/CSS 资源,充分利用客户端浏览器缓存也是减轻服务器压力的一个重要途径。
首先按照我们的“常识”,认为 JavaScript/CSS 默认就是在浏览器中缓存“很长”一段时间的,作为最简单的策略,我们就依赖这种缓存机制来降低服务器的压力。然而,对于一个不断改进、更新的网站来说,JavaScript/CSS 的变动是相当频繁的。
如何让浏览器知道这些静态资源的改变呢?让我们从最简单的解决方案开始摸索和学习,慢慢改进。
刀耕火种的方式
把这件事扔给开发者吧。以 JSP 页面为例,首先在一个统一的 jsp 文件(例如 version.jsp)中定义所有静态资源的版本号:
[code lang=”html”]
<%
String dialogVersion = “20090101”;
…
%>
[/code]
然后在每个用到 JavaScript/CSS 的页面中包含该文件并且给静态资源加上版本号:
[code lang=”html”]
<%@ include file="/WEB-INF/include/common.jspf" %>
…
…
[/code]
开发者负责在每次 commit 代码前检查自己修改过的静态资源,去修改相应的版本号。这给本来就不堪重负的开发者又增加了一项艰巨任务,他们难免会有疏忽的时候,等到有客户反馈问题,才想起来——哦,忘记改版本号了!
自动化
一个成熟的开发团队应该使用版本控制工具来管理代码,一般最常用的是 Subversion. 除此之外,还应该有一套完善的部署脚本,执行一两个命令即可完成整个部署过程,而不是手动地去复制、修改代码。这个过程可能包含从 svn 检出代码、编译(解释型语言则省去了这一步)、选择性的复制(避免覆盖生产环境中的特定配置)等等。
有了这些基础,把上面手工修改版本号的工作交给自动脚本去做就可以了。”svn info” 命令可以列出文件最后被修改的版本号,我们只要得到这个版本号,把它写在 version.jsp 中即可。写这个脚本可能稍微麻烦一点,不过 python, perl, bash, ruby, 随便什么语言都可以,或者是一个 ant task. 这是一劳永逸的事情。开发者们可以把精力集中在真正的前端开发商了。
不要用 query string!
我本来以为到此为止就很完美了,但查阅了一些资料之后发现我的许多观点都是完全错误的。
首先,在上述的方法中,版本号是作为 query string 附在静态资源的 URL 上的,然而:
According the letter of the HTTP caching specification, user agents should never cache URLs with query strings. While Internet Explorer and Firefox ignore this, Opera and Safari don’t – to make sure all user agents can cache your resources, we need to keep query strings out of their URLs.
以上摘自文章 “Serving JavaScript Fast”
正确的办法是将版本号插在文件名之中,后缀之前,然后利用 Web Server 的 URL Rewrite 功能指向不带版本号的真实文件,实际上就是欺骗一下客户端。具体做法请参考上文,或者 “Automatically Version Your CSS and JavaScript Files“.
仅仅 304 还不行!
假设浏览器的缓存中什么都没有,这时用户去访问一个网站,引用到的 JavaScript/CSS 都要到服务器上去下载。服务器将这些静态资源返回给客户端的同时,会附加一个 Last-Modified 时间值。下次客户端再请求该资源,会把这个时间值附加在 If-Modified-Since 这个 header 字段中传给服务器,意思就是如果在这个时间之后修改过才有必要重新传输。如果没有修改,响应的状态码就是 304.
下面用 fiddler 抓取 IE 浏览器请求我的 blog 首页的过程,做个测试。第一张图,蓝色虚线上面是缓存为空时访问页面的请求,虚线下面是重启浏览器之后再次访问的请求情况。为了说明问题,我只保留了与问题密切相关的一些请求,可以点击查看大图。
请求 #0, #28 都是请求页面本身,两次都是全部传输,状态码是200. 请求 #1, #2 分别和 #29, #30 对应,一个是 JavaScript,一个是 CSS. 从 body 那一列可以看出消息体长度都是 0,而状态码是 304. 再看请求 #3,在第二次访问时,浏览器根本没有再次请求该资源,关键就在于 Caching 那一列的属性。
尽管减少了数据传输,请求数却是影响服务器承载能力的最重要因素。理想的情况自然是像 #3 请求的 widget02.css 那样,告诉浏览器在一年以后该资源才会过期。
如果使用的 Web server 是 Apache httpd,那么可以利用它的 mod_expires 来实现 JavaScript/CSS 自动添加 Expires 头信息。具体方法在前面提到的文章里都有。
最后再次列一下参考的宝贵资源:
- Automatically Version Your CSS and JavaScript Files
- Serving JavaScript Fast
- Best Practices for Speeding Up Your Web Site
- What Is Caching and How Does It Apply to the Web?
事实上本人的实际经验还停留在刀耕火种的程度,只是把最近学到和想到的东西分享一下而已……在未来的项目中我肯定会实践这些想法,并在可能的情况下把实际的代码分享出来。文中如有错误,欢迎指正。
Leave a Reply