webpack多頁應用架構系列(十六):善用瀏覽器緩存,該去則去,該留則留

本文首發於 Array_Huang的技術博客—— 實用至上,非經做者贊成,請勿轉載。
原文地址: https://segmentfault.com/a/1190000010317802
若是您對本系列文章感興趣,歡迎關注訂閱這裏: https://segmentfault.com/blog/array_huang

前言

一個成熟的項目,天然離不開迭代更新;那麼在部署前端這一塊,咱們免不了老是要顧及到瀏覽器緩存的,本文將介紹如何在 webpack (架構)的幫助下,妥善處理好瀏覽器緩存。javascript

實際上,我很早之前就想寫這一part了,只是苦於當時我所掌握的方案不如人意,便不敢獻醜了;而自從
webpack 升級到 v2 版本後,以及第三方plugin的日益豐富,咱們也有了更多的手段來處理cache。css

瀏覽器緩存簡單介紹

下面來簡單介紹一下瀏覽器緩存,以及爲什麼我要在標題中強調「該去則去,該留則留」。html

瀏覽器緩存是啥?

瀏覽器緩存(Browser Cache),是瀏覽器爲了節省網絡帶寬、加快網站訪問速度而推出的一項功能。瀏覽器緩存的運行機制是這樣的:前端

  1. 用戶使用瀏覽器第一次訪問某網站頁面,該頁面上引入了各類各樣的靜態資源(js/css/圖片/字體……),瀏覽器會把這些靜態資源,甚至是頁面自己(html文件),都一一儲存到本地。
  2. 用戶在後續的訪問中,若是須要再次請求一樣的靜態資源(根據 url 進行匹配),且靜態資源沒有過時(服務器端有一系列判別資源是否過時的策略,好比Cache-ControlPragmaETagExpiresLast-Modified),則直接使用前面本地儲存的資源,而不須要重複請求。

因爲webpack只負責構建生成網站前端的靜態資源,不涉及服務器,所以本文不討論以HTTP Header爲基礎的緩存控制策略;那咱們討論什麼呢?java

很簡單,因爲瀏覽器是根據靜態資源的url來判斷該靜態資源是否已有緩存,而靜態資源的文件目錄又是相對固定的,那麼重點明顯就在於靜態資源的文件名了;咱們就經過操控靜態資源的文件名,來決定靜態資源的「去留」。webpack

瀏覽器緩存,該留不留會怎麼樣?

每次部署上線新版本,靜態資源的文件名如有變化,則瀏覽器判斷是第一次讀取這個靜態資源;那麼,即使這個靜態資源的內容跟上一版的徹底一致,瀏覽器也要從新下載這個靜態資源,浪費網絡帶寬、拖慢頁面加載速度。git

瀏覽器緩存,該去不去會怎麼樣?

每次部署上線新版本,靜態資源的文件名若沒有變化,則瀏覽器判斷可加載以前緩存下來的靜態資源;那麼,即使這個靜態資源的內容跟上一版的有所變化,瀏覽器也察覺不到,使用了老版本的靜態資源。那這會形成什麼樣的影響呢?可大可小,小至用戶看到的依然是老版的資源,達不到上線更新版本的目的;大至形成網站運行報錯、佈局錯位等問題。github

如何經過操控靜態資源的文件名達到控制瀏覽器緩存的目的呢?

在webpack關於文件名命名的配置中,存在一系列的變量(或者理解成命名規則也可),經過這些變量,咱們能夠根據所要生成的文件的具體狀況來進行命名,而沒必要預設好一個固定的名稱。在緩存處理這一塊,咱們主要用到[hash][chunkhash]這兩個變量。關於這兩個變量的介紹,我在以前的文章 —— 《webpack配置經常使用部分有哪些?》就已經解釋過是什麼意思了,這裏就再也不累述。web

這裏總結下[hash][chunkhash]這兩個變量的用法:算法

  • [hash]的話,因爲每次使用 webpack 構建代碼的時候,此 hash 字符串都會更新,所以至關於強制刷新瀏覽器緩存
  • [chunkhash]的話,則會根據具體 chunk 的內容來造成一個 hash 字符串來插入到文件名上;換句說, chunk 的內容不變,該 chunk 所對應生成出來的文件的文件名也不會變,由此,瀏覽器緩存便能得以繼續利用

有哪些資源是須要兼顧瀏覽器緩存的?

