http快取實戰(讓你再也不會學過就忘)

黃小蟲發表於2019-12-23

“學過就忘”,一個知識點長時間不去讀取,那麼在你腦海中的基因片段會逐漸模糊,但是基因索引還是存在的。本篇文章通過實戰理論想結合的方式來幫助大家更深刻的記憶常用知識點。 如有不足之處,懇請斧正。

http快取實戰(讓你再也不會學過就忘)

什麼是HTTP快取 ?

http快取通俗來說: 當客戶端向伺服器請求資源時,不會直接向伺服器獲取資源(喂:伺服器,給我一張最新的自拍圖片)。而是先看下自己的儲存裡有沒有上次儲存的還在有效期內的資源。如果有的話,就省下了一筆運費(流量)。這裡舉一個簡單的例子:

  • 客戶端需要a.js,於是傳送一個請求頭(1kb)
  • 服務端響應後,返回a.js(10kb) + 響應頭(1kb)
  • 如此反覆每次就是11kb的流量傳送

但是我們我們需要的檔案a.js的內容往往並沒有發生變化,卻仍然需要浪費流量,為了解決這個問題,於是人們提出了http快取這個概念。

HTTP快取可以分為強快取和協商快取。

強快取

  • Expires
  • Cache-Control
  • Pragma

協商快取

  • Last-Modified
  • If-Modified-Since
  • ETag
  • If-Not-Match

強快取與協商快取的區別:

使用者大人:我現在需要a.js,你們幫我拿回來
強快取: 稍等,我找下我這裡有沒有關於a.js的備份,找到了。(消耗0kb流量)
協商快取: 我這裡也有備份,不過我得問下服務端這個備份是不是最新款,傳送請求頭(1kb流量)。服務端回覆(1kb流量)響應頭則使用本地備份,若是返回a.js(10kb)和響用頭(1kb)則使用伺服器返回的最新資料。

強快取

Expires

這是 HTTP 1.0 的欄位,表示快取到期時間,是一個絕對的時間 (當前時間 + 快取時間)。在響應訊息頭中,請求資源前瀏覽器會用當前時間與其值對比,若是未過期則不需要再次請求。

新建cache.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="icon" href="data:;base64,=">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>cache</title>
  <script src="cache.js"></script>
</head>
<body>
  <h1>cache</h1>
</body>
</html>
複製程式碼

新建cache.js

let http = require('http'),
fs = require('fs'),
path = require('path'),
url = require("url")
http.createServer((req,res)=>{
  let pathname = __dirname + url.parse(req.url).pathname; // 獲取檔案路徑
  let statusCode = 200 // 響應頭狀態碼
  let extname = path.extname(pathname) // 獲取副檔名
  let headType = 'text/html' // 內容型別
  if( extname){
    switch(extname){
      case '.html':
       headType = 'text/html'
      break;
      case '.js':
        headType = 'text/javascript'
      break;
    }
  }
  fs.readFile(pathname, function (err, data) {
    res.writeHead(statusCode, {
      'Content-Type': headType,
      'Expires':new Date(Date.now() + 10000).toUTCString() // 設定檔案過期時間為10秒後
    });
    res.end(data);
  });
}).listen(3000)

console.log("Server running at http://127.0.0.1:3000/");
複製程式碼

啟動

node cache.js
複製程式碼

開啟瀏覽器訪問http://127.0.0.1:3000/cache.html 檢視network

  1. 首次訪問全部走網路請求資源
    cache1.png
  2. 10s內再次重新整理從記憶體中載入(from memory cache)
    cache2.png
  3. 10s內關閉tab,重新開啟請求的cache.html,從磁碟載入(from disk cache)
    cache3.png
  4. 10s以後請求,快取已經失效,重複第1步

缺點:

  1. Expires過期控制不穩定。因為如果我們修改電腦的本地時間,會導致瀏覽器判斷快取失效。

Cache-control

這是HTTP1.1的欄位,這是一個相對時間'Cache-Control':'max-age=20'意思是20秒內使用快取資源,不用請求伺服器

