緩存詳解

緩存詳解

前言

總括: 緩存從來都是前端的一個痛點,很多前端搞不清楚緩存到底是何物,從而給自己創造了一些麻煩,本文一如既往的用通俗易懂的文字和實例來講述緩存,希望能讓您有所得。

正文

緩存是一種保存資源副本並在下次請求時直接使用該副本的技術。

說實話,我起始真的不知道怎麼去介紹緩存,所以引用了上面相對官方的定義。我想幾乎每個開發者都碰到過緩存的問題吧,甚至有很多情況下我們會說這個問題已經修復了,你清理下緩存就好了。這篇文章我們就細細的來挖掘下緩存的種種軼事。

🦋緩存的種類

很多開發者習慣把cookie、webStorage以及IndexedDB存儲的數據也稱之爲緩存,理由是都是保存在客戶端的數據,沒有什麼區別。其實這是不嚴謹的,cookie的存在更多的是爲了讓服務端區別用戶,webStorage和IndexedDB則更多用在保存具體的數據和在客戶端存儲大量結構化數據(文件/blobs)上面。

實際上所謂的緩存只有一種——它是請求資源的副本。試想一下,如果每一個資源我們客戶端都會保存一份副本,這會怎麼樣?客戶端會炸掉,開發者會瘋掉!所以我們需要一份協議來處理緩存,可以讓開發者控制緩存的建立和刪除。誰呢?還能有誰,HTTP唄。HTTP協議裏定義了很多關於緩存的請求和響應字段,這也是接下來我們重點要逼逼叨的對象,研究下究竟是哪些字段怎麼影響緩存的。

納尼?你問我爲什麼要緩存?😱

那就太容易說道了🤣,緩存好處有很多:

  1. 緩解服務器壓力(不用每次去請求資源);
  2. 提升性能(打開本地資源速度當然比請求回來再打開要快得多);
  3. 減少帶寬消耗(我相信你可以理解);

🤦‍♀️那麼問題又來了,既然緩存這麼好,如果我請求的服務器中間有代理也緩存了怎麼辦?代理服務器緩存了我的資源導致我沒法從源服務器拿到最新的資源怎麼辦?HTTP當然也想到了這塊的訴求。接下來我們也會逐層剖析。

🍉緩存在宏觀上可以分成兩類:私有緩存共享緩存。共享緩存就是那些能被各級代理緩存的緩存(咋覺得有點繞)。私有緩存就是用戶專享的,各級代理不能緩存的緩存。

🐜微觀上可以分下面三類:

1. 瀏覽器緩存

我相信只要你經常使用某個瀏覽器🌎(Chrome,Firefox,IE等),肯定知道這些瀏覽器在設置裏面都是有個清除緩存功能,這個功能存在的作用就是刪除存儲在你本地磁盤上資源副本,也就是清除緩存。

緩存存在的意義就是當用戶點擊back按鈕或是再次去訪問某個頁面的時候能夠更快的響應。尤其是在多頁應用的網站中,如果你在多個頁面使用了一張相同的圖片,那麼緩存這張圖片就變得特別的有用。😏

2. 代理服務器緩存

代理服務器緩存原理和瀏覽器端類似,但規模要大得多,因爲是爲成千上萬的用戶提供緩存機制,大公司和大型的ISP提供商通常會將它們設立在防火牆上或是作爲一個獨立的設備來運營。(下文如果沒有特殊說明,所有提到的緩存服務器都是指代理服務器。)

由於緩存服務器不是客戶端或是源服務器的一部分,它們存在於網絡中,請求路由必須經過它們纔會生效,所以實際上你可以去手動設置瀏覽器的代理,或是通過一箇中間服務器來進行轉發,這樣用戶自然就察覺不到代理服務器的存在了。🤥

代理服務器緩存就是一個共享緩存,不只爲一個用戶服務,經常爲大量用戶使用,因此在減少相應時間和帶寬使用方面很有效:因爲同一個緩存可能會被重用多次。

3. 網關緩存

也被稱爲代理緩存或反向代理緩存,網關也是一箇中間服務器,網關緩存一般是網站管理員自己部署,從讓網站擁有更好的性能。🙂

CDNS(網絡內容分發商)分佈網關緩存到整個(或部分)互聯網上,並出售緩存服務給需要的網站,比如國內的七牛雲、又拍雲都有這種服務。

4. 數據庫緩存