理論上來講,除了HTML文件外(HTML文件的路徑須要保持相對固定,只能從服務器端入手),webpack生成的全部文件都須要處理好瀏覽器緩存的問題。

js

在 webpack 架構下,js文件也有不一樣類型,所以也須要不一樣的配置:

  1. 入口文件(Entry):在webpack配置中的output.filename參數中,讓生成的文件名中帶上[chunkhash]便可。
  2. 異步加載的chunk:output.chunkFilename參數,操做同上。
  3. 經過CommonsChunkPlugin生成的文件:在CommonsChunkPlugin的配置參數中有filename這一項,操做同上。但須要注意的是,若是你使用[chunkhash]的話,webpack 構建的時候但是會報錯的哦;那可咋辦呢,用[hash]的話,這common chunk不就每次上線新版本都強制刷新了嗎?這實際上是由於,webpack 的 runtime && manifest 會統一保存在你的common chunk裏,解決的方法,就請看下面關於「webpack 的 runtime && manifest」的部分了。

css

對於css來講,若是你是用style-loader直接把css內聯到<head>裏的,那麼,你管好引入該css的js文件的瀏覽器緩存就行了。

而若是你是使用extract-text-webpack-plugin把css獨立打包成css文件的,那麼在文件名的配置上,一樣加上[chunkhash]便可加上[contenthash]便可(感謝@FLYiNg_hbt 提醒)。這個[contenthash]是什麼東西呢?其實就是extract-text-webpack-plugin爲了與[chunkhash]區分開,而自定義的一個命名規則,其實際含義跟[chunkhash]能夠說是一致的,只是[chunkhash]已被佔用做爲 chunk 的內容 hash 字符串了,繼續用[chunkhash]會形成下述問題

圖片、字體文件等靜態資源

《據說webpack連圖片和字體也能打包?》裏介紹的,處理這類靜態資源通常使用url-loaderfile-loader

對於url-loader來講,就不須要關心瀏覽器緩存了,由於它是把靜態資源轉化成 dataurl 了,而並不是獨立的文件。

而對於file-loader來講,一樣是在文件名的配置上加上[chunkhash]便可。另外須要注意的是,url-loader通常搭配有降級到file-loader的配置(使用loader加載的文件大於一個你設定的值就降級到使用file-loader來加載),一樣須要在文件名的配置上加上[chunkhash]

webpack 的runtime && manifest

所謂的runtime,就是幫助 webpack 編譯構建後的打包文件在瀏覽器運行的一些輔助代碼段,換句話說,打包後的文件,除了你本身的源碼和npm庫外,還有 webpack 提供的一點輔助代碼段。

而 manifest,則是 webpack 用以查找 chunk 真實路徑所使用的一份關係表,簡單來講,就是 chunk 名對應 chunk 路徑的關係表。manifest 通常來講會被藏到 runtime 裏,所以咱們查看 runtime 的時候,雖然能找獲得 manifest,但通常都不那麼直觀,形以下面這一段(僅common chunk部分):

u.type = "text/javascript", u.charset = "utf-8", u.async = !0, u.timeout = 12e4, n.nc && u.setAttribute("nonce", n.nc), u.src = n.p + "" + e + "." + {
    0: "e6d1dff43f64d01297d3",
    1: "7ad996b8cbd7556a3e56",
    2: "c55991cf244b3d833c32",
    3: "ecbcdaa771c68c97ac38",
    4: "6565e12e7bad74df24c3",
    5: "9f2774b4601839780fc6"
}[e] + ".bundle.js";

runtime && manifest被打包到哪裏去了?

那麼,這runtime && manifest的代碼段,會被放到哪裏呢?通常來講,若是沒有使用CommonsChunkPlugin生成common chunkruntime && manifest會被放在以入口文件爲首的chunk(俗稱「大包」)裏,若是是咱們這種多頁(又稱多入口)應用,則會每一個大包一份runtime && manifest;這誇張的冗餘咱們天然是不能忍的,那麼
用上CommonsChunkPlugin後,runtime && manifest就會統一遷到common chunk了。

runtime && manifestcommon chunk帶來的緩存危機

雖然說把runtime && manifest遷到common chunk後,代碼冗餘的問題算是解決了,但卻形成另外一問題:因爲咱們在上述的靜態資源的文件名命名上都採用了[chunkhash]的方案,所以也使得只要咱們稍一改動源代碼,就會有起碼一個 chunk 的命名會產生變化,這就會致使咱們的runtime && manifest也產生變化,從而致使咱們的common chunk也發生變化,這或許就是 webpack 規定含有runtime && manifestcommon chunk不能使用[chunkhash]的緣由吧(反正chunkhash確定會變的,還不如不用呢是否是)。

