node.js中實現http伺服器與瀏覽器之間的內容快取
一、快取的作用
1、減少了資料傳輸,節約流量。
2、減少伺服器壓力,提高伺服器效能。
3、加快客戶端載入頁面的速度。
二、快取的分類
1、強制快取,如果快取有效,則不需要與伺服器發生互動,直接使用快取。
2、對比快取,每次都需要與伺服器發生互動,對快取進行比較判斷是否可以使用快取。
三、通過使用 Last-Modified / If-Modified-Since 來進行快取判斷
1、Last-Modified 是伺服器向客戶端傳送的頭資訊,用於告訴客戶端資源的 最後修改時間,該資訊瀏覽器會儲存起來。
2、If-Modified-Since 是客戶端向伺服器傳送的頭資訊,當客戶端再次請求資源時,瀏覽器會帶上該資訊傳送給伺服器,伺服器通過該資訊來判斷資源是否過期。
3、如果沒有過期,則響應 304 表示 未更新,告訴瀏覽器使用儲存的快取。
4、如果過期了,則響應 200,返回最新的資源。
const http = require('http'); const url = require('url'); const path = require('path'); const fs = require('fs'); const util = require('util'); const mime = require('mime'); //建立http伺服器並監聽埠 let server = http.createServer(); server.listen(1234, '0.0.0.0', function () { console.log('開始監聽'); }); function sendFile(req, res, filePath, stats) { //設定檔案內容型別 res.setHeader('Content-Type', mime.getType(filePath)); //設定資源最後修改時間頭資訊 res.setHeader('Last-Modified', stats.ctime.toGMTString()); //通過管道將檔案資料傳送給客戶端 fs.createReadStream(filePath).pipe(res); } server.on('request', function (req, res) { let {pathname} = url.parse(req.url, true); //獲取檔案真實路徑 let filePath = path.join(__dirname, pathname); //判斷檔案是否存在 fs.stat(filePath, function (err, stats) { if (err) { return res.end(util.inspect(err)); } if (!stats.isFile()) { return res.end('is not file'); } //獲取客戶端請求的If-Modified-Since頭資訊 let ifModifiedSince = req.headers['if-modified-since']; if (ifModifiedSince) { //如果最後修改時間相同,說明該資源並未修改,直接響應 304,讓瀏覽器從快取中獲取資料。 if (ifModifiedSince == stats.ctime.toGMTString()) { res.statusCode = 304; res.end(); } else { sendFile(req, res, filePath, stats); } } else { sendFile(req, res, filePath, stats); } }); });
通過最後修改時間判斷快取是否可用,並不是很精確,有如下幾個問題:
1、Last-Modified 只精確到秒,秒以下的時間修改,將無法準確判斷。
2、檔案最後修改時間變了,但 內容並沒有發生改變。
3、檔案存在於多個 CDN 上,那該檔案的最後修改時間是不一樣的。
四、通過 ETag / If-None-Match 進行判斷
ETag 表示 實體標籤,將內容通過 hash 演算法生成一段字串,用以標識資源,如果資源發生變化,則 ETag 也會變化。
ETag 是伺服器生成的,傳送給客戶端的。
1、客戶端請求資源,伺服器根據資源生成ETag,傳送給客戶端。瀏覽器會儲存該資訊。
2、當客戶端再次請求時,瀏覽器會發送 If-None-Match 給伺服器,值為第1步儲存的資訊,伺服器通過該資訊進行判斷,資源是否修改過。
3、如果沒有修改過,則響應 304 未更新,告訴瀏覽器使用儲存的快取。
4、如果修改過,則響應 200,返回最新資源。
const http = require('http'); const url = require('url'); const path = require('path'); const fs = require('fs'); const util = require('util'); const crypto = require('crypto'); const mime = require('mime'); //建立http伺服器並監聽埠 let server = http.createServer(); server.listen(1234, '0.0.0.0', function () { console.log('開始監聽'); }); function sendFile(req, res, filePath, eTag) { //設定檔案內容型別 res.setHeader('Content-Type', mime.getType(filePath)); //設定ETag頭資訊 res.setHeader('ETag', eTag); //通過管道將檔案資料傳送給客戶端 fs.createReadStream(filePath).pipe(res); } server.on('request', function (req, res) { let {pathname} = url.parse(req.url, true); //獲取檔案真實路徑 let filePath = path.join(__dirname, pathname); //判斷檔案是否存在 fs.stat(filePath, function (err, stats) { if (err) { return res.end(util.inspect(err)); } if (!stats.isFile()) { return res.end('is not file'); } //獲取客戶端請求的If-None-Match頭資訊 let ifNoneMatch = req.headers['if-none-match']; //建立可讀流 let rs = fs.createReadStream(filePath); //建立md5演算法 let md5 = crypto.createHash('md5'); rs.on('data', function (data) { md5.update(data); }); rs.on('end', function () { let eTag = md5.digest('hex'); if (ifNoneMatch) { //判斷eTag與客戶端傳送過來的If-None-Match是否相等 if (ifNoneMatch == eTag) { res.statusCode = 304; res.end(); } else { sendFile(req, res, filePath, eTag); } } else { sendFile(req, res, filePath, eTag); } }); }); });
五、讓瀏覽器在快取有效期內不用發請求
Expires 是http1.0的內容,用於設定快取的有效期,在有效期內瀏覽器直接從瀏覽器快取中獲取資料。
Cache-Control 與Expires作用一樣,是http1.1的內容,用於指明當前資源的有效期,優先順序高於Expires。
Cache-Control可以設定的值 :
1、private 客戶端可以快取
2、public 客戶端和代理伺服器都可以快取
3、max-age=10 快取內容在10秒後失效
4、no-cache 使用對比快取驗證,強制向伺服器驗證
5、no-store 內容都不快取,強制快取和對比快取都不會觸發
const http = require('http'); const url = require('url'); const path = require('path'); const fs = require('fs'); const util = require('util'); const mime = require('mime'); //建立http伺服器並監聽埠 let server = http.createServer(); server.listen(1234, '0.0.0.0', function () { console.log('開始監聽'); }); function sendFile(req, res, filePath, stats) { //設定檔案內容型別 res.setHeader('Content-Type', mime.getType(filePath)); //設定快取失效時間60秒 res.setHeader('Expires', new Date(Date.now() + 60 * 1000).toUTCString()); //設定快取失效時間60秒 res.setHeader('Cache-Control', 'max-age=60'); //通過管道將檔案資料傳送給客戶端 fs.createReadStream(filePath).pipe(res); } server.on('request', function (req, res) { let {pathname} = url.parse(req.url, true); //獲取檔案真實路徑 let filePath = path.join(__dirname, pathname); //判斷檔案是否存在 fs.stat(filePath, function (err, stats) { if (err) { return res.end(util.inspect(err)); } if (!stats.isFile()) { return res.end('is not file'); } sendFile(req, res, filePath, stats) }); });