1. 程式人生 > >web緩沖機制

web緩沖機制

子目錄 就會 rfc 獲得 時間不一致 由於 緩存 script 決定

最近項目裏用到了Html5緩存機制,於是很想搞清楚 瀏覽器緩存,HTML5離線緩存,還有項目中用到的 CDN緩存 這三部分的關系以及更新機制。看了一堆關於HTML5緩存機制的文章,各有所長,各有疏漏。因此本人想在此做一總結,本文假設讀者對基本的HTML5緩存應用已有所了解,因此不再詳述概念,可以將本文當做釋疑匯總吧。

以下部分內容引用自網絡。

一、Web緩存的類型

在Web應用領域,Web緩存大致可以分為以下幾種類型:

數據庫數據緩存

Web應用,特別是SNS類型的應用,往往關系比較復雜,數據庫表繁多,如果頻繁進行數據庫查詢,很容易導致數據庫不堪重荷。為了提供查詢的性能,會將查詢後的數據放到內存中進行緩存,下次查詢時,直接從內存緩存直接返回,提供響應效率。比如常用的緩存方案有memcached等。

服務器端緩存

代理服務器緩存

代理服務器是瀏覽器和源服務器之間的中間服務器,瀏覽器先向這個中間服務器發起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 beforethem

# 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 isdone

# 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 willbe served in place of all images in images/large/# offline.html will be servedin 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.

這些事件都是可取消的,它們的目的是讓瀏覽器顯示下載進度信息。但如果你要顯示自己的更新UI,那麽取消這些事件會避免顯示冗余的用戶進度信息。

更新機制

應用在離線後將保持緩存狀態,除非發生以下某種情況:

1. 用戶清除了瀏覽器對您網站的數據存儲。

2. 清單文件經過修改。請註意:更新清單中列出的某個文件並不意味著瀏覽器會重新緩存該資源。清單文件本身必須進行更改。

3. 應用緩存通過編程方式進行更新。

緩存狀態

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 newcache.

}

請註意:以這種方式使用 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 newapp cache.

// Swap it in and reload thepage to get the new hotness.

window.applicationCache.swapCache();

if (confirm(‘A new version ofthis 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比較難解決的問題:

1. Last-Modified標註的最後修改只能精確到秒級,如果某些文件在1秒鐘以內,被修改多次的話,它將不能準確標註文件的新鮮度

2. 如果某些文件會被定期生成,當有時內容並沒有任何變化,但Last-Modified卻改變了,導致文件沒法使用緩存

3. 有可能存在服務器沒有準確獲取文件修改時間,或者與代理服務器時間不一致等情形

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進行強制刷新的時候,只是所有的緩存機制都將失效,重新從服務器拉去資源。

相關有趣的分享:

《瀏覽器緩存機制》:不同瀏覽器對用戶操作行為處理比較 (強烈推薦此文)

《HTTP304客戶端緩存優化的神奇作用和用法》:強行在代碼層面比對文件的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步將會欺騙你。如果你的緩存名單文件沒有被更改,瀏覽器將不會註意到之前緩存的某個資源已被更改。

五、哪些請求不能被緩存?

無法被瀏覽器緩存的請求:

1. HTTP信息頭中包含Cache-Control:no-cache,pragma:no-cache,或Cache-Control:max-age=0等告訴瀏覽器不用緩存的請求

2. 需要根據Cookie,認證信息等決定輸入內容的動態請求是不能被緩存的

3. 經過HTTPS安全加密的請求(有人也經過測試發現,ie其實在頭部加入Cache-Control:max-age信息,firefox在頭部加入Cache-Control:Public之後,能夠對HTTPS的資源進行緩存,參考《HTTPS的七個誤解》)

4. POST請求無法被緩存

5. 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找不到相應路徑,就會去服務器去拿同時緩存在其本地, 以後再訪問就從緩存中拿。

web緩沖機制