瀏覽器和服務器之間通訊是經過HTTP協議,HTTP協議永遠都是客戶端發起請求,服務器回送響應。模型以下:html
HTTP報文就是瀏覽器和服務器間通訊時發送及響應的數據塊。瀏覽器向服務器請求數據,發送請求(request)報文;服務器向瀏覽器返回數據,返回響應(response)報文。報文信息主要分爲兩部分:前端
報文頭部:一些附加信息(cookie,緩存信息等),與緩存相關的規則信息,均包含在頭部中
數據主體部分:HTTP請求真正想要傳輸的數據內容數據庫
咱們爲何使用緩存,是由於緩存能夠給咱們的 Web 項目帶來如下好處,以提升性能和用戶體驗。express
加快了瀏覽器加載網頁的速度;
減小了冗餘的數據傳輸,節省網絡流量和帶寬;
減小服務器的負擔,大大提升了網站的性能。瀏覽器
因爲從本地緩存讀取靜態資源,加快瀏覽器的網頁加載速度是必定的,也確實的減小了數據傳輸,就提升網站性能來講,可能一兩個用戶的訪問對於減少服務器的負擔沒有明顯效果,但若是這個網站在高併發的狀況下,使用緩存對於減少服務器壓力和整個網站的性能都會發生質的變化。緩存
搭建一個Express的服務器,不加任何緩存信息頭:服務器
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}`) })
咱們能夠看到請求結果以下:cookie
請求過程以下:網絡
循環請求。。併發
看得出來這種請求方式的流量與請求次數有關,同時,缺點也很明顯:
爲了方便理解,咱們認爲瀏覽器存在一個緩存數據庫,用於存儲緩存信息(實際上靜態資源是被緩存到了內存和磁盤中),在瀏覽器第一次請求數據時,此時緩存數據庫沒有對應的緩存數據,則須要請求服務器,服務器會將緩存規則和數據返回,瀏覽器將緩存規則和數據存儲進緩存數據庫。
當瀏覽器地址欄輸入地址後請求的 index.html 是不會被緩存的,但 index.html 內部請求的其餘資源會遵循緩存策略,HTTP 緩存有多種規則,根據是否須要向服務器發送請求主要分爲兩大類,強制緩存和協商緩存。
Http緩存能夠分爲兩大類,強制緩存(也稱強緩存)和協商緩存。兩類緩存規則不一樣,強制緩存在緩存數據未失效的狀況下,不須要再和服務器發生交互;而協商緩存,顧名思義,須要進行比較判斷是否可使用緩存。
兩類緩存規則能夠同時存在,強制緩存優先級高於協商緩存,也就是說,當執行強制緩存的規則時,若是緩存生效,直接使用緩存,再也不執行協商緩存規則。
強制緩存是第一次訪問服務器獲取數據後,在有效時間內不會再請求服務器,而是直接使用緩存數據,強制緩存的流程以下:
強制緩存分爲兩種狀況,Expires和Cache-Control。
Expires的值是服務器告訴瀏覽器的緩存過時時間(值爲GMT時間,即格林尼治時間),即下一次請求時,若是瀏覽器端的當前時間尚未到達過時時間,則直接使用緩存數據。下面經過咱們的Express服務器來設置一下Expires響應頭信息。
//其餘代碼... 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'; } //其餘代碼...
咱們在demo.js中添加了一個Expires響應頭,不過因爲是格林尼治時間,因此經過momentjs轉換一下。第一次請求的時候仍是會向服務器發起請求,同時會把過時時間和文件一塊兒返回給咱們;可是當咱們刷新的時候,纔是見證奇蹟的時刻:
能夠看出文件是直接從緩存(memory cache)中讀取的,並無發起請求。咱們在這邊設置過時時間爲兩分鐘,兩分鐘事後能夠刷新一下頁面看到瀏覽器再次發送請求了。
雖然這種方式添加了緩存控制,節省流量,可是仍是有如下幾個問題的:
不過Expires 是HTTP 1.0的東西,如今默認瀏覽器均默認使用HTTP 1.1,因此它的做用基本忽略。
針對瀏覽器和服務器時間不一樣步,加入了新的緩存方案;此次服務器不是直接告訴瀏覽器過時時間,而是告訴一個相對時間Cache-Control=10秒,意思是10秒內,直接使用瀏覽器緩存。
Cache-Control各個值的含義:
**private**:客戶端能夠緩存; **public**:客戶端和代理服務器均可以緩存(對於前端而言,能夠認爲與 private 效果相同); **max-age=xxx**:緩存的內容將在 xxx 秒後過時(相對時間,秒爲單位); **no-cache**:須要使用協商緩存(後面介紹)來驗證數據是否過時; **no-store**:全部內容都不會緩存,強制緩存和協商緩存都不會觸發。
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) })
其實緩存的儲存是內存和磁盤兩個位置,由當前瀏覽器自己的策略決定,比較隨機,從內存的緩存中取出的數據會顯示 (from memory cache),從磁盤的緩存中取出的數據會顯示 (from disk cache)。
強制緩存的弊端很明顯,即每次都是根據時間來判斷緩存是否過時;可是當到達過時時間後,若是文件沒有改動,再次去獲取文件就有點浪費服務器的資源了。
協商緩存又叫對比緩存,設置協商緩存後,第一次訪問服務器獲取數據時,服務器會將數據和緩存標識一塊兒返回給瀏覽器,客戶端會將數據和標識存入緩存數據庫中,下一次請求時,會先去緩存中取出緩存標識發送給服務器進行詢問,當服務器數據更改時會更新標識,因此服務器拿到瀏覽器發來的標識進行對比,相同表明數據未更改,響應瀏覽器通知數據未更改,瀏覽器會去緩存中獲取數據,若是標識不一樣,表明服務器更改過數據,因此會將新的數據和新的標識返回瀏覽器,瀏覽器會將新的數據和標識存入緩存中,協商緩存的流程以下:
協商緩存和強制緩存不一樣的是,協商緩存每次請求都須要跟服務器通訊,並且命中緩存服務器返回狀態碼再也不是 200,而是 304。
協商緩存有兩組報文結合使用:
HTTP 1.0 版本中:
爲了節省服務器的資源,再次改進方案。瀏覽器和服務器協商,服務器每次返回文件的同時,告訴瀏覽器文件在服務器上最近的修改時間。請求過程以下:
代碼實現過程以下:
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) } })
雖然這個方案比前面三個方案有了進一步的優化,瀏覽器檢測文件是否有修改,若是沒有變化就再也不發送文件;可是仍是有如下缺點:
HTTP 1.1 版本中:
爲了解決文件修改時間不精確帶來的問題,服務器和瀏覽器再次協商,此次不返回時間,返回文件的惟一標識ETag。只有當文件內容改變時,ETag才改變。請求過程以下:
const md5 = require('md5'); app.get('/demo.js',(req, res)=>{ let jsPath = path.resolve(__dirname,'./static/js/demo.js'); let cont = fs.readFileSync(jsPath); let etag = md5(cont); if(req.headers['if-none-match'] === etag){ res.writeHead(304, 'Not Modified'); res.end(); } else { res.setHeader('ETag', etag); res.writeHead(200, 'OK'); res.end(cont); } })
請求結果以下:
爲了使緩存策略更加健壯、靈活,HTTP 1.0 版本 和 HTTP 1.1 版本的緩存策略會同時使用,甚至強制緩存和協商緩存也會同時使用,對於強制緩存,服務器通知瀏覽器一個緩存時間,在緩存時間內,下次請求,直接使用緩存,超出有效時間,執行協商緩存策略,對於協商緩存,將緩存信息中的 Etag 和 Last-Modified 經過請求頭 If-None-Match 和 If-Modified-Since 發送給服務器,由服務器校驗同時設置新的強制緩存,校驗經過並返回 304 狀態碼時,瀏覽器直接使用緩存,若是協商緩存也未命中,則服務器從新設置協商緩存的標識。
當該字段值爲no-cache的時候,會告訴瀏覽器不要對該資源緩存,即每次都得向服務器發一次請求才行:
res.setHeader('Pragma', 'no-cache') //禁止緩存 res.setHeader('Cache-Control', 'public,max-age=120') //2分鐘
經過Pragma來禁止緩存,經過Cache-Control設置兩分鐘緩存,可是從新訪問咱們會發現瀏覽器會再次發起一次請求,說明了Pragma的優先級高於Cache-Control
Pragma > Cache-Control > Expires > ETag > Last-Modified