最近在看面試題的時候總會看到有一些關於Http緩存的題目,可是老是隻知其一;不知其二,不甚理解;尤爲是Http頭信息中有一大堆的字段,什麼if-modified-since,什麼if-none-match,真是使人頭疼。後來忽然想到,要是能經過本身構建一個服務器,本身添加頭信息,而後看實現的效果,不就更好了麼。說幹就幹,在網上各類找資料,而後再使用expressjs添加各類頭信息,就可以很好的理解Http緩存了。javascript
我的博客瞭解下謝小飛的博客html
瀏覽器和服務器之間通訊是經過HTTP協議,HTTP協議永遠都是客戶端發起請求,服務器回送響應。模型以下:java
HTTP報文就是瀏覽器和服務器間通訊時發送及響應的數據塊。瀏覽器向服務器請求數據,發送請求(request)報文;服務器向瀏覽器返回數據,返回響應(response)報文。報文信息主要分爲兩部分:git
本文用到的一些報文頭以下:github
字段名稱 | 字段所屬 |
---|---|
Pragma | 通用頭 |
Expires | 響應頭 |
Cache-Control | 通用頭 |
Last-Modified | 響應頭 |
If-Modified-Sice | 請求頭 |
ETag | 響應頭 |
If-None-Match | 請求頭 |
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}`)
})
複製代碼
咱們能夠看到請求結果以下:瀏覽器
請求過程以下:緩存
看得出來這種請求方式的流量與請求次數有關,同時,缺點也很明顯:
接下來咱們開始在頭信息中添加緩存信息。
強制緩存分爲兩種狀況,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秒內,直接使用瀏覽器緩存。
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)
})
複製代碼
強制緩存的弊端很明顯,即每次都是根據時間來判斷緩存是否過時;可是當到達過時時間後,若是文件沒有改動,再次去獲取文件就有點浪費服務器的資源了。協商緩存有兩組報文結合使用:
爲了節省服務器的資源,再次改進方案。瀏覽器和服務器協商,服務器每次返回文件的同時,告訴瀏覽器文件在服務器上最近的修改時間。請求過程以下:
If-Modified-Since
(等於上一次請求的Last-Modified)請求服務器If-Modified-Since
和文件的上次修改時間。若是果一致就繼續使用本地緩存(304),若是不一致就再次返回文件內容和Last-Modified。代碼實現過程以下:
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)
}
})
複製代碼
咱們屢次刷新頁面,能夠看到請求結果以下:
雖然這個方案比前面三個方案有了進一步的優化,瀏覽器檢測文件是否有修改,若是沒有變化就再也不發送文件;可是仍是有如下缺點:
爲了解決文件修改時間不精確帶來的問題,服務器和瀏覽器再次協商,此次不返回時間,返回文件的惟一標識ETag。只有當文件內容改變時,ETag才改變。請求過程以下:
If-None-Match
(等於上一次請求的ETag)請求服務器If-None-Match
和文件的ETag。若是一致就繼續使用本地緩存(304),若是不一致就再次返回文件內容和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);
}
})
複製代碼
請求結果以下:
在報文頭的表格中咱們能夠看到有一個字段叫Pragma,這是一段塵封的歷史....
在「遙遠的」http1.0時代,給客戶端設定緩存方式可經過兩個字段--Pragma和Expires。雖然這兩個字段早可拋棄,但爲了作http協議的向下兼容,你仍是能夠看到不少網站依舊會帶上這兩個字段。
當該字段值爲no-cache
的時候,會告訴瀏覽器不要對該資源緩存,即每次都得向服務器發一次請求才行。
res.setHeader('Pragma', 'no-cache') //禁止緩存
res.setHeader('Cache-Control', 'public,max-age=120') //2分鐘
複製代碼
經過Pragma來禁止緩存,經過Cache-Control設置兩分鐘緩存,可是從新訪問咱們會發現瀏覽器會再次發起一次請求,說明了Pragma的優先級高於Cache-Control
咱們看到Cache-Control中有一個屬性是public,那麼這表明了什麼意思呢?其實Cache-Control不光有max-age,它常見的取值private、public、no-cache、max-age,no-store,默認值爲private,各個取值的含義以下:
因此咱們在刷新頁面的時候,若是隻按F5只是單純的發送請求,按Ctrl+F5會發現請求頭上多了兩個字段Pragma: no-cache和Cache-Control: no-cache。
上面咱們說過強制緩存的優先級高於協商緩存,Pragma的優先級高於Cache-Control,那麼其餘緩存的優先級順序怎麼樣呢?網上查閱了資料得出如下順序(PS:有興趣的童鞋能夠驗證一下正確性告訴我):
Pragma > Cache-Control > Expires > ETag > Last-Modified
若是以爲寫得還不錯,請關注個人掘金主頁。更多文章請訪問謝小飛的博客
參考資料: