http強制緩存、協商緩存、指紋ETag詳解
每個瀏覽器都有一個自己的緩存區,使用緩存區的數據有諸多好處,減少冗餘的數據傳輸,節省網絡傳輸。減少服務器負擔, 提高網站的性能。加快客戶端加載網頁的速度等,而這裡指的緩存,指代的靜態文件的緩存,動態數據緩存需要走redis。今天我們使用node搭建服務,簡單演示一下幾種緩存的設置及配合使用。
緩存分為disk cache
和 memory cache
兩種,瀏覽器自行處理,代碼層面無法控制。而我們一般在用的時候都是在nginx
層做處理,但核心是一樣的,都是設置header
簡單說一下,Chrome瀏覽器的緩存文件位置在哪,感興趣的同學可以自己找一找:
-
chrome瀏覽器地址欄中輸入:
chrome://version/
-
找到個人資料路徑(我的是):
C:\Users\Lenovo\AppData\Local\Google\Chrome\User Data\Default
-
計算機中找到對應的目錄,可以在這個目錄下查看到
Cache
和Code Cache
目錄,這個就是緩存文件目錄進入對應的目錄,可以進行手動刪除
實操目錄及步驟
初始化package.json: npm init -y
下載第三方模塊:npm i mime
.
│
└─cache
├─node_modules
├─public // 靜態文件目錄
├─1.js // 請求的文件資源
├─index.html
├─1.cache.js // 強制緩存 完整代碼案例
├─2.cache.js // 協商緩存 完整代碼案例
├─3.cache.js // 指紋對比 完整代碼案例
緩存分類
強制緩存
:直接緩存至瀏覽器中,不會再次向服務器發送請求;對比緩存
:也叫協商緩存,客服各執一份文件修改時間
,相互對比,若相同用客戶端緩存指紋Etag
:為解決對比緩存存在的一些問題,客服各執一份文件簽名
,相互對比,若相同用客戶端緩存
強制緩存
-
服務器與瀏覽器約定一個緩存的最大存活時間,如10s,那麼10s內,瀏覽器請求相同的資源便不會在請求服務器,會默認走瀏覽器的緩存區,並且響應碼依然為
200
-
如果返回的是一個html,其中又引用了其他資源,還會繼續向服務器發送請求。
-
不對首次訪問的路徑做處理,也就是第一次訪問時,不走強制緩存的,必然會請求到服務器端,因為如果連首頁都走緩存了,那麼在斷網或服務器宕機的情況下也可以訪問該網站,顯然是不合理的
-
可以根據不同的文件後綴,設置不同的強制緩存的時間
-
在緩存數據生效期間,可以直接使用緩存數據,在沒有緩存數據時,瀏覽器向服務器請求數據,服務器會將數據和緩存規則一併返回,緩存規則信息包含在響應頭中。
-
強制緩存常用的
兩種響應頭設置
// 10s 表示當前時間 + 10s,屬於相對時間 (用於新版瀏覽器) res.setHeader('Cache-Control', 'max-age=10'); // 設置 絕對時間 (用於舊版瀏覽器或IE老版本 或 http1.0) // 設置header的值只能是數字,字符串或數組,不能為對象,new Date()返回的是對象,所以需要轉一下。 res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toUTCString());
-
完整代碼:
const http = require('http') const url = require('url') const path = require('path'); const fs = require('fs'); const mime = require('mime'); const server = http.createServer((req, res) => { let { pathname } = url.parse(req.url, true) let filepath = path.join(__dirname, 'public', pathname); // 訪問路徑拼接 public // 10s 表示當前時間 + 10s,屬於相對時間 (用於新版瀏覽器) res.setHeader('Cache-Control', 'max-age=10'); // 設置 絕對時間 (用於舊版瀏覽器或IE老版本 或 http1.0) // 設置header的值只能是數字,字符串或數組,不能為對象,new Date()返回的是對象,所以需要轉一下。 res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toUTCString()); fs.stat(filepath, function (err, statObj) { if (err) { // 獲取文件信息報錯,則則響應 404 res.statusCode = 404; res.end('Not Found!') } else { // 如果是文件,設置對應類型的響應頭,並返響應文件內容 if (statObj.isFile()) { res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8'); fs.createReadStream(filepath).pipe(res); } else { // 如果是目錄,需要找目錄下的 index.html let htmlPath = path.join(filepath, 'index.html') // 拼接路徑 fs.access(htmlPath, function (err) { if (err) { // 查看文件的可訪問性,如不能訪問則響應 404 res.statusCode = 404; res.end('Not Found!') } else { res.setHeader('Content-Type', 'text/html;charset=utf-8'); fs.createReadStream(htmlPath).pipe(res) } }) } } }) }); // 服務監聽 3000 端口 server.listen(3000, function () { console.log('server is running....'); })
對比緩存
-
瀏覽器首次請求資源時,服務器會將
緩存標識(文件修改時間)
與資源一同返回給瀏覽器。 -
再次請求時,客戶端請求頭會攜帶
緩存標識(If-Modified-Since)
,並在服務端對比兩個時間。 -
若相等,直接返回
304狀態碼
,讀取瀏覽器的緩存中對應緩存文件; -
若不相等,返回最新內容,並給文件設置新的修改時間。
-
對比緩存
不管是否生效,都需要與服務端發生交互
-
強制緩存和對比緩存可以配合使用,如10s內強制緩存,超過10s走對比緩存,同時在設置10s的強制緩存
-
響應頭設置
// no-cache: 需要使用對比緩存驗證數據,會向服務器發送請求,且數據會存到瀏覽器的緩存中 res.setHeader('Cache-Control', 'no-cache'); // 設置響應頭,文件的最後修改時間 res.setHeader('Last-Modified',ctime)
-
Last-Modify & If-Modified-Since
-
完整代碼:
const http = require('http') const url = require('url') const path = require('path'); const fs = require('fs'); const mime = require('mime'); const server = http.createServer((req, res) => { let { pathname } = url.parse(req.url, true) let filepath = path.join(__dirname, 'public', pathname); // 強制緩存和對比緩存配合使用,10s內走強制緩存,超過10s會走對比緩存,同時在設置10s的強制緩存 // res.setHeader('Cache-Control', 'max-age=10'); res.setHeader('Cache-Control', 'no-cache'); fs.stat(filepath, function (err, statObj) { if (err) { res.statusCode = 404; res.end('Not Found!') } else { // 如果是文件 if (statObj.isFile()) { const ctime = statObj.ctime.toGMTString(); // 判斷請求頭存儲的時間與服務器端文件的最後修改時間是否相等 if(req.headers['if-modified-since'] === ctime){ res.statusCode = 304; // 設置響應狀態碼,瀏覽器默認會自動解析,從緩存中讀取對應文件 res.end(); // 表示此時服務器沒有響應結果 }else{ // 設置響應頭,文件的最後修改時間 res.setHeader('Last-Modified',ctime) // 設置對應類型的響應頭,並返響應文件內容 res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8'); fs.createReadStream(filepath).pipe(res); } } else { // 如果是目錄,需要找目錄下的index.html let htmlPath = path.join(filepath, 'index.html') // 拼接路徑 fs.access(htmlPath, function (err) { if (err) { // 查看文件的可訪問性,如不能訪問則響應 404 res.statusCode = 404; res.end('Not Found!') } else { res.setHeader('Content-Type', 'text/html;charset=utf-8'); fs.createReadStream(htmlPath).pipe(res) } }) } } }) }); // 服務監聽 3000 端口 server.listen(3000, function () { console.log('server is running....'); })
指紋 Etag
在講指紋
之前,還需要介紹一下摘要算法
及加密算法
,crypto
是node中提供好的用於加密的模塊,各種摘要算法和加密算法。
摘要及加密算法
MD5:常見的MD5算法,也叫hash算法或者摘要算法,具有以下特點:
- 不能反解,不可逆,
- 相同的內容,摘要出的結果相同
- 不同的內容,摘要出長度是相同的
- 不同的內容,摘要的結果完全不同 (也稱雪崩效應,有一點不一樣,結果就完全不一樣)
- 網上在線解密MD5其實只是通常意義的撞庫
- 撞庫不叫解密,為了安全,可以將一個md5值多次加密,一般三次以上就無法破解了md5(md5(md5(xxx))),
sah1/sha256:加鹽算法,是真正的加密算法,設定一個鹽值(秘鑰)
,內容一致,鹽值不同,結果不同
const crypto = require('crypto');
/** md5*/
// 摘要的內容 摘要的格式
let r1 = crypto.createHash('md5').update('abcd').digest('base64');
// 分開摘要, 如果內部使用了流,可以讀一點摘要一點
let r2 = crypto.createHash('md5').update('a').update('b').update('cd').digest('base64');
console.log(r1, r2);
/** sha256 */
const crypto = require('crypto');
let r3 = crypto.createHmac('sha256','n').update('ab')..update('cd').digest('base64');
let r4 = crypto.createHmac('sha256','h').update('a')..update('bcd').digest('base64');
console.log(r3, r4);
進入正題,對比緩存使用的最後修改時間方案也存在一定的問題:
- 某些服務器不能精確得到文件的最後修改時間, 這樣就無法通過最後修改時間來判斷文件是否更新了。
- 某些文件的修改非常頻繁,在秒以下的時間內進行多次修改,而Last-Modified只能精確到秒。
- 一些文件的最後修改時間改變了,但是內容並未改變(典型吃了吐)。 因此不希望被認為是修改。
- 如果同樣的一個文件位於多個CDN服務器,內容雖然一樣,修改時間不一樣。
Etag的出現,可以在一定程度上解決這個問題,但不能說完全解決,他也存在他的問題,接下來分析一下他的實現原理:
-
ETag(實體標籤),根據
摘要算法
將實體內容生成的一段hash字符串,文件改變,ETag也隨之改變 -
但是對於大文件,不會直接全量比對,可以用文件的大小,開頭、或某一段生成一個指紋
-
瀏覽器首次請求資源時,服務器會將
ETag
與資源一同返回給瀏覽器。 -
再次請求時,客戶端請求頭會攜帶
簽名標識(If-None-Match)
,並在服務端對比兩個簽名。 -
若相等,直接返回
304狀態碼
,讀取瀏覽器的緩存中對應緩存文件; -
若不相等,返回最新內容,並給文件設置新的修改時間。
-
ETag
不管是否生效,都需要與服務端發生交互
-
響應頭設置
// no-cache: 需要使用對比緩存驗證數據,會向服務器發送請求,且數據會存到瀏覽器的緩存中 res.setHeader('Cache-Control', 'no-cache'); // 設置響應頭,文件的最後修改時間 res.setHeader('Last-Modified',ctime)
-
ETag & If-None-Match
-
完整代碼:
const http = require('http') const url = require('url') const path = require('path'); const fs = require('fs'); const mime = require('mime'); const crypto = require('crypto'); const server = http.createServer((req, res) => { let { pathname } = url.parse(req.url, true) let filepath = path.join(__dirname, 'public', pathname); fs.stat(filepath, function (err, statObj) { if (err) { res.statusCode = 404; res.end('Not Found!') } else { // 如果是文件 if (statObj.isFile()) { let content = fs.readFileSync(filepath); let etag = crypto.createHash('md5').update(content).digest('base64'); // 判斷請求頭存儲的簽名與服務端文件的生成的簽名是否相等 if(req.headers['if-none-match'] === etag){ res.statusCode = 304; // 設置響應狀態碼,瀏覽器默認會自動解析,從緩存中讀取對應文件 res.end() // 表示此時服務器沒有響應結果 }else{ // 設置響應頭,簽名 res.setHeader('Etag',etag) // 設置對應類型的響應頭,並返響應文件內容 res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8'); fs.createReadStream(filepath).pipe(res); } } else { // 如果是目錄,需要找目錄下的index.html let htmlPath = path.join(filepath, 'index.html') // 拼接路徑 fs.access(htmlPath, function (err) { if (err) { // 查看文件的可訪問性,如不能訪問則響應 404 res.statusCode = 404; res.end('Not Found!') } else { res.setHeader('Content-Type', 'text/html;charset=utf-8'); fs.createReadStream(htmlPath).pipe(res) } }) } } }) }); // 服務監聽 3000 端口 server.listen(3000, function () { console.log('server is running....'); })
緩存總結
-
強制緩存如果生效,不會再和服務器發生交互
,而對比緩存不管是否生效,都需要與服務端發生交互
-
緩存規則可以同時存在,
強制緩存優先級高於對比緩存
,也就是說,當強制緩存規則生效時,直接使用緩存,不再執行對比緩存規則 -
可以設置不同的匹配規則,採用不同的緩存方式
-
重要代碼:
// 第一次發送文件,先設置強制緩存,在執行強制緩存時,默認不會執行對比緩存,因為不走服務器 res.setHeader('Cache-Control','max-age=10'); res.setHeader('Expires',new Date(Date.now() + 10 * 1000).toGMTString()); // 每次強制緩存時間到了,就會走對比緩存,然後在變成強制緩存 const lastModified = statObj.ctime.toGMTString(); const etag = crypto.createHash('md5').update(readFileSync(requestFile)).digest('base64'); res.setHeader('Last-Modified',lastModified); res.setHeader('Etag',etag); let ifModifiedSince = req.headers['if-modified-since']; let ifNoneMatch = req.headers['if-none-match']; // 如果文件修改時間不一樣,就直接返回最新的 if(lastModified !== ifModifiedSince){ // 有可能時間一樣,但是內容不一樣 return createReadStream(requestFile).pipe(res);; } if(etag !== ifNoneMatch){ // 一般情況,指紋生成不會是根據文件全量生成,有可能只是根據文件大小等 return createReadStream(requestFile).pipe(res);; } res.statusCode = 304; return res.end();