一文了解HTTP中的緩存

簡述

HTTP緩存相信都不陌生,由於它是在前端性能優化中必不可少的一個環節。在首次進入或者請求數據正常傳輸數據,而當再次進入或者請求數據時,能夠走本地或者服務器上的緩存,來節省流量優化性能提升用戶體驗下降網絡負荷等等。javascript

web緩存主要用來緩存html文件js文件css文件數據,基本上都是提高客戶端/瀏覽器請求到服務器之間的速度,固然也能夠結合數據壓縮如gzip7z等等加快響應數據傳輸。css

在整個應用中能夠錯多層緩存結構這裏很少作介紹,由於前面已經大體介紹過了,這裏主要介紹和前端比較相關的HTTP緩存html

HTTP緩存

大體分爲下面幾步來加深對HTTP中的緩存理解和應用場景。前端

  1. 必知緩存策略的基礎
  2. 緩存的判斷策略
  3. 用戶操做對緩存策略的影響
  4. 緩存儲存的位置
  5. 緩存策略之間的對比

必知緩存策略的基礎

大體把協議分爲強緩存(過時策略)協商緩存(協商策略)兩類緩存,可能不太準確只是本身的如今的看法,瀏覽器/客戶端經過這兩種策略決定使用緩存中的副本仍是從服務器中獲取最新的資源。java

  • 強緩存(過時策略):也就是緩存副本有效期。 一個緩存副本必須知足如下任一條件,瀏覽器會認爲它是有效的,足夠新的,而直接從緩存中獲取副本並渲染:
    • 含有完整的過時時間控制頭信息(HTTP協議報頭),而且仍在有效期內
    • 瀏覽器已經使用過這個緩存副本,而且在一個會話中已經檢查過新鮮度
  • 協商緩存(協商策略)服務器返回資源的時候有時在控制頭信息帶上這個資源的實體標籤Etag(Entity Tag),它能夠用來做爲瀏覽器再次請求過程的校驗標識。若是發現校驗標識不匹配,說明資源已經被修改或過時,瀏覽器需求從新獲取資源內容。
key 描述 緩存策略 首部類型
Pragma 指定緩存機制(http 1.0 字段) 強緩存(過時策略) 響應首部字段
Cache-COntrol Cache-Control 通用消息頭字段,被用於在http請求和響應中,經過指定指令來實現緩存機制。 強緩存(過時策略) 響應/請求首部字段
Expires Expires 響應頭包含日期/時間, 即在此時候以後,響應過時。 強緩存(過時策略) 響應首部字段
Last-Modified Last-Modified 是一個響應首部,其中包含源頭服務器認定的資源作出修改的日期及時間。 協商緩存(協商策略) 響應首部字段
If-Modified-Since If-Modified-Since 是一個條件式請求首部,服務器只在所請求的資源在給定的日期時間以後對內容進行過修改的狀況下才會將資源返回,狀態碼爲 200 協商緩存(協商策略) 請求首部字段
ETag ETagHTTP響應頭是資源的特定版本的標識符。 協商緩存(協商策略) 響應首部字段
If-None-Match If-None-Match 是一個條件式請求首部。對於 GETHEAD 請求方法來講,當且僅當服務器上沒有任何資源的 ETag 屬性值與這個首部中列出的相匹配的時候,服務器端會才返回所請求的資源,響應碼爲 200 協商緩存(協商策略) 請求首部字段
If-Match(輔助) If-Match 的使用表示這是一個條件請求。在請求方法爲 GETHEAD 的狀況下,服務器僅在請求的資源知足此首部列出的 ETag值時纔會返回資源。 協商緩存(協商策略) 請求首部字段
If-Unmodified-Since(輔助) If-Unmodified-Since 只有當資源在指定的時間以後沒有進行過修改的狀況下,服務器纔會返回請求的資源,或是接受 POST 或其餘 non-safe 方法的請求。 協商緩存(協商策略) 請求首部字段
Vary(輔助) Vary 是一個HTTP響應頭部信息,它決定了對於將來的一個請求頭,應該用一個緩存的回覆(response)仍是向源服務器請求一個新的回覆。 協商緩存(協商策略) 響應首部字段

緩存又分爲強緩存和協商緩存。其中強緩存包括ExpiresCache-Control主要是在過時策略生效時應用的緩存。弱緩存包括Last-ModifiedETag是在協商策略後應用的緩存強弱緩存之間的主要區別在於獲取資源時是否會發送請求node

強緩存和協商緩存git

  • 若是本地緩存過時,則要依靠協商緩存
  • 強緩存的 http狀態碼是 200 OK
  • 協商緩存的 http 狀態碼是 304 Not Modified

強緩存(過時策略)

屬於**強緩存(過時策略)**的有以下:web

  • Cache-COntrol
  • Expires

Cache-Control

Cache-Control用於指定資源的緩存機制,能夠同時在請求和響應頭中設定。可是Cache-Control中的屬性也分爲請求和響應緩存指令,大體分爲以下:算法

緩存請求指令 客戶端能夠在HTTP請求中使用的標準 Cache-Control 指令。chrome

Cache-Control: max-age= / max-stale[=] / min-fresh= / no-cache / no-store / no-transform / only-if-cached

緩存響應指令 服務器能夠在響應中使用的標準 Cache-Control 指令。

Cache-control: must-revalidate / no-cache / no-store / no-transform / public / private / proxy-revalidate / max-age= / s-maxage=

Cache-Control: cache-directive[,cache-directive]cache-directive爲緩存指令,大小寫不敏感,共有12個與HTTP緩存標準相關,以下所示。其中請求指令7種,響應指令9種。Cache-Control能夠設置多個緩存指令,以逗號,分隔。

可緩存性

  • public: 代表響應能夠被任何對象(包括:發送請求的客戶端、代理服務器、CDN等中間代理服務器等等)緩存,以下圖所示:
    http-cache-public
  • private:代表響應只能被單個用戶緩存不能做爲共享緩存(即代理服務器不能緩存它)
    http-cache-public
  • no-cache:指定不緩存響應,代表資源不進行緩存,可是設置了 no-cache 以後並不表明瀏覽器不緩存,而是在獲取緩存前要向服務器確認資源是否被更改。至關於max-age: 0, must-revalidate
  • no-store絕對禁止緩存,請求和響應都不緩存,每次請求都從服務器獲取完整資源

到期

  • max-age=: 設置緩存存儲的最大週期,超過這個時間緩存被認爲過時(單位秒)。
  • s-maxage=: 覆蓋max-age或者Expires頭,可是僅適用於共享緩存(好比各個代理),私有緩存會忽略它。
  • max-stale[=]: 指定時間內,即便緩存過時,資源依然有效
  • min-fresh=表示客戶端但願獲取一個能在指定的秒數內保持其最新狀態的響應

從新驗證和從新加載

  • muse-revalidate: 使用緩存資源以前,必須先驗證狀態,若是頁面是過時的(如max-age),則去服務器進行獲取
  • proxy-revalidate: 與must-revalidate做用相同,但它僅適用於共享緩存(例如代理),並被私有緩存忽略。

其餘

  • no-transform:強制要求代理服務器不要對資源進行轉換,禁止代理服務器對Content-EncodingContent-RangeContent-Type等字段的修改,所以代理服務器的gzip壓縮將不被容許

no-cache和no-store

還有一點須要注意的是,no-cache並非指不緩存文件no-store纔是指不緩存文件no-cache僅僅是代表跳過強緩存,強制進入協商策略

經常使用設置

禁止緩存 Cache-Control: no-cache, no-store, must-revalidate

緩存靜態資源 Cache-Control:public, max-age=86400

Expires

Expires指定緩存的過時時間,爲絕對時間,即某一時刻。

注意:參考本地時間進行比對,在指定時刻後過時。RFC 2616建議最大值不要超過1年

max-age與Expires

Cache-Control中的max-age指令用於指定緩存過時的相對時間。資源達到指定時間後過時。該功能與Expires相似。但其優先級高於Expires,若是同時設置max-ageExpiresmax-age生效,忽略Expires

Cache-Control > Expires

強緩存大體流程

強緩的設置流程圖大體以下:

http-cache-public

協商緩存

在沒有強緩存時,就會走協商緩存,協商緩存大體流程:

  • 第一次請求時,服務端返回給客戶端一個**key(如Etag的資源值、Last-Modified最後修改時間)**和資源
  • 第二次請求時,客戶端帶上第一次服務端返回的key
  • 服務器端驗證當前的key是否和上次返回給客戶端的是否一致,一致返回304使用緩存,不一致從新返回key和新的資源

屬於**協商緩存(協商策略)**的有以下:

  • Last-Modified/If-Modified-Since/If-Unmodified-Since
  • ETag/If-Match/If-None-Match

Last-Modified/If-Modified-Since/If-Unmodified-Since

Last-Modified/If-Modified-Since大體流程以下:

  • 第一次請求時,服務器會獲取資源的最後修改時間經過設置Last-Modified,返回給客戶端
  • 後面請求時,客戶端(瀏覽器)會自動帶上If-Modified-Since字段
  • 服務器重新獲取修改時間與If-Modified-Since中的時間對比,若是沒有變化返回304狀態碼(瀏覽器得知304狀態碼,資源從緩存中獲取),若是改變返回200而且更新資源、更新Last-Modified

上面的流程是在設置不使用強緩存時的場景,這個只是如今的理解可能有不少的不太完善的地方。

Last-Modified

Last-Modified用於標記請求資源的最後一次修改時間

語法

Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT

Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
複製代碼

注意

  • GMT(格林尼治標準時間)
  • Last-Modified只能精確到秒,所以不適合在一秒內屢次改變的資源。

If-Modified-Since

If-Modified-Since 是一個條件式請求首部,與Last-Modified何用。有兩種結果以下:

  • If-Modified-Since/Last-Modified相同返回304狀態碼,客戶端使用緩存
  • If-Modified-Since/Last-Modified不相同返回200狀態碼,返回新的資源

If-Modified-Since 只能夠用在 GETHEAD 請求中。

If-Unmodified-Since

If-Unmodified-Since表示資源未修改則正常執行更新,不然返回412(Precondition Failed)狀態碼的響應。主要有以下兩種場景。

  1. 用於不安全的請求中從而是請求具有條件性(如POST或者其餘不安全的方法),如請求更新wiki文檔,文檔未修改時才執行更新
  2. If-Range字段同時使用時,能夠用來保證新的片斷請求來自一個未修改的文檔。

ETag/If-Match/If-None-Match

根據實體內容生成一段惟一hash字符串,標識資源的狀態,由服務端產生。瀏覽器會將這串字符串傳回服務器,驗證資源是否已經修改,若是沒有修改,過程以下:

http-cache-public

ETag HTTP響應頭是資源的特定版本的標識符。 語法

ETag: W/"<etag_value>"
  ETag: "<etag_value>"
複製代碼

W/ 可選 'W/'(大小寫敏感) 表示使用弱驗證器。 弱驗證器很容易生成,但不利於比較。 強驗證器是比較的理想選擇,但很難有效地生成。

"<etag_value>" 實體標籤惟一地表示所請求的資源。 它們是位於雙引號之間的ASCII字符串(如「675af34563dc-tr34」)。

注意:ETag和If-None-Match的值均爲雙引號包裹的。 ETag的優先級高於Last-Modified。當ETagLast-ModifiedETag優先級更高,但不會忽略Last-Modified,須要服務端實現。

ETagIf-None-Match 常被用來處理協商緩存。而 ETagIf-Match 能夠 避免「空中碰撞」

ETag HTTP 響應頭是資源的特定版本的標識符。這可讓緩存更高效,並節省帶寬,由於若是內容沒有改變,Web 服務器不須要發送完整的響應。而若是內容發生了變化,使用 ETag 有助於防止資源的同時更新相互覆蓋(「空中碰撞」)

實例

當編輯MDN時,當前的WIki內容被散列,並在相應中放入Etag:

ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
複製代碼

將更改保存到WIKI頁面(發佈數據)時,POST請求將包含有ETag值的If-Match頭來檢車是否爲最新版本。

If-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
複製代碼

若是哈希值不匹配,則意味着文檔已經被編輯,拋出 412 ( Precondition Failed) 前提條件失敗錯誤。

If-None-Match 是客戶端發送給服務器時的請求頭,其值是服務器返回給客戶端的 ETag,當 If-None-Match 和服務器資源最新的 Etag 不一樣時,返回最新的資源及其 Etag

緩存的判斷策略

緩存策略分爲強緩存協商緩存,首先通過強緩存過時策略,纔會走後面的協商緩存協商策略,大體把緩存分爲三個階段本地緩存階段(強緩存)協商緩存階段(本地+服務器)緩存失敗階段

大體在每一個階段中作的什麼判斷:

  1. 本地緩存階段:若是設置了強緩存,那麼會如今本地查找該資源,若是發現該資源,並且該資源尚未過時,就使用這個資源副本,徹底不會發起http請求到服務器。(主要應用是強緩存serverWorker);
  2. 協商緩存階段:若是在本地緩存找到對應的資源,可是不知道該資源是否過時或者已通過期,則發一個http請求到服務器,而後服務器判斷這個請求,若是請求的資源在服務器上沒有改動過,則返回304,讓瀏覽器使用本地找到的那個資源;
  3. 緩存失敗階段: 當服務器發現請求的資源已經修改過,或者這是一個新的請求(在原本沒有找到資源),服務器則返回該資源的數據,而且返回200, 固然這個是指找到資源的狀況下,若是服務器上沒有這個資源,則返回404

大體流程以下圖所示:

http-cache-public

這張圖中沒有包含serverWorker的緩存判斷流程b,可是在後面會有一篇文章專門介紹serverWorker,由於他是屬於PWA中的內容。

存儲策略發生在收到請求響應後,用於決定是否緩存相應資源;過時策略發生在請求前,用於判斷緩存是否過時協商策略發生在請求中,用於判斷緩存資源是否更新

用戶操做對緩存策略的影響

在用戶刷新頁面(F5)時,會對緩存產生影響,這裏就會記錄用戶操做對緩存產生的影響。用戶操做事項以下表所示:

用戶操做 強緩存 協商緩存
(新標籤)地址欄回車 有效 有效
(地址不變)地址欄回車 兼容性問題Chrome(失效)/Firefox(有效) 有效
連接跳轉 有效 有效
前進/後退 有效 有效
從收藏欄打開連接 有效 有效
(window.open)新開窗口 有效 有效
刷新(Command/Ctrl + R / F5) 失效 有效
強制刷新(Command + Shift + R / Ctrl + F5) 失效 失效

基本上包含了一些常見的用戶操做對強緩存協商緩存的影響,大體的判斷流程以下:

http-cache-public

注意

  • (地址不變)地址欄回車:它比較特殊,爲何它在Chrome失效,在Firefox中是有效。由於Chrome地址不變回車等同於刷新當前頁面,而在Firefox都是做爲新地址回車處理的。
  • webkit(Chrome內核)資源分爲主資源派生資源主資源是地址欄輸入的URL請求返回的資源派生資源是主資源中所引用的JS、CSS、圖片等資源
  • Chrome下刷新時,只有主資源的緩存應用方式如上圖所示,派生資源緩存應用方式與新標籤打開相似,會判斷緩存是否過時。強緩存生效時的區別在於新標籤打開爲from disk cache,而當前頁刷新派生資源是from memory cache
  • 而在Firefox下,當前頁面刷新,全部資源都會如上圖所示。

緩存儲存的位置

從緩存的位置上來講分爲四種,而且各自有優先級,當依次由上到下查找緩存且都沒有命中的時候,纔會去請求網絡,大體以下:

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

Service Worker

Service Worker 是一種獨立於主線程以外的 Javascript 線程。它能夠幫咱們實現離線緩存消息推送網絡代理等功能。

使用 Service Worker的話,傳輸協議必須HTTPS。由於 Service Worker 中涉及到請求攔截,因此必須使用 HTTPS 協議來保障安全。

Service Worker 實現緩存大體分爲如下幾個步驟:

  • 首先須要在頁面的 JavaScript 主線程中註冊 Service Worker
  • 註冊成功後後臺開始安裝步驟, 一般在安裝的過程當中須要緩存一些靜態資源。
  • 安裝成功後開始激活 Service Worker
  • 激活成功後 Service Worker 能夠控制頁面了(監聽 fetch 和 message 事件),可是隻針對在成功註冊了 Service Worker 後打開的頁面。

在這裏就不細說了,後面有一個單獨的章節來說述Service Worker, Service Worker 的緩存與瀏覽器其餘內建的緩存機制不一樣,它可讓咱們自由控制緩存哪些文件、如何匹配緩存、如何讀取緩存,而且緩存是持續性的

Memory Cache

Memory Cache 是內存中的緩存。主要包含的是當前頁面中請求到的數據如圖片(base64)腳本(JavaScript)樣式(css)等靜態數據。讀取內存中的數據確定比磁盤中的,可是內存中的緩存持續性很短,它會隨着當前Tab頁面關閉,內存中的緩存也就被釋放

好比在百度首頁刷新頁面,效果以下圖所示:

http-cache-public
preload <link> 元素的 rel 屬性的屬性值 preload<link rel="preload">來顯示的指定的預加載資源,也會被放入 memory cache中。

prefetch <link rel="prefetch"> 已經被許多瀏覽器支持了至關長的時間,但它是意圖預獲取一些資源,以備下一個導航/頁面使用(好比,當你去到下一個頁面時)。 瀏覽器會給使用prefetch的資源一個相對較低的優先級與使用preload的資源相比。

