HTTP 緩存機制做爲 Web 應用性能優化的重要手段,對於從事 Web 開發的同窗們來講,應該是知識體系的基礎環節,也是想要成爲前端架構的必備技能。javascript
咱們爲何使用緩存,是由於緩存能夠給咱們的 Web 項目帶來如下好處,以提升性能和用戶體驗。html
因爲從本地緩存讀取靜態資源,加快瀏覽器的網頁加載速度是必定的,也確實的減小了數據傳輸,就提升網站性能來講,可能一兩個用戶的訪問對於減少服務器的負擔沒有明顯效果,但若是這個網站在高併發的狀況下,使用緩存對於減少服務器壓力和整個網站的性能都會發生質的變化。前端
爲了方便理解,咱們認爲瀏覽器存在一個緩存數據庫,用於存儲緩存信息(實際上靜態資源是被緩存到了內存和磁盤中),在瀏覽器第一次請求數據時,此時緩存數據庫沒有對應的緩存數據,則須要請求服務器,服務器會將緩存規則和數據返回,瀏覽器將緩存規則和數據存儲進緩存數據庫。java
當瀏覽器地址欄輸入地址後請求的 index.html
是不會被緩存的,但 index.html
內部請求的其餘資源會遵循緩存策略,HTTP 緩存有多種規則,根據是否須要向服務器發送請求主要分爲兩大類,強制緩存和協商緩存。數據庫
強制緩存是第一次訪問服務器獲取數據後,在有效時間內不會再請求服務器,而是直接使用緩存數據,強制緩存的流程以下。npm
那麼如何判斷緩存是否到期呢?其實仍是根據第一次訪問時服務器的響應頭來實現的,在 HTTP 1.0
版本和 HTTP 1.1
版本有所不一樣。瀏覽器
在 HTTP 1.0
版本,服務器使用的響應頭字段爲 Expires
,值爲將來的絕對時間(時間戳),瀏覽器請求時的當前時間超過了 Expires
設置的時間,表明緩存失效,須要再次向服務器發送請求,不然都會直接從緩存數據庫中獲取數據。緩存
在 HTTP 1.1
版本,服務器使用的響應頭字段爲 Cache-Control
,有多個值,意義各不相同。性能優化
private
效果相同);xxx
秒後過時(相對時間,秒爲單位);Cache-Control
的值中最經常使用的爲 max-age=xxx
,緩存自己就是爲了數據傳輸的優化和性能而存在的,因此 no-store
幾乎不會使用。服務器
注意:在 HTTP 1.0
版本中,Expires
字段的絕對時間是從服務器獲取的,因爲請求須要時間,因此瀏覽器的請求時間與服務器接收到請求所獲取的時間是存在偏差的,這也致使了緩存命中的偏差,在 HTTP 1.1
版本中,由於 Cache-Control
的值 max-age=xxx
中的 xxx
是以秒爲單位的相對時間,因此在瀏覽器接收到資源後開始倒計時,規避了 HTTP 1.0
中緩存命中存在偏差的缺點,爲了兼容低版本 HTTP 協議,正常開發中兩種響應頭會同時使用,HTTP 1.1
版本的實現優先級高於 HTTP 1.0
。
咱們經過 Chrome 瀏覽器的開發者工具,打開 NetWork 查看強制緩存的相關信息。
上面是百度網站 Logo 圖片的響應,咱們能夠清楚的看到,其中兼容了 HTTP 1.0
和 HTTP 1.1
版本,並使用強制緩存存儲了 10
年。
下面看一看經過緩存取出的數據在 Network 中與其餘資源的區別。
其實緩存的儲存是內存和磁盤兩個位置,由當前瀏覽器自己的策略決定,比較隨機,從內存的緩存中取出的數據會顯示 (from memory cache)
,從磁盤的緩存中取出的數據會顯示 (from disk cache)
。
// 強制緩存
const http = require("http");
const url = require("url");
const path = require("path");
const mime = require("mime");
const fs = require("fs");
let server = http.createServer((req, res) => {
let { pathname } = url.parse(req.url, true);
pathname = pathname !== "/" ? pathname : "/index.html";
// 獲取讀取文件的絕對路徑
let p = path.join(__dirname, pathname);
// 查看路徑是否合法
fs.access(p, err => {
// 路徑不合法則直接中斷鏈接
if (err) return res.end("Not Found");
// 設置強制緩存
res.setHeader("Expires", new Date(Date.now() + 30000).toGMTString());
res.setHeader("Cache-Control", "max-age=30");
// 設置文件類型並響應給瀏覽器
res.setHeader("Content-Type", `${mime.getType(p)};charset=utf8`);
fs.createReadStream(p).pipe(res);
});
});
server.listen(3000, () => {
console.log("server start 3000");
});
複製代碼
上面 mime
模塊的 getType
方法能夠成功返回傳入路徑下文件對應的文件類型,如 text/html
和 application/javascript
等,是第三方模塊,使用以前須要安裝。
npm install mime
協商緩存又叫對比緩存,設置協商緩存後,第一次訪問服務器獲取數據時,服務器會將數據和緩存標識一塊兒返回給瀏覽器,客戶端會將數據和標識存入緩存數據庫中,下一次請求時,會先去緩存中取出緩存標識發送給服務器進行詢問,當服務器數據更改時會更新標識,因此服務器拿到瀏覽器發來的標識進行對比,相同表明數據未更改,響應瀏覽器通知數據未更改,瀏覽器會去緩存中獲取數據,若是標識不一樣,表明服務器更改過數據,因此會將新的數據和新的標識返回瀏覽器,瀏覽器會將新的數據和標識存入緩存中,協商緩存的流程以下。
協商緩存和強制緩存不一樣的是,協商緩存每次請求都須要跟服務器通訊,並且命中緩存服務器返回狀態碼再也不是 200
,而是 304
。
強制緩存是經過過時時間來控制是否訪問服務器,而協商緩存每次都要與服務器交互對比緩存標識,一樣的,對於協商緩存的實如今 HTTP 1.0
版本和 HTTP 1.1
版本也有所不一樣。
在 HTTP 1.0
版本中,服務器經過 Last-Modified
響應頭來設置緩存標識,一般取請求數據的最後修改時間(絕對時間)做爲值,而瀏覽器將接收到返回的數據和標識存入緩存,再次請求會自動發送 If-Modified-Since
請求頭,值爲以前返回的最後修改時間(標識),服務器取出 If-Modified-Since
的值與數據的上次修改時間對比,若是上次修改時間大於了 If-Modified-Since
的值,說明被修改過,則經過 Last-Modified
響應頭返回新的最後修改時間和新的數據,不然未被修改,返回狀態碼 304
通知瀏覽器命中緩存。
在 HTTP 1.1
版本中,服務器經過 Etag
響應頭來設置緩存標識(惟一標識,像一個指紋同樣,生成規則由服務器來決定),瀏覽器接收到數據和惟一標識後存入緩存,下次請求時,經過 If-None-Match
請求頭將惟一標識帶給服務器,服務器取出惟一標識與以前的標識對比,不一樣,說明修改過,返回新標識和數據,相同,則返回狀態碼 304
通知瀏覽器命中緩存。
HTTP 協商緩存策略流程圖以下:
注意:使用協商緩存時 HTTP 1.0
版本仍是不太靠譜,假設一個文件增長了一個字符後又刪除了,文件至關於沒更改,可是最後修改時間變了,會被看成修改處理,本應該命中緩存,服務器卻從新發送了數據,所以 HTTP 1.1
中使用的 Etag
惟一標識是根據文件內容或摘要生成的,保證了只要文件內容不變,則必定會命中緩存,爲了兼容低版本 HTTP 協議,開發中兩種響應頭也會同時使用,一樣 HTTP 1.1
版本的實現優先級高於 HTTP 1.0
。
咱們一樣經過 Chrome 瀏覽器的開發者工具,打開 NetWork 查看協商緩存的相關信息。
再次請求服務器的請求頭信息:
命中協商緩存的響應頭信息:
下面看一看經過協商緩存取出的數據在 Network 中與第一次加載的區別。
第一次請求:
緩存後請求:
經過兩圖的對比,咱們能夠發現,協商緩存生效時的狀態碼爲 304
,而且報文大小和請求時間大大減小,緣由是服務端在進行標識比對後只返回了 header
部分,經過狀態碼來通知瀏覽器使用緩存,再也不須要將報文主體部分一塊兒返回給瀏覽器。
// 協商緩存
const http = require("http");
const url = require("url");
const path = require("path");
const mime = require("mime");
const fs = require("fs");0
const crytpo = require("crytpo");
let server = http.createServer((req, res) => {
let { pathname } = url.parse(req.url, true);
pathname = pathname !== "/" ? pathname : "/index.html";
// 獲取讀取文件的絕對路徑
let p = path.join(__dirname, pathname);
// 查看路徑是否合法
fs.stat(p, (err, statObj) => {
// 路徑不合法則直接中斷鏈接
if (err) return res.end("Not Found");
let md5 = crypto.createHash("md5"); // 建立加密的轉換流
let rs = fs.createReadStream(p); // 建立可讀流
// 讀取文件內容並加密
rs.on("data", data => md5.update(data));
rs.on("end", () => {
let ctime = statObj.ctime.toGMTString(); // 獲取文件最後修改時間
let flag = md5.digest("hex"); // 獲取加密後的惟一標識
// 獲取協商緩存的請求頭
let ifModifiedSince = req.headers["if-modified-since"];
let ifNoneMatch = req.headers["if-none-match"];
if (ifModifiedSince === ctime || ifNoneMatch === flag) {
res.statusCode = 304;
res.end();
} else {
// 設置協商緩存
res.setHeader("Last-Modified", ctime);
res.setHeader("Etag", flag);
// 設置文件類型並響應給瀏覽器
res.setHeader("Content-Type", `${mime.getType(p)};charset=utf8`);
rs.pipe(res);
}
});
});
});
server.listen(3000, () => {
console.log("server start 3000");
});
複製代碼
在上面的代碼中是經過可讀流讀取文件內容,並經過 crypto
模塊進行了 md5
加密後的結果做爲了惟一標識,這樣就能保證只要文件內容不變,就會命中緩存,其中兼容了 HTTP 1.0
和 HTTP 1.1
兩個版本,只要知足一個則直接返回 304
通知瀏覽器命中緩存。
注意:其實讀取文件內容加密這種作法並不可取,假如讀取的是大文件,在讀取文件內容和進行 md5
加密這個過程會很是消耗時間,因此在開發中要針對業務的實際狀況選擇能夠保證服務器性能的方式生成惟一標識,好比根據文件的摘要。
爲了使緩存策略更加健壯、靈活,HTTP 1.0
版本 和 HTTP 1.1
版本的緩存策略會同時使用,甚至強制緩存和協商緩存也會同時使用,對於強制緩存,服務器通知瀏覽器一個緩存時間,在緩存時間內,下次請求,直接使用緩存,超出有效時間,執行協商緩存策略,對於協商緩存,將緩存信息中的 Etag
和 Last-Modified
經過請求頭 If-None-Match
和 If-Modified-Since
發送給服務器,由服務器校驗同時設置新的強制緩存,校驗經過並返回 304
狀態碼時,瀏覽器直接使用緩存,若是協商緩存也未命中,則服務器從新設置協商緩存的標識。