// 省略其他程式碼
  fs.readFile(pathname, function (err, data) {
    res.writeHead(statusCode, {
      'Content-Type': headType,
      'Cache-Control':'max-age=20'
    });
複製程式碼

cache5.png

Cache-Control 除了max-age 相對過期時間以外,還有很多屬性

  1. no-store:所有內容都不快取
  2. no-cache:快取,但是瀏覽器使用快取前,都會請求伺服器判斷快取資源是否是最新,它是個比較高貴的存在,因為它只用不過期的快取。
  3. public 客戶端和代理伺服器(CDN)都可快取
  4. private 只有客戶端可以快取

更多屬性可參考 Cache-Control

Tips:也許大家在這裡有一些小疑問,為什麼對cache.html設定ExpiresCache-Control在谷歌瀏覽器裡不生效。這時候檢視request header 發現 Cache-Control: max-age=0,瀏覽器強制不用快取。

cache6.png
這是因為瀏覽器會針對的使用者不同行為採用不同的快取策略,這樣會導致在不同的瀏覽器會產生不同的現象:

Chrome does something quite different: 'Cache-Control' is always set to 'max-age=0′, no matter if you press enter, f5 or ctrl+f5. Except if you start Chrome and enter the url and press enter.

pragma

pragma是http/1.1之前版本的歷史遺留欄位,僅作為與http的向後相容而定義。這裡不做討論。感興趣的朋友可以點選瞭解 Pragma

node 熱更新

這裡如果大家覺得每次修改cache.js檔案都需要重新執行node run cache.js,這裡我們可以配置node 熱更新

npm init
npm i -D nodemon
複製程式碼

npm i -Dnpm install --save-dev的縮寫

修改package.json

"scripts": {
    "dev": "nodemon ./bin/www"
  },
複製程式碼

下面我們只需要執行一次npm run dev即可

對比快取(協商快取)

上面的強快取依舊存在著很大的缺陷。當設定的時間過期後,不管檔案內容有沒有變化,我們不得不再次向伺服器請求資源。這時候我們就需要用到協商快取了。

Last-Modified

響應值,由伺服器返回給客戶端關於請求資源的最近修改時間 (GMT標準格式)

If-Modified-Since

請求值 , 由客戶端傳送給服務端上次返回的資源修改時間

首次請求時服務端會攜帶Last-Modified返回給客戶端,客戶端將其數值儲存起來,並重新命名為If-Modified-Since 再次請求時,客戶端會先傳送一個攜帶If-Modified-Since的請求頭髮送到服務端,服務端會比較請求頭的If-Modified-Since和伺服器請求資源上次的修改時間(Last-Modified).

  1. 如果資源已經被修改:那麼返回響應資源a.js(10kb) + 響應頭(1kb),狀態碼:200 OK
  2. 如果沒有被修改:那麼只返回響應頭(1kb),狀態碼:304 Not Modified
// 省略其他程式碼
let stat = fs.statSync(pathname);
  fs.readFile(pathname, function (err, data) {
    // 判斷請求頭的檔案修改時間是否等於服務端的檔案修改時間
    if(req.headers['if-modified-since'] === stat.mtime.toUTCString()) { // mtime為檔案內容改變的時間戳
      statusCode = 304;
    }
    res.writeHead(statusCode, {
      'Content-Type': headType,
      'Last-Modified':stat.mtime.toUTCString()
    });
    res.end(data);    
  });
複製程式碼

cache7.png
但使用Last-Modified同樣存在缺陷

  1. last-modified是以秒為單位的,如果資源在1s內修改多次,由於1s內last-modified並未改變,客戶端仍然會使用快取。
  2. 如果在伺服器上請求的資源(a.js)被修改了,但其實際內容根本沒發生改變,會因為 Last-Modified 時間匹配不上而重新返回 a.js 給瀏覽器(舉例:伺服器動態生成檔案)

為了解決上述問題,使用新的欄位 ETag 和 If-None-Match

ETag

響應值,由伺服器返回給客戶端根據檔案內容,算出的一個唯一的值 (GMT標準格式)

If-Not-Match

請求值 , 由客戶端傳送給服務端上次返回的資源唯一值

請求流程與Last-Modified一致

上面我們所述last_modified 一般由 mtime 組成,而 ETag 一般由 last_modified content_length 組成

  // 省略其他程式碼
  fs.readFile(pathname, function (err, data) {
    let Etag = `${data.length.toString(16)}${stat.mtime.toString(16)}`
    if((req.headers['if-modified-since'] === stat.mtime.toUTCString()) || (req.headers['if-none-match'] === Etag)) {
      statusCode = 304;
    }
    res.writeHead(statusCode, {
      'Content-Type': headType,
      Etag
    });
    res.end(data);    
  });
複製程式碼

cache8.png

快取優先順序

當多種快取同時存在時,瀏覽器改使用哪種快取方式呢?這裡就有了一個優先順序關係

  • 強快取與協商快取同時存在時,如果強快取還在生效期則強制快取,否則使用協商快取。(強快取優先順序 > 對比快取優先順序)
  • 強快取expirescache-control同時存在時,cache-control會覆蓋expires (cache-control優先順序 > expires優先順序。)
  • 對比快取EtagLast-Modified同時存在時,Etag會覆蓋Last-Modified效。(ETag優先順序 > Last-Modified)優先順序。

最佳實踐

上面只是讓大家更好的瞭解了http快取的大概知識點,那麼在實際開發中我們是如何如何利用快取實現更好的使用者體驗的呢?
相信webpack大家已經並不陌生,如果有過實際配置經驗的同學一定記得我們在配置出口檔案output或者打包圖片的filename時往往會加上hash || chunkhash || contenthash這些欄位

module.exports = {
     output:{
        path:path.resolve(__dirname,'../dist'),
        filename:'js/[name].[contenthash:8].js',
        chunkFilename:'js/[name].[chunkhash:8].js'
    },
    loader:{
        rules:[
             {
                test:/\.(jep?g|png|gif)$/,
                use:{
                  loader:'url-loader',
                  options:{
                    limit:10240,
                    fallback:{
                      loader:'file-loader',
                      options:{
                        name:'img/[name].[hash:8].[ext]'
                      }
                    }
                  }
                }
            }
        ]
    }
}
複製程式碼

這樣打包出來的檔名稱往往如下

http快取實戰(讓你再也不會學過就忘)
這樣如果每次檔案有變化,那麼檔名稱隨即變化。瀏覽器則會重新請求這些檔案資源。如果檔名稱與上次一致,那麼則會使用到http快取策略。

最後附上使用者行為對瀏覽器快取的影響

http快取實戰(讓你再也不會學過就忘)

擴充閱讀

面試精選之http快取

相關文章