subresource <link rel="subresource">被Chrome支持了有一段時間,而且已經有些搔到預加載當前導航/頁面(所含有的資源)的癢處了。這些資源會以一個至關低的優先級被加載。 Memory Cache不會輕易的命中一個請求,除了要有匹配的URL,還要有相同的資源類型CORS模式以及一些其餘特性Memory Cache是不關心HTTP語義的,好比Cache-Control: max-age=0的資源,仍然能夠在同一個導航中被重用。可是在特定的狀況下,Memory Cache會遵照Cache-Control: no-store指令,不緩存相應的資源。

Memory Cache匹配規則在標準中沒有詳盡的描述,因此不一樣的瀏覽器內核在實現上會有所不一樣。

Disk Cache/HTTP Cache

HTTP Cache也被叫作Disk Cache。從字面的意思上理解Disk Cache就是儲存在硬盤上的緩存,所以它是持久存儲的,是實際存在於文件系統中的。 並且它容許相同的資源在跨會話,甚至跨站點的狀況下使用,例如兩個站點都使用了同一張圖片

HTTP Cache會根據HTTP Herder中的字段判斷哪些資源須要緩存,哪些資源能夠不請求直接使用,哪些資源已通過期須要從新請求。 當命中緩存以後,瀏覽器會從硬盤中讀取資源,雖然比起從內存中讀取慢了一些,但比起網絡請求仍是快了很多的。絕大部分的緩存都來自 disk cache

凡是持久性存儲都會面臨容量增加的問題,disk cache 也不例外。在瀏覽器自動清理時,會有神祕的算法去把「最老的」或者「最可能過期的」資源刪除,所以是一個一個刪除的。不過每一個瀏覽器識別「最老的」和「最可能過期的」資源的算法不盡相同,可能也是它們差別性的體現。

Push Cache

Push Cache(推送緩存)是 HTTP/2 中的內容,當以上三種緩存都沒有命中時,它纔會被使用。 它只在會話(Session)中存在,一旦會話結束就被釋放,而且緩存時間也很短暫,在Chrome瀏覽器中只有5分鐘左右,同時它也並不是嚴格執行HTTP頭中的緩存指令

Push Cache 在國內可以查到的資料不多,也是由於 HTTP/2 在國內不夠普及。這裏推薦閱讀Jake ArchibaldHTTP/2 push is tougher than I thought 這篇文章,文章中的幾個結論:

  • 全部的資源都能被推送,而且可以被緩存,可是 Edge 和 Safari 瀏覽器支持相對比較差
  • 能夠推送 no-cache 和 no-store 的資源
  • 一旦鏈接被關閉,Push Cache 就被釋放
  • 多個頁面可使用同一個HTTP/2的鏈接,也就可使用同一個Push Cache。這主要仍是依賴瀏覽器的實現而定,出於對性能的考慮,有的瀏覽器會對相同域名但不一樣的tab標籤使用同一個HTTP鏈接。
  • Push Cache 中的緩存只能被使用一次
  • 瀏覽器能夠拒絕接受已經存在的資源推送
  • 你能夠給其餘域名推送資源

若是以上四種緩存都沒有命中的話,那麼只能發起請求來獲取資源了。

那麼爲了性能上的考慮,大部分的接口都應該選擇好緩存策略,一般瀏覽器緩存策略分爲兩種:強緩存和協商緩存,而且緩存策略都是經過設置 HTTP Header 來實現的。

關於 memory cache 和 disk cache

這兩種緩存類型存在於 Chrome 中。 disk cache 存在硬盤,能夠存不少,容量上限比內容緩存高不少,而 memory cache 從內存直接讀取,速度上佔優點,這兩個各有各的好處!

由於關於在何時用到什麼緩存的文檔至關的少因此真的很差判斷,是當前使用的是哪一個緩存,好比下面這個例子:

http-cache-public

緩存策略之間的對比

其實緩存之間也沒有太好的對比性,大體能夠從緩存策略緩存位置兩個角度對比緩存的優缺點。

  • 強緩存返回 http 200 OK狀態碼,而協商緩存返回http 304 Not Modified 狀態碼
  • 強緩存中的優先級爲: Cache-Control > Expires
  • 協商緩存中的優先級: Etag/If-None-Match > Last-Modified/Last-Since-Modified
  • 協商緩存中,Last-Modified不能記錄秒級如下的更新緩存,而Etag能夠。可是Etag生成惟一資源標識符又比叫困難,而Last-Modified實現起來比價簡單
  • Service Worker相對於Disk Cache/Memory Cache配置會麻煩一點,可是Service Worker應用場景更廣,性能也會好一點。
  • Service Worker必需要在Https協議中才會生效。
  • Disk Cache相對於Memory Cache,它的優勢在於容量大、儲存週期長、可被多域使用,缺點在於讀取速度慢
  • Memory Cache相對於Memory Cache,它的優勢在於速度快、對前端link字段支持性,缺點在於儲存週期短(tab也關閉)、空間有限