數據庫緩存是指當我們的應用極其複雜,表自然也很繁雜,我們必須進行頻繁的進行數據庫查詢,這樣可能導致數據庫不堪重負,一個好的辦法就是將查詢後的數據放到內存中,下一次查詢直接從內存中取就好了。關於數據庫緩存本篇不會展開。🙃

🦄瀏覽器的緩存策略

緩存的目標:

  • 一個檢索請求的成功響應: 對於 GET請求,響應狀態碼爲:200,則表示爲成功。一個包含例如HTML文檔,圖片,或者文件的響應;
  • 不變的重定向: 響應狀態碼:301;
  • 可用緩存響應:響應狀態碼:304,這個存在疑問,Chrome會緩存304中的緩存設置,Firefox;
  • 錯誤響應: 響應狀態碼:404 的一個頁面;
  • 不完全的響應: 響應狀態碼 206,只返回局部的信息;
  • 除了 GET 請求外,如果匹配到作爲一個已被定義的cache鍵名的響應;

以上,對於我們可以和應該緩存的目標有個瞭解。🤗

瀏覽器對於緩存的處理是根據第一次請求資源時返回的響應頭來確定的。

那麼瀏覽器怎麼確定一個資源該不該緩存,如何去緩存呢❓響應頭!響應頭!響應頭!重要的事情說三遍。✌️

我們看🌰:

Age:23146
Cache-Control:max-age=2592000
Date:Tue, 28 Nov 2017 12:26:41 GMT
ETag:W/<span class="hljs-string">"5a1cf09a-63c6"</span>
Expires:Thu, 28 Dec 2017 05:27:45 GMT
Last-Modified:Tue, 28 Nov 2017 05:14:02 GMT
Vary:Accept-Encoding

1. 強緩存階段

以上請求頭來自百度首頁某個CSS文件的響應頭。我去除了一些和緩存無關的字段,只保留了以上部分。我們來分析下,Expires是HTTP/1.0中的定義緩存的字段,它規定了緩存過期的一個絕對時間。Cache-Control:max-age=2592000是HTTP/1.1定義的關於緩存的字段,它規定了緩存過期的一個相對時間。優先級上當然是版本高的優先了,max-age > Expires

這就是強緩存階段,當瀏覽器再次試圖訪問這個CSS文件,發現有這個文件的緩存,那麼就判斷根據上一次的響應判斷是否過期,如果沒過期,使用緩存。加載文件,OVER!✌️

Firefox瀏覽器表現爲一個灰色的200狀態碼。

Chrome瀏覽器狀態碼表現爲:

200 (from disk cache)或是200 OK (from memory cache)

**多說一點:**關於緩存是從磁盤中獲取還是從內存中獲取,查找了很多資料,得出了一個較爲可信的結論:Chrome會根據本地內存的使用率來決定緩存存放在哪,如果內存使用率很高,放在磁盤裏面,內存的使用率很高會暫時放在內存裏面。這就可以比較合理的解釋了爲什麼同一個資源有時是from memory cache有時是from disk cache的問題了。

那麼當這個CSS文件過期了怎麼辦?ETagLast-Modified就該閃亮登場了。

先說Last-Modified,這個字段是文件最後一次修改的時間;

ETag呢?ETag是對文件的一個標記,嗯,可以這麼說,具體生成方式HTTP並沒有給出一個明確的方式,所以理論上只要不會重複生成方式無所謂,比如對資源內容使用抗碰撞散列函數,使用最近修改的時間戳的哈希值,甚至只是一個版本號。

2. 協商緩存階段

利用這兩個字段瀏覽器可以進入協商緩存階段,當瀏覽器再次試圖訪問這個CSS文件,發現緩存過期,於是會在本次請求的請求頭裏攜帶If-Moified-SinceIf-None-Match這兩個字段,服務器通過這兩個字段來判斷資源是否有修改,如果有修改則返回狀態碼200和新的內容,如果沒有修改返回狀態碼304,瀏覽器收到200狀態碼,該咋處理就咋處理(相當於首次訪問這個文件了),發現返回304,於是知道了本地緩存雖然過期但仍然可以用,於是加載本地緩存。然後根據新的返回的響應頭來設置緩存。(這一步有所差異,發現不同瀏覽器的處理是不同的,chrome會爲304設置緩存,firefox則不會)😑

具體兩個字段攜帶的內容如下(分別和上面的Last-ModifiedETag攜帶的值對應):

If-Moified-Since: Tue, 28 Nov 2017 05:14:02 GMT
If-None-Match: W/"5a1cf09a-63c6"

