1. ETag
HTTP 1.1中引入了ETag来解决缓存的问题。ETag全称是Entity Tag,由服务端生成,服务端可以决定它的生成规则。如果根据文件内容生成散列值。那么条件请求将不会受到时间戳的改动造成带宽浪费。下面是根据内容生成散列值的方法:
1 var getHash = function(str) { 2 var shasum = crypto.createHash('sha1'); 3 return shasum.update(str).digest('base64'); 4 }
与If-Modified-Since/Last-Modified不同的是,ETag的请求和响应是If-None-Match/ETag。浏览器在收到带有ETag:'14-389247298365'字段的响应头后,会在后面的请求中将其设置在请求头中:If-None-Match: '14-389247298365'。服务器端收到带If-None-Match: '14-389247298365'的报头后,会进行如下判断来决定返回新的内容还是只响应一个304状态码让浏览器使用本地缓存版本:
1 var handle = function(req, res) { 2 fs.readFile(filename, function(err, file){ 3 var hash = getHash(file); 4 var noneMatch = req['if-none-match']; 5 if (hash === noneMatch) { 6 res.writeHead(304, "Not Modified"); 7 res.end(); 8 } else { 9 res.setHeader("ETag", hash); 10 res.writeHead(200, "OK"); 11 res.end(file); 12 } 13 }) 14 }
2. Last-Modified
通常来说,如果请求头中不包含ETag,服务端会通过判断Last-Modified值来决定响应304状态码还是新的文件内容。Last-Modified顾名思义指的是文件的最后一次修改时间。与ETag一样,在浏览器首次访问站点后,服务端会在其响应头中设置一个Last-Modified的字段,它的值是一个UTC格式的时间字符串。随后,在浏览器对站点的第二次访问中,会在其请求头中设置一个If-Modified-Since,其值就是上一次返回的Last-Modified的值。服务器端会根据这个值是否与其本地文件的最后一次修改时间相同来判断是否使用缓存。代码如下:
1 var handle = function(req, res) { 2 fs.stat(filename, function(err, stat){ 3 var lastModified = stat.mtime.toUTCString(); 4 if (lastModified === req.headers['if-modified-since']) { 5 res.writeHead(304, "Not Modified"); 6 res.end(); 7 } else { 8 fs.readFile(filename, function(err, file){ 9 var lastModified = stat.mtime.toUTCString(); 10 res.setHeader("Last-Modified", lastModified); 11 res.writeHead(200, "OK"); 12 res.end(file); 13 }); 14 } 15 }) 16 }
3. Expires 和 Cache-Control
以上两种的缓存判断都需要客户端向服务端先发送一个条件请求,根据返回来决定是否使用缓存。需要一定的时间开销和带宽。而实际上浏览器最先判断的是Expires 和 Cache-Control。在服务端相应里设置Expires 或 Cache-Control,浏览器会根据该值进行缓存。Expires是一个GMT格式的时间字符串。浏览器再接收到这个过期值后,只要本地还存在对应缓存文件,在到期时间之前它都不会再发起请求。但它的缺陷是浏览器与服务器之间的时间可能不一致,导致文件提前过期或已经过期却还没删除。
而Cache-Control恰恰解决了这个问题:
1 var handle = function(req, res) { 2 fs.readFile(filename, function(err, file){ 3 res.setHeader("Cache-Control", "max-age=" + 10*365*24*60*60); 4 res.writeHead(200, "OK"); 5 res.end(file); 6 }); 7 }
上面的代码为Cache-Control设置了max-age值为10年,max-age会告诉浏览器文件多长时间后过期,进行倒计时式的计算。这样就可以避免客户端与服务器端时间不一致带来的问题了。此外Cache-Control还可以public、private、no-cache、no-store等更精细地控制缓存的选项。HTTP1.0时还不支持max-age,如今的服务端在模块的支持下多半同时对Expires 和 Cache-Control进行支持,如果浏览器中两个值都存在且同时被支持,max-age会覆盖Expires。
这两种方法虽然节省了带宽和请求时间,但其缺陷是当服务端的文件内容进行了更新时,无法通知客户端更新。因为浏览器是根据URL进行缓存的,所以我们一般在对静态资源使用缓存时也会对其设定版本号。使得客户端能请求到新的内容。一般更新机制有如下两种方式:
- 每次发布,web应用或静态资源的路径中附带对应的版本号:http://url.com/?v=20141216
- 每次发布,web应用或静态资源的路径中附带文件内容的hash码:http://url.com/?hash=sdasd4d
因为文件内容更新并不意味着新的版本。所以使用hash值得方式会更加妥当一些。