最近在對項目作 IE 11 兼容,由 IE 的緩存問題,引起我對於瀏覽器緩存策略的思考。javascript
web緩存主要能夠分爲下面幾類:html
這裏咱們主要關注客戶端,也就是瀏覽器緩存。java
瀏覽器和服務器通訊是經過 HTTP 協議,瀏覽器向服務器發起 HTTP 請求,服務器做出響應。當再次發起請求的時候,能夠直接讀取緩存中的數據,減小網絡帶寬的消耗,提高頁面的訪問速度。git
根據是否從新發起 HTTP 請求,能夠將瀏覽器緩存分爲兩種:強制緩存和協商緩存。github
與強制緩存有關的 HTTP 頭部有 Expires 和 Cache-Controlweb
Expires 響應頭包含一個 HTTP 日期(GMT 時間,非本地時間),表示資源過時的時間。chrome
當設置無效值,例如 0,表示資源當即過時,即不使用緩存。數據庫
//...
const getGMT = () => `${moment().utc().add(1, 'm').format('ddd, DD MMM YYYY HH:mm:ss')} GMT`
app.get('/expries', (req, res) => {
res.setHeader('Expires', getGMT());
res.end('ok')
});
複製代碼
這裏使用 express 建立了一個 web 服務,在 header 中添加了 Expires 響應頭,利用 moment 轉化爲相應的 GMT 格式,設置爲 10s 後過時,能夠看到首次請求時向服務端發起了 HTTP 請求,第二次則使用了緩存(disk cache),超過 10s 以後再請求時(第三次)緩存過時,從新向服務端發起 HTTP 請求。express
請求時帶上 Expries 請求頭:瀏覽器
Cache-Control 是一個通用首部,既能夠設置在請求頭中,也能夠設置在響應頭中,經常使用的取值包括如下幾種:
Cache-Control 取值 | 含義 |
---|---|
no-store | 絕對禁止緩存 |
no-cache | 會被緩存,可是馬上過時,要求將請求提交給原始服務器進行驗證,至關於 max-age=0 |
private | 只有瀏覽器能夠緩存,禁止代理服務器、CDN等中間人緩存 |
public | 資源能夠被任何對象緩存 |
max-age | 表示資源被緩存的最大時間,單位秒;當設置該值時,Expries 頭部會被忽略 |
其中private
、public
只能用於響應頭部中
在強制緩存中,咱們根據時間來判斷資源是否過時,這會存在必定弊端,當過時時間到了,即便服務端資源未改動,也會從新獲取。由此咱們引進了協商緩存的概念,協商緩存須要瀏覽器和服務器共同實現,與協商緩存有關的響應頭部字段主要爲如下兩組:
Last-Modified
和 If-Modified-Since
ETag
和 If-None-Match
Last-Modified
表示資源最後的修改時間(GMT 格式),具體過程以下:
Last-Modified
響應頭部,告訴瀏覽器該資源的最後修改時間If-Modified-Since
這個請求頭部,它的值即爲上一次請求響應的 Last-Modified
,服務端比較兩個字段的值,若是一致,說明資源未改動,返回 304,不然返回更改後的資源。能夠看到再次請求時自動加上 If-Modified-Since
請求頭部:
服務端實現以下:
const filePath = path.join(__dirname, '../static/index.html')
app.get('/lastModified', (req, res) => {
const stat = fs.statSync(filePath);
const file = fs.readFileSync(filePath);
const lastModified = stat.mtime.toUTCString();
res.setHeader('Cache-Control', 'public,max-age=10');
if (lastModified === req.headers['if-modified-since']) {
res.writeHead(304, 'Not Modified');
res.end();
} else {
res.setHeader('Last-Modified', lastModified);
res.writeHead(200, 'OK');
res.end(file);
}
});
複製代碼
當資源發生屢次改動,可是資源內容未改變時,此時服務器仍須要從新返回資源。爲了提高判斷的精確度,引入 ETag 響應頭部,表示資源特定版本的標識符,當文件內容未發生變化時,該標識符的值不會改變。具體過程以下:
ETag
響應頭部,告訴瀏覽器該資源的特殊標識If-None-Match
這個請求頭部,它的值即爲上一次請求響應的 ETag
,服務端比較兩個字段的值,若是一致,說明資源未改動,返回 304,不然返回更改後的資源。當文件發生變化時,響應頭部的 ETag
和請求頭部的 If-None-Match
不一致:
服務端實現以下:
const filePath = path.join(__dirname, '../static/index.html')
// 建立 md5 加密
const cryptoFile = (file) => {
const md5 = crypto.createHash('md5');
return md5.update(file).digest('hex');
}
app.get('/eTag', (req, res) => {
const file = fs.readFileSync(filePath);
const eTag = cryptoFile(file)
res.setHeader('Cache-Control', 'public,max-age=10');
if (eTag === req.headers['if-none-match']) {
res.writeHead(304, 'Not Modified');
res.end();
} else {
res.setHeader('ETag', eTag);
res.writeHead(200, 'OK');
res.end(file);
}
})
複製代碼
在 HTTP/1.0 時期存在一個通用首部 Pragma
,當它的值爲 no-cache
時,與 Cache-Control: no-cache
的行爲一致。它在「請求-響應」鏈中可能會有不一樣的效果,如今通常用於向後兼容只支持 HTTP/1.0 的客戶端。
Chrome 下測試,在請求頭部/響應頭部中設置 Pragma: 'no-cache'
都可以實現禁用緩存:
但在 IE 11 下,當 Pragma
置於響應頭部時並未生效,能夠在 IE 11 下運行測試代碼進行驗證。
在 chrome 下控制檯能夠看到瀏覽器本地緩存分爲兩類:memory cache
和 disk cache
,即內存緩存和磁盤緩存。
那麼瀏覽器是如何區分哪些資源存放在內存中,哪些又存在磁盤中呢?
其實這個問題沒有一個標準答案,廣泛認爲和系統當前內存的使用狀況有關,若是當前系統內存使用率高,那麼會優先存儲在磁盤中;另一個就是對於大文件,通常存儲在磁盤中。
關於優先級,強制緩存的優先級老是大於協商緩存,只有在強制緩存失效後纔會發起請求進行協商緩存;
而在協商緩存中,Last-Modified
表示的是一個 GMT 格式的時間,只能精確到秒,所以 ETag
的精確度要高於 Last-Modified
,但同時每次進行 hash 運算生成標識也會帶來額外的開銷。兩者都存在時,服務端應以 ETag
爲準。
總的優先級以下:
Pragma > Cache-Control > Expries > ETag > Last-Modified
在Chrome下驗證,當 Pragma
爲 no-cache,Cache-Control
設置 1000s 緩存時,瀏覽器會禁用緩存:
一樣,設置響應頭爲 Cache-Control: 'no-cache'
和 Expries
爲 1000s 後過時,瀏覽器依然禁用緩存:
總體的緩存過程以下:
兼容 IE 11 的過程當中踩過一些坑,在實際項目中遇到的印象比較深入的問題是下面這個:
因爲 IE 對 GET 接口的緩存,當用戶首次進入系統時,由於未登陸跳轉至sso,登陸成功以後仍然返回的是緩存中的未登陸,致使登陸以後出現閃屏,在原系統和sso之間不停來回跳轉。
另外,因爲 IE 瀏覽器打開控制檯以後默認開啓始終從服務端刷新,在 debug 階段着實給我形成了不小的困擾,後來放棄使用控制檯,經過抓包工具Charles進行截取、分析,這才定位到問題。
究其緣由,是 IE 對於 GET 請求的緩存策略問題:
屢次發起 GET 請求時,若 url 未發生變化,IE 則認爲這是非首次請求,直接讀取緩存。
經過在 get 請求的 url 中加入隨機標識,例如時間戳、隨機數等,來達到變動 url 的目的,此時瀏覽器不會從緩存中讀取數據
服務端設置響應頭部禁止瀏覽器緩存
{
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Expires': -1,
}
複製代碼
在實際項目中我採用的是這種解決方案
{
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
}
複製代碼