每個瀏覽器都有一個自己的快取區,使用快取區的資料有諸多好處,減少冗餘的資料傳輸,節省網路傳輸。減少伺服器負擔, 提高網站的效能。加快客戶端載入網頁的速度等,而這裡指的快取,指代的靜態檔案的快取,動態資料快取需要走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();