前一陣被人問到一個問題:
開發人員修改一文件,版本下發后期望用戶可以訪問到修改的最新文件,而不是被瀏覽器緩存過的歷史文件,請問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頭實施緩存策略。
github頁面上公共圖標
下面這個是知乎中個人頭像的緩存情況,可以看到采用了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'); ?> />