圖解 HTTP 快取

政採雲前端團隊發表於2020-05-11
48 篇原創好文~
本文首發於政採雲前端團隊部落格:圖解 HTTP 快取

前言

HTTP 的快取機制,可以說這是前端工程師需要掌握的重要知識點之一。本文將針對 HTTP 快取整體的流程做一個詳細的講解,爭取做到大家讀完整篇文章後,對快取有一個整體的瞭解。

HTTP 快取分為 2 種,一種是強快取,另一種是協商快取。主要作用是可以加快資源獲取速度,提升使用者體驗,減少網路傳輸,緩解服務端的壓力。這是快取運作的一個整體流程圖:

Http快取.jpg

強快取

不需要傳送請求到服務端,直接讀取瀏覽器本地快取,在 Chrome 的 Network 中顯示的 HTTP 狀態碼是 200 ,在 Chrome 中,強快取又分為 Disk Cache (存放在硬碟中)和 Memory Cache (存放在記憶體中),存放的位置是由瀏覽器控制的。是否強快取由 Expires、Cache-Control 和 Pragma 3 個 Header 屬性共同來控制。

○ Expires

Expires 的值是一個 HTTP 日期,在瀏覽器發起請求時,會根據系統時間和 Expires 的值進行比較,如果系統時間超過了 Expires 的值,快取失效。由於和系統時間進行比較,所以當系統時間和伺服器時間不一致的時候,會有快取有效期不準的問題。Expires 的優先順序在三個 Header 屬性中是最低的。

○ Cache-Control

Cache-Control 是 HTTP/1.1 中新增的屬性,在請求頭和響應頭中都可以使用,常用的屬性值如有:

  • max-age:單位是秒,快取時間計算的方式是距離發起的時間的秒數,超過間隔的秒數快取失效
  • no-cache:不使用強快取,需要與伺服器驗證快取是否新鮮
  • no-store:禁止使用快取(包括協商快取),每次都向伺服器請求最新的資源
  • private:專用於個人的快取,中間代理、CDN 等不能快取此響應
  • public:響應可以被中間代理、CDN 等快取
  • must-revalidate:在快取過期前可以使用,過期後必須向伺服器驗證

○ Pragma

Pragma 只有一個屬性值,就是 no-cache ,效果和 Cache-Control 中的 no-cache 一致,不使用強快取,需要與伺服器驗證快取是否新鮮,在 3 個頭部屬性中的優先順序最高。

本地通過 express 起一個服務來驗證強快取的 3 個屬性,程式碼如下:

const express = require('express');
const app = express();
var options = { 
  etag: false, // 禁用協商快取
  lastModified: false, // 禁用協商快取
  setHeaders: (res, path, stat) => {
    res.set('Cache-Control', 'max-age=10'); // 強快取超時時間為10秒
  },
};
app.use(express.static((__dirname + '/public'), options));
app.listen(3000);

第一次載入,頁面會向伺服器請求資料,並在 Response Header 中新增 Cache-Control ,過期時間為 10 秒。

快取1.jpg

第二次載入,Date 頭屬性未更新,可以看到瀏覽器直接使用了強快取,實際沒有傳送請求。

快取2.jpg

過了 10 秒的超時時間之後,再次請求資源:

快取3.jpg

當 Pragma 和 Cache-Control 同時存在的時候,Pragma 的優先順序高於 Cache-Control。

快取5.jpg

協商快取

當瀏覽器的強快取失效的時候或者請求頭中設定了不走強快取,並且在請求頭中設定了If-Modified-Since 或者 If-None-Match 的時候,會將這兩個屬性值到服務端去驗證是否命中協商快取,如果命中了協商快取,會返回 304 狀態,載入瀏覽器快取,並且響應頭會設定 Last-Modified 或者 ETag 屬性。

○ ETag/If-None-Match

ETag/If-None-Match 的值是一串 hash 碼,代表的是一個資源的識別符號,當服務端的檔案變化的時候,它的 hash碼會隨之改變,通過請求頭中的 If-None-Match 和當前檔案的 hash 值進行比較,如果相等則表示命中協商快取。ETag 又有強弱校驗之分,如果 hash 碼是以 "W/" 開頭的一串字串,說明此時協商快取的校驗是弱校驗的,只有伺服器上的檔案差異(根據 ETag 計算方式來決定)達到能夠觸發 hash 值字尾變化的時候,才會真正地請求資源,否則返回 304 並載入瀏覽器快取。

○ Last-Modified/If-Modified-Since