要解決上述問題(這問題很嚴重啊我摔,common chunk怎麼能用不上緩存啊,這但是最大的chunk啊),咱們就須要把runtime && manifest給獨立出去。方法也很簡單,在用來打包common chunkCommonsChunkPlugin後,再加一CommonsChunkPlugin

/* 抽取出全部通用的部分 */
  new webpack.optimize.CommonsChunkPlugin({
    name: 'commons/commons',      // 須要注意的是,chunk的name不能相同!!!
    filename: '[name]/bundle.[chunkhash].js', // 因爲runtime獨立出去了,這裏即可以使用[chunkhash]了
    minChunks: 4,
  }),
  /* 抽取出webpack的runtime代碼,避免稍微修改一下入口文件就會改動commonChunk,致使本來有效的瀏覽器緩存失效 */
  new webpack.optimize.CommonsChunkPlugin({
    name: 'webpack-runtime',
    filename: 'commons/commons/webpack-runtime.[hash].js', // 注意runtime只能用[hash]
  }),

這樣一來,runtime && manifest代碼段就會被打包到這個名爲webpack-runtime的 chunk 裏了。這是什麼原理呢?聽說是在使用CommonsChunkPlugin的狀況下, webpack 會把runtime && manifest打包到最後面的一個CommonsChunkPlugin生成的 chunk 裏,而若是這個chunk沒有其它代碼,那麼天然就達到了把runtime && manifest獨立出去的目的了。

須要注意的是,若是你用了html-webpack-plugin來生成html頁面,記得要把這runtime && manifest的 chunk 插入到html頁面上,否則頁面報錯了可不怪我哦。

至此,因爲runtime && manifest獨立出去成一個chunk了,因而common chunk的命名即可以使用[chunkhash]了,也就是說,common chunk如今也能作到公共模塊內容有更新了,才更新文件名;另外一方面,這個獨立出去的 runtime && manifest chunk,是每次 webpack 打包構建的時候都會更新了。

有必要把 manifest 從 runtime && manifest chunk 中獨立出去嗎?

是的,不用驚訝,的確是有這麼一個騷操做。

把 manifest 獨立出去的理由是這樣的:manifest 獨立出去後,runtime 的部分基本上就不會有變更了;到這裏,咱們就知道,runtime && manifest裏實際上就是 manifest 在變;所以把 manifest 獨立出去,也是進一步地利用瀏覽器緩存(能夠把 runtime 的緩存保留下來)。

具體是怎麼作的呢?主流有倆方案:

我試用過第二種方案,好使,但最終仍是放棄了,爲何呢?

把 manifest 獨立出去後,只剩下 runtime 的 chunk 的命名仍是隻能用[hash],而不能利用[chunkhash],這就致使咱們根本無法利用瀏覽器緩存。後來,我又想出一個折衷的辦法,連[hash]也不要了,直接寫死一個文件名;這樣的話,的確瀏覽器緩存就能保存下來了。但後來我仍是反轉了本身,這種方法雖然能留下瀏覽器緩存,卻作不到「該去則去」。或許你們會有疑問,你不是說 runtime 不會變的嗎,那留下緩存有什麼關係呀?是的,在同一 webpack 環境下 runtime 的確不會變,但難保 webpack 環境改變後,這runtime會怎麼樣呀。好比說 webpack 的版本升級了、 webpack 的配置改了、loader & plugin 的版本升級了,在這些狀況下,誰敢保證 runtime 永遠不會變啊?這 runtime 一用錯了過時的緩存,那極可能整個系統都會崩潰的啊,這個險我實在是冒不起,因此只能做罷。

不過我看了下Array-Huang/webpack-seedruntime && manifest chunk,也才 2kb 而已嘛,大家管好本身的強迫症和代碼潔癖好嗎?!

緩存問題雜項

模塊id帶來的緩存問題

webpack 處理模塊(module)間依賴關係時,須要給各個模塊定一個 id 以做標識。webpack 默認的 id 命名規則是根據模塊引入的順序,賦予一個整數(一、二、3……)。當你在源碼中任意增添或刪減一個模塊的依賴,都會對整個
id 序列形成極大的影響,可謂是「牽一髮而動全身」了。那麼這對咱們的瀏覽器緩存會有什麼樣直接的影響呢?影響就是會形成,各個chunk中都不必定有實質的變化,但引用的依賴模塊id卻都變了,這明顯就會形成 chunk 的文件名的變更,從而影響瀏覽器緩存。

