前一阵被人问到一个问题:
开发人员修改一文件,版本下发后期望用户可以访问到修改的最新文件,而不是被浏览器缓存过的历史文件,请问Http有机制可以保证用户访问到最新的文件吗?如果没有,在考虑性能的前提下,如何设计一种可行方案呢?
相信不少人第一直觉会想到和浏览器缓存有关的一些缓存头,例如:
- 与请求内容新鲜度有关的:expires,cache-control
- expires指定了文档的失效时间,但是前提要求客户端和服务器端的时钟是同步的,不然就不准确了
- cache-control头比实际想象的要复杂的多,cache-control:no-cache表明不应使用缓存文件,而应该直接从服务器重新获取,cache-control:max-age=3600表明从服务器将文档传来之时起,可以认为此文档处于新鲜状态的秒数。
- 与条件请求有关的头,If-Modified-Since,If-None-Match,Last-Modified,Etag。
浏览器认定文档新鲜度过期后,需要重新请求服务器,此时可以附带一些条件参数,例如文档最近一次修改的时间,文档的实体标记etag值,服务器会拿请求报文中的值与服务器中保存的值进行比较,如果两者一致,表明文档还可以继续使用,此时以304(文档未修改)状态码作为回应,否则将新的内容返回客户端。
我们把问题细化一下,修改的文件存在两种情况:
该文件的内容是需要动态填充的,这时缓存的策略为不缓存,每次请求都去服务器重新验证
-
对于静态文件的修改,举几个例子看看:
下面这个是github页面上公共图标的缓存情况,cache-control配置了一个很大的失效时间,同时结合last-modified头实施缓存策略。
下面这个是知乎中个人头像的缓存情况,可以看到采用了cache-control和etag控制缓存
现在的问题是:上述图标要是发生了改变,用户浏览器如何才能及时得到更新呢?
因为cache-control配置了一个很大的失效时间间隔,在用户本地存在缓存的情况下,浏览器是不会再次发起请求的
对于github的图标还好理解,因为是网站公共的图标,被更改的频率会很小,在这种背景下,可能在下一次用户请求该网站时,用户浏览器已经不存在此网站的缓存了,所以是可以更新到最新状态的。
对于知乎用户头像的缓存策略,初看起来似乎很矛盾,用户更改头像是随时可能会发生的事情,如何在用户头像更改之后网站内容可以及时更新呢?仔细想想,其实我们的担心是多余的,用户上传新的头像后,系统会给新头像分配新名称,这样在用户重新请求主页面时,动态填充的内容已经发生了变化,服务器会返回新的主页面给浏览器,浏览器解析到了新的用户头像连接,由于在浏览器缓存中并没有找到对应的缓存文件,所以浏览器会针对新的用户头像发起Http请求,进而得到最新的用户头像
图片和样式文件的更改一般不会给网站带来灾难性的影响,但如果是js文件被修改但是用户浏览器依旧使用的是过期的缓存文件,这种情况相比较而言对网站的影响就要大得多。
如何避免此类问题呢?结合知乎个人头像的例子,不难想到的一种方案就是对修改的脚本文件添加一个修改的标志,类似下面这个样子
<script src="dir/test.js?modify=true"></script>
如果频繁修改呢,下面这种方式似乎给好一点
<script src="dir/test.js?version=2.0"></script>
上面的方案都是基于script标签的,在模块化大行其道的今天,脚本加载器应该是会考虑诸如此类实际问题的,例如在seajs中有下面的配置功能
seajs.config({ vars: { 'version': '2' } });
define(function(require, exports, module) {
var lang = require('./dir/test.js?version={version}');
});
考虑一下现实吧,假设文件A在系统中很重要,因此存在大量文件引用,如果还采用上述的方案,这无疑是烦人的体力劳动,如何解脱呢?
总体的方案是:
在动态请求的文件中给静态文件动态添加类似于版本号的标志,然后对服务器配置url重写功能(例如apache服务器),在java中可以配置过滤器,对特定的文件进行url重写。
下面给出stackoverflow上一个基于php的实现方案,原文在这里
+ 首先,在apache的配置文件.htaccess中开启重写功能,并且添加规则
RewriteEngine on RewriteRule ^(.*)\.[\d]{10}\.(css|js)$ $1.$2 [L]
- 给文件追加mtime标志
function auto_version($file){ if(strpos($file, '/') !== 0 || !file_exists($_SERVER['DOCUMENT_ROOT'] . $file)) return $file; $mtime = filemtime($_SERVER['DOCUMENT_ROOT'] . $file); return preg_replace('{\\.([ ^./]+)$}', ".$mtime.\$1", $file); }
- 实际使用
<script href="<?php echo auto_version('/js/base.js'); ?> />