到這協商緩存結束。

3. 啓發式緩存階段

我們把上面的響應頭改下:

Age:23146
Cache-Control: public
Date:Tue, 28 Nov 2017 12:26:41 GMT
Last-Modified:Tue, 28 Nov 2017 05:14:02 GMT
Vary:Accept-Encoding

發現沒?瀏覽器用來確定緩存過期時間的字段一個都沒有!那該怎麼辦?有人可能會說下次請求直接進入協商緩存階段,攜帶If-Moified-Since唄,不是的,瀏覽器還有個啓發式緩存階段😎

根據響應頭中2個時間字段 Date 和 Last-Modified 之間的時間差值,取其值的10%作爲緩存時間週期。

這就是啓發式緩存階段。這個階段很容讓人忽視,但實際上每時每刻都在發揮着作用。所以在今後的開發過程中如果遇到那種默認緩存的坑,不要叫囂,不要生氣,瀏覽器只是在遵循啓發式緩存協議而已。

我畫了下面這張圖,來解釋瀏覽器整個緩存策略的過程:

緩存

👌對於緩存策略介紹到這,接下來再細細分析不同的HTTP首部字段的內容,以及它們之間的關係。

🦀HTTP中和緩存相關的首部字段

HTTP報文是什麼呢?就是HTTP報文,這是一個概念,主要由以下兩部分構成:

  1. 首部(header):包含了很多字段,比如:cookie、緩存、報文大小、報文格式等等);
  2. 主體(body):HTTP請求真正要傳輸的部分,比如:一個HTML文檔,一個js文件;

以上我們知道瀏覽器對於緩存的處理過程,也簡單的提到了幾個相關的字段。🤧接下來我們具體看下這幾個字段:

1. 通用首部字段

字段名稱 說明
Cache-Control 控制緩存具體的行爲
Pragma HTTP1.0時的遺留字段,當值爲"no-cache"時強制驗證緩存
Date 創建報文的日期時間(啓發式緩存階段會用到這個字段)

2. 響應首部字段

字段名稱 說明
ETag 服務器生成資源的唯一標識
Vary 代理服務器緩存的管理信息
Age 資源在緩存代理中存貯的時長(取決於max-age和s-maxage的大小)

3. 請求首部字段

字段名稱 說明
If-Match 條件請求,攜帶上一次請求中資源的ETag,服務器根據這個字段判斷文件是否有新的修改
If-None-Match 和If-Match作用相反,服務器根據這個字段判斷文件是否有新的修改
If-Modified-Since 比較資源前後兩次訪問最後的修改時間是否一致
If-Unmodified-Since 比較資源前後兩次訪問最後的修改時間是否一致

4. 實體首部字段

字段名稱 說明
Expires 告知客戶端資源緩存失效的絕對時間
Last-Modified 資源最後一次修改的時間

🦅瀏覽器緩存控制

HTTP/1.1一共規範了47種首部字段,而和緩存相關的就有以上12個之多。接下來的兩個小節會一個一個介紹給大家。🤓

1. Cache-Control

通過cache-control的指令可以控制告訴客戶端或是服務器如何處理緩存。這也是11個字段中指令最多的一個,我們先來看看請求指令

指令 參數 說明
no-cache 強制源服務器再次驗證
no-store 不緩存請求或是響應的任何內容
max-age=[秒] 緩存時長,單位是秒 緩存的時長,也是響應的最大的Age值
min-fresh=[秒] 必需 期望在指定時間內響應仍然有效
no-transform 代理不可更改媒體類型
only-if-cached 從緩存獲取
cache-extension - 新的指令標記(token)

響應指令

