前一陣被人問到一個(gè)問題:
開發(fā)人員修改一文件,版本下發(fā)后期望用戶可以訪問到修改的最新文件,而不是被瀏覽器緩存過的歷史文件,請問Http有機(jī)制可以保證用戶訪問到最新的文件嗎?如果沒有,在考慮性能的前提下,如何設(shè)計(jì)一種可行方案呢?
相信不少人第一直覺會(huì)想到和瀏覽器緩存有關(guān)的一些緩存頭,例如:
- 與請求內(nèi)容新鮮度有關(guān)的:expires,cache-control
- expires指定了文檔的失效時(shí)間,但是前提要求客戶端和服務(wù)器端的時(shí)鐘是同步的,不然就不準(zhǔn)確了
- cache-control頭比實(shí)際想象的要復(fù)雜的多,cache-control:no-cache表明不應(yīng)使用緩存文件,而應(yīng)該直接從服務(wù)器重新獲取,cache-control:max-age=3600表明從服務(wù)器將文檔傳來之時(shí)起,可以認(rèn)為此文檔處于新鮮狀態(tài)的秒數(shù)。
- 與條件請求有關(guān)的頭,If-Modified-Since,If-None-Match,Last-Modified,Etag。
瀏覽器認(rèn)定文檔新鮮度過期后,需要重新請求服務(wù)器,此時(shí)可以附帶一些條件參數(shù),例如文檔最近一次修改的時(shí)間,文檔的實(shí)體標(biāo)記etag值,服務(wù)器會(huì)拿請求報(bào)文中的值與服務(wù)器中保存的值進(jìn)行比較,如果兩者一致,表明文檔還可以繼續(xù)使用,此時(shí)以304(文檔未修改)狀態(tài)碼作為回應(yīng),否則將新的內(nèi)容返回客戶端。
我們把問題細(xì)化一下,修改的文件存在兩種情況:
該文件的內(nèi)容是需要?jiǎng)討B(tài)填充的,這時(shí)緩存的策略為不緩存,每次請求都去服務(wù)器重新驗(yàn)證
-
對于靜態(tài)文件的修改,舉幾個(gè)例子看看:
下面這個(gè)是github頁面上公共圖標(biāo)的緩存情況,cache-control配置了一個(gè)很大的失效時(shí)間,同時(shí)結(jié)合last-modified頭實(shí)施緩存策略。
github頁面上公共圖標(biāo)
下面這個(gè)是知乎中個(gè)人頭像的緩存情況,可以看到采用了cache-control和etag控制緩存

現(xiàn)在的問題是:上述圖標(biāo)要是發(fā)生了改變,用戶瀏覽器如何才能及時(shí)得到更新呢?
因?yàn)閏ache-control配置了一個(gè)很大的失效時(shí)間間隔,在用戶本地存在緩存的情況下,瀏覽器是不會(huì)再次發(fā)起請求的
對于github的圖標(biāo)還好理解,因?yàn)槭蔷W(wǎng)站公共的圖標(biāo),被更改的頻率會(huì)很小,在這種背景下,可能在下一次用戶請求該網(wǎng)站時(shí),用戶瀏覽器已經(jīng)不存在此網(wǎng)站的緩存了,所以是可以更新到最新狀態(tài)的。
對于知乎用戶頭像的緩存策略,初看起來似乎很矛盾,用戶更改頭像是隨時(shí)可能會(huì)發(fā)生的事情,如何在用戶頭像更改之后網(wǎng)站內(nèi)容可以及時(shí)更新呢?仔細(xì)想想,其實(shí)我們的擔(dān)心是多余的,用戶上傳新的頭像后,系統(tǒng)會(huì)給新頭像分配新名稱,這樣在用戶重新請求主頁面時(shí),動(dòng)態(tài)填充的內(nèi)容已經(jīng)發(fā)生了變化,服務(wù)器會(huì)返回新的主頁面給瀏覽器,瀏覽器解析到了新的用戶頭像連接,由于在瀏覽器緩存中并沒有找到對應(yīng)的緩存文件,所以瀏覽器會(huì)針對新的用戶頭像發(fā)起Http請求,進(jìn)而得到最新的用戶頭像
圖片和樣式文件的更改一般不會(huì)給網(wǎng)站帶來災(zāi)難性的影響,但如果是js文件被修改但是用戶瀏覽器依舊使用的是過期的緩存文件,這種情況相比較而言對網(wǎng)站的影響就要大得多。
如何避免此類問題呢?結(jié)合知乎個(gè)人頭像的例子,不難想到的一種方案就是對修改的腳本文件添加一個(gè)修改的標(biāo)志,類似下面這個(gè)樣子
<script src="dir/test.js?modify=true"></script>
如果頻繁修改呢,下面這種方式似乎給好一點(diǎn)
<script src="dir/test.js?version=2.0"></script>
上面的方案都是基于script標(biāo)簽的,在模塊化大行其道的今天,腳本加載器應(yīng)該是會(huì)考慮諸如此類實(shí)際問題的,例如在seajs中有下面的配置功能
seajs.config({ vars: { 'version': '2' } });
define(function(require, exports, module) {
var lang = require('./dir/test.js?version={version}');
});
考慮一下現(xiàn)實(shí)吧,假設(shè)文件A在系統(tǒng)中很重要,因此存在大量文件引用,如果還采用上述的方案,這無疑是煩人的體力勞動(dòng),如何解脫呢?
總體的方案是:
在動(dòng)態(tài)請求的文件中給靜態(tài)文件動(dòng)態(tài)添加類似于版本號的標(biāo)志,然后對服務(wù)器配置url重寫功能(例如apache服務(wù)器),在java中可以配置過濾器,對特定的文件進(jìn)行url重寫。
下面給出stackoverflow上一個(gè)基于php的實(shí)現(xiàn)方案,原文在這里
+ 首先,在apache的配置文件.htaccess中開啟重寫功能,并且添加規(guī)則
RewriteEngine on RewriteRule ^(.*)\.[\d]{10}\.(css|js)$ $1.$2 [L]
- 給文件追加mtime標(biāo)志
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); }
- 實(shí)際使用
<script href="<?php echo auto_version('/js/base.js'); ?> />
