基於 webpack 的持久化緩存方案
如何基於 webpack 做持久化緩存似乎一直處於沒有最佳實踐的狀態。網路上各式各樣的文章很多,open 的 bug 反饋和建議成堆,很容易讓人迷茫和心智崩潰。
作為開發者最大的訴求是:在 entry 內部內容未發生變更的情況下構建之後也能穩定不變。
TL;DR;
拉到最後看總結 XD
hash 的兩種計算方式
想要做持久化緩存的首要一步是 hash,在 webpack 中提供了兩種方式,hash
和 chunkhash
在此或許有不少同學就這兩者之間的差別就模糊了:
hash
:在 webpack 一次構建中會產生一個 compilation 對象,該 hash 值是對 compilation 內所有的內容計算而來的,
chunkhash
:每一個 chunk 都根據自身的內容計算而來。
單從上訴描述來看,chunkhash
應該在持久化緩存中更為有效。
到底是否如此呢,接下來我們設定一個應用場景。
設定場景
entry入口文件入口文件依賴鏈pageAa.jsa.less <- a.css
common.js <- common.less <- common.css
lodashpageBb.jsb.less <- b.css
common.js <- common.less <- common.css
lodash
- hash 計算方式為
hash
時:
......nmodule.exports = {n entry: {n "pageA": "./a.js",n "pageB": "./b.js",n },n output: {n path: path.join(cwd, dist),n filename: [name]-[hash].jsn },n module: {n rules: ...n },n plugins: [n new ExtractTextPlugin([name]-[hash].css),n ]n}n
構建結果:
Hash: 7ee8fcb953c70a896294nVersion: webpack 3.8.1nTime: 6308msn Asset Size Chunks Chunk Namesn pageB-7ee8fcb953c70a896294.js 525 kB 0 [emitted] [big] pageBn pageA-7ee8fcb953c70a896294.js 525 kB 1 [emitted] [big] pageAnpageA-7ee8fcb953c70a896294.css 147 bytes 1 [emitted] pageAnpageB-7ee8fcb953c70a896294.css 150 bytes 0 [emitted] pageBn
如果細心一點,多嘗試幾次,可以發現即使在全部內容未變動的情況下 hash 值也會發生變更,原因在於我們使用了 extract,extract 本身涉及到非同步的抽取流程,所以在生成 assets 資源時存在了不確定性(先後順序),而 updateHash 則對其敏感,所以就出現了如上所說的 hash 異動的情況。另外所有 assets 資源的 hash 值保持一致,這對於所有資源的持久化緩存來說並沒有深遠的意義。
- hash 計算方式為
chunkhash
時:
......nmodule.exports = {n entry: {n "pageA": "./a.js",n "pageB": "./b.js",n },n output: {n path: path.join(cwd, dist),n filename: [name]-[chunkhash].jsn },n module: {n rules: ...n },n plugins: [n new ExtractTextPlugin([name]-[chunkhash].css),n ]n}n
構建結果:
Hash: 1b432b2e0ea7c80439ffnVersion: webpack 3.8.1nTime: 1069msn Asset Size Chunks Chunk Namesn pageB-58011d1656e7b568204e.js 525 kB 0 [emitted] [big] pageBn pageA-5c744cecf5ed9dd0feaf.js 525 kB 1 [emitted] [big] pageAnpageA-5c744cecf5ed9dd0feaf.css 147 bytes 1 [emitted] pageAnpageB-58011d1656e7b568204e.css 150 bytes 0 [emitted] pageBn
此時可以發現,運行多少次,hash 的異動沒有了,每個 entry 擁有了自己獨一的 hash 值,細心的你或許會發現此時樣式資源的 hash 值和 入口腳本保持了一致,這似乎並不符合我們的想法,冥冥之中告訴我們發生了某些壞事情。
然後嘗試隨意修改 b.css
然後重新構建得到以下日誌,
Hash: 50abba81a316ad20f82anVersion: webpack 3.8.1nTime: 1595msn Asset Size Chunks Chunk Namesn pageB-58011d1656e7b568204e.js 525 kB 0 [emitted] [big] pageBn pageA-5c744cecf5ed9dd0feaf.js 525 kB 1 [emitted] [big] pageAnpageA-5c744cecf5ed9dd0feaf.css 147 bytes 1 [emitted] pageAnpageB-58011d1656e7b568204e.css 147 bytes 0 [emitted] pageBn
不可思議的恐怖的事情發生了,居然 PageB 腳本和樣式的 hash 值均未發生改變。為什麼?細想一下不難理解,因為在 webpack 中所有的內容都視為 js 的一部分,而當構建發生,extract 生效後,樣式被抽離出 entry chunk,此時對於 entry chunk 來說其本身並未發生改變,因為改變的部分已經被抽離變成 normal chunk,而 chunkhash 是根據 chunk 內容而來,所以不變更應該是符合預期的行為。雖然原理和結果符合預期,但是這並不是持久化緩存所需要的。幸運的是,extract-text-plugin 為抽離出來的內容提供了 contenthash
即:new ExtractTextPlugin([name]-[contenthash].css)
Hash: 50abba81a316ad20f82anVersion: webpack 3.8.1nTime: 1177msn Asset Size Chunks Chunk Namesn pageB-58011d1656e7b568204e.js 525 kB 0 [emitted] [big] pageBn pageA-5c744cecf5ed9dd0feaf.js 525 kB 1 [emitted] [big] pageAnpageA-3ebfe4559258be46a13401ec147e4012.css 147 bytes 1 [emitted] pageAnpageB-c584acc56d4dd7606ab09eb7b3bd5e9f.css 147 bytes 0 [emitted] pageBn
此時我們再修改 b.css
然後重新構建得到以下日誌,
Hash: 08c8682f823ef6f0d661nVersion: webpack 3.8.1nTime: 1313msn Asset Size Chunks Chunk Namesn pageB-58011d1656e7b568204e.js 525 kB 0 [emitted] [big] pageBn pageA-5c744cecf5ed9dd0feaf.js 525 kB 1 [emitted] [big] pageAnpageA-3ebfe4559258be46a13401ec147e4012.css 147 bytes 1 [emitted] pageAnpageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 0 [emitted] pageBn
很棒!一切符合預期,只有 pageB 的樣式 hash 發生了變更。你以為事情都結束了,然而總是會一波三折
接下來我們嘗試在 a.js
中除去依賴 a.less
,再進行一次構建,得到以下日誌
Hash: 649f27b36d142e5e39ccnVersion: webpack 3.8.1nTime: 1557msn Asset Size Chunks Chunk Namesn pageB-0ca5aed30feb05b1a5e2.js 525 kB 0 [emitted] [big] pageBn pageA-1a8ce6dcab969d4e4480.js 525 kB 1 [emitted] [big] pageAnpageA-f83ea969c4ec627cb92bea42f12b75d6.css 91 bytes 1 [emitted] pageAnpageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 0 [emitted] pageBn
奇怪的事情再次發生,這邊我們可以理解 pageA 的腳本和樣式發生變化。但是對於 pageB 的腳本也發生變化感覺並不符合預期。
所以我們 pageB.js 去看一看到底是什麼發生了變更。
通過如下命令我們可以獲知具體的變更位置
$ git diff dist/pageB-58011d1656e7b568204e.js dist/pageB-0ca5aed30feb05b1a5e2.jsn
結果為:
/******/ __webpack_require__.p = "";n /******/n /******/ // Load entry module and return exportsn-/******/ return __webpack_require__(__webpack_require__.s = 75);n+/******/ return __webpack_require__(__webpack_require__.s = 74);n /******/ })n /************************************************************************/n /******/ ([n/***/ }),n /* 73 */,n-/* 74 */,n-/* 75 */n+/* 74 */n /***/ (function(module, exports, __webpack_require__) {nn "use strict";nnn console.log(bx);n-__webpack_require__(76);n+__webpack_require__(75);n __webpack_require__(38);n __webpack_require__(40);nn /***/ }),n-/* 76 */n+/* 75 */n /***/ (function(module, exports) {nn // removed by extract-text-webpack-pluginn
以上我們可以明確的知道,當 pageA 內移除 a.less 後整體的 id 發生了變更。那麼可以推測的是 id 代表著具體的引用的模塊。
其實在構建結束時,webpack 會給到我們具體的每個模塊分配到的 id 。
case: pageA 移除 a.less 前
[73] ./a.js 93 bytes {1} [built]n[74] ./a.less 41 bytes {1} [built]n[75] ./b.js 94 bytes {0} [built]n[76] ./b.less 41 bytes {0} [built]n
case: pageA 移除 a.less 後
[73] ./a.js 72 bytes {1} [built]n[74] ./b.js 94 bytes {0} [built]n[75] ./b.less 41 bytes {0} [built]n
通過比較發現,在 pageA 移除 a.less 的依賴前,居然在其構建出來的代碼中,隱藏著/* 73 */,
和 /* 74 */,
,也就是說 pageB 的腳本中包含著 a.js
, a.less
的模塊 id 信息。這對於持久化來說並不符合預期。我們期待的是 pageB 中不會包含任何和它並不相關的內容。
這邊衍生出兩個命題
命題1:如何把不相關的 module id 或者說內容摒除在外
命題2:如何能讓 module id 儘可能的保持不變
module id 異動
我們來一個一個看。
命題1:如何把不相關的 module id 或者說內容摒除在外
簡單來說,我們的目標就是把這些不相關的內容摒除在 pageA 和 pageB 的 entry chunk 之外。
對 webpack 熟悉的人或多或少聽說過 Code Splitting,本質上是對 chunk 進行拆分再組合的過程。那誰能完成此任務呢?
相信你已經猜到了 - CommonsChunkPlugin
接下來我們回退所有之前的變更。來檢驗我們的猜測是否正確。
在構建配置中我們加上 CommonsChunkPlugin
...nplugins: [n new ExtractTextPlugin([name]-[contenthash].css),n+ new webpack.optimize.CommonsChunkPlugin({n+ name: runtimen+ }),n],n...n
case: pageA 移除 a.less 前
Hash: fc0f3a602209ca0adea9nVersion: webpack 3.8.1nTime: 1182msn Asset Size Chunks Chunk Namesn pageB-ec1c1e788034e2312e56.js 316 bytes 0 [emitted] pageBn pageA-cd16b75b434f1ff41442.js 315 bytes 1 [emitted] pageAn runtime-3f77fc83f59d6c4208c4.js 529 kB 2 [emitted] [big] runtimen pageA-8c3d50283e85cb98eafa5ed6a3432bab.css 56 bytes 1 [emitted] pageAn pageB-64db1330bc88b15e8c5ae69a711f8179.css 59 bytes 0 [emitted] pageBnruntime-f83ea969c4ec627cb92bea42f12b75d6.css 91 bytes 2 [emitted] runtimen
case: pageA 移除 a.less 後
Hash: 8881467bf592ceb67696nVersion: webpack 3.8.1nTime: 1185msn Asset Size Chunks Chunk Namesn pageB-8e3a2584840133ffc827.js 316 bytes 0 [emitted] pageBn pageA-a5d2ad06fbaf6a0e42e0.js 190 bytes 1 [emitted] pageAn runtime-f8bc79ce500737007969.js 529 kB 2 [emitted] [big] runtimen pageB-64db1330bc88b15e8c5ae69a711f8179.css 59 bytes 0 [emitted] pageBnruntime-f83ea969c4ec627cb92bea42f12b75d6.css 91 bytes 2 [emitted] runtimen
此時我們再通過如下命令
$ git diff dist/pageB-8e3a2584840133ffc827.js dist/pageB-ec1c1e788034e2312e56.jsn
對 pageB 的腳本來進行對比
webpackJsonp([0],{nn-/***/ 74:n+/***/ 75:n /***/ (function(module, exports, __webpack_require__) {nn "use strict";nnn console.log(bx);n-__webpack_require__(75);n+__webpack_require__(76);n __webpack_require__(27);n __webpack_require__(28);nn /***/ }),nn-/***/ 75:n+/***/ 76:n /***/ (function(module, exports) {nn // removed by extract-text-webpack-pluginnn /***/ })nn-},[74]);n No newline at end of filen+},[75]);n No newline at end of filen
發現模塊的內容終於不再包含和 pageB 不相關的其他的內容。換言之 CommonsChunkPlugin
達到了我們的預期,其實這部分內容即是 webpack 的 runtime,他存儲著 webpack 對 module 和 chunk 的信息。另外有趣的是 pageA 和 pageB 在尺寸上也有了驚人的減小,原因在於默認行為的 CommonsChunkPlugin
會把 entry chunk 都包含的 module 抽取到這個名為 runtime 的 normal chunk 中。在持久化緩存中我們的目標是力爭變更達到最小化。但是在如上兩次變更中不難發現我們僅僅是變更了 pageA 但是 runtime pageB pageA 卻都發生了變更,另外由於 runtime 中由於 CommonsChunkPlugin
的默認行為抽取了 lodash,我們有充分的理由相信 lodash 並未更新但卻需要花費高昂的代價去更新,這並不符合最小化原則。
所以在這邊需要談到的另外一點便是 CommonsChunkPlugin
的用法並不僅僅局限於自動化的抽取,在持久化緩存的背景下我們也需要人為去干預這部分內容,真正意義上去抽取公共內容,並盡量保證後續不再變更。
在這裡需要再邁出一步去自定義公共部分的內容。注意 runtime
要放在最後!
...nentry: {n "pageA": "./a.js",n "pageB": "./b.js",n+ "vendor": [ "lodash" ],n},n...nplugins: [n new ExtractTextPlugin([name]-[contenthash].css),n+ new webpack.optimize.CommonsChunkPlugin({n+ name: vendor,n+ minChunks: Infinityn+ }),n new webpack.optimize.CommonsChunkPlugin({n name: runtimen }),n],n...n
我們再對所有的變更進行回退。再來看看是否會滿足我們的期望!
case: pageA 移除 a.less 前
Hash: 719ec2641ed362269d4enVersion: webpack 3.8.1nTime: 4190msn Asset Size Chunks Chunk Namesn vendor-32e0dd05f48355cde3dd.js 523 kB 0 [emitted] [big] vendorn pageB-204aff67bf5908c0939c.js 559 bytes 1 [emitted] pageBn pageA-44af68ebd687b6c800f7.js 558 bytes 2 [emitted] pageAn runtime-77e92c75831aa5a249a7.js 5.88 kB 3 [emitted] runtimenpageA-3ebfe4559258be46a13401ec147e4012.css 147 bytes 2 [emitted] pageAnpageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 1 [emitted] pageBn
case: pageA 移除 a.less 後
Hash: 93ab4ab5c33423421e51nVersion: webpack 3.8.1nTime: 4039msn Asset Size Chunks Chunk Namesn vendor-329a6b18e90435921ff8.js 523 kB 0 [emitted] [big] vendorn pageB-96f40d170374a713b0ce.js 559 bytes 1 [emitted] pageBn pageA-1d31b041a29dcde01cc5.js 433 bytes 2 [emitted] pageAn runtime-f612a395e44e034757a4.js 5.88 kB 3 [emitted] runtimenpageA-f83ea969c4ec627cb92bea42f12b75d6.css 91 bytes 2 [emitted] pageAnpageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 1 [emitted] pageBn
到此為止,合理利用 CommonsChunkPlugin
我們解決了命題 1
命題2:如何能讓 module id 儘可能的保持不變
module id 是一個模塊的唯一性標識,且該標識會出現在構建之後的代碼中,如以下 pageB 腳本片段
/***/ 74:n/***/ (function(module, exports, __webpack_require__) {nn"use strict";nnnconsole.log(bx);n__webpack_require__(75);n__webpack_require__(13);n__webpack_require__(15);nn/***/ }),n
模塊的增減肯定或者引用權重的變更肯定會導致 id 的變更(這邊對 id 如何進行分配不做展開討論,如有興趣可以以 webpack@1 中的 OccurrenceOrderPlugin 作為切入,該插件在 webpack@2 中被默認內置)。所以不難想像如果要解決這個問題,肯定是需要再找一個能保持唯一性的內容,並在構建期間進行 id 訂正。
所以命題二被拆分成兩個部分。
- 找到替代數值型 module id 方式
- 找到時機進行 id 訂正
找到替代數值型 module id 方式
直覺的第一反應肯定是路徑,因為在一次構建中資源的路徑肯定是唯一的,另外我們也可以非常慶幸在 webpack 中肯定在 resolve module 的環節中拿到資源的路徑。
不過談到路徑,我們不得不擔憂一下,windows 和 macos 下路徑的 sep 是不一致的,如果我們把 id 生成這一塊單獨拿出來自己做了,會不會還要處理一大堆可能存在的差異性問題。帶著這樣的困惑我查閱了 webpack 的源碼其中在 ContextModule#74 和 ContextModule#35 中 webpack 對 module 的路徑做了差異性修復。
也就是說我們可以放心的通過 module 的 libIdent 方法來獲取模塊的路徑
找到時機進行 id 訂正
時機就不是難事了,在 webpack 中我一直認為最 NB 的地方在於其整體插件的實現全部基於它的 tapable 事件系統,在靈活性上堪稱完美。事件機制這部分內容我會在後續著重寫文章分享。
這邊我們只需要知道的是,在整個 webpack 執行過程中涉及 moudle id 的事件有
before-module-ids
-> optimize-module-ids
-> after-optimize-module-ids
所以我們只需要在 before-module-ids
這個時機內進行 id 訂正即可。
實現 module id 穩定
// 插件實現核心片段napply(compiler) {ntcompiler.plugin("compilation", (compilation) => {nttcompilation.plugin("before-module-ids", (modules) => {ntttmodules.forEach((module) => {nttttif(module.id === null && module.libIdent) {ntttttmodule.id = module.libIdent({nttttttcontext: this.options.context || compiler.options.contextnttttt});ntttt}nttt});ntt});nt});n}n
這部分內容,已經被 webpack 抽取為一個內置插件 NamedModulesPlugin
所以只需一小步在構建配置中添加該插件即可
...nentry: {n "pageA": "./a.js",n "pageB": "./b.js",n "vendor": [ "lodash" ],n},n...nplugins: [n new ExtractTextPlugin([name]-[contenthash].css),n+ new webpack.NamedModulesPlugin(),n new webpack.optimize.CommonsChunkPlugin({n name: vendor,n minChunks: Infinityn }),n new webpack.optimize.CommonsChunkPlugin({n name: runtimen }),n],n...n
回滾之前所有的代碼修改,我們再來做相應的比較
case: pageA 移除 a.less 前
Hash: 563971a30d909bbcb0dbnVersion: webpack 3.8.1nTime: 1271msn Asset Size Chunks Chunk Namesn vendor-a5620db988a639410257.js 539 kB 0 [emitted] [big] vendorn pageB-42b894ca482a061570ae.js 681 bytes 1 [emitted] pageBn pageA-b7d7de62392f41af1f78.js 680 bytes 2 [emitted] pageAn runtime-dc322ed118963cd2e12a.js 5.88 kB 3 [emitted] runtimenpageA-3ebfe4559258be46a13401ec147e4012.css 147 bytes 2 [emitted] pageAnpageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 1 [emitted] pageBn
case: pageA 移除 a.less 後
Hash: 0d277f49f54159bc7286nVersion: webpack 3.8.1nTime: 950msn Asset Size Chunks Chunk Namesn vendor-a5620db988a639410257.js 539 kB 0 [emitted] [big] vendorn pageB-42b894ca482a061570ae.js 681 bytes 1 [emitted] pageBn pageA-bedb93c1db950da4fea1.js 539 bytes 2 [emitted] pageAn runtime-85b317d7b21588411828.js 5.88 kB 3 [emitted] runtimenpageA-f83ea969c4ec627cb92bea42f12b75d6.css 91 bytes 2 [emitted] pageAnpageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 1 [emitted] pageBn
自此利用 NamedModulesPlugin
我們做到了 pageA 中的變更只引發了 pageA 的腳本、樣式、和 runtime 的變更,而 vendor,pageB 的腳本和樣式均未發生變更。
一窺 pageB 的代碼片段
/***/ "./b.js":n/***/ (function(module, exports, __webpack_require__) {nn"use strict";nnnconsole.log(bx);n__webpack_require__("./b.less");n__webpack_require__("./common.js");n__webpack_require__("./node_modules/_lodash@4.17.4@lodash/lodash.js");nn/***/ }),n
確實模塊的 id 被替換成了模塊的路徑。但是不得不規避的問題是,尺寸變大了,因為 id 數字 和 路徑的字元數不是一個量級,以 vendor 為例,應用方案前後尺寸上增加了 16KB
。或許有同學已經想到,那我對路徑做次 hash 然後取幾位不就得了,是的沒錯,webpack 官方就是這麼做的。NamedModulesPlugin
適合在開發環境,而在生產環境下請使用 HashedModuleIdsPlugin。
所以在生產環境下,為了獲得最佳尺寸我們需要變更下構建的配置
...nentry: {n "pageA": "./a.js",n "pageB": "./b.js",n "vendor": [ "lodash" ],n},n...nplugins: [n new ExtractTextPlugin([name]-[contenthash].css),n- new webpack.NamedModulesPlugin(),n+ new webpack.HashedModuleIdsPlugin(),n new webpack.optimize.CommonsChunkPlugin({n name: vendor,n minChunks: Infinityn }),n new webpack.optimize.CommonsChunkPlugin({n name: runtimen }),n],n...nHash: 80871a9833e531391384nVersion: webpack 3.8.1nTime: 1230msn Asset Size Chunks Chunk Namesn vendor-2e968166c755a7385f9b.js 524 kB 0 [emitted] [big] vendorn pageB-68be4dda51b5b08538f2.js 595 bytes 1 [emitted] pageBn pageA-a70b7fa4d67cb16cb1f7.js 461 bytes 2 [emitted] pageAn runtime-6897b6cc7d074a5b2039.js 5.88 kB 3 [emitted] runtimenpageA-f83ea969c4ec627cb92bea42f12b75d6.css 91 bytes 2 [emitted] pageAnpageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 1 [emitted] pageBn
在生產環境下把 NamedModulesPlugin 替換為 HashedModuleIdsPlugin,在包的尺寸增加幅度上上達到了可接受的範圍,以 vendor 為例,只增加了 1KB。
事情到此我以為可以結束了,直到我 diff 了一下 runtime 才發現持久化緩存似乎還可以繼續深挖。
$ diff --git a/dist/runtime-85b317d7b21588411828.js b/dist/runtime-dc322ed118963cd2e12a.jsn/******/ if (__webpack_require__.nc) {n /******/ script.setAttribute("nonce", __webpack_require__.nc);n /******/ }n-/******/ script.src = __webpack_require__.p + "" + chunkId + "-" + {"0":"a5620db988a639410257","1":"42b894ca482a061570ae","2":"bedb93c1db950da4fea1"}[chunkId] + ".js";n+/******/ script.src = __webpack_require__.p + "" + chunkId + "-" + {"0":"a5620db988a639410257","1":"42b894ca482a061570ae","2":"b7d7de62392f41af1f78"}[chunkId] + ".js";n /******/ var timeout = setTimeout(onScriptComplete, 120000);n /******/ script.onerror = script.onload = onScriptComplete;n /******/ function onScriptComplete() {n
我們發現在 3 個 entry 入口未改變的情況下,變更某個 entry chunk 的內容,對應 runtime 腳本的變更只是涉及到了 chunk id 的變更。基於 module id 的經驗,自然想到了是不是有相應的唯一性內容來取代現有的 chunk id,因為數值型的 chunk id 總會存在不確定性。
所以至此問題又再次被拆分成兩個命題:
- 找到替代現有 chunk id 表達唯一性的方式
- 找到時機進行 chunk id 訂正
chunk id 的不穩定性
接下來我們一個一個看
命題1:找到替代現有 chunk id 表達唯一性的方式
因為我們知道在 webpack 中 entry 其實是具有唯一性的,而 entry chunk 的 name 即來源於我們對 entry 名的設置。所以這裡的問題變得很簡單我們只需要把每個 chunk 對應的 id 指向到對應 chunk 的 name 即可。
命題2:找到時機進行 chunk id 訂正
在整個 webpack 執行過程中涉及 moudle id 的事件有
before-chunk-ids
-> optimize-chunk-ids
-> after-optimize-chunk-ids
所以我們只需要在 before-chunk-ids
這個時機內進行 chunk id 訂正即可。
偽代碼:
apply(compiler) {ntcompiler.plugin("compilation", (compilation) => {nttcompilation.plugin("before-chunk-ids", (chunks) => {ntttchunks.forEach((chunk) => {nttttif(chunk.id === null) {ntttttchunk.id = chunk.name;ntttt}nttt});ntt});nt});n}n
非常簡單。
在 webpack@2 時期作者把這個部分的實現引入到了官方插件,即 NamedChunksPlugin
。
所以在一般需求下我們只需要在構建配置中添加 NamedChunksPlugin
的插件即可。
...nentry: {n "pageA": "./a.js",n "pageB": "./b.js",n "vendor": [ "lodash" ],n},n...nplugins: [n new ExtractTextPlugin([name]-[contenthash].css),n new webpack.NamedModulesPlugin(),n+ new webpack.NamedChunksPlugin(),n new webpack.optimize.CommonsChunkPlugin({n name: vendor,n minChunks: Infinityn }),n new webpack.optimize.CommonsChunkPlugin({n name: runtimen }),n],n...n
runtime 的 diff
/******/n /******/ // objects to store loaded and loading chunksn /******/ var installedChunks = {n-/******/ 3: 0n+/******/ "runtime": 0n /******/ };n /******/n /******/ // The require functionn@@ -91,7 +91,7 @@n /******/ if (__webpack_require__.nc) {n /******/ script.setAttribute("nonce", __webpack_require__.nc);n /******/ }n-/******/ script.src = __webpack_require__.p + "" + chunkId + "-" + {"0":"a5620db988a639410257","1":"42b894ca482a061570ae","2":"b7d7de62392f41af1f78"}[chunkId] + ".js";n+/******/ script.src = __webpack_require__.p + "" + chunkId + "-" + {"vendor":"45cd76029c7d91d6fc76","pageA":"0abd02f11fa4c29e99b3","pageB":"2b8c3672b02ff026db06"}[chunkId] + ".js";n /******/ var timeout = setTimeout(onScriptComplete, 120000);n /******/ script.onerror = script.onload = onScriptComplete;n /******/ function onScriptComplete() {n
可以看到標示 chunk 唯一性的 id 值被替換成了我們 entry 入口的名稱。非常棒!感覺出岔子的機會又減小了不少。
討論這個問題的另外一個原因是像 webpack@2 中的 dynamic import 或者 webpack@1 時的 require.ensure 會將代碼抽離出來形成一個獨立的 bundle,在 webpack 中我們把這種行為叫成 Code Splitting,一旦代碼被抽離出來,最終在構建結果中會出現 0.[hash].js 1.[hash].js ,或多或少大家對此都有過困擾。
可以預想的是通過該 plugin 我們能比較好解決這個問題,一方面我們可以嘗試定義這些被動態載入的模塊的名稱,另外一方面我們也可以遇見,假定一個構建場景會生成多個 [chunk-id].[chunkhash].js, 當 Code Splitting 的 chunk 需要變更時,比如減少了一個,此時你沒法保證在新一個 compilation 中還繼續分配到上一個 compilation 中的 [chunk-id],所以通過 name 命名的方式恰好可以順帶解決這個問題。
只是在這邊我們需要稍微對 NamedChunksPlugin
做一些變更。
...nentry: {n "pageA": "./a.js",n "pageB": "./b.js",n "vendor": [ "lodash" ],n},n...nplugins: [n new ExtractTextPlugin([name]-[contenthash].css),n new webpack.NamedModulesPlugin(),n- new webpack.NamedChunksPlugin(),n+ new webpack.NamedChunksPlugin((chunk) => {n+ if (chunk.name) {n+ return chunk.name;n+ }nn+ return chunk.mapModules(m => path.relative(m.context, m.request)).join("_");n+ }),n new webpack.optimize.CommonsChunkPlugin({n name: vendor,n minChunks: Infinityn }),n new webpack.optimize.CommonsChunkPlugin({n name: runtimen }),n],n...n
總結
要做到持久化緩存需要做好以下幾點:
- 對腳本文件應用
[chunkhash]
對 extractTextPlugin 應用的的文件應用[contenthash]
; - 使用
CommonsChunkPlugin
合理抽出公共庫vendor
(包含社區工具庫這些 如 lodash), 如果必要也可以抽取業務公共庫common
(公共部分的業務邏輯),以及 webpack的runtime
; - 在開發環境下使用
NamedModulesPlugin
來固化 module id,在生產環境下使用HashedModuleIdsPlugin
來固化 module id - 使用
NamedChunksPlugin
來固化 runtime 內以及在使用動態載入時分離出的 chunk 的 chunk id。 - 建議閱讀一下全文,因為不看你很難明白為什麼要如上這麼做。
推薦閱讀:
※webpack之loader和plugin簡介
※如果HTTP2普及了,Webpack、Rollup這種打包工具還有意義嗎?
※【譯】webpack 小札: 充分利用 CommonsChunkPlugin()
※讀懂webpack生成的26行代碼
TAG:webpack |