指令 參數 說明
public 任意一方都能緩存該資源(客戶端、代理服務器等)
private 可省略 只能特定用戶緩存該資源
no-cache 可省略 緩存前必須先確認其有效性
no-store 不緩存請求或響應的任何內容
no-transform 代理不可更改媒體類型
must-revalidate 可緩存但必須再向源服務器進確認
proxy-revalidate 要求中間緩存服務器對緩存的響應有效性再進行確認
max-age=[秒] 緩存時長,單位是秒 緩存的時長,也是響應的最大的Age值
s-maxage=[秒] 必需 公共緩存服務器響應的最大Age值
cache-extension - 新指令標記(token

請注意no-cache指令很多人誤以爲是不緩存,這是不準確的,no-cache的意思是可以緩存,但每次用應該去想服務器驗證緩存是否可用。no-store纔是不緩存內容。另外部分指令也可以組合使用,比如:

Cache-Control: max-age=100, must-revalidate, public

上面指令的意思是緩存的有效時間爲100秒,之後訪問需要向源服務器發送請求驗證,此緩存可被代理服務器和客戶端緩存。

2. Pragma

這是HTTP/1.0裏面的一個字段,但優先級很高,測試發現,Chrome和Firefox中Pragma的優先級高於Cache-Control和Expires,爲了向下兼容,這個字段依然發揮着它的作用。🤔一般可能我們會這麼用:

<meta http-equiv="Pragma" content="no-cache">

Pragma屬於通用首部字段,在客戶端上使用時,常規要求我們往html上加上上面這段meta元標籤(而且可能還得做些hack放到body後面去

事實上這種禁用緩存的形式用處很有限:

  1. 僅有IE才能識別這段meta標籤含義,其它主流瀏覽器僅能識別Cache-Control: no-store的meta標籤(見出處)
  2. 在IE中識別到該meta標籤含義,並不一定會在請求字段加上Pragma,但的確會讓當前頁面每次都發新請求(僅限頁面,頁面上的資源則不受影響)。——淺談瀏覽器http的緩存機制

讀者可以自行拷貝後面模擬服務端決策的代碼進行測試。

服務端響應添加'Pragma': 'no-cache',瀏覽器表現行爲和強制刷新類似。

3. Expires

這又是一個HTTP/1.0的字段,上面也說過了定義的是緩存到期的絕對時間。

同樣,我們也可以在html文件裏直接使用:

<meta http-equiv="expires" content="Thu, 30 Nov 2017 11:17:26 GMT">

如果設置的是已經過去的時間會怎樣呢?YES!!!則刷新頁面會重新發送請求。

**Pragma禁用緩存,如果又給Expires定義一個還未到期的時間,那麼Pragma字段的優先級會更高。**🤖

🤖Expires有一個很大的弊端,就是它返回的是服務器的時間,但判斷的時候用的卻是客戶端的時間,這就導致Expires很被動,因爲用戶有可能改變客戶端的時間,導致緩存時間判斷出錯,這也是引入Cache-Control:max-age指令的原因之一。

4. Last-Midified

接下來這幾個字段都是校驗字段,或者說是在協商緩存階段發揮作用的字段。第一個就是Last-modified,這個字段不光協商緩存起作用,在啓發式緩存階段同樣起到至關重要的作用。

在瀏覽器第一次請求某一個URL時,服務器端的返回狀態碼會是200,響應的實體內容是客戶端請求的資源,同時有一個Last-Modified的屬性標記此文件在服務器端最後被修改的時間。like this:

Last-Modified : Fri , 12 May 2006 18:53:33 GMT
If-Modified-Since

當瀏覽器第二次請求這個URL的時候,根據HTTP協議規定,瀏覽器會把第一次Last-Modified的值存儲在If-Modified-Since裏面發送給服務端來驗證資源有沒有修改。like this:

If-Modified-Since : Fri , 12 May 2006 18:53:33 GMT

服務端通過If-Modified-Since字段來判斷在這兩次訪問期間資源有沒有被修改過,從而決定是否返回完整的資源。如果有修改正常返回資源,狀態碼200,如果沒有修改只返回響應頭,狀態碼304,告知瀏覽器資源的本地緩存還可用。

用途:

  • 驗證本地緩存是否可用
If-Unmodified-Since

這個字段字面意思和If-Modified-Since相反,但處理方式並不是相反的。如果文件在兩次訪問期間沒有被修改則返回200和資源,如果文件修改了則返回狀態碼412(預處理錯誤)。

用途:

  • 與含有 If-Range消息頭的範圍請求搭配使用,實現斷點續傳的功能,即如果資源沒修改繼續下載,如果資源修改了,續傳的意義就沒有了。
  • POST、PUT請求中,優化併發控制,即當多用戶編輯用一份文檔的時候,如果服務器的資源已經被修改,那麼在對其作出編輯會被拒絕提交。

😈Last-Modified有幾個缺點:沒法準確的判斷資源是否真的修改了,比如某個文件在1秒內頻繁更改了多次,根據Last-Modified的時間(單位是秒)是判斷不出來的,再比如,某個資源只是修改了,但實際內容並沒有發生變化,Last-Modified也無法判斷出來,因此在HTTP/1.1中還推出了ETag這個字段👇

5. ETag

服務器可以通過某種自定的算法對資源生成一個唯一的標識(比如md5標識),然後在瀏覽器第一次請求某一個URL時把這個標識放到響應頭傳到客戶端。服務器端的返回狀態會是200。

ETag: abc-123456

ETag的值有可能包含一個 W/ 前綴,來提示應該採用弱比較算法(這個是畫蛇添足,因爲 If-None-Match 用且僅用這一算法)。🙄

If-None-Match

If-None-Match和If-Modified-Since同時存在的時候If-None-Match優先級更高。

當瀏覽器第二次請求這個URL的時候,根據HTTP協議規定,瀏覽器回把第一次ETag的值存儲在If-None-Match裏面發送給服務端來驗證資源有沒有修改。like this:

If-None-Match: abc-123456

Get請求中,當且僅當服務器上沒有任何資源的ETag屬性值與這個首部中列出的相匹配的時候,服務器端會才返回所請求的資源,響應碼爲200。如果沒有資源的ETag值相匹配,那麼返回304狀態碼。

POST、PUT等請求改變文件的請求,如果沒有資源的ETag值相匹配,那麼返回412狀態碼。

If-Match

在請求方法爲 GET) 和 HEAD的情況下,服務器僅在請求的資源滿足此首部列出的 ETag之一時纔會返回資源。而對於 PUT或其他非安全方法來說,只有在滿足條件的情況下纔可以將資源上傳。

用途:

  • For GET和 HEAD 方法,搭配 Range首部使用,可以用來保證新請求的範圍與之前請求的範圍是對同一份資源的請求。如果 ETag 無法匹配,那麼需要返回 416(範圍請求無法滿足) 響應。
  • 對於其他方法來說,尤其是 PUT, If-Match 首部可以用來避免更新丟失問題。它可以用來檢測用戶想要上傳的不會覆蓋獲取原始資源之後做出的更新。如果請求的條件不滿足,那麼需要返回412(預處理錯誤) 響應。

當然和Last-Modified相比,ETag也有自己的缺點,比如由於需要對資源進行生成標識,性能方面就勢必有所犧牲。😕

關於強校驗和弱校驗:

ETag 1 ETag 2 Strong Comparison Weak Comparison
W/"1" W/"1" no match match
W/"1" W/"2" no match no match
W/"1" "1" no match match
"1" "1" match match

🐝服務端緩存控制

ExpiresCache-Control:max-age=xxx同時存在的時候取決於緩存服務器應用的HTTP版本。應用HTTP/1.1版本的服務器會優先處理max-age,忽略Expires,而應用HTTP/1.0版本的緩存服務器則會優先處理Expires而忽略max-age。接下來看下和緩存服務器相關的兩個字段。

6. Vary

Vary用來做什麼的呢?試想這麼一個場景:在某個網頁中網站提供給移動端的內容是不同的,怎麼讓緩存服務器區分移動端和PC端呢?不知道你是否注意,瀏覽器在每次請求都會攜帶UA字段來表明來源,所以我們可以利用User-Agent字段來區分不同的客戶端,用法如下:

Vary: User-Agent

再比如,源服務器啓用了gzip壓縮,但用戶使用了比較舊的瀏覽器,不支持壓縮,緩存服務器如何返回?就可以這麼設定:

Vary: Accept-Encoding

當然,也可以這麼用:

Vary: User-Agent, Accept-Encoding

這意味着緩存服務器會以User-AgentAccept-Encoding兩個請求首部字段來區分緩存版本。根據請求頭裏的這兩個字段來決定返回給客戶端什麼內容。

7. Age

這個字段說的是資源在緩存服務器存在的時長,前面也說了Cache-Control: max-age=[秒]就是Age的最大值。

這個字段存在的意義是什麼呢?用來區分請求的資源來自源服務器還是緩存服務器的緩存的。

🤧但得結合另一個字段來進行判斷,就是Date,Date是報文創建的時間。

Date

如果按F5頻繁刷新發現響應裏的Date沒有改變,就說明命中了緩存服務器的緩存以下面的一個響應爲🍐:

Accept-Ranges: bytes
Age: 1016859
Cache-Control: max-age=2592000
Content-Length: 14119
Content-Type: image/png
Date: Fri, 01 Dec 2017 12:27:25 GMT
ETag: "5912bfd0-3727"
Expires: Tue, 19 Dec 2017 17:59:46 GMT
Last-Modified: Wed, 10 May 2017 07:22:56 GMT
Ohc-Response-Time: 1 0 0 0 0 0
Server: bfe/1.0.8.13-sslpool-patch

如上圖來自百度首頁某個圖片的響應字段。我們可以看到Age=1016859,說明這個資源已經在緩存服務器存在了1016859秒。如果文件被修改或替換,Age會重新由0開始累計。

Age消息頭的值通常接近於0。表示此消息對象剛剛從原始服務器獲取不久;其他的值則是表示代理服務器當前的系統時間與此應答消息中的通用消息頭 Date的值之差。

上面這個結論歸結爲一個等式就是:

靜態資源Age + 靜態資源Date = 原服務端Date

🐲用戶操作行爲對緩存的影響

搜索了很久有沒有關於這方面的權威總結,最後竟然在百度百科找到了也是很驚訝,我自己加了一條用戶強制刷新操作瀏覽器的反應。強制刷新,window下是Ctrl+F5,mac下就是command+shift+R操作了。:relieved:

操作 說明
打開新窗口 如果指定cache-control的值爲private、no-cache、must-revalidate,那麼打開新窗口訪問時都會重新訪問服務器。而如果指定了max-age值,那麼在此值內的時間裏就不會重新訪問服務器,例如:Cache-control: max-age=5 表示當訪問此網頁後的5秒內不會去再次訪問服務器.
在地址欄回車 如果值爲private或must-revalidate,則只有第一次訪問時會訪問服務器,以後就不再訪問。如果值爲no-cache,那麼每次都會訪問。如果值爲max-age,則在過期之前不會重複訪問。
按後退按扭 如果值爲private、must-revalidate、max-age,則不會重訪問,而如果爲no-cache,則每次都重複訪問.
按刷新按扭 無論爲何值,都會重複訪問.(可能返回狀態碼:200、304,這個不同瀏覽器處理是不一樣的,FireFox正常,Chrome則會啓用緩存(200 from cache))
按強制刷新按鈕 當做首次進入重新請求(返回狀態碼200)

來自百度百科

:wink:如果想在瀏覽器點擊“刷新”按鈕的時候不讓瀏覽器去發新的驗證請求呢?辦法找到一個,知乎上面一個回答,在頁面加載完畢後通過腳本動態地添加資源:

$(window).load(function() {
  	var bg='http://img.infinitynewtab.com/wallpaper/100.jpg';
  	setTimeout(function() {
    	$('#bgOut').css('background-image', 'url('+bg+')');
  	},0);
});

來自知乎

🐩HTML5的緩存

這部分準備的說應該叫離線存儲。現在比較普遍用的是Appcache,但Appcache已經從web標準移除了,在可預見的未來裏,ServiceWorker可能會是一個比較適合的解決方案。

1. Appcache

這是HTML5的一個新特性,通過離線存儲達到用戶在沒有網絡連接的情況下也能訪問頁面的功能。離線狀態下即使用戶點擊刷新都能正常加載文檔。

使用方法如下,在HTML文件中引入appcache文件:

<!DOCTYPE html>
<html manifest="manifest.appcache">
<head>
  <meta charset="UTF-8">
  <title>***</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

🤠web 應用中的 manifest 特性可以指定爲緩存清單文件的相對路徑或一個絕對 URL(絕對 URL 必須與應用同源)。緩存清單文件可以使用任意擴展名,但傳輸它的 MIME 類型必須爲 text/cache-manifest。

**注意:**在 Apache 服務器上,若要設置適用於清單(.appcache)文件的 MIME 類型,可以向根目錄或應用的同級目錄下的一個 .htaccess 文件中增加 AddType text/cache-manifest .appcache

CACHE MANIFEST
# 註釋:需要緩存的文件,無論在線與否,均從緩存裏讀取
# v1 2017-11-30
# This is another comment
/static/logo.png

# 註釋:不緩存的文件,始終從網絡獲取
NETWORK:
example.js

# 註釋:獲取不到資源時的備選路徑,如index.html訪問失敗,則返回404頁面
FALLBACK:
index.html 404.html

上面就是一個完整的緩存清單文件的示例。

**注意:**主頁一定會被緩存起來的,因爲AppCache主要是用來做離線應用的,如果主頁不緩存就無法離線查看了,因此把index.html添加到NETWORK中是不起效果的。

實際上這個特性已經web標準中刪除,但現在爲止還有很多瀏覽器支持它,所以這裏提一下。

你可以用最新的Firefox(版本 57.0.1)測試下,控制檯會有這麼一行字👉:

程序緩存 API(AppCache)已不贊成使用,幾天後將被移除。需離線支持請嘗試使用 Service Worker。

最新Chrome(版本 62.0.3202.94)倒是沒有這個警告。🐻

AppCache之所以不受待見我想了下面幾個原因:

  1. 一旦使用了manifest後,沒辦法清空這些緩存,只能更新緩存,或者得用戶自己去清空瀏覽器的緩存;
  2. 假如更新的資源中有一個資源更新失敗了,那麼所有的資源就會全部更新失敗,將用回上一版本的緩存;
  3. 主頁會被強制緩存(使用了manifest的頁面),並且無法清除;
  4. appache文件可能會無法被及時更新,因爲各大瀏覽器對於appcache文件的處理方式不同;
  5. 以上幾個弊端一旦出問題,會讓用戶抓狂更會讓開發者抓狂!

2. Service Worker

Service worker還是一個實驗性的功能,線上環境不推薦使用。🐒這裏大概介紹一下。

Service worker本質上充當Web應用程序與瀏覽器之間的代理服務器。

🙂首先講個小故事:

我們都知道瀏覽器的js引擎處理js是單線程的,它就好像一個大Boss高高在上,同一個時間它只做一個事情(就是那麼傲嬌),基於這個弊端,W3C(HR)給大Boss招聘了一個祕書(web worker),大Boss可以把瑣碎的事情交給祕書web worker去做,做完了發個微信(postMessage)通知大Boss,大Boss通過onmessage來獲取祕書web worker做的事情的結果。傍晚時分,下班時間到!大Boss回家哄兒子了,祕書也出去約會去了,沒人加班了!這怎麼行!W3C(HR)又提出了招個程序🐵的想法的想法,OK,Service Worker應聘成功!於是,程序🙈就堅持在工作崗位上了,從此開啓沒完沒了的加班之路。總的來說這隻猿的工作是這樣的:

  • 後臺數據同步
  • 響應來自其它源的資源請求
  • 集中接收計算成本高的數據更新,比如地理位置和陀螺儀信息,這樣多個頁面就可以利用同一組數據
  • 在客戶端進行CoffeeScript,LESS,CJS/AMD等模塊編譯和依賴管理(用於開發目的)
  • 後臺服務鉤子
  • 自定義模板用於特定URL模式
  • 性能增強,比如預取用戶可能需要的資源

——Service Worker API

注意:Service workers之所以優於以前同類嘗試(如上面提到的AppCache)),是因爲它們無法支持當操作出錯時終止操作。Service workers可以更細緻地控制每一件事情。如何控制的呢?

