瀏覽器緩存策略

最近在對項目作 IE 11 兼容,由 IE 的緩存問題,引起我對於瀏覽器緩存策略的思考。javascript

緩存類型

web緩存主要能夠分爲下面幾類:html

  • 客戶端緩存
  • 服務端緩存
  • 數據庫緩存

這裏咱們主要關注客戶端,也就是瀏覽器緩存java

瀏覽器和服務器通訊是經過 HTTP 協議,瀏覽器向服務器發起 HTTP 請求,服務器做出響應。當再次發起請求的時候,能夠直接讀取緩存中的數據,減小網絡帶寬的消耗,提高頁面的訪問速度。git

根據是否從新發起 HTTP 請求,能夠將瀏覽器緩存分爲兩種:強制緩存協商緩存github

1.強制緩存

與強制緩存有關的 HTTP 頭部有 ExpiresCache-Controlweb

Expires

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 是一個通用首部,既能夠設置在請求頭中,也能夠設置在響應頭中,經常使用的取值包括如下幾種:

Cache-Control 取值 含義
no-store 絕對禁止緩存
no-cache 會被緩存,可是馬上過時,要求將請求提交給原始服務器進行驗證,至關於 max-age=0
private 只有瀏覽器能夠緩存,禁止代理服務器、CDN等中間人緩存
public 資源能夠被任何對象緩存
max-age 表示資源被緩存的最大時間,單位秒;當設置該值時,Expries 頭部會被忽略

其中privatepublic只能用於響應頭部中

2.協商緩存

在強制緩存中,咱們根據時間來判斷資源是否過時,這會存在必定弊端,當過時時間到了,即便服務端資源未改動,也會從新獲取。由此咱們引進了協商緩存的概念,協商緩存須要瀏覽器和服務器共同實現,與協商緩存有關的響應頭部字段主要爲如下兩組:

  • Last-ModifiedIf-Modified-Since
  • ETagIf-None-Match

Last-Modified

Last-Modified 表示資源最後的修改時間(GMT 格式),具體過程以下:

  1. 首次請求,服務端返回 Last-Modified 響應頭部,告訴瀏覽器該資源的最後修改時間
  2. 再次請求,若是瀏覽器緩存未過時,直接讀取緩存中的資源(disk cache
  3. 當瀏覽器的緩存資源過時,此時再發起 HTTP 請求時,會自動帶上 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 響應頭部,表示資源特定版本的標識符,當文件內容未發生變化時,該標識符的值不會改變。具體過程以下:

  1. 首次請求,服務端返回 ETag 響應頭部,告訴瀏覽器該資源的特殊標識
  2. 再次請求,若是瀏覽器緩存未過時,直接讀取緩存中的資源(disk cache
  3. 當瀏覽器的緩存資源過時,此時再發起 HTTP 請求時,會自動帶上 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);
  }
})
複製代碼

3.其餘

Pragma

在 HTTP/1.0 時期存在一個通用首部 Pragma ,當它的值爲 no-cache 時,與 Cache-Control: no-cache 的行爲一致。它在「請求-響應」鏈中可能會有不一樣的效果,如今通常用於向後兼容只支持 HTTP/1.0 的客戶端。

Chrome 下測試,在請求頭部/響應頭部中設置 Pragma: 'no-cache' 都可以實現禁用緩存:

但在 IE 11 下,當 Pragma 置於響應頭部時並未生效,能夠在 IE 11 下運行測試代碼進行驗證。

cache

在 chrome 下控制檯能夠看到瀏覽器本地緩存分爲兩類:memory cachedisk cache,即內存緩存磁盤緩存

  • 內存緩存主要包含當前頁面已經加載的資源,例如圖片、腳本等,當關閉TAB頁時,內存中的緩存將被釋放。
  • 磁盤緩存讀取較內存緩存慢,可是勝在時效性和存儲容量上,頁面關閉後緩存依然存在。

那麼瀏覽器是如何區分哪些資源存放在內存中,哪些又存在磁盤中呢?

其實這個問題沒有一個標準答案,廣泛認爲和系統當前內存的使用狀況有關,若是當前系統內存使用率高,那麼會優先存儲在磁盤中;另一個就是對於大文件,通常存儲在磁盤中。

緩存優先級

關於優先級,強制緩存的優先級老是大於協商緩存,只有在強制緩存失效後纔會發起請求進行協商緩存;

而在協商緩存中,Last-Modified表示的是一個 GMT 格式的時間,只能精確到秒,所以 ETag 的精確度要高於 Last-Modified,但同時每次進行 hash 運算生成標識也會帶來額外的開銷。兩者都存在時,服務端應以 ETag 爲準。

總的優先級以下:

Pragma > Cache-Control > Expries > ETag > Last-Modified

Chrome下驗證,當 Pragmano-cacheCache-Control 設置 1000s 緩存時,瀏覽器會禁用緩存:

一樣,設置響應頭爲 Cache-Control: 'no-cache'Expries1000s 後過時,瀏覽器依然禁用緩存:

緩存過程

總體的緩存過程以下:

IE 下的緩存

兼容 IE 11 的過程當中踩過一些坑,在實際項目中遇到的印象比較深入的問題是下面這個:

因爲 IE 對 GET 接口的緩存,當用戶首次進入系統時,由於未登陸跳轉至sso,登陸成功以後仍然返回的是緩存中的未登陸,致使登陸以後出現閃屏,在原系統和sso之間不停來回跳轉。

另外,因爲 IE 瀏覽器打開控制檯以後默認開啓始終從服務端刷新,在 debug 階段着實給我形成了不小的困擾,後來放棄使用控制檯,經過抓包工具Charles進行截取、分析,這才定位到問題。

IE 緩存問題

究其緣由,是 IE 對於 GET 請求的緩存策略問題:

  • 對於首次請求,會對服務器發起請求
  • 對於非首次請求,會直接從緩存中讀取數據

屢次發起 GET 請求時,若 url 未發生變化,IE 則認爲這是非首次請求,直接讀取緩存。

解決方法

1. 在 url 中加入隨機標識

經過在 get 請求的 url 中加入隨機標識,例如時間戳、隨機數等,來達到變動 url 的目的,此時瀏覽器不會從緩存中讀取數據

2. 設置 header 響應頭部

服務端設置響應頭部禁止瀏覽器緩存

{
  'Cache-Control': 'no-cache',
  'Pragma': 'no-cache',
  'Expires': -1,
}
複製代碼

3. 設置 header 請求頭部

在實際項目中我採用的是這種解決方案

{
  'Cache-Control': 'no-cache',
  'Pragma': 'no-cache',
}
複製代碼

4. 更改成 post 請求(不推薦)

參考

相關文章
相關標籤/搜索