標籤:

基於 webpack 的持久化緩存方案

如何基於 webpack 做持久化緩存似乎一直處於沒有最佳實踐的狀態。網路上各式各樣的文章很多,open 的 bug 反饋和建議成堆,很容易讓人迷茫和心智崩潰。

作為開發者最大的訴求是:在 entry 內部內容未發生變更的情況下構建之後也能穩定不變。

TL;DR;

拉到最後看總結 XD

hash 的兩種計算方式

想要做持久化緩存的首要一步是 hash,在 webpack 中提供了兩種方式,hashchunkhash

在此或許有不少同學就這兩者之間的差別就模糊了:

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

總結

要做到持久化緩存需要做好以下幾點:

  1. 對腳本文件應用 [chunkhash] 對 extractTextPlugin 應用的的文件應用 [contenthash]
  2. 使用 CommonsChunkPlugin 合理抽出公共庫 vendor(包含社區工具庫這些 如 lodash), 如果必要也可以抽取業務公共庫 common(公共部分的業務邏輯),以及 webpack的 runtime
  3. 在開發環境下使用 NamedModulesPlugin 來固化 module id,在生產環境下使用 HashedModuleIdsPlugin來固化 module id
  4. 使用 NamedChunksPlugin 來固化 runtime 內以及在使用動態載入時分離出的 chunk 的 chunk id。
  5. 建議閱讀一下全文,因為不看你很難明白為什麼要如上這麼做。

推薦閱讀:

webpack之loader和plugin簡介
如果HTTP2普及了,Webpack、Rollup這種打包工具還有意義嗎?
【譯】webpack 小札: 充分利用 CommonsChunkPlugin()
讀懂webpack生成的26行代碼

TAG:webpack |