Service workers利用了ES6中比較重要的特性Promise,並且在攔截請求的時候使用的是新的fetch API,之所以使用fetch就是因爲fetch返回的是Promise對象。可以說Service workers重要組成部分就是三塊:事件、Promise和Fetch請求。OK,talk is cheap,show you the code。🤓

首先我們看下app.js文件:告訴瀏覽器註冊某個JavaScript文件爲service worker,檢查service worker API是否可用,如果可用就註冊service worker:

//使用 ServiceWorkerContainer.register()方法首次註冊service worker。
if (navigator.serviceWorker) {
  	navigator.serviceWorker.register('./sw.js', {scope: './'})
      	.then(function (registration) {
          	console.log(registration);
      	})
      	.catch(function (e) {
          	console.error(e);
      	});
} else {
  	console.log('該瀏覽器不支持Service Worker');
}

再來看看具體作爲service worker的文件sw.js,例子如下:

const CACHE_VERSION = 'v1'; // 緩存文件的版本
const CACHE_FILES = [ // 需要緩存的文件
	'./test.js',
	'./app.js',
	'https://code.jquery.com/jquery-3.0.0.min.js'
];

self.addEventListener('install', function (event) { // 監聽worker的install事件
    event.waitUntil( // 延遲install事件直到緩存初始化完成
        caches.open(CACHE_VERSION)
		.then(function (cache) {
			console.log('緩存打開');
			return cache.addAll(CACHE_FILES);
		})
    );
});