它們的值優缺點如上所示,如在chromefirefoxieMemory cacheDisk cache也是不太相同的。

關於 Chrome、FF、IE 的緩存區別

Chrome瀏覽器的速度比其餘兩個瀏覽器的速度更快一點,主要是由於V8引擎的執行速度更快,另外一方面應該就是它的緩存策略的使用。 從這四個方面強緩存協商緩存Disk CacheMemory Cache來對比,爲何說Chrome執行效果比其它的兩個瀏覽器的執行速度和加載速度更快。

就以百度首頁爲例看一下ChromeFirefox的差異。

ChromeFirefox中打開https://www.baidu.com/首頁,結果以下圖所示 Firefox效果以下:

http-cache-public
Chrome效果以下:
http-cache-public

咱們以百度的bd_logo1.png的請求爲例,logo的請求是一個Get請求,同時它被設置了四個緩存配置,可是它在兩個瀏覽器中表現並不相同,以下圖所示

Firefox效果以下:

http-cache-public

Chrome效果以下:

http-cache-public

首先在再次請求時瀏覽器端都沒有攜帶協商緩存須要的頭部字段,因此它們確定走的是強緩存,在強緩存Cache-Control的優先級是最高的,因此都是走的Cache-Control的策略。

能夠看到它們的區別以下幾點:

  • 狀態碼: 首先它們返回的狀態碼是不一樣的,Chrome返回的狀態碼是200,Firefox返回的狀態碼是304
  • 使用的資源: 能夠看到Chrome中的資源大小爲0(耗時 0ms,也就是 1ms 之內),那麼它使用的本地的資源。而Firefox中它是從服務器獲取的資源。

測試實例

在這裏咱們來一個一個測試expires/cache-control/etag/last-modified/pragma它們是否和咱們上面所總結的一致。

測試環境chrome 78.0.3904.70node 12.9.1koa 2.x.

總體的目錄結構以下圖所示:

http-cache-public

代碼可能寫的比較粗糙,可是後面會優化一下,公共代碼以下:

index.html代碼以下

<!DOCTYPE html>
  <html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
      <link rel="stylesheet" href="/index/index.css">
      <script src="/index/index.js"></script>
  </head>
  <body>
      測試cache
      <img src="/index/rotateX.png" alt="">
  </body>
  </html>
複製代碼

app.js代碼以下

const Koa = require('koa');
const Router = require('koa-router');
const Static = require('koa-static');
const fs = require('fs-extra');
const Path = require('path');
const mime = require('mime');

const app = new Koa();
const router = new Router();

router.get('/', async (ctx, next) => {
    ctx.type = mime.getType('.html');
    // console.log(__dirname)
    const content = await fs.readFile(Path.resolve(__dirname + '/index/index.html'), 'UTF-8');
    // console.log(content);
    ctx.body = content;
    await next();
})
// 待優化
router.get('/index/rotateX.png', async (ctx, next) => {
    const { response, path } = ctx;
    ctx.type = mime.getType(path);
    const imageBuffer = await fs.readFile(Path.resolve(__dirname, `.${path}`));
    ctx.body = imageBuffer;
    await next();
})

// 待優化
router.get('/index/index.css', async (ctx, next) => {
    const { path } = ctx;
    ctx.type = mime.getType(path);
  
    const content = await fs.readFile(Path.resolve(__dirname, `.${path}`), 'UTF-8');
    ctx.body = content;

    await next();
});

// 待優化
router.get('/index/index.js', async (ctx, next) => {
    const { path } = ctx;
    ctx.type = mime.getType(path);

    const content = await fs.readFile(Path.resolve(__dirname, `.${path}`), 'UTF-8');
    ctx.body = content;

    await next();
});

// app.use(Static('./index'))
app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, function (err) {
    // console.log()
    if (err) {
        console.log(err)
    } else {
        console.log('啓動成功')
    }
})
複製代碼

下面的代碼都是在這個代碼上修改,index.jsindex.cssrotateX.png本身寫就能夠,或者去網上下載一個稍微超過2kb大小的文件。

Cache-Control實例

使用Cache-Control緩存測試效果,修改代碼以下:

修改app.js

