前言
http的緩存是老生常談的問題热芹,基本面試專用的,看到的文章挺多的桐臊,但都是一些原理性的文章胎撤,基本沒有真正實(shí)踐過怎么緩存,就會(huì)形成一種道理大家都懂断凶,但誰真正實(shí)踐過呢伤提,從而到用的時(shí)候,卻發(fā)現(xiàn)不會(huì)怎么做緩存认烁,如何配置呢肿男,也一無所知。所以本文從理論和實(shí)踐上試了一下怎么認(rèn)識(shí)緩存砚著。這里有借鑒前輩們的經(jīng)驗(yàn)文章次伶,但對(duì)于生產(chǎn)環(huán)境上服務(wù)器的緩存還是懵懂的,請(qǐng)各位大佬賜教稽穆,同時(shí)歡迎各位大佬指正冠王。
緩存的規(guī)則
我們知道HTTP的緩存屬于客戶端緩存,后面會(huì)提到為什么屬于客戶端緩存舌镶。所以我們認(rèn)為瀏覽器存在一個(gè)緩存數(shù)據(jù)庫(kù)柱彻,用于儲(chǔ)存一些不經(jīng)常變化的靜態(tài)文件(圖片、css餐胀、js等)哟楷。我們將緩存分為強(qiáng)制緩存和協(xié)商緩存。下面我將分別詳細(xì)的介紹這兩種緩存的緩存規(guī)則否灾。
強(qiáng)制緩存
當(dāng)緩存數(shù)據(jù)庫(kù)中已有所請(qǐng)求的數(shù)據(jù)時(shí)卖擅。客戶端直接從緩存數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)墨技。當(dāng)緩存數(shù)據(jù)庫(kù)中沒有所請(qǐng)求的數(shù)據(jù)時(shí)惩阶,客戶端的才會(huì)從服務(wù)端獲取數(shù)據(jù)。
協(xié)商緩存
又稱對(duì)比緩存扣汪,客戶端會(huì)先從緩存數(shù)據(jù)庫(kù)中獲取到一個(gè)緩存數(shù)據(jù)的標(biāo)識(shí)断楷,得到標(biāo)識(shí)后請(qǐng)求服務(wù)端驗(yàn)證是否失效(新鮮),如果沒有失效服務(wù)端會(huì)返回304崭别,此時(shí)客戶端直接從緩存中獲取所請(qǐng)求的數(shù)據(jù)冬筒,如果標(biāo)識(shí)失效恐锣,服務(wù)端會(huì)返回更新后的數(shù)據(jù)。
小貼士:
我們可以看到兩類緩存規(guī)則的不同舞痰,強(qiáng)制緩存如果生效土榴,不需要再和服務(wù)器發(fā)生交互,而對(duì)比緩存不管是否生效匀奏,都需要與服務(wù)端發(fā)生交互鞭衩。
兩類緩存規(guī)則可以同時(shí)存在学搜,強(qiáng)制緩存優(yōu)先級(jí)高于對(duì)比緩存娃善,也就是說,當(dāng)執(zhí)行強(qiáng)制緩存的規(guī)則時(shí)瑞佩,如果緩存生效聚磺,直接使用緩存,不再執(zhí)行對(duì)比緩存規(guī)則炬丸。
緩存的方案
上面的內(nèi)容讓我們大概了解了緩存機(jī)制是怎樣運(yùn)行的瘫寝,但是,服務(wù)器是如何判斷緩存是否失效呢稠炬?我們知道瀏覽器和服務(wù)器進(jìn)行交互的時(shí)候會(huì)發(fā)送一些請(qǐng)求數(shù)據(jù)和響應(yīng)數(shù)據(jù)焕阿,我們稱之為HTTP報(bào)文。報(bào)文中包含首部header和主體部分body首启。與緩存相關(guān)的規(guī)則信息就包含在header中暮屡。boby中的內(nèi)容是HTTP請(qǐng)求真正要傳輸?shù)牟糠帧Ee個(gè)HTTP報(bào)文header部分的例子如下:
HTTP/1.1 200 OK
Server: nginx/1.14.0
Date: Sat, 09 Mar 2019 02:59:02 GMT
Content-Type: application/javascript; charset=utf8
Content-Length: 1544092
Last-Modified: Sat, 09 Mar 2019 00:52:32 GMT
Connection: keep-alive
ETag: "5c830e50-178f9c"
Accept-Ranges: bytes
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Host: xlink-iot.qinyuan.cn
If-Modified-Since: Fri, 08 Mar 2019 06:56:18 GMT
If-None-Match: "5c821212-178f9c"
Referer: https://xlink-iot.qinyuan.cn/iot/
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36
接下來我們將對(duì)HTTP報(bào)文中出現(xiàn)的與緩存規(guī)則相關(guān)的信息做出詳細(xì)解釋毅桃。(我們依舊分為強(qiáng)制緩存和協(xié)商緩存兩個(gè)方面來介紹)
強(qiáng)制緩存
對(duì)于強(qiáng)制緩存褒纲,服務(wù)器響應(yīng)的header中會(huì)用兩個(gè)字段來表明——Expires和Cache-Control
Expires
Exprires的值為服務(wù)端返回的數(shù)據(jù)到期時(shí)間。當(dāng)再次請(qǐng)求時(shí)的請(qǐng)求時(shí)間小于返回的此時(shí)間钥飞,則直接使用緩存數(shù)據(jù)莺掠。但由于服務(wù)端時(shí)間和客戶端時(shí)間可能有誤差,這也將導(dǎo)致緩存命中的誤差读宙,另一方面彻秆,Expires是HTTP1.0的產(chǎn)物,故現(xiàn)在大多數(shù)使用Cache-Control替代结闸。
Cache-Control
Cache-Control有很多屬性唇兑,不同的屬性代表的意義也不同。
private:客戶端可以緩存
public:客戶端和代理服務(wù)器都可以緩存
max-age=t:緩存內(nèi)容將在t秒后失效
no-cache:需要使用協(xié)商緩存來驗(yàn)證緩存數(shù)據(jù)
no-store:所有內(nèi)容都不會(huì)緩存膀估。
協(xié)商緩存
協(xié)商緩存需要進(jìn)行對(duì)比判斷是否可以使用緩存幔亥。瀏覽器第一次請(qǐng)求數(shù)據(jù)時(shí),服務(wù)器會(huì)將緩存標(biāo)識(shí)與數(shù)據(jù)一起響應(yīng)給客戶端察纯,客戶端將它們備份至緩存中帕棉。再次請(qǐng)求時(shí)针肥,客戶端會(huì)將緩存中的標(biāo)識(shí)發(fā)送給服務(wù)器,服務(wù)器根據(jù)此標(biāo)識(shí)判斷香伴。若未失效逃顶,返回304狀態(tài)碼,瀏覽器拿到此狀態(tài)碼就可以直接使用緩存數(shù)據(jù)了稼钩。
對(duì)于協(xié)商緩存來說谭网,緩存標(biāo)識(shí)我們需要著重理解一下,下面我們將著重介紹它的兩種緩存方案低斋。
Last-Modified
Last-Modified:
服務(wù)器在響應(yīng)請(qǐng)求時(shí)蜂厅,會(huì)告訴瀏覽器資源的最后修改時(shí)間。
if-Modified-Since:
瀏覽器再次請(qǐng)求服務(wù)器的時(shí)候膊畴,請(qǐng)求頭會(huì)包含此字段掘猿,后面跟著在緩存中獲得的最后修改時(shí)間。服務(wù)端收到此請(qǐng)求頭發(fā)現(xiàn)有if-Modified-Since唇跨,則與被請(qǐng)求資源的最后修改時(shí)間進(jìn)行對(duì)比稠通,如果一致則返回304和響應(yīng)報(bào)文頭,瀏覽器只需要從緩存中獲取信息即可买猖。
從字面上看改橘,就是說:從某個(gè)時(shí)間節(jié)點(diǎn)算起,是否文件被修改了
- 如果真的被修改:那么開始傳輸響應(yīng)一個(gè)整體玉控,服務(wù)器返回:200 OK
- 如果沒有被修改:那么只需傳輸響應(yīng)header飞主,服務(wù)器返回:304 Not Modified
Last-Modified 有個(gè)問題,因?yàn)槿绻诜?wù)器上奸远,一個(gè)資源被修改了既棺,但其實(shí)際內(nèi)容根本沒發(fā)生改變,會(huì)因?yàn)長(zhǎng)ast-Modified時(shí)間匹配不上而返回了整個(gè)實(shí)體給客戶端(即使客戶端緩存里有個(gè)一模一樣的資源)懒叛。為了解決這個(gè)問題丸冕,HTTP1.1推出了Etag。
Etag
Etag:
服務(wù)器響應(yīng)請(qǐng)求時(shí)薛窥,通過此字段告訴瀏覽器當(dāng)前資源在服務(wù)器生成的唯一標(biāo)識(shí)(生成規(guī)則由服務(wù)器決定)
If-None-Match:
再次請(qǐng)求服務(wù)器時(shí)胖烛,瀏覽器的請(qǐng)求報(bào)文頭部會(huì)包含此字段,后面的值為在緩存中獲取的標(biāo)識(shí)诅迷。服務(wù)器接收到次報(bào)文后發(fā)現(xiàn)If-None-Match則與被請(qǐng)求資源的唯一標(biāo)識(shí)進(jìn)行對(duì)比佩番。
- 不同,說明資源被改動(dòng)過罢杉,則響應(yīng)整個(gè)資源內(nèi)容趟畏,返回狀態(tài)碼200。
- 相同滩租,說明資源無心修改赋秀,則響應(yīng)header利朵,瀏覽器直接從緩存中獲取數(shù)據(jù)信息。返回狀態(tài)碼304.
緩存的優(yōu)點(diǎn)
- 減少了冗余的數(shù)據(jù)傳遞猎莲,節(jié)省寬帶流量
- 減少了服務(wù)器的負(fù)擔(dān)绍弟,大大提高了網(wǎng)站性能
- 加快了客戶端加載網(wǎng)頁(yè)的速度
這也正是HTTP緩存屬于客戶端緩存的原因。
問題
強(qiáng)緩存
1.服務(wù)器怎么返回?cái)?shù)據(jù)到期時(shí)間著洼,怎么緩存(需要服務(wù)器進(jìn)行配置)
Cache-Control
let http = require('http')
let url = require('url')
let fs = require('mz/fs')
let path = require('path');
let p = path.resolve(__dirname);
http.createServer(async function(req, res) {
let {pathname} = url.parse(req.url);
let realPath = path.join(p, pathname);
console.log(realPath, '來請(qǐng)求服務(wù)了')
try{
let statObj = await fs.stat(realPath);
res.setHeader('Cache-Control','max-age=180') //強(qiáng)制緩存 180s內(nèi)不需要再次請(qǐng)求服務(wù)器
//res.setHeader('Cache-Control','no-cache')
res.setHeader('Content-Type',require('mime').getType(realPath)+';charset=utf8')
fs.createReadStream(realPath).pipe(res)
}catch(e) {
res.statusCode = 404;
res.end('404')
}
}).listen(3000)
// 我們請(qǐng)求一個(gè)本地的文件index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="./index.css">
<!-- <meta http-equiv="Cache-control" content="no-cache"> -->
</head>
<body>
<img src="/he.png"/>
</body>
<script src="./index.js"></script>
</html>
返回頭:
Cache-Control: max-age=180
Connection: keep-alive
Content-Type: text/html;charset=utf8
Date: Sat, 09 Mar 2019 06:42:58 GMT
Transfer-Encoding: chunked
上述中的文件樟遣,針對(duì)強(qiáng)制緩存,除html文件外其他的資源在180s內(nèi)均在緩存中讀取身笤,另外強(qiáng)調(diào)一點(diǎn):主網(wǎng)頁(yè)只有對(duì)比緩存沒有強(qiáng)制緩存豹悬,html文件每次都是重新請(qǐng)求服務(wù)器文件即使設(shè)置了<meta http-equiv="Cache-Control" content="max-age=180" />
expires
//Expires:Sun, 22 Jul 2018 02:43:42 GMT
//備注:如果Cache-Control和Expires同時(shí)存在,Cache-Control說了算
res.setHeader('Expires', new Date(Date.now() + 10*1000).toGMTString()) //強(qiáng)制緩存的另一種方式
2.客戶端需要如何設(shè)置
沒有展鸡,暫時(shí)找不到
協(xié)商緩存
//對(duì)比緩存 為了更加明顯的看到對(duì)比緩存屿衅,我們將在以下的代碼中都將強(qiáng)制緩存關(guān)閉
//res.setHeader('Cache-Control','no-cache')
//響應(yīng)頭設(shè)置了res.setHeader('Last-Modified',statObj.ctime.toGMTString())
//請(qǐng)求頭就會(huì)帶上req.headers['if-modified-since']
let http = require('http')
let url = require('url')
let util = require('util')
let fs = require('mz/fs')
let stat = util.promisify(fs.stat);
let path = require('path');
let p = path.resolve(__dirname);
http.createServer(async function(req, res) {
let {pathname} = url.parse(req.url);
let realPath = path.join(p, pathname);
console.log(realPath)
try{
let statObj = await fs.stat(realPath);
console.log(statObj)
// res.setHeader('Cache-Control','max-age=10') //強(qiáng)制緩存 10s內(nèi)不需要再次請(qǐng)求服務(wù)器
res.setHeader('Cache-Control','no-cache')
res.setHeader('Content-Type',require('mime').getType(realPath)+';charset=utf8')
res.setHeader('Expires', new Date(Date.now() + 10*1000).toGMTString()) //強(qiáng)制緩存 因?yàn)樯厦嬖O(shè)置了no-cache埃难,所以這里的設(shè)置其實(shí)無效
let since = req.headers['if-modified-since'];
if (since === statObj.ctime.toGMTString()) {
res.statusCode = 304 //服務(wù)器的緩存
res.end();
} else {
res.setHeader('Last-Modified',statObj.ctime.toGMTString())
fs.createReadStream(realPath).pipe(res)
}
}catch(e) {
res.statusCode = 404;
res.end('404')
}
}).listen(3000)
返回頭
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/html;charset=utf8
Date: Sat, 09 Mar 2019 07:22:17 GMT
Last-Modified: Sat, 09 Mar 2019 07:04:41 GMT
Transfer-Encoding: chunked
在瀏覽器打開localhost:3000/index.html 刷新看到就是304莹弊,我們返回狀態(tài)碼304,瀏覽器就乖乖地去讀緩存中的文件了涡尘。我們稍微改動(dòng)一下index.html就可以看到 200
Etag
//對(duì)比緩存
//Etag內(nèi)容的標(biāo)識(shí)
// 響應(yīng)頭設(shè)置了res.setHeader('Etag',statObj.size.toString()); 這里設(shè)置的是文件大小
//請(qǐng)求頭就會(huì)帶上req.headers['if-none-match'];
let http = require('http');
let util = require('util');
let fs = require('fs');
let stat = util.promisify(fs.stat);
let url = require('url');
let path = require('path');
let p = path.resolve(__dirname);
// 比較內(nèi)容 stat.size
// 第一次請(qǐng)求Etag:內(nèi)容的標(biāo)識(shí)
// 第二次在請(qǐng)求我的時(shí)候 if-none-match
http.createServer(async function(req,res){
let {pathname} = url.parse(req.url);
let realPath = path.join(p,pathname);
try{
let statObj = await stat(realPath);
console.log(realPath)
res.setHeader('Cache-Control','no-cache');
let match = req.headers['if-none-match'];
if(match){
if(match === statObj.size.toString()){
res.statusCode = 304;
res.end();
}else{
res.setHeader('Etag',statObj.size.toString());
fs.createReadStream(realPath).pipe(res);
}
}else{
res.setHeader('Etag',statObj.size.toString());
fs.createReadStream(realPath).pipe(res);
}
}catch(e){
res.statusCode = 404;
res.end(`not found`);
}
}).listen(3000);
兩種緩存的區(qū)別
- 強(qiáng)制緩存
- 設(shè)置強(qiáng)制緩存的方式就是 res.setHeader('Cache-Control','max-age=10')
- res.setHeader('Expires', new Date(Date.now() + 10*1000).toGMTString())
- 以上兩種以第一種方式取決定作用
- 對(duì)比緩存
- 通過時(shí)間對(duì)比 Last-Modified ---- if-modified-since
- 通過標(biāo)識(shí)對(duì)比 Etag ---- if-none-match
總結(jié)
緩存只能服務(wù)端配置H坛凇!?汲细疚!,目前暫時(shí)沒有找到客戶端能做的事川梅,html5的meta標(biāo)簽好像在也沒什么用疯兼,感謝大佬指正