self.addEventListener('activate', function(event) {// 監聽worker的activate事件
    event.waitUntil(// 延遲activate事件直到
        caches.keys().then(function(keys) {
            return Promise.all(keys.map(function(key, i){
                if(key !== CACHE_VERSION){
                    return caches.delete(keys[i]); // 清除舊版本緩存
                }
            }))
        })
    )
});

self.addEventListener('fetch', function(event) { // 截取頁面的資源請求
    event.respondWith(
        caches.match(event.request).then(function(res) { // 判斷緩存是否命中
            if (res) { // 返回緩存中的資源
                return res;
            }
            _request(event); // 執行請求備份操作
        })
    )
});

function _request(event) {
    var url = event.request.clone();
    return fetch(url).then(function(res) {// 使用fetch請求線上資源
        // 錯誤判斷
        if (!res || res.status !== 200 || res.type !== 'basic') {
            return res;
        }

        var response = res.clone(); // 創建了一個響應對象的克隆,儲藏在一個單獨的變量中

        caches.open(CACHE_VERSION).then(function(cache) {// 緩存從線上獲取的資源
            cache.put(event.request, response);
        });
        return res;
    })
}

清除一個Service Worker也很簡單:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', {scope: './'}).then(function(registration) {
    // registration worked
    console.log('Registration succeeded.');
    registration.unregister().then(function(boolean) {
      // if boolean = true, unregister is successful
    });
  }).catch(function(error) {
    // registration failed
    console.log('Registration failed with ' + error);
  });
};

