淺談HTTP 緩存的那些事兒
前言
HTTP 緩存機制作為 Web 應用性能優(yōu)化的重要手段,對于從事 Web 開發(fā)的同學們來說,應該是知識體系的基礎環(huán)節(jié),也是想要成為前端架構的必備技能。
緩存的作用
我們?yōu)槭裁词褂镁彺?,是因為緩存可以給我們的 Web 項目帶來以下好處,以提高性能和用戶體驗。
- 加快了瀏覽器加載網頁的速度;
- 減少了冗余的數據傳輸,節(jié)省網絡流量和帶寬;
- 減少服務器的負擔,大大提高了網站的性能。
由于從本地緩存讀取靜態(tài)資源,加快瀏覽器的網頁加載速度是一定的,也確實的減少了數據傳輸,就提高網站性能來說,可能一兩個用戶的訪問對于減小服務器的負擔沒有明顯效果,但如果這個網站在高并發(fā)的情況下,使用緩存對于減小服務器壓力和整個網站的性能都會發(fā)生質的變化。
緩存規(guī)則簡介
為了方便理解,我們認為瀏覽器存在一個緩存數據庫,用于存儲緩存信息(實際上靜態(tài)資源是被緩存到了內存和磁盤中),在瀏覽器第一次請求數據時,此時緩存數據庫沒有對應的緩存數據,則需要請求服務器,服務器會將緩存規(guī)則和數據返回,瀏覽器將緩存規(guī)則和數據存儲進緩存數據庫。

當瀏覽器地址欄輸入地址后請求的 index.html 是不會被緩存的,但 index.html 內部請求的其他資源會遵循緩存策略,HTTP 緩存有多種規(guī)則,根據是否需要向服務器發(fā)送請求主要分為兩大類,強制緩存和協(xié)商緩存。
強制緩存
1、強制緩存流程
強制緩存是第一次訪問服務器獲取數據后,在有效時間內不會再請求服務器,而是直接使用緩存數據,強制緩存的流程如下。

2、強制緩存判斷到期時間
那么如何判斷緩存是否到期呢?其實還是根據第一次訪問時服務器的響應頭來實現的,在 HTTP 1.0 版本和 HTTP 1.1 版本有所不同。
在 HTTP 1.0 版本,服務器使用的響應頭字段為 Expires,值為未來的絕對時間(時間戳),瀏覽器請求時的當前時間超過了 Expires 設置的時間,代表緩存失效,需要再次向服務器發(fā)送請求,否則都會直接從緩存數據庫中獲取數據。
在 HTTP 1.1 版本,服務器使用的響應頭字段為 Cache-Control,有多個值,意義各不相同。
- private:客戶端可以緩存;
- public:客戶端和代理服務器都可以緩存(對于前端而言,可以認為與
private效果相同); - max-age=xxx:緩存的內容將在
xxx秒后過期(相對時間,秒為單位); - no-cache:需要使用協(xié)商緩存(后面介紹)來驗證數據是否過期;
- no-store:所有內容都不會緩存,強制緩存和協(xié)商緩存都不會觸發(fā)。
Cache-Control 的值中最常用的為 max-age=xxx,緩存本身就是為了數據傳輸的優(yōu)化和性能而存在的,所以 no-store 幾乎不會使用。
注意:在 HTTP 1.0 版本中,Expires 字段的絕對時間是從服務器獲取的,由于請求需要時間,所以瀏覽器的請求時間與服務器接收到請求所獲取的時間是存在誤差的,這也導致了緩存命中的誤差,在 HTTP 1.1 版本中,因為 Cache-Control 的值 max-age=xxx 中的 xxx 是以秒為單位的相對時間,所以在瀏覽器接收到資源后開始倒計時,規(guī)避了 HTTP 1.0 中緩存命中存在誤差的缺點,為了兼容低版本 HTTP 協(xié)議,正常開發(fā)中兩種響應頭會同時使用,HTTP 1.1 版本的實現優(yōu)先級高于 HTTP 1.0。
3、通過 Network 查看強制緩存
我們通過 Chrome 瀏覽器的開發(fā)者工具,打開 NetWork 查看強制緩存的相關信息。

上面是百度網站 Logo 圖片的響應,我們可以清楚的看到,其中兼容了 HTTP 1.0 和 HTTP 1.1 版本,并使用強制緩存存儲了 10 年。
下面看一看通過緩存取出的數據在 Network 中與其他資源的區(qū)別。

其實緩存的儲存是內存和磁盤兩個位置,由當前瀏覽器本身的策略決定,比較隨機,從內存的緩存中取出的數據會顯示 (from memory cache),從磁盤的緩存中取出的數據會顯示 (from disk cache)。
4、NodeJS 服務器實現強制緩存
// 強制緩存
const http = require("http");
const url = require("url");
const path = require("path");
const mime = require("mime");
const fs = require("fs");
const 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
協(xié)商緩存
1、協(xié)商緩存流程
協(xié)商緩存又叫對比緩存,設置協(xié)商緩存后,第一次訪問服務器獲取數據時,服務器會將數據和緩存標識一起返回給瀏覽器,客戶端會將數據和標識存入緩存數據庫中,下一次請求時,會先去緩存中取出緩存標識發(fā)送給服務器進行詢問,當服務器數據更改時會更新標識,所以服務器拿到瀏覽器發(fā)來的標識進行對比,相同代表數據未更改,響應瀏覽器通知數據未更改,瀏覽器會去緩存中獲取數據,如果標識不同,代表服務器更改過數據,所以會將新的數據和新的標識返回瀏覽器,瀏覽器會將新的數據和標識存入緩存中,協(xié)商緩存的流程如下。