// ...省略代碼
  router.get('/index/rotateX.png', async (ctx, next) => {
    // ...省略代碼
    // 添加代碼
    ctx.set('Cache-Control', 'max-age=' + 10);
  })
  // ...省略代碼
  router.get('/index/index.css', async (ctx, next) => {
    // ...省略代碼
    // 添加代碼
    ctx.set('Cache-Control', 'max-age=' + 10);
  })
  // ...省略代碼
  router.get('/index/index.js', async (ctx, next) => {
    // ...省略代碼
    // 添加代碼
    ctx.set('Cache-Control', 'max-age=' + 10);
  })
複製代碼

咱們在經過nodemon app.js運行代碼,運行效果大體以下:

  1. 第一個打開localhost:3000時,由於沒有任何緩存因此資源是從服務器中請求來的,以下圖所示
    http-cache-public
  2. 當咱們刷新頁面時,由於咱們設置了Cache-Control: max-age=10,因此會走本地緩存,以下圖所示
    http-cache-public
    第二次請求,三個請求都來自 memory cache。由於咱們沒有關閉 TAB,因此瀏覽器把緩存的應用加到了memory cache。(耗時 0ms,也就是 1ms 之內)
  3. 當咱們跳轉到https://www.baidu.com,再返回頁面時,它也會走本地緩存,以下圖所示
    http-cache-public

由於跳轉頁面等因而關閉了 TABmemory cache 也隨之清空。可是 disk cache 是持久的,因而全部資源來自 disk cache。(大約耗時 3ms,由於文件有點小)並且對比 2 和 3,很明顯看到 memory cache 仍是比 disk cache快得多的。

no-cache和no-store對比

咱們來對比一下no-cacheno-store的區別,修改代碼以下:

修改index.html

<!DOCTYPE html>
  <html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
      <link rel="stylesheet" href="/index/index.css">
      <link rel="stylesheet" href="/index/index.css">
      <script src="/index/index.js"></script>
      <script src="/index/index.js"></script>
  </head>
  <body>
      測試cache
      <img src="/index/rotateX.png" alt="">
      <img src="/index/rotateX.png" alt="">
      <!-- 異步請求圖片 -->
      <script> setTimeout(function () { let img = document.createElement('img') img.src = '/index/rotateX.png' document.body.appendChild(img) }, 1000) </script>
  </body>
  </html>
複製代碼

咱們暫時不修改緩存的配置,經過nodemon app.js運行代碼,運行效果大體以下:

  • 同步請求方面,瀏覽器會自動把當次 HTML 中的資源存入到緩存 (memory cache),這樣碰到相同 src 的圖片就會自動讀取緩存(但不會在 Network 中顯示出來)
  • 異步請求方面,瀏覽器一樣是不發請求而直接讀取緩存返回。但一樣不會在 Network 中顯示。

下面咱們修改app.js中的代碼以下:

// ...省略代碼
  router.get('/index/rotateX.png', async (ctx, next) => {
    // ...省略代碼
    // 替換原來代碼
    ctx.set('Cache-Control', 'no-cache');
  })
  // ...省略代碼
  router.get('/index/index.css', async (ctx, next) => {
    // ...省略代碼
    // 替換原來代碼
    ctx.set('Cache-Control', 'no-cache');
  })
  // ...省略代碼
  router.get('/index/index.js', async (ctx, next) => {
    // ...省略代碼
    // 替換原來代碼
    ctx.set('Cache-Control', 'no-cache');
  })
複製代碼

咱們運行代碼看的效果以下圖所示:

http-cache-public

  • 同步請求方面,瀏覽器會自動把當次 HTML 中的資源存入到緩存 (memory cache),這樣碰到相同 src 的圖片就會自動讀取緩存(但不會在 Network 中顯示出來)

若是把no-cache修改成no-store

修改app.js

// ...省略代碼
  router.get('/index/rotateX.png', async (ctx, next) => {
    // ...省略代碼
    // 替換原來代碼
    ctx.set('Cache-Control', 'no-store');
  })
  // ...省略代碼
  router.get('/index/index.css', async (ctx, next) => {
    // ...省略代碼
    // 替換原來代碼
    ctx.set('Cache-Control', 'no-store');
  })
  // ...省略代碼
  router.get('/index/index.js', async (ctx, next) => {
    // ...省略代碼
    // 替換原來代碼
    ctx.set('Cache-Control', 'no-store');
  })
複製代碼

咱們運行代碼看的效果以下圖所示:

http-cache-public