相對AppCache來說,Service Worker的API增多了不少,用法也更復雜了些,但看得出Service Worker纔是未來,對於web app來說,更是如虎添翼。現在支持Service Worker的瀏覽器除了Chrome和Firefox,最近新添一個生力軍——Safari也支持Service Worker了。期待它在未來大放異彩吧。🤗

🦉模擬實現服務端決策

如下,使用node原生代碼簡單的模擬下服務器發送響應的過程,包括對於協商緩存的處理過程:

var http = require('http');
var fs = require('fs');
var url = require('url');

process.env.TZ = 'Europe/London';

let tag = '123456';

http.createServer( function (request, response) {  

   var pathname = url.parse(request.url).pathname;

   	console.log("Request for " + pathname + " received.");
   	const fileMap = {
	   'js': 'application/javascript; charset=utf-8',
	   'html': 'text/html',
	   'png': 'image/png',
	   'jpg': 'image/jpeg',
	   'gif': 'image/gif',
	   'ico': 'image/*',
       'appcache': 'text/cache-manifest'
   	}
   	fs.readFile(pathname.substr(1), function (err, data) {
		if (request.headers['if-none-match'] === tag) {
			response.writeHead(304, {
				'Content-Type': fileMap[pathname.substr(1).split('.')[1]],
				'Expires': new Date(Date.now() + 30000),
				'Cache-Control': 'max-age=10, public',
				'ETag': tag,
				'Last-Modified': new Date(Date.now() - 30000),
				'Vary': 'User-Agent'
			});
	   } else {             
			response.writeHead(200, {
				'Content-Type': fileMap[pathname.substr(1).split('.')[1]],
				'Cache-Control': 'max-age=10, public',
				'Expires': new Date(Date.now() + 30000),
				'ETag': tag,
				'Last-Modified': new Date(Date.now() - 30000),
				'Vary': 'User-Agent'
			});
			response.write(fs.readFileSync(pathname.substr(1)));        
      	}
      	response.end();
   	});   
}).listen(8081);

