快取
快取作用
- 減少了冗餘的資料傳輸,節省了網費。
- 減少了伺服器的負擔, 大大提高了網站的效能
- 加快了客戶端載入網頁的速度
快取分類
強制快取
強制快取:說白了就是第一次請求資料時,服務端將資料和快取規則一併返回,下一次請求時瀏覽器直接根據快取規則進行判斷,有就直接讀快取資料庫,不用連線伺服器;沒有,再去找伺服器。
對比快取
- 對比快取,顧名思義,需要進行比較判斷是否可以使用快取。
- 瀏覽器第一次請求資料時,伺服器會將快取標識與資料一起返回給客戶端,客戶端將二者備份至快取資料庫中。
- 再次請求資料時,客戶端將備份的快取標識傳送給伺服器,伺服器根據快取標識進行判斷,判斷成功後,返回304狀態碼,通知* 客戶端比較成功,可以使用快取資料。
請求流程
第一次請求,此時沒有快取
第二次請求
從上張圖我們可以看到,判斷快取是否可用,有兩種方式
- ETag是實體標籤的縮寫,根據實體內容生成的一段hash字串,可以標識資源的狀態。當資源發生改變時,ETag也隨之發生變化。ETag是Web服務端產生的,然後發給瀏覽器客戶端。
- Last-Modified是此資源的最後修改時間,
- 如果客戶端在請求到的資源中發現實體首部裡有Last-Modified宣告,再次請求就會在頭裡帶上if-Modified-Since欄位
- 服務端收到請求後發現if-Modified-Since欄位則與被請求資源的最後修改時間進行對比
說了這麼多,不如直接來實現一下快取
快取的實現
通過最後修改時間來判斷快取是否可用
let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');//用來生成MimeType
let app = http.createServer((req, res) => {
// 根據url獲取客戶端要請求的檔案路徑
let { parsename } = url.parse(req.url);
let p = path.join(__dirname, 'public', '.' + pathname);
// fs.stat()用來讀取檔案資訊,檔案最後修改時間就是stat.ctime
fs.stat(p, (err, stat) => {
if (!err) {
let since = req.headers['if-modified-since'];//客戶端發來的檔案最後修改時間
if (since) {
if (since === stat.ctime.toUTCString()) {//最後修改時間相等,讀快取
res.statusCode = 304;
res.end();
} else {
sendFile(req, res, p, stat);//最後修改時間不相等,返回新內容
}
} else {
sendError(res);
}
}
})
})
function sendError(res) {
res.statusCode = 404;
res.end();
}
function sendFile(req, res, p, stat) {
res.setHeader('Cache-Control', 'no-cache');// 設定通用首部欄位 控制快取行為
res.setHeader('Last-Modified', stat.ctime.toUTCString());// 實體首部欄位 資源最後修改時間
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8')
fs.createReadStream(p).pipe(res);
}
app.listen(3000, () => {
console.log('server is starting on port 3000');
});
複製程式碼
最後修改時間存在問題:
1. 某些伺服器不能精確得到檔案的最後修改時間, 這樣就無法通過最後修改時間來判斷檔案是否更新了。
2. 某些檔案的修改非常頻繁,在秒以下的時間內進行修改. Last-Modified只能精確到秒。
3. 一些檔案的最後修改時間改變了,但是內容並未改變。 我們不希望客戶端認為這個檔案修改了。
4. 如果同樣的一個檔案位於多個CDN伺服器上的時候內容雖然一樣,修改時間不一樣。
通過ETag來判斷快取是否可用
ETag就是根據檔案內容來判斷,說白了就是採用MD5(md5並不叫加密演算法,它不可逆,應該叫摘要演算法)產生資訊摘要,用摘要來進行比對。
let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');
// crypto是node.js中實現加密和解密的模組 具體詳解請自行了解
let crypto = require('crypto');
let app = http.createServer((req, res) => {
// 根據url獲取客戶端要請求的檔案路徑
let { parsename } = url.parse(req.url);
let p = path.join(__dirname, 'public', '.' + pathname);
// fs.stat()用來讀取檔案資訊,檔案最後修改時間就是stat.ctime
fs.stat(p, (err, stat) => {
let md5 = crypto.createHash('md5');//建立md5物件
let rs = fs.createReadStream(p);
rs.on('data', function (data) {
md5.update(data);
});
rs.on('end', () => {
let r = md5.digest('hex'); // 對檔案進行md5加密
// 下次就拿最新檔案的加密值 和客戶端請求來比較
let ifNoneMatch = req.headers['if-none-match'];
if (ifNoneMatch) {
if (ifNoneMatch === r) {
res.statusCode = 304;
res.end();
} else {
sendFile(req, res, p, r);
}
} else {
sendFile(req, res, p, r);
}
});
})
});
function sendError(res) {
res.statusCode = 404;
res.end();
}
function sendFile(req, res, p, stat) {
res.setHeader('Cache-Control', 'no-cache');// 設定通用首部欄位 控制快取行為
res.setHeader('Etag', r);// 響應首部欄位 資源的匹配資訊
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8')
fs.createReadStream(p).pipe(res);
}
app.listen(3000, () => {
console.log('server is starting on port 3000');
});
複製程式碼
最後
本人水平有限,有不足之處,望大家指出改正。