Web快取機制綜述(HTML5快取總結與細節釋疑)
開篇:
最近專案裡用到了HTML5快取機制,於是很想搞清楚 瀏覽器快取,HTML5離線快取,還有專案中用到的 CDN快取 這三部分的關係以及更新機制。看了一堆關於HTML5快取機制的文章,各有所長,各有疏漏。因此本人想在此做一總結,本文假設讀者對基本的HTML5快取應用已有所瞭解,因此不再詳述概念,可以將本文當做釋疑彙總吧。
以下部分內容引用自網路。
一、Web快取的型別
在Web應用領域,Web快取大致可以分為以下幾種型別:
資料庫資料快取
Web應用,特別是SNS型別的應用,往往關係比較複雜,資料庫表繁多,如果頻繁進行資料庫查詢,很容易導致資料庫不堪重荷。為了提供查詢的效能,會將查詢後的資料放到記憶體中進行快取,下次查詢時,直接從記憶體快取直接返回,提供響應效率。比如常用的快取方案有
伺服器端快取
代理伺服器快取
代理伺服器是瀏覽器和源伺服器之間的中間伺服器,瀏覽器先向這個中間伺服器發起Web請求,經過處理後(比如許可權驗證,快取匹配等),再將請求轉發到源伺服器。代理伺服器快取的運作原理跟瀏覽器的運作原理差不多,只是規模更大。可以把它理解為一個共享快取,不只為一個使用者服務,一般為大量使用者提供服務,因此在減少相應時間和頻寬使用方面很有效,同一個副本會被重用多次。常見代理伺服器快取解決方案有Squid等,這裡不再詳述。
CDN快取
CDN(Content delivery networks)快取,也叫閘道器快取、反向代理快取。CDN快取一般是由網站管理員自己部署,為了讓他們的網站更容易擴充套件並獲得更好的效能。瀏覽器先向CDN閘道器發起Web請求,閘道器伺服器後面對應著一臺或多臺負載均衡源伺服器,會根據它們的負載請求,動態將請求轉發到合適的源伺服器上。雖然這種架構負載均衡源伺服器之間的快取沒法共享,但卻擁有更好的處擴充套件性。從瀏覽器角度來看,整個CDN就是一個源伺服器,從這個層面來說,本文討論瀏覽器和伺服器之間的快取機制,在這種架構下同樣適用。
瀏覽器端快取
瀏覽器快取根據一套與伺服器約定的規則進行工作,在同一個會話過程中會檢查一次並確定快取的副本足夠新。如果你瀏覽過程中,比如前進或後退,訪問到同一個圖片,這些圖片可以從瀏覽器快取中調出而即時顯現。
這裡解釋一下,HTML5時代所謂“瀏覽器”快取有兩部分:browser cache (瀏覽器快取)和app cache(HTML5的離線應用快取)後面會詳細介紹。
Web應用層快取
應用層快取指的是從程式碼層面上,通過程式碼邏輯和快取策略,實現對資料,頁面,圖片等資源的快取,可以根據實際情況選擇將資料存在檔案系統或者記憶體中,減少資料庫查詢或者讀寫瓶頸,提高響應效率。
二、瀏覽器快取與HTML5離線快取
瞭解了Web快取的組成,現在把重點放在專案中的HTML5快取機制上。我們都知道HTML5沒生出來以前,瀏覽器自身也是有快取機制的,所以我得搞清楚現在它和HTML5離線快取之間到底是如果呼叫和更新的。
快取清單
引用清單檔案
要啟用某個應用的應用快取,請在文件的
html
標記中新增 manifest 屬性:
<html manifest="example.appcache"> ... </html>
您應在要快取的網路應用的每個頁面上都新增 manifest
屬性。如果網頁不包含 manifest
屬性,瀏覽器就不會快取該網頁(除非清單檔案中明確列出了該屬性)。這就意味著使用者瀏覽的每個包含manifest
的網頁都會隱式新增到應用快取。因此,您無需在清單中列出每個網頁。
manifest
屬性可指向絕對網址或相對路徑,但絕對網址必須與相應的網路應用同源。
清單檔案可使用任何副檔名,但必須以正確的 MIME 型別提供
<html manifest="http://www.example.com/example.mf"> ... </html>
清單檔案必須以 text/cache-manifest
MIME 型別提供,且必須以UTF-8編碼。您可能需要向網路伺服器或 .htaccess
配置新增自定義檔案型別。
例如,要在 Apache 中提供此 MIME 型別,請在您的配置檔案中新增下面一行內容:(副檔名自定義)
AddType text/cache-manifest .appcache
另外,Web開發時,也可直接在web.xml中指定MIME型別:
<mime-mapping>
<extension>manifest</extension>
<mime-type>text/cache-manifest</mime-type>
</mime-mapping>
清單檔案結構
CACHE MANIFEST
# the above line is required
# this is a comment
# there can be as many of these anywhere in the file
# they are all ignored
# comments can have spaces before them
# but must be alone on the line
# blank lines are ignored too
# these are files that need to be cached they can either be listed
# first, or a "CACHE:" header could be put before them, as is done
# lower down.
images/sound-icon.png
images/background.png
# note that each file has to be put on its own line
# here is a file for the online whitelist -- it isn't cached, and
# references to this file will bypass the cache, always hitting the
# network (or trying to, if the user is offline).
NETWORK:
comm.cgi
# here is another set of files to cache, this time just the CSS file.
CACHE:
style/default.css
# static.html will be served if main.py is inaccessible# offline.jpg will be served in place of all images in images/large/# offline.html will be served in place of all other .html filesFALLBACK:/main.py /static.htmlimages/large/ images/offline.jpg*.html /offline.html
以“#”開頭的行是註釋行,但也可用於其他用途。應用快取只在其清單檔案發生更改時才會更新。例如,如果您修改了圖片資源或更改了 JavaScript 函式,這些更改不會重新快取。您必須修改清單檔案本身才能讓瀏覽器重新整理快取檔案。使用生成的版本號、檔案雜湊值或時間戳建立註釋行,可確保使用者獲得您的軟體的最新版。您還可以在出現新版本後,以程式設計方式更新快取
-
CACHE
: -
這是條目的預設部分。系統會在首次下載此標頭下列出的檔案(或緊跟在
CACHE MANIFEST
後的檔案)後顯式快取這些檔案。 -
NETWORK
: - 此部分下列出的檔案是需要連線到伺服器的白名單資源。無論使用者是否處於離線狀態,對這些資源的所有請求都會繞過快取。可使用萬用字元( 這個用法很講究)。
-
FALLBACK
: - 此部分是可選的,用於指定無法訪問資源時的後備網頁。其中第一個 URI 代表資源,第二個代表後備網頁。兩個 URI 必須相關,並且必須與清單檔案同源。可使用萬用字元。
請注意:這些部分可按任意順序排列,且每個部分均可在同一清單中重複出現。
請注意:系統會自動快取引用清單檔案的 HTML 檔案。因此您無需將其新增到清單中,但我們建議您這樣做。
請注意:HTTP 快取標頭以及對通過 SSL 提供的網頁設定的快取限制將被替換為快取清單。因此,通過 https 提供的網頁可實現離線執行。
這裡有一種特例需要解釋一下:
CACHE MANIFEST
FALLBACK:
/ /offline.html
NETWORK:
*
上面的程式碼定義了一個匹配所有的錯誤頁offline.html,離線訪問時,所有在同一域名下的頁面都會應用到它。同時這段程式碼也指定了白名單萬用字元狀態為 開啟(Open),意思是訪問其他域名下的資源不會被阻塞。
白名單萬用字元*,有兩種狀態(是否使用) Open 和 Blocking。
Open狀態意思是所有未在CACHE中宣告的URL都會被隱式認為屬於NETWORK;
Blocking狀態意思是所有未明確地在manifest檔案中宣告的URL都會被認為不可用(unavailable.),都將不能訪問。
使用了NETWORK的萬用字元“*”, 只要你有網路連線,任何不在應用程式快取中的資源將仍然從原網路地址下載,這對開放的應用程式非常重要。這意味著瀏覽器可以獲取各種資源,即使它們和你的應用程式不在同一個域名下。如果沒有此萬用字元,當你線上時,我們設想支援離線的應用將會行為詭異——它將不會載入任何不同域名下的資源。事件流
當用戶訪問一個聲明瞭manifest的頁面時,瀏覽器會嘗試獲取一份manifest檔案的拷貝,如果發現有更新,則下載該manifest檔案中宣告的所有資源並重新快取它們。
同時,這將觸發一系列事件,如下所示:
Event name | Interface | Fired when... | Next events |
---|---|---|---|
checking |
Event |
The user agent is checking for an update, or attempting to download the manifest for the first time.This is always the first event in the sequence. | noupdate ,downloading ,obsolete ,error |
noupdate |
Event |
The manifest hadn't changed. | Last event in sequence. |
downloading |
Event |
The user agent has found an update and is fetching it, or is downloading the resources listed by the manifest for the first time. | progress ,error ,cached ,updateready |
progress |
ProgressEvent |
The user agent is downloading resources listed by the manifest. The event object'stotal attribute returns the total number of files to be downloaded. The event object'sloaded attribute returns the number of files processed so far. |
progress ,error ,cached ,updateready |
cached |
Event |
The resources listed in the manifest have been downloaded, and the application is now cached. | Last event in sequence. |
updateready |
Event |
The resources listed in the manifest have been newly redownloaded, and the script can useswapCache() to switch to the new cache. |
Last event in sequence. |
obsolete |
Event |
The manifest was found to have become a 404 or 410 page, so the application cache is being deleted. | Last event in sequence. |
error |
Event |
The manifest was a 404 or 410 page, so the attempt to cache the application has been aborted. | Last event in sequence. |
The manifest hadn't changed, but the page referencing the manifest failed to download properly. | |||
A fatal error occurred while fetching the resources listed in the manifest. | |||
The manifest changed while the update was being run. | The user agent will try fetching the files again momentarily. |
更新機制
應用在離線後將保持快取狀態,除非發生以下某種情況:
- 使用者清除了瀏覽器對您網站的資料儲存。
- 清單檔案經過修改。請注意:更新清單中列出的某個檔案並不意味著瀏覽器會重新快取該資源。清單檔案本身必須進行更改。
- 應用快取通過程式設計方式進行更新。
快取狀態
window.applicationCache
物件是對瀏覽器的應用快取的程式設計訪問方式。其 status
屬性可用於檢視快取的當前狀態:
var appCache = window.applicationCache;
switch (appCache.status) {
case appCache.UNCACHED: // UNCACHED == 0
return 'UNCACHED';
break;
case appCache.IDLE: // IDLE == 1
return 'IDLE';
break;
case appCache.CHECKING: // CHECKING == 2
return 'CHECKING';
break;
case appCache.DOWNLOADING: // DOWNLOADING == 3
return 'DOWNLOADING';
break;
case appCache.UPDATEREADY: // UPDATEREADY == 4
return 'UPDATEREADY';
break;
case appCache.OBSOLETE: // OBSOLETE == 5
return 'OBSOLETE';
break;
default:
return 'UKNOWN CACHE STATUS';
break;
};
要以程式設計方式更新快取,請先呼叫 applicationCache.update()
。此操作將嘗試更新使用者的快取(前提是已更改清單檔案)。最後,當applicationCache.status
處於UPDATEREADY
狀態時,呼叫 applicationCache.swapCache()
即可將原快取換成新快取。
var appCache = window.applicationCache;
appCache.update(); // Attempt to update the user's cache.
...
if (appCache.status == window.applicationCache.UPDATEREADY) {
appCache.swapCache(); // The fetch was successful, swap in the new cache.
}
請注意:以這種方式使用 update()
和 swapCache()
不會向用戶提供更新的資源。此流程只是讓瀏覽器檢查是否有新的清單、下載指定的更新內容以及重新填充應用快取。因此,還需要對網頁進行兩次重新載入才能向用戶提供新的內容,其中第一次是獲得新的應用快取,第二次是重新整理網頁內容。
好訊息是,您可以避免重新載入兩次的麻煩。要使使用者更新到最新版網站,可設定監聽器,以監聽網頁載入時的 updateready
事件:
// Check if a new cache is available on page load.
window.addEventListener('load', function(e) {
window.applicationCache.addEventListener('updateready', function(e) {
if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
// Browser downloaded a new app cache.
// Swap it in and reload the page to get the new hotness.
window.applicationCache.swapCache();
if (confirm('A new version of this site is available. Load it?')) {
window.location.reload();
}
} else {
// Manifest didn't changed. Nothing new to server.
}
}, false);
}, false);
更新流程示意圖
它們各自的更新機制如下:
Browser cache
App cache
其中browser cache的機制大家都很清楚了, 其中離線應用的更新是: 除了第一次訪問是直接拉取server的, 然後後臺更新app cache之外, 其餘的情況都是直接訪問app cache. 因此, 要如果離線應用的程式碼更新了, 只有下次開啟或者重新整理才會生效.。
三、瀏覽器快取的控制
使用HTML Meta 標籤
Web開發者可以在HTML頁面的<head>節點中加入<meta>標籤,程式碼如下:
<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
上述程式碼的作用是告訴瀏覽器當前頁面不被快取,每次訪問都需要去伺服器拉取。使用上很簡單,但只有部分瀏覽器可以支援,而且所有快取代理伺服器都不支援,因為代理不解析HTML內容本身。
可以通過這個頁面測試你的瀏覽器是否支援:Pragma No-Cache Test 。
使用快取有關的HTTP訊息報頭
一個URI的完整HTTP協議互動過程是由HTTP請求和HTTP響應組成的。有關HTTP詳細內容可參考《Hypertext Transfer Protocol — HTTP/1.1》、《HTTP協議詳解》等。
在HTTP請求和響應的訊息報頭中,常見的與快取有關的訊息報頭有:
Cache-Control與Expires
Cache-Control與Expires的作用一致,都是指明當前資源的有效期,控制瀏覽器是否直接從瀏覽器快取取資料還是重新發請求到伺服器取資料。只不過Cache-Control的選擇更多,設定更細緻,如果同時設定的話,其優先順序高於Expires。
Last-Modified/ETag與Cache-Control/Expires
配置Last-Modified/ETag的情況下,瀏覽器再次訪問統一URI的資源,還是會發送請求到伺服器詢問檔案是否已經修改,如果沒有,伺服器會只發送一個304回給瀏覽器,告訴瀏覽器直接從自己本地的快取取資料;如果修改過那就整個資料重新發給瀏覽器;
Cache-Control/Expires則不同,如果檢測到本地的快取還是有效的時間範圍內,瀏覽器直接使用本地副本,不會發送任何請求。兩者一起使用時,Cache-Control/Expires的優先順序要高於Last-Modified/ETag。即當本地副本根據Cache-Control/Expires發現還在有效期內時,則不會再次傳送請求去伺服器詢問修改時間(Last-Modified)或實體標識(Etag)了。
一般情況下,使用Cache-Control/Expires會配合Last-Modified/ETag一起使用,因為即使伺服器設定快取時間, 當用戶點選“重新整理”按鈕時,瀏覽器會忽略快取繼續向伺服器傳送請求,這時Last-Modified/ETag將能夠很好利用304,從而減少響應開銷。
Last-Modified與ETag
你可能會覺得使用Last-Modified已經足以讓瀏覽器知道本地的快取副本是否足夠新,為什麼還需要Etag(實體標識)呢?HTTP1.1中Etag的出現主要是為了解決幾個Last-Modified比較難解決的問題:
- Last-Modified標註的最後修改只能精確到秒級,如果某些檔案在1秒鐘以內,被修改多次的話,它將不能準確標註檔案的新鮮度
- 如果某些檔案會被定期生成,當有時內容並沒有任何變化,但Last-Modified卻改變了,導致檔案沒法使用快取
- 有可能存在伺服器沒有準確獲取檔案修改時間,或者與代理伺服器時間不一致等情形
Etag是伺服器自動生成或者由開發者生成的對應資源在伺服器端的唯一識別符號,能夠更加準確的控制快取。Last-Modified與ETag是可以一起使用的,伺服器會優先驗證ETag,一致的情況下,才會繼續比對Last-Modified,最後才決定是否返回304。Etag的伺服器生成規則和強弱Etag的相關內容可以參考,《互動百科-Etag》和《HTTP Header definition》,這裡不再深入。
使用者操作行為與快取
使用者在使用瀏覽器的時候,會有各種操作,比如輸入地址後回車,按F5重新整理等,這些行為會對快取有什麼影響呢?
通過上表我們可以看到,當用戶在按F5進行重新整理的時候,會忽略Expires/Cache-Control的設定,會再次傳送請求去伺服器請求,而Last-Modified/Etag還是有效的,伺服器會根據情況判斷返回304還是200;而當用戶使用Ctrl+F5進行強制重新整理的時候,只是所有的快取機制都將失效,重新從伺服器拉去資源。
相關有趣的分享:
《瀏覽器快取機制》:不同瀏覽器對使用者操作行為處理比較 (強烈推薦此文)
《HTTP 304客戶端快取優化的神奇作用和用法》:強行在程式碼層面比對檔案的Last-Modified時間,保證使用者使用Ctrl+F5進行重新整理的時候也能正常返回304
四、為什麼有時候你的快取不能如你所願及時更新
這段標紅,因為這個才是我全文的重點。
以上分析了瀏覽器快取和HTML5離線快取各自的更新機制。那麼,當我們同時使用了瀏覽器快取和HTML5的離線快取,結果會是怎樣的呢?看圖:
看得出來,瀏覽器自身的快取對離線快取的更新有干擾,如果你的資源曾經被cache過,那麼只有你的伺服器manifest有更新且在瀏覽器自身快取過期的情況下,伺服器才會真正去伺服器重新下載資源。不過這裡要知道的是,如果使用者強制重新整理(Ctrl+F5),那瀏覽器自身的快取就失效被跳過了,瀏覽器會直接對比本地的manifest並判斷是否有更新來決定是否傳送請求到伺服器重新下載資源。
過程如下:
1. 通過標準的HTTP語義,你的瀏覽器將會檢測快取名單是否已經過期。就像任何其他由HTTP服務的檔案,你的網路伺服器將會包含典型的關於此檔案在HTTP響應頭中的元資訊。這些HTTP頭中的一些(Expires和Cache-Control)將告訴你的瀏覽器如何允許快取檔案而不詢問伺服器此檔案是否已更改。此種類型的快取和離線網路應用程式沒有任何關係。它發生在幾乎每個HTML頁面,樣式表,圖片或者其他網路資源。
2. 如果快取名單已過期(根據它的HTTP頭),那麼你的瀏覽器將會詢問伺服器是否有新版本,如果有,瀏覽器將會下載它。要做到這一點,你的瀏覽器產生一個包含此快取名單last-modified資料的HTTP請求,你的網路伺服器將瀏覽器下載名單檔案的最後時間包含在HTTP響應頭中。如果網路伺服器判斷從那個時間之後沒有被更改,它將簡單的返回一個304(未改變)狀態。同樣的,這不是離線網路應用程式所特有的。它發生於實質上每種型別的網路資源。
3. 如果網路伺服器認為名單檔案在那個時間之後有被更改,它將返回一個200(OK)HTTP狀態碼,後面是新檔案的內容和新的Cache-Control頭,以及一個新的last-modified時間,因此,第1步和第2步將可能在下次發生。(HTTP非常酷,網路伺服器總是為將來做打算。如果你的網路伺服器絕對需要給你傳送一個檔案,他盡所有可能確認他不需要無故傳送第二次。)一旦下載了新的快取名單檔案,你的瀏覽器將根據它上次下載的副本檢測內容。如果快取名單檔案的內容跟上次的一樣,你的瀏覽器將不會重新下載此名單中列出的任何資源。
當你開發和測試你的離線網路應用程式時,這些步驟的任意一個都可能讓你犯錯誤。例如,比如說你釋出了一個新版本的快取名單檔案,10分鐘後,你發現你需要在裡面新增另一個資源。沒問題,對吧?僅僅新增另一行並重新發布。這是將要發生的事情:你重新載入頁面,你的瀏覽器發現了manifest屬性,它觸發了checking事件,然後…沒啥了。你的瀏覽器堅持認為快取名單檔案並沒有被更改。為啥?因為你的網路伺服器可能由於預設配置,告訴瀏覽器需要快取靜態檔案幾個小時(通過HTTP語義,使用Cache-Control頭)。這意味著你的瀏覽器將不會通過三相程序中的第1步。當然,網路伺服器知道檔案已經更改,但你的瀏覽器甚至不會有足夠的時間去詢問網路伺服器。為啥?因為你的瀏覽器上次下載了快取名單,網路伺服器告訴它需要快取這個資源幾個小時(通過HTTP語義,使用Cache-Control頭)。那麼10分鐘後,這就是你的瀏覽器確切會做的事情。
你必須清楚,這不是錯誤,而只是個特性。一切都按照它應該的方式工作著。如果網路伺服器沒有辦法告訴瀏覽器(和中間代理)去快取資源,網路可能很快崩潰。但沒人來安慰你花上幾個小時去嘗試想出為啥你的瀏覽器沒有注意到你更新過的快取名單。(實際上,如果你等待得足夠久,它將神祕重新開始工作!因為HTTP快取過期了!就像它應該的那樣!)
所以,這兒有一件你必須要做的事情:重新配置你的網路伺服器,以便你的快取名單檔案不會因為HTTP語義而可快取。如果你使用基於Apache的網路伺服器,你.htaccess檔案中的這兩行將會達到目的:
ExpiresActive On
ExpiresDefault “access”
這將會使每一個在此目錄和所有其子目錄中的檔案快取失效。這可能是你在製作中所不希望的,所以你應該使用一個<Files>指令來限制它,以致其只對你的快取名單檔案有效,或者建立一個只包含這個.htaccess檔案和快取名單檔案的子目錄。通常,配置細節隨網路伺服器而變化,所以查閱你的伺服器說明文件來確認如何控制HTTP快取頭。
一旦你使快取名單檔案本身的HTTP快取失效,你將仍然看到過時的卻已在應用程式快取中更改的某個資源,只因為它仍然以相同的URL存在於你的網路伺服器。這裡,三相程序中的第2步將會欺騙你。如果你的快取名單檔案沒有被更改,瀏覽器將不會注意到之前快取的某個資源已被更改。五、哪些請求不能被快取?
無法被瀏覽器快取的請求:
- HTTP資訊頭中包含Cache-Control:no-cache,pragma:no-cache,或Cache-Control:max-age=0等告訴瀏覽器不用快取的請求
- 需要根據Cookie,認證資訊等決定輸入內容的動態請求是不能被快取的
- 經過HTTPS安全加密的請求(有人也經過測試發現,ie其實在頭部加入Cache-Control:max-age資訊,firefox在頭部加入Cache-Control:Public之後,能夠對HTTPS的資源進行快取,參考《HTTPS的七個誤解》)
- POST請求無法被快取
- HTTP響應頭中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的請求無法被快取
六、CDN快取
一般情況下瀏覽器快取和HTML5離線快取已經可以為你省一大筆網路開銷了,但大型網路應用還會用到CDN(比如我們的專案)。
凡是專案中應用到的所有資源JS,CSS,image,audio我們都直接去訪問CDN,不是其背後的伺服器。在使用者第一次訪問頁面時,會直接產生瀏覽器快取與CDN伺服器快取兩個拷貝,它們同時存在且URI都是一樣的。噁心的地方來了,如果你不想辦法讓CDN快取在需要更新時失效,那即使瀏覽器快取過期,瀏覽器讀取的還會是老資料,它來自於CDN快取。所以這個時候如果你的資源沒更新,要同時檢查三個地方的快取是否都正常更新。
好在專案中我們用路徑中加入隨機碼的機制來使CDN快取在需要更新時失效(http://cdn/path/to/xxxxx/the/resource.png),該隨機碼並非真隨機,是由各個資源的MD5編碼計算來的。資源如有更新,MD5值同時更新,這樣在manifest中的資源路徑就和上一次完全不同了,所以一旦瀏覽器快取過期,訪問CDN時,CDN找不到相應路徑,就會去伺服器去拿同時快取在其本地, 以後再訪問就從快取中拿。