如上代碼。如果你沒使用過node,拷貝下代碼存爲file.js,安裝node,命令行輸入node file.js,可以在同目錄下建立index.html文件,在html文件中引用一些圖片,CSS等文件,瀏覽器輸入localhost:8081/index.html進行模擬。🤓

🦆關於緩存的一些問答

1. 問題:請求被緩存,導致新代碼未生效

解決方案:

  • 服務端響應添加Cache-Control:no-cache,must-revalidate指令;
  • 修改請求頭If-modified-since:0If-none-match
  • 修改請求URL,請求URL後加隨機數,隨機數可以是時間戳,哈希值,比如:http://damonare.cn?a=1234

2. 問題:服務端緩存導致本地代碼未更新

解決方案:

  • 合理設置Cache-Control:s-maxage指令;
  • 設置Cache-Control:private指令,防止代理服務器緩存資源;
  • CDN緩存可以使用管理員設置的緩存刷新接口進行刷新;

3. 問題: Cache-Control: max-age=0 和 no-cache有什麼不同

回答:

max-age=0no-cache應該是從語氣上不同。max-age=0是告訴客戶端資源的緩存到期應該向服務器驗證緩存的有效性。而no-cache則告訴客戶端使用緩存前必須向服務器驗證緩存的有效性。

後記

參考文檔:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章