淺談HTTP 緩存的那些事兒
前言
HTTP 緩存機(jī)制作為 Web 應(yīng)用性能優(yōu)化的重要手段,對于從事 Web 開發(fā)的同學(xué)們來說,應(yīng)該是知識體系的基礎(chǔ)環(huán)節(jié),也是想要成為前端架構(gòu)的必備技能。
緩存的作用
我們?yōu)槭裁词褂镁彺?,是因為緩存可以給我們的 Web 項目帶來以下好處,以提高性能和用戶體驗。
- 加快了瀏覽器加載網(wǎng)頁的速度;
- 減少了冗余的數(shù)據(jù)傳輸,節(jié)省網(wǎng)絡(luò)流量和帶寬;
- 減少服務(wù)器的負(fù)擔(dān),大大提高了網(wǎng)站的性能。
由于從本地緩存讀取靜態(tài)資源,加快瀏覽器的網(wǎng)頁加載速度是一定的,也確實的減少了數(shù)據(jù)傳輸,就提高網(wǎng)站性能來說,可能一兩個用戶的訪問對于減小服務(wù)器的負(fù)擔(dān)沒有明顯效果,但如果這個網(wǎng)站在高并發(fā)的情況下,使用緩存對于減小服務(wù)器壓力和整個網(wǎng)站的性能都會發(fā)生質(zhì)的變化。
緩存規(guī)則簡介
為了方便理解,我們認(rèn)為瀏覽器存在一個緩存數(shù)據(jù)庫,用于存儲緩存信息(實際上靜態(tài)資源是被緩存到了內(nèi)存和磁盤中),在瀏覽器第一次請求數(shù)據(jù)時,此時緩存數(shù)據(jù)庫沒有對應(yīng)的緩存數(shù)據(jù),則需要請求服務(wù)器,服務(wù)器會將緩存規(guī)則和數(shù)據(jù)返回,瀏覽器將緩存規(guī)則和數(shù)據(jù)存儲進(jìn)緩存數(shù)據(jù)庫。
當(dāng)瀏覽器地址欄輸入地址后請求的 index.html
是不會被緩存的,但 index.html
內(nèi)部請求的其他資源會遵循緩存策略,HTTP 緩存有多種規(guī)則,根據(jù)是否需要向服務(wù)器發(fā)送請求主要分為兩大類,強(qiáng)制緩存和協(xié)商緩存。
強(qiáng)制緩存
1、強(qiáng)制緩存流程
強(qiáng)制緩存是第一次訪問服務(wù)器獲取數(shù)據(jù)后,在有效時間內(nèi)不會再請求服務(wù)器,而是直接使用緩存數(shù)據(jù),強(qiáng)制緩存的流程如下。
2、強(qiáng)制緩存判斷到期時間
那么如何判斷緩存是否到期呢?其實還是根據(jù)第一次訪問時服務(wù)器的響應(yīng)頭來實現(xiàn)的,在 HTTP 1.0
版本和 HTTP 1.1
版本有所不同。
在 HTTP 1.0
版本,服務(wù)器使用的響應(yīng)頭字段為 Expires
,值為未來的絕對時間(時間戳),瀏覽器請求時的當(dāng)前時間超過了 Expires
設(shè)置的時間,代表緩存失效,需要再次向服務(wù)器發(fā)送請求,否則都會直接從緩存數(shù)據(jù)庫中獲取數(shù)據(jù)。
在 HTTP 1.1
版本,服務(wù)器使用的響應(yīng)頭字段為 Cache-Control
,有多個值,意義各不相同。
- private:客戶端可以緩存;
- public:客戶端和代理服務(wù)器都可以緩存(對于前端而言,可以認(rèn)為與
private
效果相同); - max-age=xxx:緩存的內(nèi)容將在
xxx
秒后過期(相對時間,秒為單位); - no-cache:需要使用協(xié)商緩存(后面介紹)來驗證數(shù)據(jù)是否過期;
- no-store:所有內(nèi)容都不會緩存,強(qiáng)制緩存和協(xié)商緩存都不會觸發(fā)。
Cache-Control
的值中最常用的為 max-age=xxx
,緩存本身就是為了數(shù)據(jù)傳輸?shù)膬?yōu)化和性能而存在的,所以 no-store
幾乎不會使用。
注意:在 HTTP 1.0
版本中,Expires
字段的絕對時間是從服務(wù)器獲取的,由于請求需要時間,所以瀏覽器的請求時間與服務(wù)器接收到請求所獲取的時間是存在誤差的,這也導(dǎo)致了緩存命中的誤差,在 HTTP 1.1
版本中,因為 Cache-Control
的值 max-age=xxx
中的 xxx
是以秒為單位的相對時間,所以在瀏覽器接收到資源后開始倒計時,規(guī)避了 HTTP 1.0
中緩存命中存在誤差的缺點,為了兼容低版本 HTTP 協(xié)議,正常開發(fā)中兩種響應(yīng)頭會同時使用,HTTP 1.1
版本的實現(xiàn)優(yōu)先級高于 HTTP 1.0
。
3、通過 Network 查看強(qiáng)制緩存
我們通過 Chrome 瀏覽器的開發(fā)者工具,打開 NetWork 查看強(qiáng)制緩存的相關(guān)信息。
上面是百度網(wǎng)站 Logo 圖片的響應(yīng),我們可以清楚的看到,其中兼容了 HTTP 1.0
和 HTTP 1.1
版本,并使用強(qiáng)制緩存存儲了 10
年。
下面看一看通過緩存取出的數(shù)據(jù)在 Network 中與其他資源的區(qū)別。
其實緩存的儲存是內(nèi)存和磁盤兩個位置,由當(dāng)前瀏覽器本身的策略決定,比較隨機(jī),從內(nèi)存的緩存中取出的數(shù)據(jù)會顯示 (from memory cache)
,從磁盤的緩存中取出的數(shù)據(jù)會顯示 (from disk cache)
。
4、NodeJS 服務(wù)器實現(xiàn)強(qiáng)制緩存
// 強(qiáng)制緩存 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"); // 設(shè)置強(qiáng)制緩存 res.setHeader("Expires", new Date(Date.now() + 30000).toGMTString()); res.setHeader("Cache-Control", "max-age=30"); // 設(shè)置文件類型并響應(yīng)給瀏覽器 res.setHeader("Content-Type", `${mime.getType(p)};charset=utf8`); fs.createReadStream(p).pipe(res); }); }); server.listen(3000, () => { console.log("server start 3000"); });
上面 mime
模塊的 getType
方法可以成功返回傳入路徑下文件對應(yīng)的文件類型,如 text/html
和 application/javascript
等,是第三方模塊,使用之前需要安裝。
npm install mime
協(xié)商緩存
1、協(xié)商緩存流程
協(xié)商緩存又叫對比緩存,設(shè)置協(xié)商緩存后,第一次訪問服務(wù)器獲取數(shù)據(jù)時,服務(wù)器會將數(shù)據(jù)和緩存標(biāo)識一起返回給瀏覽器,客戶端會將數(shù)據(jù)和標(biāo)識存入緩存數(shù)據(jù)庫中,下一次請求時,會先去緩存中取出緩存標(biāo)識發(fā)送給服務(wù)器進(jìn)行詢問,當(dāng)服務(wù)器數(shù)據(jù)更改時會更新標(biāo)識,所以服務(wù)器拿到瀏覽器發(fā)來的標(biāo)識進(jìn)行對比,相同代表數(shù)據(jù)未更改,響應(yīng)瀏覽器通知數(shù)據(jù)未更改,瀏覽器會去緩存中獲取數(shù)據(jù),如果標(biāo)識不同,代表服務(wù)器更改過數(shù)據(jù),所以會將新的數(shù)據(jù)和新的標(biāo)識返回瀏覽器,瀏覽器會將新的數(shù)據(jù)和標(biāo)識存入緩存中,協(xié)商緩存的流程如下。
協(xié)商緩存和強(qiáng)制緩存不同的是,協(xié)商緩存每次請求都需要跟服務(wù)器通信,而且命中緩存服務(wù)器返回狀態(tài)碼不再是 200
,而是 304
。
2、協(xié)商緩存判斷標(biāo)識
強(qiáng)制緩存是通過過期時間來控制是否訪問服務(wù)器,而協(xié)商緩存每次都要與服務(wù)器交互對比緩存標(biāo)識,同樣的,對于協(xié)商緩存的實現(xiàn)在 HTTP 1.0
版本和 HTTP 1.1
版本也有所不同。
在 HTTP 1.0
版本中,服務(wù)器通過 Last-Modified
響應(yīng)頭來設(shè)置緩存標(biāo)識,通常取請求數(shù)據(jù)的最后修改時間(絕對時間)作為值,而瀏覽器將接收到返回的數(shù)據(jù)和標(biāo)識存入緩存,再次請求會自動發(fā)送 If-Modified-Since
請求頭,值為之前返回的最后修改時間(標(biāo)識),服務(wù)器取出 If-Modified-Since
的值與數(shù)據(jù)的上次修改時間對比,如果上次修改時間大于了 If-Modified-Since
的值,說明被修改過,則通過 Last-Modified
響應(yīng)頭返回新的最后修改時間和新的數(shù)據(jù),否則未被修改,返回狀態(tài)碼 304
通知瀏覽器命中緩存。
在 HTTP 1.1
版本中,服務(wù)器通過 Etag
響應(yīng)頭來設(shè)置緩存標(biāo)識(唯一標(biāo)識,像一個指紋一樣,生成規(guī)則由服務(wù)器來決定),瀏覽器接收到數(shù)據(jù)和唯一標(biāo)識后存入緩存,下次請求時,通過 If-None-Match
請求頭將唯一標(biāo)識帶給服務(wù)器,服務(wù)器取出唯一標(biāo)識與之前的標(biāo)識對比,不同,說明修改過,返回新標(biāo)識和數(shù)據(jù),相同,則返回狀態(tài)碼 304
通知瀏覽器命中緩存。
HTTP 協(xié)商緩存策略流程圖如下:
注意:使用協(xié)商緩存時 HTTP 1.0
版本還是不太靠譜,假設(shè)一個文件增加了一個字符后又刪除了,文件相當(dāng)于沒更改,但是最后修改時間變了,會被當(dāng)作修改處理,本應(yīng)該命中緩存,服務(wù)器卻重新發(fā)送了數(shù)據(jù),因此 HTTP 1.1
中使用的 Etag
唯一標(biāo)識是根據(jù)文件內(nèi)容或摘要生成的,保證了只要文件內(nèi)容不變,則一定會命中緩存,為了兼容低版本 HTTP 協(xié)議,開發(fā)中兩種響應(yīng)頭也會同時使用,同樣 HTTP 1.1
版本的實現(xiàn)優(yōu)先級高于 HTTP 1.0
。
3、通過 Network 查看協(xié)商緩存
我們同樣通過 Chrome 瀏覽器的開發(fā)者工具,打開 NetWork 查看協(xié)商緩存的相關(guān)信息。
再次請求服務(wù)器的請求頭信息:
命中協(xié)商緩存的響應(yīng)頭信息:
下面看一看通過協(xié)商緩存取出的數(shù)據(jù)在 Network 中與第一次加載的區(qū)別。
第一次請求:
緩存后請求:
通過兩圖的對比,我們可以發(fā)現(xiàn),協(xié)商緩存生效時的狀態(tài)碼為 304
,并且報文大小和請求時間大大減少,原因是服務(wù)端在進(jìn)行標(biāo)識比對后只返回了 header
部分,通過狀態(tài)碼來通知瀏覽器使用緩存,不再需要將報文主體部分一起返回給瀏覽器。
4、NodeJS 服務(wù)器實現(xiàn)協(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)建加密的轉(zhuǎn)換流 let rs = fs.createReadStream(p); // 創(chuàng)建可讀流 // 讀取文件內(nèi)容并加密 rs.on("data", data => md5.update(data)); rs.on("end", () => { let ctime = statObj.ctime.toGMTString(); // 獲取文件最后修改時間 let flag = md5.digest("hex"); // 獲取加密后的唯一標(biāo)識 // 獲取協(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 { // 設(shè)置協(xié)商緩存 res.setHeader("Last-Modified", ctime); res.setHeader("Etag", flag); // 設(shè)置文件類型并響應(yīng)給瀏覽器 res.setHeader("Content-Type", `${mime.getType(p)};charset=utf8`); rs.pipe(res); } }); }); }); server.listen(3000, () => { console.log("server start 3000"); });
在上面的代碼中是通過可讀流讀取文件內(nèi)容,并通過 crypto
模塊進(jìn)行了 md5
加密后的結(jié)果作為了唯一標(biāo)識,這樣就能保證只要文件內(nèi)容不變,就會命中緩存,其中兼容了 HTTP 1.0
和 HTTP 1.1
兩個版本,只要滿足一個則直接返回 304
通知瀏覽器命中緩存。
注意:其實讀取文件內(nèi)容加密這種做法并不可取,假如讀取的是大文件,在讀取文件內(nèi)容和進(jìn)行 md5
加密這個過程會非常消耗時間,所以在開發(fā)中要針對業(yè)務(wù)的實際情況選擇可以保證服務(wù)器性能的方式生成唯一標(biāo)識,比如根據(jù)文件的摘要。
總結(jié)
為了使緩存策略更加健壯、靈活,HTTP 1.0
版本 和 HTTP 1.1
版本的緩存策略會同時使用,甚至強(qiáng)制緩存和協(xié)商緩存也會同時使用,對于強(qiáng)制緩存,服務(wù)器通知瀏覽器一個緩存時間,在緩存時間內(nèi),下次請求,直接使用緩存,超出有效時間,執(zhí)行協(xié)商緩存策略,對于協(xié)商緩存,將緩存信息中的 Etag
和 Last-Modified
通過請求頭 If-None-Match
和 If-Modified-Since
發(fā)送給服務(wù)器,由服務(wù)器校驗同時設(shè)置新的強(qiáng)制緩存,校驗通過并返回 304
狀態(tài)碼時,瀏覽器直接使用緩存,如果協(xié)商緩存也未命中,則服務(wù)器重新設(shè)置協(xié)商緩存的標(biāo)識。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
淺談node如何優(yōu)雅地獲取mac系統(tǒng)版本
這篇文章主要和大家聊聊node如何優(yōu)雅地獲取mac系統(tǒng)版本,文中有詳細(xì)的代碼示例和流程步驟,對我們學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-06-06淺析Node.js的Stream模塊中的Readable對象
這篇文章主要介紹了淺析Node.js的Stream模塊中的Readable對象,是Node.js入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2015-07-07[將免費進(jìn)行到底]在Amazon的一年免費服務(wù)器上安裝Node.JS, NPM和OurJS博客
此文是介紹如何在Amazon的一年免費計劃上安裝Node.JS, NPM還有搭建OurJS博客系統(tǒng)。,需要的朋友可以參考下2014-08-08參考EventEmitter實現(xiàn)完整訂閱發(fā)布功能函數(shù)
這篇文章主要為大家介紹了參考EventEmitter實現(xiàn)完整訂閱發(fā)布功能函數(shù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02如何使用axios庫在Node.js中進(jìn)行代理請求(實踐案例)
axios是一個強(qiáng)大的基于Promise的HTTP客戶端,它在瀏覽器和Node.js環(huán)境中均可使用,本文將介紹如何充分利用axios庫,在Node.js中進(jìn)行代理請求的最佳實踐,并通過一個實際案例來展示其應(yīng)用,感興趣的朋友一起看看吧2024-03-03Node+Express+MongoDB實現(xiàn)登錄注冊功能實例
這篇文章主要介紹了Node+Express+MongoDB實現(xiàn)登錄注冊功能,需要的朋友可以參考下2017-04-04node.js中的buffer.Buffer.byteLength方法使用說明
這篇文章主要介紹了node.js中的buffer.Buffer.byteLength方法使用說明,本文介紹了buffer.Buffer.byteLength的方法說明、語法、接收參數(shù)、使用實例和實現(xiàn)源碼,需要的朋友可以參考下2014-12-12