前言
最近遇到了一個問題,我們團隊開發了一個js庫,提供給各個業務方引入,採集相關業務資料,漸漸發現,每次更新js庫的程式碼,重新發布,並清除dns快取後,還是有些業務方在請求舊的js庫的程式碼,怎麼才能讓業務方拿到最新的sdk程式碼呢?我陷入了困擾,每次重新發布修改js庫的檔名?這似乎不太合理,接入的業務方那麼多,每次釋出一個個去通知他們更改引入的檔名?怕是會被業務方們扔雞蛋,且更改了檔名業務方也不會因為這個小改動重新發版,這顯然不靠譜.於是我研究了一下瀏覽器的靜態資源快取的策略,原以為nginx配置那裡修改相應頭的Etag欄位,告訴瀏覽器我這個資源更新了,瀏覽器便會從伺服器下載新的資原始檔了.但後來經過一番折騰後,發現nginx只能保證ETag開啟和關閉,也就是詳情頭裡是否包含該欄位,但是並不能保證資源更新後該ETag欄位的更新,我的問題並沒有解決,但是花功夫仔細研究了一下Expires, Last-Modified, Etag的快取機制.在此,我把我對瀏覽器的靜態資源快取策略的相關內容做一個分享,希望可以幫助遇到類似困擾的童鞋們~
Cache-Control(快取控制)
在開始重點內容的分享之前,我想先分享一下關於請求頭和響應頭中的Cache-Control欄位,這個欄位在http請求中充當著快取控制的角色,是控制快取的開關,用於標識請求或訪問中是否開啟了快取,使用了哪種快取方式.
Cache-Control通用訊息頭欄位被用於在http請求和響應中通過指令來實現快取機制,快取指令是單向的,這意味著在請求設定的指令,在響應中不一定包含相同的指令.Cache-Control響應指令允許源伺服器覆蓋一個響應預設的快取功能.
Cache-Control欄位
在請求中使用Cache-Control,它可選的值有:
欄位名稱 | 說明 |
---|---|
no-cache | 告知(代理)伺服器不直接使用快取,要求向原伺服器發起請求(這並不代表你每次都可以取到最新的資源) |
no-store | 所有內容都不會被儲存到快取或Internet臨時檔案中 |
max-age=delta-seconds | 告知伺服器希望接收一個存在時間(Age)不大於delta-seconds秒的資源 |
min-fresh=delta-seconds | 告知(代理)伺服器客戶端希望接一個在小於delta-seconds秒內被更新過的資源 |
no-transform | 告知(代理)伺服器客戶端希望獲取實體資料沒有被轉換(比如壓縮)過的資源 |
only-if-cached | 告知(代理)伺服器客戶端希望獲取快取的內容,而不用向原伺服器發去請求 |
cache-extension | 自定義擴充套件值,若伺服器不識別該值將被忽略掉 |
在響應中使用Cache-Control時,它可選的值有:
欄位名稱 | 說明 |
---|---|
public | 表明任何情況下都得快取該資源(即使是需要HTTP認證的資源) |
Private[="field-name"] | 表明返回報文中全部或部分(若指定了field-name則為field-name的欄位資料)僅開放給某些使用者(伺服器指定的share-user,如代理伺服器)做快取使用,其他使用者則不能快取這些資料 |
no-cache | 不直接使用快取,要求向伺服器發起(新鮮度校驗)請求(這就到了Etag和Last-Modified起作用的時候了) |
no-store | 所有內容都不會被儲存到快取或Internet臨時檔案中 |
no-transform | 告知客戶端快取檔案時不得對實體資料做出任何改變 |
only-if-cached | 告知(代理)伺服器客戶端希望獲取快取的內容(若有),而不用向原伺服器發去請求 |
must-revalidate | 當前資源一定是向原伺服器發去驗證請求的,若請求失敗會返回504(而非代理伺服器上的快取) |
proxy-revalidate | 與must-revalidate類似,但技能應用於共享快取(如代理) |
max-age=delta-seconds | 告訴客戶端,該資源在delta-seconds秒內是新鮮的,無需向伺服器發請求 |
s-maxage=delta-seconds | 同max-age,但僅應用於共享快取(如代理) |
cache-extension | 自定義擴充套件值,若客戶端不識別該值將被忽略掉 |
需要注意的是,在Cache-Control中,這些值可以自由組合,多個值衝突時,是有優先順序的,no-store優先順序最高.
快取校驗: 在快取中,我們需要一個機制來驗證快取是否有效.比如伺服器的資源更新了,客戶端需要及時重新整理快取,又或者客戶端的資源過了有效期,但是伺服器上的資源還是舊的,此時並不需要重新請求資源.快取校驗就是用來解決這些問題的.
Expires(快取校驗)
伺服器設定Expires欄位為一個日期,客戶端請求該資源時將這個日期與客戶端當前日期進行比對,如果當前時間小於這個日期,則表示資源未過期,使用快取,如果當前時間大於這個日期,則表示資源已過期,客戶端就會重新請求該資源.但是這一策略會收到客戶端與伺服器時間不一致的問題的影響.如果客戶端時間晚於伺服器的時間,會導致資源還未過期就重新請求,反之,會導致客戶端還在使用過期的舊資源.
Last-Modified / If-Modified-Since(快取校驗)
當瀏覽器第一次請求一個資源時,服務端返回狀態碼200,返回請求的資源的同時HTTP響應頭會有一個Last-Modified標記著檔案在服務端最後被修改的時間.
瀏覽器第二次請求上次請求過的資源時,瀏覽器會在HTTP請求頭中新增一個If-Modified-Since的標記,用來詢問伺服器該時間之後檔案是否被修改過
如果伺服器端的資源沒有變化,則自動返回304狀態,使用瀏覽器快取,從而保證了瀏覽器不會重複從伺服器端獲取資源,也保證了伺服器有變化時,客戶端能夠及時得到最新的資源.
Etag / If-None-Match(快取校驗)
當瀏覽器第一次請求一個資源時,服務端返回的狀態碼為200,同時HTTP相應頭會有一個Etag欄位,存放著伺服器端生成的一個序列值.
瀏覽器第二次請求上次請求過的url時,瀏覽器會在HTTP請求頭新增一個If-None-Match的標記,用來詢問伺服器該檔案有沒有被修改。
如果伺服器的資源沒有變化,Etag欄位沒有被修改依然與If-None-Match的值保持一致,則請求自動返回304狀態,使用瀏覽器快取.如果不一致,則說明資源被更改,則重新去下載新的資源.
Etag主要為了解決Last-Modified無法解決的一些問題:
(1) 一些檔案也許週期性的更改,但是它的內容並不改變(僅僅改變的是修改時間),這個時候我們不希望客戶端認為這個檔案被修改了,而重新獲取資源.
(2) 某些檔案修改非常頻繁,比如在秒一下的時間內進行修改(比如1s內修改了N次),If-Modified-Since能檢查到的粒度是秒級的,這種修改是無法判斷的(或者說UNIX記錄MTIME只能精確到秒);
(3) 某些伺服器不能精確的得到檔案的最後修改時間;
nginx配置裡ETag選項預設開啟的,所以請求的資原始檔若發生改動,會在響應頭裡生成新的ETag值.這樣客戶端就能夠發現If-None-Match的值和Etag欄位的值不匹配,從而去請求最新的資原始檔.
我的困惑
靜態資源修改以後.對應的檔案最後更改時間和ETag欄位有時似乎都不會做相應的更改,瀏覽器快取該靜態資原始檔,導致在檔案更改後不能及時更新.是不是靜態資源的快取只能依靠修改檔名的策略來拿到最新的資源? 那麼Expires, Last-Modified, Etag這三個欄位只能在後臺程式碼裡才能修改嗎?各路英雄可還有更好的解決方案?望各路英雄不吝賜教~