當咱們設置了Cache-Control: no-store時,能夠看到cssjs文件都被請求了兩次,png請求了三次。

  • 如以前原理所述,雖然 memory cache 是無視 HTTP 頭信息的,可是 no-store 是特別的。在這個設置下,memory cache 也不得不每次都請求資源。
  • 異步請求和同步遵循相同的規則,在 no-store 狀況下,依然是每次都發送請求,不進行任何緩存。

Last-Modified/If-Modified-Since

這裏來設置協商緩存Last-Modified/If-Modified-Since,代碼修改以下:

修改app.js

const responseFile = async (path, context, encoding) => {
    const fileContent = await fs.readFile(path, encoding);
    context.type = mime.getType(path);
    context.body = fileContent;
  };
  router.get('/index/rotateX.png', async (ctx, next) => {
    ctx.set('Pragma', 'no-cache');
    const { response, request, path } = ctx;
    const imagePath = Path.resolve(__dirname, `.${path}`);
    const ifModifiedSince = request.headers['if-modified-since'];
    console.log(ifModifiedSince)
    const imageStatus = await fs.stat(imagePath);
    const lastModified = imageStatus.mtime.toGMTString();
    if (ifModifiedSince === lastModified) {
        response.status = 304;
    } else {
        response.lastModified = lastModified;
        await responseFile(imagePath, ctx);
    }
    await next();
  })
複製代碼

大體流程以下:

  1. Chrome中選中Disable Cache禁用緩存,能夠經過下面圖片看到服務器端發送給客戶端Last-Modified: Thu, 24 Oct 2019 05:12:37 GMT

    http-cache-public

  2. 關閉 disable cache 後再次訪問圖片時,發現帶上了 if-modified-since 請求頭,值就是上次請求響應的 last-modified 值,由於圖片最後修改時間不變,因此 304 Not Modified。效果以下圖所示

    http-cache-public

啓用Disable Cache時,咱們能夠看到客戶端/瀏覽器端自動帶上了Pragma':'no-cache''Cache-Control': 'no-cache'這兩個字段,不適用緩存。

Etag/If-None-Match

修改app.js,經過npm i crypto -D安裝crypto,用於生成md5

// 處理 css 文件
router.get('/index/index.css', async (ctx, next) => {
    const { request, response, path } = ctx;
    ctx.type = mime.getType(path);
    response.set('pragma', 'no-cache');

    const ifNoneMatch = request.headers['if-none-match'];
    const imagePath = Path.resolve(__dirname, `.${path}`);
    const hash = crypto.createHash('md5');
    const imageBuffer = await fs.readFile(imagePath);
    hash.update(imageBuffer);
    const etag = `"${hash.digest('hex')}"`;
    if (ifNoneMatch === etag) {
        response.status = 304;
    } else {
        response.set('etag', etag);
        ctx.body = imageBuffer;
    }

    await next();
});
複製代碼

運行效果以下圖所示:

http-cache-public

他的過程和Last-Modified/If-Modified-Since,可是由於Last-Modified/If-Modified-Since它不能監聽1s之內的資源變化,因此通常用他來作Etag/If-None-Match的補充方案。

總結

緩存大體分爲:強緩存協商緩存

  • 強緩存: pragmacache-controlexpires
  • 協商緩存: last-modified/If-modified-sinceetag/if-none-match
  • 強緩存優先級: cache-control > pragma > expires
  • 協商緩存優先級: etag/if-none-match > last-modified/If-modified-since

緩存位置分爲: Service WorkerMemory CacheDisk CachePush Cache,也是從左到右若是命中就使用。

上面的實例只是比較簡單的應用,其實還有不少有意思的實例能加深對緩存的理解,以下:

  • pragmacache-controlexpires優先級
  • last-modified/If-modified-sinceetag/if-none-match優先級
  • cache-control: no-cachecache-control: max-age=0, must-revalidate效果是否相同
  • chromefirefoxie之間的緩存差異

本篇文章有意避開Service Worker的詳細介紹,由於會有單獨的一篇文章來介紹Service Worker在真實應用的使用。

在線代碼,能夠刷新頁面(刷新內部頁面)控制檯中查看當前效果。

感受寫的不錯請點一下贊,據說長的帥的人都會點贊,而且點贊後走上人生巔峯

參考

一文搞懂瀏覽器緩存機制

關於 http 緩存,這些知識點你可能都不懂

瀏覽器緩存策略

一文讀懂前端緩存

經過 koa2 服務器實踐探究瀏覽器HTTP緩存機制

相關文章
相關標籤/搜索