前端也要懂Http缓存机制
??最近在看面試題的時(shí)候總會(huì)看到有一些關(guān)于Http緩存的題目,但是總是一知半解,不甚理解;尤其是Http頭信息中有一大堆的字段,什么if-modified-since,什么if-none-match,真是令人頭疼。后來(lái)突然想到,要是能通過(guò)自己構(gòu)建一個(gè)服務(wù)器,自己添加頭信息,然后看實(shí)現(xiàn)的效果,不就更好了么。說(shuō)干就干,在網(wǎng)上各種找資料,然后再使用expressjs添加各種頭信息,就能夠很好的理解Http緩存了。
個(gè)人博客了解下謝小飛的博客
Http簡(jiǎn)介
??瀏覽器和服務(wù)器之間通信是通過(guò)HTTP協(xié)議,HTTP協(xié)議永遠(yuǎn)都是客戶(hù)端發(fā)起請(qǐng)求,服務(wù)器回送響應(yīng)。模型如下:
??HTTP報(bào)文就是瀏覽器和服務(wù)器間通信時(shí)發(fā)送及響應(yīng)的數(shù)據(jù)塊。瀏覽器向服務(wù)器請(qǐng)求數(shù)據(jù),發(fā)送請(qǐng)求(request)報(bào)文;服務(wù)器向?yàn)g覽器返回?cái)?shù)據(jù),返回響應(yīng)(response)報(bào)文。報(bào)文信息主要分為兩部分:
??本文用到的一些報(bào)文頭如下:
| Pragma | 通用頭 |
| Expires | 響應(yīng)頭 |
| Cache-Control | 通用頭 |
| Last-Modified | 響應(yīng)頭 |
| If-Modified-Sice | 請(qǐng)求頭 |
| ETag | 響應(yīng)頭 |
| If-None-Match | 請(qǐng)求頭 |
Http緩存的分類(lèi)
??Http緩存可以分為兩大類(lèi),強(qiáng)制緩存(也稱(chēng)強(qiáng)緩存)和協(xié)商緩存。兩類(lèi)緩存規(guī)則不同,強(qiáng)制緩存在緩存數(shù)據(jù)未失效的情況下,不需要再和服務(wù)器發(fā)生交互;而協(xié)商緩存,顧名思義,需要進(jìn)行比較判斷是否可以使用緩存。
??兩類(lèi)緩存規(guī)則可以同時(shí)存在,強(qiáng)制緩存優(yōu)先級(jí)高于協(xié)商緩存,也就是說(shuō),當(dāng)執(zhí)行強(qiáng)制緩存的規(guī)則時(shí),如果緩存生效,直接使用緩存,不再執(zhí)行協(xié)商緩存規(guī)則。
原始模型
??我們先簡(jiǎn)單搭建一個(gè)Express的服務(wù)器,不加任何緩存信息頭。
const express = require('express'); const app = express(); const port = 8080; const fs = require('fs'); const path = require('path');app.get('/',(req,res) => {res.send(`<!DOCTYPE html><html lang="en"><head><title>Document</title></head><body>Http Cache Demo<script src="/demo.js"></script></body></html>`) })app.get('/demo.js',(req, res)=>{let jsPath = path.resolve(__dirname,'./static/js/demo.js');let cont = fs.readFileSync(jsPath);res.end(cont) })app.listen(port,()=>{console.log(`listen on ${port}`) }) 復(fù)制代碼??我們可以看到請(qǐng)求結(jié)果如下:
??請(qǐng)求過(guò)程如下:
- 瀏覽器請(qǐng)求靜態(tài)資源demo.js
- 服務(wù)器讀取磁盤(pán)文件demo.js,返給瀏覽器
- 瀏覽器再次請(qǐng)求,服務(wù)器又重新讀取磁盤(pán)文件 a.js,返給瀏覽器。
- 循環(huán)請(qǐng)求。。
??看得出來(lái)這種請(qǐng)求方式的流量與請(qǐng)求次數(shù)有關(guān),同時(shí),缺點(diǎn)也很明顯:
- 浪費(fèi)用戶(hù)流量
- 浪費(fèi)服務(wù)器資源,服務(wù)器要讀磁盤(pán)文件,然后發(fā)送文件到瀏覽器
- 瀏覽器要等待js下載并且執(zhí)行后才能渲染頁(yè)面,影響用戶(hù)體驗(yàn)
??接下來(lái)我們開(kāi)始在頭信息中添加緩存信息。
一、強(qiáng)制緩存
??強(qiáng)制緩存分為兩種情況,Expires和Cache-Control。
Expires
??Expires的值是服務(wù)器告訴瀏覽器的緩存過(guò)期時(shí)間(值為GMT時(shí)間,即格林尼治時(shí)間),即下一次請(qǐng)求時(shí),如果瀏覽器端的當(dāng)前時(shí)間還沒(méi)有到達(dá)過(guò)期時(shí)間,則直接使用緩存數(shù)據(jù)。下面通過(guò)我們的Express服務(wù)器來(lái)設(shè)置一下Expires響應(yīng)頭信息。
//其他代碼... const moment = require('moment');app.get('/demo.js',(req, res)=>{let jsPath = path.resolve(__dirname,'./static/js/demo.js');let cont = fs.readFileSync(jsPath);res.setHeader('Expires', getGLNZ()) //2分鐘res.end(cont) })function getGLNZ(){return moment().utc().add(2,'m').format('ddd, DD MMM YYYY HH:mm:ss')+' GMT'; } //其他代碼... 復(fù)制代碼??我們?cè)赿emo.js中添加了一個(gè)Expires響應(yīng)頭,不過(guò)由于是格林尼治時(shí)間,所以通過(guò)momentjs轉(zhuǎn)換一下。第一次請(qǐng)求的時(shí)候還是會(huì)向服務(wù)器發(fā)起請(qǐng)求,同時(shí)會(huì)把過(guò)期時(shí)間和文件一起返回給我們;但是當(dāng)我們刷新的時(shí)候,才是見(jiàn)證奇跡的時(shí)刻:
??可以看出文件是直接從緩存(memory cache)中讀取的,并沒(méi)有發(fā)起請(qǐng)求。我們?cè)谶@邊設(shè)置過(guò)期時(shí)間為兩分鐘,兩分鐘過(guò)后可以刷新一下頁(yè)面看到瀏覽器再次發(fā)送請(qǐng)求了。
??雖然這種方式添加了緩存控制,節(jié)省流量,但是還是有以下幾個(gè)問(wèn)題的:
- 由于瀏覽器時(shí)間和服務(wù)器時(shí)間不同步,如果瀏覽器設(shè)置了一個(gè)很后的時(shí)間,過(guò)期時(shí)間一直沒(méi)有用
- 緩存過(guò)期后,不管文件有沒(méi)有發(fā)生變化,服務(wù)器都會(huì)再次讀取文件返回給瀏覽器
??不過(guò)Expires 是HTTP 1.0的東西,現(xiàn)在默認(rèn)瀏覽器均默認(rèn)使用HTTP 1.1,所以它的作用基本忽略。
Cache-Control
??針對(duì)瀏覽器和服務(wù)器時(shí)間不同步,加入了新的緩存方案;這次服務(wù)器不是直接告訴瀏覽器過(guò)期時(shí)間,而是告訴一個(gè)相對(duì)時(shí)間Cache-Control=10秒,意思是10秒內(nèi),直接使用瀏覽器緩存。
app.get('/demo.js',(req, res)=>{let jsPath = path.resolve(__dirname,'./static/js/demo.js');let cont = fs.readFileSync(jsPath);res.setHeader('Cache-Control', 'public,max-age=120') //2分鐘res.end(cont) }) 復(fù)制代碼二、協(xié)商緩存
??強(qiáng)制緩存的弊端很明顯,即每次都是根據(jù)時(shí)間來(lái)判斷緩存是否過(guò)期;但是當(dāng)?shù)竭_(dá)過(guò)期時(shí)間后,如果文件沒(méi)有改動(dòng),再次去獲取文件就有點(diǎn)浪費(fèi)服務(wù)器的資源了。協(xié)商緩存有兩組報(bào)文結(jié)合使用:
Last-Modified
??為了節(jié)省服務(wù)器的資源,再次改進(jìn)方案。瀏覽器和服務(wù)器協(xié)商,服務(wù)器每次返回文件的同時(shí),告訴瀏覽器文件在服務(wù)器上最近的修改時(shí)間。請(qǐng)求過(guò)程如下:
- 瀏覽器請(qǐng)求靜態(tài)資源demo.js
- 服務(wù)器讀取磁盤(pán)文件demo.js,返給瀏覽器,同時(shí)帶上文件上次修改時(shí)間 Last-Modified(GMT標(biāo)準(zhǔn)格式)
- 當(dāng)瀏覽器上的緩存文件過(guò)期時(shí),瀏覽器帶上請(qǐng)求頭If-Modified-Since(等于上一次請(qǐng)求的Last-Modified)請(qǐng)求服務(wù)器
- 服務(wù)器比較請(qǐng)求頭里的If-Modified-Since和文件的上次修改時(shí)間。如果果一致就繼續(xù)使用本地緩存(304),如果不一致就再次返回文件內(nèi)容和Last-Modified。
- 循環(huán)請(qǐng)求。。
??代碼實(shí)現(xiàn)過(guò)程如下:
app.get('/demo.js',(req, res)=>{let jsPath = path.resolve(__dirname,'./static/js/demo.js')let cont = fs.readFileSync(jsPath);let status = fs.statSync(jsPath)let lastModified = status.mtime.toUTCString()if(lastModified === req.headers['if-modified-since']){res.writeHead(304, 'Not Modified')res.end()} else {res.setHeader('Cache-Control', 'public,max-age=5')res.setHeader('Last-Modified', lastModified)res.writeHead(200, 'OK')res.end(cont)} }) 復(fù)制代碼??我們多次刷新頁(yè)面,可以看到請(qǐng)求結(jié)果如下:
??雖然這個(gè)方案比前面三個(gè)方案有了進(jìn)一步的優(yōu)化,瀏覽器檢測(cè)文件是否有修改,如果沒(méi)有變化就不再發(fā)送文件;但是還是有以下缺點(diǎn):
- 由于Last-Modified修改時(shí)間是GMT時(shí)間,只能精確到秒,如果文件在1秒內(nèi)有多次改動(dòng),服務(wù)器并不知道文件有改動(dòng),瀏覽器拿不到最新的文件
- 如果服務(wù)器上文件被多次修改了但是內(nèi)容卻沒(méi)有發(fā)生改變,服務(wù)器需要再次重新返回文件。
ETag
??為了解決文件修改時(shí)間不精確帶來(lái)的問(wèn)題,服務(wù)器和瀏覽器再次協(xié)商,這次不返回時(shí)間,返回文件的唯一標(biāo)識(shí)ETag。只有當(dāng)文件內(nèi)容改變時(shí),ETag才改變。請(qǐng)求過(guò)程如下:
- 瀏覽器請(qǐng)求靜態(tài)資源demo.js
- 服務(wù)器讀取磁盤(pán)文件demo.js,返給瀏覽器,同時(shí)帶上文件的唯一標(biāo)識(shí)ETag
- 當(dāng)瀏覽器上的緩存文件過(guò)期時(shí),瀏覽器帶上請(qǐng)求頭If-None-Match(等于上一次請(qǐng)求的ETag)請(qǐng)求服務(wù)器
- 服務(wù)器比較請(qǐng)求頭里的If-None-Match和文件的ETag。如果一致就繼續(xù)使用本地緩存(304),如果不一致就再次返回文件內(nèi)容和ETag。
- 循環(huán)請(qǐng)求。。
??請(qǐng)求結(jié)果如下:
一些額外的東西
??在報(bào)文頭的表格中我們可以看到有一個(gè)字段叫Pragma,這是一段塵封的歷史....
??在“遙遠(yuǎn)的”http1.0時(shí)代,給客戶(hù)端設(shè)定緩存方式可通過(guò)兩個(gè)字段--Pragma和Expires。雖然這兩個(gè)字段早可拋棄,但為了做http協(xié)議的向下兼容,你還是可以看到很多網(wǎng)站依舊會(huì)帶上這兩個(gè)字段。
關(guān)于Pragma
??當(dāng)該字段值為no-cache的時(shí)候,會(huì)告訴瀏覽器不要對(duì)該資源緩存,即每次都得向服務(wù)器發(fā)一次請(qǐng)求才行。
res.setHeader('Pragma', 'no-cache') //禁止緩存 res.setHeader('Cache-Control', 'public,max-age=120') //2分鐘 復(fù)制代碼??通過(guò)Pragma來(lái)禁止緩存,通過(guò)Cache-Control設(shè)置兩分鐘緩存,但是重新訪(fǎng)問(wèn)我們會(huì)發(fā)現(xiàn)瀏覽器會(huì)再次發(fā)起一次請(qǐng)求,說(shuō)明了Pragma的優(yōu)先級(jí)高于Cache-Control
關(guān)于Cache-Control
??我們看到Cache-Control中有一個(gè)屬性是public,那么這代表了什么意思呢?其實(shí)Cache-Control不光有max-age,它常見(jiàn)的取值private、public、no-cache、max-age,no-store,默認(rèn)值為private,各個(gè)取值的含義如下:
- private: 客戶(hù)端可以緩存
- public: 客戶(hù)端和代理服務(wù)器都可緩存
- max-age=xxx: 緩存的內(nèi)容將在 xxx 秒后失效
- no-cache: 需要使用對(duì)比緩存來(lái)驗(yàn)證緩存數(shù)據(jù)
- no-store: 所有內(nèi)容都不會(huì)緩存,強(qiáng)制緩存,對(duì)比緩存都不會(huì)觸發(fā)
??所以我們?cè)谒⑿马?yè)面的時(shí)候,如果只按F5只是單純的發(fā)送請(qǐng)求,按Ctrl+F5會(huì)發(fā)現(xiàn)請(qǐng)求頭上多了兩個(gè)字段Pragma: no-cache和Cache-Control: no-cache。
緩存的優(yōu)先級(jí)
??上面我們說(shuō)過(guò)強(qiáng)制緩存的優(yōu)先級(jí)高于協(xié)商緩存,Pragma的優(yōu)先級(jí)高于Cache-Control,那么其他緩存的優(yōu)先級(jí)順序怎么樣呢?網(wǎng)上查閱了資料得出以下順序(PS:有興趣的童鞋可以驗(yàn)證一下正確性告訴我):
Pragma > Cache-Control > Expires > ETag > Last-Modified
如果覺(jué)得寫(xiě)得還不錯(cuò),請(qǐng)關(guān)注我的掘金主頁(yè)。更多文章請(qǐng)?jiān)L問(wèn)謝小飛的博客
??參考資料:
http緩存優(yōu)先級(jí)問(wèn)題
徹底弄懂HTTP緩存機(jī)制及原理
HTTP緩存控制小結(jié)
淺談瀏覽器http的緩存機(jī)制
通過(guò)express框架簡(jiǎn)單實(shí)踐幾種設(shè)置HTTP對(duì)緩存的控制
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的前端也要懂Http缓存机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: IT工作一年的总结——来自一个小菜鸟
- 下一篇: [Web 前端 ] 还在用浮动吗