Last-Modified/If-Modified-Since 的值代表的是檔案的最後修改時間,第一次請求服務端會把資源的最後修改時間放到 Last-Modified 響應頭中,第二次發起請求的時候,請求頭會帶上上一次響應頭中的 Last-Modified 的時間,並放到 If-Modified-Since 請求頭屬性中,服務端根據檔案最後一次修改時間和 If-Modified-Since 的值進行比較,如果相等,返回 304 ,並載入瀏覽器快取。

本地通過 express 起一個服務來驗證協商快取,程式碼如下:

const express = require('express');
const app = express();
var options = { 
  etag: true, // 開啟協商快取
  lastModified: true, // 開啟協商快取
  setHeaders: (res, path, stat) => {
    res.set({
      'Cache-Control': 'max-age=00', // 瀏覽器不走強快取
      'Pragma': 'no-cache', // 瀏覽器不走強快取
    });
  },
};
app.use(express.static((__dirname + '/public'), options));
app.listen(3001);

第一次請求資源:

快取6.jpg

第二次請求資源,服務端根據請求頭中的 If-Modified-Since 和 If-None-Match 驗證檔案是否修改。

快取7.jpg

我們再來驗證一下 ETag 在強校驗的情況下,只增加一行空格,hash 值如何變化,在程式碼中,我採用的是對檔案進行 MD5 加密來計算其 hash 值。

注:只是為了演示用,實際計算不是通過 MD5 加密的,Apache 預設通過 FileEtag 中 FileEtag INode Mtime Size 的配置自動生成 ETag,使用者可以通過自定義的方式來修改檔案生成 ETag 的方式。

為了保證 lastModified 不影響快取,我把通過 Last-Modified/If-Modified-Since 請求頭刪除了,原始碼如下:

const express = require('express');
const CryptoJS = require('crypto-js/crypto-js');
const fs = require('fs');
const app = express();
var options = { 
  etag: true, // 只通過Etag來判斷
  lastModified: false, // 關閉另一種協商快取
  setHeaders: (res, path, stat) => {
    const data = fs.readFileSync(path, 'utf-8'); // 讀取檔案
    const hash = CryptoJS.MD5((JSON.stringify(data))); // MD5加密
    res.set({
      'Cache-Control': 'max-age=00', // 瀏覽器不走強快取
      'Pragma': 'no-cache', // 瀏覽器不走強快取
      'ETag': hash, // 手動設定Etag值為MD5加密後的hash值
    });
  },
};
app.use(express.static((__dirname + '/public'), options));
app.listen(4000); // 使用新埠號,否則上面驗證的協商快取會一直存在

第一次和第二次請求如下:

快取10.jpg

快取11.jpg

然後我修改了 test.js ,增加一個空格後再刪除一個空格,保持檔案內容不變,但檔案的修改時間改變,發起第三次請求,由於我生成 ETag 的方式是通過對檔案內容進行 MD5 加密生成,所以雖然修改時間變化了,但請求依然返回了 304 ,讀取瀏覽器快取。

快取13.jpg

ETag/If-None-Match 的出現主要解決了 Last-Modified/If-Modified-Since 所解決不了的問題:

  • 如果檔案的修改頻率在秒級以下,Last-Modified/If-Modified-Since 會錯誤地返回 304
  • 如果檔案被修改了,但是內容沒有任何變化的時候,Last-Modified/If-Modified-Since 會錯誤地返回 304 ,上面的例子就說明了這個問題

總結

在實際使用場景中,比如政採雲的官網。圖片、不常變化的 JS 等靜態資源都會使用快取來提高頁面的載入速度。例如政採雲首頁的頂部導航欄,埋點 SDK 等等。

在文章的最後,我們再次回到這張流程圖,這張圖涵蓋了 HTTP 快取的整體流程,大家對整體流程熟悉後,也可以自己動手通過 Node 來驗證下 HTTP 快取。

Http快取.jpg

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 50 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的“老”兵,也有浙大、中科大、杭電等校的應屆新人。團隊在日常的業務對接之外,還在物料體系、工程平臺、搭建平臺、效能體驗、雲端應用、資料分析及視覺化等方向進行技術探索和實戰,推動並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

如果你想改變一直被事折騰,希望開始能折騰事;如果你想改變一直被告誡需要多些想法,卻無從破局;如果你想改變你有能力去做成那個結果,卻不需要你;如果你想改變你想做成的事需要一個團隊去支撐,但沒你帶人的位置;如果你想改變既定的節奏,將會是“5 年工作時間 3 年工作經驗”;如果你想改變本來悟性不錯,但總是有那一層窗戶紙的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望參與到隨著業務騰飛的過程,親手推動一個有著深入的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我覺得我們該聊聊。任何時間,等著你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章