webpack 官方文檔裏推薦咱們使用一個已內置進 webpack2 裏的 plugin:HashedModuleIdsPlugin,這個 plugin 的官方文檔在這裏

webpack1 時代便有一個NamedModulesPlugin,它的原理是直接使用模塊的相對路徑做爲模塊的 id,這樣只要模塊的相對路徑,模塊 id 也就不會變了。那麼這個HashedModuleIdsPlugin對比起NamedModulesPlugin來講又有什麼進步呢?

是這樣的,因爲模塊的相對路徑有可能會很長,那麼就會佔用大量的空間,這一點是一直爲社區所詬病的;但這個HashedModuleIdsPlugin是根據模塊的相對路徑生成(默認使用md5算法)一個長度可配置(默認截取4位)的字符串做爲模塊的 id,那麼它佔用的空間就很小了,你們也就能夠安心服用了。

To generate identifiers that are preserved over builds, webpack supplies the NamedModulesPlugin (recommended for development) and HashedModuleIdsPlugin (recommended for production).

從上可知,官方是推薦開發環境用NamedModulesPlugin,而生產環境用HashedModuleIdsPlugin的,緣由彷佛是與熱更新(hmr)有關;不過就我看來,僅在生產環境用HashedModuleIdsPlugin就好了,開發環境還管啥瀏覽器緩存啊,俺開 chrome dev-tool 設置了不用任何瀏覽器緩存的。

用法也挺簡單的,直接加到plugin參數就成了:

plugins: {
  // 其它plugin
  new webpack.HashedModuleIdsPlugin(),  
}

由某些 plugin 形成的文件改動監測失敗

有些 plugin 會生成獨立的 chunk 文件,好比CommonsChunkPluginExtractTextPlugin(從js中提取出css代碼段並生成獨立的css文件) 。

這些 plugin 在生成 chunk 的文件名時,可能沒料想到後續還會有其它 plugin (好比用來混淆代碼的UglifyJsPlugin)會對代碼進行修改,所以,由今生成的 chunk 文件名,並不能徹底反映文件內容的變化。

另外,ExtractTextPlugin有個比較嚴重的問題,那就是它生成文件名所用的[chunkhash]是直接取自於引用該css代碼段的 js chunk ;換句話說,若是我只是修改 css 代碼段,而不動 js 代碼,那麼最後生成出來的css文件名依然沒有變化,這可算是很是嚴重的瀏覽器緩存「該去不去」問題了。
2017-07-26 改動:改用[contenthash]便不會出現此問題,上見css部分

有一款 plugin 能解決以上問題:webpack-plugin-hash-output

There are other webpack plugins for hashing out there. But when they run, they don't "see" the final form of the code, because they run before plugins like webpack.optimize.UglifyJsPlugin. In other words, if you change webpack.optimize.UglifyJsPlugin config, your hashes won't change, creating potential conflicts with cached resources.

The main difference is that webpack-plugin-hash-output runs in the last compilation step. So any change in webpack or any other plugin that actually changes the output, will be "seen" by this plugin, and therefore that change will be reflected in the hash.

簡單來講,就是這個webpack-plugin-hash-output會在 webpack 編譯的最後階段,從新對全部的文件取文件內容的 md5 值,這就保證了文件內容的變化必定會反映在文件名上了。

用法也比較簡單:

plugins: {
  // 其它plugin
  new HashOutput({
    manifestFiles: 'webpack-runtime', // 指定包含 manifest 在內的 chunk
  }),
}

總結

瀏覽器緩存很重要,很重要,很重要,出問題了怕不是要給領導追着打。另外,這一塊的細節特別多,必須方方面面都顧到,否則哪一方面出了紕漏就全局泡湯。

示例代碼

諸位看本系列文章,搭配我在Github上的腳手架項目食用更佳哦(笑):Array-Huang/webpack-seedhttps://github.com/Array-Huang/webpack-seed)。

附系列文章目錄(同步更新)

本文首發於 Array_Huang的技術博客—— 實用至上,非經做者贊成,請勿轉載。
原文地址: https://segmentfault.com/a/1190000010317802
若是您對本系列文章感興趣,歡迎關注訂閱這裏: https://segmentfault.com/blog/array_huang
相關文章
相關標籤/搜索