本文首發於 Array_Huang的技術博客——實用至上
,非經做者贊成,請勿轉載。
原文地址:https://segmentfault.com/a/1190000010317802
若是您對本系列文章感興趣,歡迎關注訂閱這裏:https://segmentfault.com/blog/array_huang
一個成熟的項目,天然離不開迭代更新;那麼在部署前端這一塊,咱們免不了老是要顧及到瀏覽器緩存的,本文將介紹如何在 webpack (架構)的幫助下,妥善處理好瀏覽器緩存。javascript
實際上,我很早之前就想寫這一part了,只是苦於當時我所掌握的方案不如人意,便不敢獻醜了;而自從
webpack 升級到 v2 版本後,以及第三方plugin的日益豐富,咱們也有了更多的手段來處理cache。css
下面來簡單介紹一下瀏覽器緩存,以及爲什麼我要在標題中強調「該去則去,該留則留」。html
瀏覽器緩存(Browser Cache),是瀏覽器爲了節省網絡帶寬、加快網站訪問速度而推出的一項功能。瀏覽器緩存的運行機制是這樣的:前端
Cache-Control
、Pragma
、ETag
、Expires
、Last-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生成的全部文件都須要處理好瀏覽器緩存的問題。
在 webpack 架構下,js文件也有不一樣類型,所以也須要不一樣的配置:
output.filename
參數中,讓生成的文件名中帶上[chunkhash]
便可。output.chunkFilename
參數,操做同上。CommonsChunkPlugin
生成的文件:在CommonsChunkPlugin
的配置參數中有filename
這一項,操做同上。但須要注意的是,若是你使用[chunkhash]
的話,webpack 構建的時候但是會報錯的哦;那可咋辦呢,用[hash]
的話,這common chunk
不就每次上線新版本都強制刷新了嗎?這實際上是由於,webpack 的 runtime && manifest 會統一保存在你的common chunk
裏,解決的方法,就請看下面關於「webpack 的 runtime && manifest」的部分了。對於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-loader
或file-loader
。
對於url-loader
來講,就不須要關心瀏覽器緩存了,由於它是把靜態資源轉化成 dataurl 了,而並不是獨立的文件。
而對於file-loader
來講,一樣是在文件名的配置上加上[chunkhash]
便可。另外須要注意的是,url-loader
通常搭配有降級到file-loader
的配置(使用loader加載的文件大於一個你設定的值就降級到使用file-loader
來加載),一樣須要在文件名的配置上加上[chunkhash]
。
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 chunk
,runtime && manifest
會被放在以入口文件爲首的chunk(俗稱「大包」)裏,若是是咱們這種多頁(又稱多入口)應用,則會每一個大包一份runtime && manifest
;這誇張的冗餘咱們天然是不能忍的,那麼
用上CommonsChunkPlugin
後,runtime && manifest
就會統一遷到common chunk
了。
runtime && manifest
給common chunk
帶來的緩存危機雖然說把runtime && manifest
遷到common chunk
後,代碼冗餘的問題算是解決了,但卻形成另外一問題:因爲咱們在上述的靜態資源的文件名命名上都採用了[chunkhash]
的方案,所以也使得只要咱們稍一改動源代碼,就會有起碼一個 chunk 的命名會產生變化,這就會致使咱們的runtime && manifest
也產生變化,從而致使咱們的common chunk
也發生變化,這或許就是 webpack 規定含有runtime && manifest
的common chunk
不能使用[chunkhash]
的緣由吧(反正chunkhash確定會變的,還不如不用呢是否是)。
要解決上述問題(這問題很嚴重啊我摔,common chunk
怎麼能用不上緩存啊,這但是最大的chunk啊),咱們就須要把runtime && manifest
給獨立出去。方法也很簡單,在用來打包common chunk
的CommonsChunkPlugin
後,再加一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 打包構建的時候都會更新了。
runtime && manifest
chunk 中獨立出去嗎?是的,不用驚訝,的確是有這麼一個騷操做。
把 manifest 獨立出去的理由是這樣的:manifest 獨立出去後,runtime 的部分基本上就不會有變更了;到這裏,咱們就知道,runtime && manifest
裏實際上就是 manifest 在變;所以把 manifest 獨立出去,也是進一步地利用瀏覽器緩存(能夠把 runtime 的緩存保留下來)。
具體是怎麼作的呢?主流有倆方案:
html-webpack-plugin
來生成html頁面的話,還能夠利用inline-chunk-manifest-html-webpack-plugin(html-webpack-plugin
做者推薦)來把manifest直接輸出到html頁面上,這樣就能省一個 Http 請求了。我試用過第二種方案,好使,但最終仍是放棄了,爲何呢?
把 manifest 獨立出去後,只剩下 runtime 的 chunk 的命名仍是隻能用[hash]
,而不能利用[chunkhash]
,這就致使咱們根本無法利用瀏覽器緩存。後來,我又想出一個折衷的辦法,連[hash]
也不要了,直接寫死一個文件名;這樣的話,的確瀏覽器緩存就能保存下來了。但後來我仍是反轉了本身,這種方法雖然能留下瀏覽器緩存,卻作不到「該去則去」。或許你們會有疑問,你不是說 runtime 不會變的嗎,那留下緩存有什麼關係呀?是的,在同一 webpack 環境下 runtime 的確不會變,但難保 webpack 環境改變後,這runtime會怎麼樣呀。好比說 webpack 的版本升級了、 webpack 的配置改了、loader & plugin 的版本升級了,在這些狀況下,誰敢保證 runtime 永遠不會變啊?這 runtime 一用錯了過時的緩存,那極可能整個系統都會崩潰的啊,這個險我實在是冒不起,因此只能做罷。
不過我看了下Array-Huang/webpack-seed的runtime && manifest
chunk,也才 2kb 而已嘛,大家管好本身的強迫症和代碼潔癖好嗎?!
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 會生成獨立的 chunk 文件,好比CommonsChunkPlugin
或ExtractTextPlugin
(從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-seed(https://github.com/Array-Huang/webpack-seed
)。
本文首發於 Array_Huang的技術博客——實用至上
,非經做者贊成,請勿轉載。
原文地址:https://segmentfault.com/a/1190000010317802
若是您對本系列文章感興趣,歡迎關注訂閱這裏:https://segmentfault.com/blog/array_huang