瀏覽器快取和webpack快取配置

輝衛無敵發表於2018-07-05

瀏覽器快取

瀏覽器快取分為兩種型別:

  • 強快取:也稱為本地快取,不向伺服器傳送請求,直接使用客戶端本地快取資料
  • 協商快取:也稱304快取,向伺服器傳送請求,由伺服器判斷請求檔案是否發生改變。如果未發生改變,則返回304狀態碼,通知客戶端直接使用本地快取;如果發生改變,則直接返回請求檔案。

瀏覽器快取機制的過程如下:

瀏覽器快取機制

強快取(本地快取)

強快取是最徹底的快取,無需向伺服器傳送請求,通常用於css、js、圖片等靜態資源。瀏覽器傳送請求後會先判斷本地是否有快取。如果無快取,則直接向伺服器傳送請求;如果有快取,則判斷快取是否命中強快取,如果命中則直接使用本地快取,如果沒命中則向伺服器傳送請求。判斷是否命中本地快取的方法有兩種:ExpiresCache-Control

Expires

Expires是http1.0的響應頭,代表的含義是資源本地快取的過期時間,由伺服器設定。伺服器返回給瀏覽器的響應頭中如果包含Expires欄位,瀏覽器傳送請求時拿當前時間和Expires欄位值進行比較,判斷資源快取是否失效。如下圖所示:

Expires

Date代表請求資源的時間,Expires代表資源快取的過期時間,可以看到伺服器設定資源的快取時間為5分鐘。2017-02-10 07:53:19之前,請求這個資源就是命中本地快取。超過這個時間再去請求則不命中。

Cache-Control

Cache-Control是http1.0中新增的欄位。由於Expires設定的是資源的具體過期時間,如果伺服器時間和客戶端時間不一樣,就會造成快取錯亂,比如認為調節了客戶端的時間,所以設定資源有效期的時長更合理。http1.1新增了Cache-Control的max-age欄位。max-age代表的含義是資源有效期的時長,是一個相對時長,單位為s。

Expires

Cache-Control: max-age = 300設定資源的過期時間為5分鐘。瀏覽器再次傳送請求時,會把第一次請求的時間和max-age欄位值相加和當前時間比較,以此判斷是否命中本地快取。max-age使用的都是客戶端時間,比Expires更可靠。如果max-age和Expires同時出現,max-age的優先順序更高。Cache-Control提供了更多的欄位來控制快取:

  • no-store,不判斷強快取和協商快取,伺服器直接返回完整資源
  • no-cache,不判斷強快取,每次都需要向瀏覽器傳送請求,進行協商快取判斷
  • public,指示響應可被任何快取區快取
  • private,通常只為單個使用者快取,不允許任何共享快取對其進行快取,通常用於使用者個人資訊

協商快取

協商快取的判斷在伺服器端進行,判斷是否命中的依據就是這次請求和上次請求之間資源是否發生改變。未發生改變命中,發生改變則未命中。判斷檔案是否發生改變的方法有兩個:Last-Modified、If-Modified-SinceEtag、If-None-Match

Last-Modified、If-Modified-Since

Last-Modified是http1.0中的響應頭欄位,代表請求的資源最後一次的改變時間。If-Modified-Since是http1.0的請求頭,If-Modified-Since的值是上次請求伺服器返回的Last-Modified的值。瀏覽器第一次請求資源時,伺服器返回Last-Modified,瀏覽器快取該值。瀏覽器第二次請求資源時,用於快取的Last-Modified賦值給If-Modified-Since,傳送給伺服器。伺服器判斷If-Modified-Since和伺服器本地的Last-Modified是否相等。如果相等,說明資源未發生改變,命中協商快取;如果不相等,說明資源發生改變,未命中協商快取。

Last-Modified

可以看到該請求返回的是304狀態碼,說明資源的Last-Modified未改變,所以這次請求的Last-Modified和If-Modified-Since是一致的。

Etag、If-None-Match

Last-Modified、If-Modified-Since使用的都是伺服器提供的時間,所以相對來說還是很可靠的。但是由於修改時間的精確級別或者定期生成檔案這種情況,會造成一定的錯誤。所以http1.1新增Etag、If-None-Match欄位,完善協商快取的判斷。Etag是根據資原始檔內容生成的資源唯一識別符號,一旦資源內容發生改變,Etag就會發生改變。基於內容的識別符號比基於修改時間的更可靠。If-None-Match的值是上次請求伺服器返回的Etag的值。Etag、If-None-Match的判斷過程和Last-Modified、If-Modified-Since一致,Etag、If-None-Match的優先順序更高。