協(xié)商緩存和強制緩存不同的是,協(xié)商緩存每次請求都需要跟服務器通信,而且命中緩存服務器返回狀態(tài)碼不再是 200,而是 304。
2、協(xié)商緩存判斷標識
強制緩存是通過過期時間來控制是否訪問服務器,而協(xié)商緩存每次都要與服務器交互對比緩存標識,同樣的,對于協(xié)商緩存的實現在 HTTP 1.0 版本和 HTTP 1.1 版本也有所不同。
在 HTTP 1.0 版本中,服務器通過 Last-Modified 響應頭來設置緩存標識,通常取請求數據的最后修改時間(絕對時間)作為值,而瀏覽器將接收到返回的數據和標識存入緩存,再次請求會自動發(fā)送 If-Modified-Since 請求頭,值為之前返回的最后修改時間(標識),服務器取出 If-Modified-Since 的值與數據的上次修改時間對比,如果上次修改時間大于了 If-Modified-Since 的值,說明被修改過,則通過 Last-Modified 響應頭返回新的最后修改時間和新的數據,否則未被修改,返回狀態(tài)碼 304 通知瀏覽器命中緩存。
在 HTTP 1.1 版本中,服務器通過 Etag 響應頭來設置緩存標識(唯一標識,像一個指紋一樣,生成規(guī)則由服務器來決定),瀏覽器接收到數據和唯一標識后存入緩存,下次請求時,通過 If-None-Match 請求頭將唯一標識帶給服務器,服務器取出唯一標識與之前的標識對比,不同,說明修改過,返回新標識和數據,相同,則返回狀態(tài)碼 304 通知瀏覽器命中緩存。
HTTP 協(xié)商緩存策略流程圖如下:

注意:使用協(xié)商緩存時 HTTP 1.0 版本還是不太靠譜,假設一個文件增加了一個字符后又刪除了,文件相當于沒更改,但是最后修改時間變了,會被當作修改處理,本應該命中緩存,服務器卻重新發(fā)送了數據,因此 HTTP 1.1 中使用的 Etag 唯一標識是根據文件內容或摘要生成的,保證了只要文件內容不變,則一定會命中緩存,為了兼容低版本 HTTP 協(xié)議,開發(fā)中兩種響應頭也會同時使用,同樣 HTTP 1.1 版本的實現優(yōu)先級高于 HTTP 1.0。
3、通過 Network 查看協(xié)商緩存
我們同樣通過 Chrome 瀏覽器的開發(fā)者工具,打開 NetWork 查看協(xié)商緩存的相關信息。
再次請求服務器的請求頭信息:

命中協(xié)商緩存的響應頭信息:

下面看一看通過協(xié)商緩存取出的數據在 Network 中與第一次加載的區(qū)別。
第一次請求:

緩存后請求:

通過兩圖的對比,我們可以發(fā)現,協(xié)商緩存生效時的狀態(tài)碼為 304,并且報文大小和請求時間大大減少,原因是服務端在進行標識比對后只返回了 header 部分,通過狀態(tài)碼來通知瀏覽器使用緩存,不再需要將報文主體部分一起返回給瀏覽器。
4、NodeJS 服務器實現協(xié)商緩存
// 協(xié)商緩存
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");
const 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"); // 創(chuàng)建加密的轉換流
let rs = fs.createReadStream(p); // 創(chuàng)建可讀流
// 讀取文件內容并加密
rs.on("data", data => md5.update(data));
rs.on("end", () => {
let ctime = statObj.ctime.toGMTString(); // 獲取文件最后修改時間
let flag = md5.digest("hex"); // 獲取加密后的唯一標識
// 獲取協(xié)商緩存的請求頭
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 {
// 設置協(xié)商緩存
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 加密這個過程會非常消耗時間,所以在開發(fā)中要針對業(yè)務的實際情況選擇可以保證服務器性能的方式生成唯一標識,比如根據文件的摘要。
總結
為了使緩存策略更加健壯、靈活,HTTP 1.0 版本 和 HTTP 1.1 版本的緩存策略會同時使用,甚至強制緩存和協(xié)商緩存也會同時使用,對于強制緩存,服務器通知瀏覽器一個緩存時間,在緩存時間內,下次請求,直接使用緩存,超出有效時間,執(zhí)行協(xié)商緩存策略,對于協(xié)商緩存,將緩存信息中的 Etag 和 Last-Modified 通過請求頭 If-None-Match 和 If-Modified-Since 發(fā)送給服務器,由服務器校驗同時設置新的強制緩存,校驗通過并返回 304 狀態(tài)碼時,瀏覽器直接使用緩存,如果協(xié)商緩存也未命中,則服務器重新設置協(xié)商緩存的標識。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
express搭建的nodejs項目使用webpack進行壓縮打包
對于打包這個問題它并不是難點,但是對于我們這種初學者來說,根本就不知道應該怎么做,下面這篇文章主要給大家介紹了關于express搭建的nodejs項目使用webpack進行壓縮打包的相關資料,需要的朋友可以參考下2022-12-12
Node.js創(chuàng)建一個Express服務的方法詳解
這篇文章主要介紹了Node.js創(chuàng)建一個Express服務的方法,結合實例形式分析了node.js創(chuàng)建Express服務的具體步驟、實現方法及相關操作技巧,需要的朋友可以參考下2020-01-01

