目錄
每個(gè)瀏覽器都有一個(gè)自己的緩存區(qū),使用緩存區(qū)的數(shù)據(jù)有諸多好處嗅榕,減少冗余的數(shù)據(jù)傳輸顺饮,節(jié)省網(wǎng)絡(luò)傳輸吵聪。減少服務(wù)器負(fù)擔(dān), 提高網(wǎng)站的性能兼雄。加快客戶端加載網(wǎng)頁(yè)的速度等吟逝,而這里指的緩存,指代的靜態(tài)文件的緩存赦肋,動(dòng)態(tài)數(shù)據(jù)緩存需要走redis块攒。今天我們使用node搭建服務(wù),簡(jiǎn)單演示一下幾種緩存的設(shè)置及配合使用佃乘。
緩存分為disk cache
和 memory cache
兩種囱井,瀏覽器自行處理,代碼層面無法控制恕稠。而我們一般在用的時(shí)候都是在nginx
層做處理,但核心是一樣的扶欣,都是設(shè)置header
簡(jiǎn)單說一下鹅巍,Chrome瀏覽器的緩存文件位置在哪,感興趣的同學(xué)可以自己找一找:
chrome瀏覽器地址欄中輸入:
chrome://version/
找到個(gè)人資料路徑(我的是):
C:\Users\Lenovo\AppData\Local\Google\Chrome\User Data\Default
計(jì)算機(jī)中找到對(duì)應(yīng)的目錄料祠,可以在這個(gè)目錄下查看到
Cache
和Code Cache
目錄骆捧,這個(gè)就是緩存文件目錄進(jìn)入對(duì)應(yīng)的目錄,可以進(jìn)行手動(dòng)刪除
實(shí)操目錄及步驟
初始化package.json: npm init -y
下載第三方模塊:npm i mime
.
│
└─cache
├─node_modules
├─public // 靜態(tài)文件目錄
├─1.js // 請(qǐng)求的文件資源
├─index.html
├─1.cache.js // 強(qiáng)制緩存 完整代碼案例
├─2.cache.js // 協(xié)商緩存 完整代碼案例
├─3.cache.js // 指紋對(duì)比 完整代碼案例
緩存分類
-
強(qiáng)制緩存
:直接緩存至瀏覽器中髓绽,不會(huì)再次向服務(wù)器發(fā)送請(qǐng)求敛苇; -
對(duì)比緩存
:也叫協(xié)商緩存,客服各執(zhí)一份文件修改時(shí)間
顺呕,相互對(duì)比枫攀,若相同用客戶端緩存 -
指紋Etag
:為解決對(duì)比緩存存在的一些問題,客服各執(zhí)一份文件簽名
株茶,相互對(duì)比来涨,若相同用客戶端緩存
強(qiáng)制緩存
服務(wù)器與瀏覽器約定一個(gè)緩存的最大存活時(shí)間,如10s启盛,那么10s內(nèi)蹦掐,瀏覽器請(qǐng)求相同的資源便不會(huì)在請(qǐng)求服務(wù)器,會(huì)默認(rèn)走瀏覽器的緩存區(qū)僵闯,并且響應(yīng)碼依然為
200
如果返回的是一個(gè)html卧抗,其中又引用了其他資源,還會(huì)繼續(xù)向服務(wù)器發(fā)送請(qǐng)求鳖粟。
不對(duì)首次訪問的路徑做處理社裆,也就是第一次訪問時(shí),不走強(qiáng)制緩存的向图,必然會(huì)請(qǐng)求到服務(wù)器端浦马,因?yàn)槿绻B首頁(yè)都走緩存了时呀,那么在斷網(wǎng)或服務(wù)器宕機(jī)的情況下也可以訪問該網(wǎng)站,顯然是不合理的
可以根據(jù)不同的文件后綴晶默,設(shè)置不同的強(qiáng)制緩存的時(shí)間
在緩存數(shù)據(jù)生效期間谨娜,可以直接使用緩存數(shù)據(jù),在沒有緩存數(shù)據(jù)時(shí)磺陡,瀏覽器向服務(wù)器請(qǐng)求數(shù)據(jù)趴梢,服務(wù)器會(huì)將數(shù)據(jù)和緩存規(guī)則一并返回,緩存規(guī)則信息包含在響應(yīng)頭中币他。
-
強(qiáng)制緩存常用的
兩種響應(yīng)頭設(shè)置
// 10s 表示當(dāng)前時(shí)間 + 10s坞靶,屬于相對(duì)時(shí)間 (用于新版瀏覽器) res.setHeader('Cache-Control', 'max-age=10'); // 設(shè)置 絕對(duì)時(shí)間 (用于舊版瀏覽器或IE老版本 或 http1.0) // 設(shè)置header的值只能是數(shù)字,字符串或數(shù)組蝴悉,不能為對(duì)象彰阴,new Date()返回的是對(duì)象,所以需要轉(zhuǎn)一下拍冠。 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 表示當(dāng)前時(shí)間 + 10s尿这,屬于相對(duì)時(shí)間 (用于新版瀏覽器) res.setHeader('Cache-Control', 'max-age=10'); // 設(shè)置 絕對(duì)時(shí)間 (用于舊版瀏覽器或IE老版本 或 http1.0) // 設(shè)置header的值只能是數(shù)字,字符串或數(shù)組庆杜,不能為對(duì)象射众,new Date()返回的是對(duì)象,所以需要轉(zhuǎn)一下晃财。 res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toUTCString()); fs.stat(filepath, function (err, statObj) { if (err) { // 獲取文件信息報(bào)錯(cuò)叨橱,則則響應(yīng) 404 res.statusCode = 404; res.end('Not Found!') } else { // 如果是文件,設(shè)置對(duì)應(yīng)類型的響應(yīng)頭断盛,并返響應(yīng)文件內(nèi)容 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) { // 查看文件的可訪問性,如不能訪問則響應(yīng) 404 res.statusCode = 404; res.end('Not Found!') } else { res.setHeader('Content-Type', 'text/html;charset=utf-8'); fs.createReadStream(htmlPath).pipe(res) } }) } } }) }); // 服務(wù)監(jiān)聽 3000 端口 server.listen(3000, function () { console.log('server is running....'); })
![](https://upload-images.jianshu.io/upload_images/26303134-c8aae1659079bcec.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
對(duì)比緩存
瀏覽器首次請(qǐng)求資源時(shí)钢猛,服務(wù)器會(huì)將
緩存標(biāo)識(shí)(文件修改時(shí)間)
與資源一同返回給瀏覽器栖博。再次請(qǐng)求時(shí),客戶端請(qǐng)求頭會(huì)攜帶
緩存標(biāo)識(shí)(If-Modified-Since)
厢洞,并在服務(wù)端對(duì)比兩個(gè)時(shí)間仇让。若相等,直接返回
304狀態(tài)碼
躺翻,讀取瀏覽器的緩存中對(duì)應(yīng)緩存文件丧叽;若不相等,返回最新內(nèi)容公你,并給文件設(shè)置新的修改時(shí)間踊淳。
對(duì)比緩存
不管是否生效,都需要與服務(wù)端發(fā)生交互
強(qiáng)制緩存和對(duì)比緩存可以配合使用,如10s內(nèi)強(qiáng)制緩存迂尝,超過10s走對(duì)比緩存脱茉,同時(shí)在設(shè)置10s的強(qiáng)制緩存
-
響應(yīng)頭設(shè)置
// no-cache: 需要使用對(duì)比緩存驗(yàn)證數(shù)據(jù),會(huì)向服務(wù)器發(fā)送請(qǐng)求,且數(shù)據(jù)會(huì)存到瀏覽器的緩存中 res.setHeader('Cache-Control', 'no-cache'); // 設(shè)置響應(yīng)頭垄开,文件的最后修改時(shí)間 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); // 強(qiáng)制緩存和對(duì)比緩存配合使用,10s內(nèi)走強(qiáng)制緩存溉躲,超過10s會(huì)走對(duì)比緩存榜田,同時(shí)在設(shè)置10s的強(qiáng)制緩存 // 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(); // 判斷請(qǐng)求頭存儲(chǔ)的時(shí)間與服務(wù)器端文件的最后修改時(shí)間是否相等 if(req.headers['if-modified-since'] === ctime){ res.statusCode = 304; // 設(shè)置響應(yīng)狀態(tài)碼辩块,瀏覽器默認(rèn)會(huì)自動(dòng)解析屁魏,從緩存中讀取對(duì)應(yīng)文件 res.end(); // 表示此時(shí)服務(wù)器沒有響應(yīng)結(jié)果 }else{ // 設(shè)置響應(yīng)頭桃漾,文件的最后修改時(shí)間 res.setHeader('Last-Modified',ctime) // 設(shè)置對(duì)應(yīng)類型的響應(yīng)頭,并返響應(yīng)文件內(nèi)容 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) { // 查看文件的可訪問性脾猛,如不能訪問則響應(yīng) 404 res.statusCode = 404; res.end('Not Found!') } else { res.setHeader('Content-Type', 'text/html;charset=utf-8'); fs.createReadStream(htmlPath).pipe(res) } }) } } }) }); // 服務(wù)監(jiān)聽 3000 端口 server.listen(3000, function () { console.log('server is running....'); })
![](https://upload-images.jianshu.io/upload_images/26303134-9218c65fbab0aa60.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
指紋 Etag
在講指紋
之前撕彤,還需要介紹一下摘要算法
及加密算法
,crypto
是node中提供好的用于加密的模塊猛拴,各種摘要算法和加密算法羹铅。
摘要及加密算法
MD5:常見的MD5算法蚀狰,也叫hash算法或者摘要算法,具有以下特點(diǎn):
- 不能反解职员,不可逆麻蹋,
- 相同的內(nèi)容,摘要出的結(jié)果相同
- 不同的內(nèi)容廉邑,摘要出長(zhǎng)度是相同的
- 不同的內(nèi)容哥蔚,摘要的結(jié)果完全不同 (也稱雪崩效應(yīng),有一點(diǎn)不一樣蛛蒙,結(jié)果就完全不一樣)
- 網(wǎng)上在線解密MD5其實(shí)只是通常意義的撞庫(kù)
- 撞庫(kù)不叫解密糙箍,為了安全,可以將一個(gè)md5值多次加密牵祟,一般三次以上就無法破解了md5(md5(md5(xxx))),
sah1/sha256:加鹽算法深夯,是真正的加密算法,設(shè)定一個(gè)鹽值(秘鑰)
诺苹,內(nèi)容一致咕晋,鹽值不同,結(jié)果不同
const crypto = require('crypto');
/** md5*/
// 摘要的內(nèi)容 摘要的格式
let r1 = crypto.createHash('md5').update('abcd').digest('base64');
// 分開摘要, 如果內(nèi)部使用了流收奔,可以讀一點(diǎn)摘要一點(diǎn)
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);
進(jìn)入正題掌呜,對(duì)比緩存使用的最后修改時(shí)間方案也存在一定的問題:
- 某些服務(wù)器不能精確得到文件的最后修改時(shí)間, 這樣就無法通過最后修改時(shí)間來判斷文件是否更新了坪哄。
- 某些文件的修改非常頻繁质蕉,在秒以下的時(shí)間內(nèi)進(jìn)行多次修改,而Last-Modified只能精確到秒翩肌。
- 一些文件的最后修改時(shí)間改變了模暗,但是內(nèi)容并未改變(典型吃了吐)。 因此不希望被認(rèn)為是修改念祭。
- 如果同樣的一個(gè)文件位于多個(gè)CDN服務(wù)器兑宇,內(nèi)容雖然一樣,修改時(shí)間不一樣粱坤。
Etag的出現(xiàn)隶糕,可以在一定程度上解決這個(gè)問題,但不能說完全解決站玄,他也存在他的問題枚驻,接下來分析一下他的實(shí)現(xiàn)原理:
ETag(實(shí)體標(biāo)簽),根據(jù)
摘要算法
將實(shí)體內(nèi)容生成的一段hash字符串蜒什,文件改變测秸,ETag也隨之改變但是對(duì)于大文件,不會(huì)直接全量比對(duì),可以用文件的大小霎冯,開頭铃拇、或某一段生成一個(gè)指紋
瀏覽器首次請(qǐng)求資源時(shí),服務(wù)器會(huì)將
ETag
與資源一同返回給瀏覽器沈撞。再次請(qǐng)求時(shí)慷荔,客戶端請(qǐng)求頭會(huì)攜帶
簽名標(biāo)識(shí)(If-None-Match)
,并在服務(wù)端對(duì)比兩個(gè)簽名缠俺。若相等显晶,直接返回
304狀態(tài)碼
,讀取瀏覽器的緩存中對(duì)應(yīng)緩存文件壹士;若不相等磷雇,返回最新內(nèi)容,并給文件設(shè)置新的修改時(shí)間躏救。
ETag
不管是否生效唯笙,都需要與服務(wù)端發(fā)生交互
-
響應(yīng)頭設(shè)置
// no-cache: 需要使用對(duì)比緩存驗(yàn)證數(shù)據(jù),會(huì)向服務(wù)器發(fā)送請(qǐng)求,且數(shù)據(jù)會(huì)存到瀏覽器的緩存中 res.setHeader('Cache-Control', 'no-cache'); // 設(shè)置響應(yīng)頭盒使,文件的最后修改時(shí)間 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');
// 判斷請(qǐng)求頭存儲(chǔ)的簽名與服務(wù)端文件的生成的簽名是否相等
if(req.headers['if-none-match'] === etag){
res.statusCode = 304; // 設(shè)置響應(yīng)狀態(tài)碼崩掘,瀏覽器默認(rèn)會(huì)自動(dòng)解析,從緩存中讀取對(duì)應(yīng)文件
res.end() // 表示此時(shí)服務(wù)器沒有響應(yīng)結(jié)果
}else{
// 設(shè)置響應(yīng)頭少办,簽名
res.setHeader('Etag',etag)
// 設(shè)置對(duì)應(yīng)類型的響應(yīng)頭苞慢,并返響應(yīng)文件內(nèi)容
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) { // 查看文件的可訪問性英妓,如不能訪問則響應(yīng) 404
res.statusCode = 404;
res.end('Not Found!')
} else {
res.setHeader('Content-Type', 'text/html;charset=utf-8');
fs.createReadStream(htmlPath).pipe(res)
}
})
}
}
})
});
// 服務(wù)監(jiān)聽 3000 端口
server.listen(3000, function () {
console.log('server is running....');
})
```
緩存總結(jié)
強(qiáng)制緩存如果生效挽放,不會(huì)再和服務(wù)器發(fā)生交互
,而對(duì)比緩存不管是否生效鞋拟,都需要與服務(wù)端發(fā)生交互
緩存規(guī)則可以同時(shí)存在骂维,
強(qiáng)制緩存優(yōu)先級(jí)高于對(duì)比緩存
惹资,也就是說贺纲,當(dāng)強(qiáng)制緩存規(guī)則生效時(shí),直接使用緩存褪测,不再執(zhí)行對(duì)比緩存規(guī)則可以設(shè)置不同的匹配規(guī)則猴誊,采用不同的緩存方式
-
重要代碼:
// 第一次發(fā)送文件,先設(shè)置強(qiáng)制緩存侮措,在執(zhí)行強(qiáng)制緩存時(shí)懈叹,默認(rèn)不會(huì)執(zhí)行對(duì)比緩存,因?yàn)椴蛔叻?wù)器 res.setHeader('Cache-Control','max-age=10'); res.setHeader('Expires',new Date(Date.now() + 10 * 1000).toGMTString()); // 每次強(qiáng)制緩存時(shí)間到了分扎,就會(huì)走對(duì)比緩存澄成,然后在變成強(qiáng)制緩存 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']; // 如果文件修改時(shí)間不一樣,就直接返回最新的 if(lastModified !== ifModifiedSince){ // 有可能時(shí)間一樣,但是內(nèi)容不一樣 return createReadStream(requestFile).pipe(res);; } if(etag !== ifNoneMatch){ // 一般情況墨状,指紋生成不會(huì)是根據(jù)文件全量生成卫漫,有可能只是根據(jù)文件大小等 return createReadStream(requestFile).pipe(res);; } res.statusCode = 304; return res.end();