工程中遇到的問題

強快取的優勢很明顯,無需向伺服器傳送請求,節省了大量的時間和頻寬。但是有一個問題,快取有效期內想更新資源怎麼辦?我在工程中還遇到另外一個問題,一個專案有四個環境,測試環境、開發環境、線上確認環境、線上環境,四個環境的域名相同,這樣就會造成四個環境的快取共用問題。比如先訪問了測試環境,index.js被換成到瀏覽器中,再切換到線上環境,線上環境會請求index.js,此時瀏覽器就會使用本地快取中測試環境的index.js,造成程式碼錯亂。

如何使強快取失效,是問題的關鍵。通常的解決方法是更新檔名,檔名不一樣的話,瀏覽器就會重新請求資源。我們要保證新發布版本和不同環境中的檔名是不一樣的。其中一種方法在檔名後加版本號:

index.js?version=1
index.css?version=1
複製程式碼

webpack提供了很簡單的方法可以配置快取。

// webpack.config.js
module.exports = {
  entry: "main.js",
  output: {
    path: "/build",
    filename: "main.[hash].js"
  }
};
複製程式碼

通過hash佔位符,在每次生成打包檔案時,都會通過檔案內容生成唯一的hash,並新增到輸出的檔名中。如果有多個入口檔案,可以使用name佔位符設定輸出:

// webpack.config.js
module.exports = {
  entry: {
      main:"main.js",
      sub:"sub.js"
  },
  output: {
    path: "/dist",
    filename: "[name].[hash].js"
  }
};
複製程式碼

這時候有一個問題是,此時的hash是根據兩個檔案的內容來生成的,兩個檔名使用的hash是一致的。如果main.js和sub.js只有一個改變,兩個檔名都會改變,兩個檔案都會重新請求,造成資源浪費。webpack提供了chunkhash來代替hash在多入口情況下使用。chunkhash是根據每個入口檔案單獨生成的雜湊值,避免上述情況。

webpack打包動態生成檔名,我們需要動態地把檔案引用插入到html啟動檔案中。html-webpack-plugin可以幫我很好地解決這個問題。html-webpack-plugin可以動態地生成一個html檔案,並在html檔案中動態插入webpack打包生成的資原始檔。

var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpackConfig = {
  entry: 'main.js',
  output: {
    path: '/dist',
    publicPath: '/dist',
    filename: 'main.[hash].js'
  },
  plugins: [new HtmlWebpackPlugin()]
};
複製程式碼

預設在webpackConfig.output.path路徑下生成index.html,生成的html檔案如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
    <script src="main.2a6c1fee4b5b0d2c9285.js"></script>
  </body>
</html>
複製程式碼

通常html啟動檔案都有自定義的內容,所以html-webpack-plugin提供了模板功能,template欄位設定模板的路徑,html-webpack-plugin以template為模板,動態新增webpack打包生成的資源路徑。

var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpackConfig = {
  entry: 'main.js',
  output: {
    path: '/dist',
    publicPath: '/dist',
    filename: 'main.[hash].js'
  },
  plugins: [new HtmlWebpackPlugin(
      {
          template:'index.html'
      }
  )]
};
複製程式碼

原index.html內容(\index.html):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>stat-front</title>
    <link rel="stylesheet" href="//at.alicdn.com/t/font_ejl5slgdvtg74x6r.css">
  </head>
  <body>
    <div id="app" class="app-root">
        <router-view></router-view>
    </div>
    <!-- built files will be auto injected -->
  </body>
</html>
複製程式碼

生成的index.html內容(\dist\index.html):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>stat-front</title>
    <link rel="stylesheet" href="//at.alicdn.com/t/font_ejl5slgdvtg74x6r.css">
  </head>
  <body>
    <div id="app" class="app-root">
        <router-view></router-view>
    </div>
    <!-- built files will be auto injected -->
    <script src="main.2a6c1fee4b5b0d2c9285.js"></script>
  </body>
</html>
複製程式碼

最開始的時候靜態的index.html在根目錄下,webpack-dev-server設定的啟動路徑就是根目錄下的index.html,如果要啟動生成的index.html,還需要設定webpackConfig.output.publicPath

var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpackConfig = {
  entry: 'main.js',
  output: {
    path: '/dist',
    publicPath: '/',
    filename: 'main.[hash].js'
  },
  plugins: [new HtmlWebpackPlugin(
      {
          template:'index.html'
      }
  )]
};
複製程式碼

這樣webpack-dev-server在記憶體中生成的資源都存放在根目錄下,生成的index.html會代替原index.html啟動。

部落格地址

相關文章