如何基于 webpack 做持久化缓存似乎一直处于没有最佳实践的状态。网路上各式各样的文章很多,open 的 bug 反馈和建议成堆,很容易让人迷茫和心智崩溃。
作为开发者最大的诉求是:在 entry 内部内容未发生变更的情况下构建之后也能稳定不变。
拉到最后看总结 XD
想要做持久化缓存的首要一步是 hash,在 webpack 中提供了两种方式,hash
和 chunkhash
在此或许有不少同学就这两者之间的差别就模糊了:
hash
:在 webpack 一次构建中会产生一个 compilation 对象,该 hash 值是对 compilation 内所有的内容计算而来的,
chunkhash
:每一个 chunk 都根据自身的内容计算而来。
单从上诉描述来看,chunkhash
应该在持久化缓存中更为有效。
到底是否如此呢,接下来我们设定一个应用场景。
entry | 入口文件 | 入口文件依赖链 |
---|---|---|
pageA | a.js | a.less <- a.css common.js <- common.less <- common.css lodash |
pageB | b.js | b.less <- b.css common.js <- common.less <- common.css lodash |
hash
时:...... module.exports = { entry: { "pageA": "./a.js", "pageB": "./b.js", }, output: { path: path.join(cwd, 'dist'), filename: '[name]-[hash].js' }, module: { rules: ... }, plugins: [ new ExtractTextPlugin('[name]-[hash].css'), ] }
构建结果:
Hash: 7ee8fcb953c70a896294 Version: webpack 3.8.1 Time: 6308ms Asset Size Chunks Chunk Names pageB-7ee8fcb953c70a896294.js 525 kB 0 [emitted] [big] pageB pageA-7ee8fcb953c70a896294.js 525 kB 1 [emitted] [big] pageA pageA-7ee8fcb953c70a896294.css 147 bytes 1 [emitted] pageA pageB-7ee8fcb953c70a896294.css 150 bytes 0 [emitted] pageB
如果细心一点,多尝试几次,可以发现即使在全部内容未变动的情况下 hash 值也会发生变更,原因在于我们使用了 extract,extract 本身涉及到异步的抽取流程,所以在生成 assets 资源时存在了不确定性(先后顺序),而 updateHash 则对其敏感,所以就出现了如上所说的 hash 异动的情况。另外所有 assets 资源的 hash 值保持一致,这对于所有资源的持久化缓存来说并没有深远的意义。
chunkhash
时:...... module.exports = { entry: { "pageA": "./a.js", "pageB": "./b.js", }, output: { path: path.join(cwd, 'dist'), filename: '[name]-[chunkhash].js' }, module: { rules: ... }, plugins: [ new ExtractTextPlugin('[name]-[chunkhash].css'), ] }
构建结果:
Hash: 1b432b2e0ea7c80439ff Version: webpack 3.8.1 Time: 1069ms Asset Size Chunks Chunk Names pageB-58011d1656e7b568204e.js 525 kB 0 [emitted] [big] pageB pageA-5c744cecf5ed9dd0feaf.js 525 kB 1 [emitted] [big] pageA pageA-5c744cecf5ed9dd0feaf.css 147 bytes 1 [emitted] pageA pageB-58011d1656e7b568204e.css 150 bytes 0 [emitted] pageB
此时可以发现,运行多少次,hash 的异动没有了,每个 entry 拥有了自己独一的 hash 值,细心的你或许会发现此时样式资源的 hash 值和 入口脚本保持了一致,这似乎并不符合我们的想法,冥冥之中告诉我们发生了某些坏事情。
然后尝试随意修改 b.css
然后重新构建得到以下日志,
Hash: 50abba81a316ad20f82a Version: webpack 3.8.1 Time: 1595ms Asset Size Chunks Chunk Names pageB-58011d1656e7b568204e.js 525 kB 0 [emitted] [big] pageB pageA-5c744cecf5ed9dd0feaf.js 525 kB 1 [emitted] [big] pageA pageA-5c744cecf5ed9dd0feaf.css 147 bytes 1 [emitted] pageA pageB-58011d1656e7b568204e.css 147 bytes 0 [emitted] pageB
不可思议的恐怖的事情发生了,居然 PageB 脚本和样式的 hash 值均未发生改变。为什么?细想一下不难理解,因为在 webpack 中所有的内容都视为 js 的一部分,而当构建发生,extract 生效后,样式被抽离出 entry chunk,此时对于 entry chunk 来说其本身并未发生改变,因为改变的部分已经被抽离变成 normal chunk,而 chunkhash 是根据 chunk 内容而来,所以不变更应该是符合预期的行为。虽然原理和结果符合预期,但是这并不是持久化缓存所需要的。幸运的是,extract-text-plugin 为抽离出来的内容提供了 contenthash
即: new ExtractTextPlugin('[name]-[contenthash].css')
Hash: 50abba81a316ad20f82a Version: webpack 3.8.1 Time: 1177ms Asset Size Chunks Chunk Names pageB-58011d1656e7b568204e.js 525 kB 0 [emitted] [big] pageB pageA-5c744cecf5ed9dd0feaf.js 525 kB 1 [emitted] [big] pageA pageA-3ebfe4559258be46a13401ec147e4012.css 147 bytes 1 [emitted] pageA pageB-c584acc56d4dd7606ab09eb7b3bd5e9f.css 147 bytes 0 [emitted] pageB
此时我们再修改 b.css
然后重新构建得到以下日志,
Hash: 08c8682f823ef6f0d661 Version: webpack 3.8.1 Time: 1313ms Asset Size Chunks Chunk Names pageB-58011d1656e7b568204e.js 525 kB 0 [emitted] [big] pageB pageA-5c744cecf5ed9dd0feaf.js 525 kB 1 [emitted] [big] pageA pageA-3ebfe4559258be46a13401ec147e4012.css 147 bytes 1 [emitted] pageA pageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 0 [emitted] pageB
很棒!一切符合预期,只有 pageB 的样式 hash 发生了变更。你以为事情都结束了,然而总是会一波三折
接下来我们尝试在 a.js
中除去依赖 a.less
,再进行一次构建,得到以下日志
Hash: 649f27b36d142e5e39cc Version: webpack 3.8.1 Time: 1557ms Asset Size Chunks Chunk Names pageB-0ca5aed30feb05b1a5e2.js 525 kB 0 [emitted] [big] pageB pageA-1a8ce6dcab969d4e4480.js 525 kB 1 [emitted] [big] pageA pageA-f83ea969c4ec627cb92bea42f12b75d6.css 91 bytes 1 [emitted] pageA pageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 0 [emitted] pageB
奇怪的事情再次发生,这边我们可以理解 pageA 的脚本和样式发生变化。但是对于 pageB 的脚本也发生变化感觉并不符合预期。
所以我们 pageB.js 去看一看到底是什么发生了变更。
通过如下命令我们可以获知具体的变更位置
$ git diff dist/pageB-58011d1656e7b568204e.js dist/pageB-0ca5aed30feb05b1a5e2.js
结果为:
/******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = 75); +/******/ return __webpack_require__(__webpack_require__.s = 74); /******/ }) /************************************************************************/ /******/ ([
/***/ }), /* 73 */, -/* 74 */, -/* 75 */ +/* 74 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; console.log('bx'); -__webpack_require__(76); +__webpack_require__(75); __webpack_require__(38); __webpack_require__(40); /***/ }), -/* 76 */ +/* 75 */ /***/ (function(module, exports) { // removed by extract-text-webpack-plugin
以上我们可以明确的知道,当 pageA 内移除 a.less 后整体的 id 发生了变更。那么可以推测的是 id 代表着具体的引用的模块。
其实在构建结束时,webpack 会给到我们具体的每个模块分配到的 id 。
case: pageA 移除 a.less 前
[73] ./a.js 93 bytes {1} [built] [74] ./a.less 41 bytes {1} [built] [75] ./b.js 94 bytes {0} [built] [76] ./b.less 41 bytes {0} [built]
case: pageA 移除 a.less 后
[73] ./a.js 72 bytes {1} [built] [74] ./b.js 94 bytes {0} [built] [75] ./b.less 41 bytes {0} [built]
通过比较发现,在 pageA 移除 a.less 的依赖前,居然在其构建出来的代码中,隐藏着/* 73 */,
和 /* 74 */,
,也就是说 pageB 的脚本中包含着 a.js
, a.less
的模块 id 信息。这对于持久化来说并不符合预期。我们期待的是 pageB 中不会包含任何和它并不相关的内容。
这边衍生出两个命题
命题1:如何把不相关的 module id 或者说内容摒除在外
命题2:如何能让 module id 尽可能的保持不变
我们来一个一个看。
简单来说,我们的目标就是把这些不相关的内容摒除在 pageA 和 pageB 的 entry chunk 之外。
对 webpack 熟悉的人或多或少听说过 Code Splitting,本质上是对 chunk 进行拆分再组合的过程。那谁能完成此任务呢?
相信你已经猜到了 - CommonsChunkPlugin
接下来我们回退所有之前的变更。来检验我们的猜测是否正确。
在构建配置中我们加上 CommonsChunkPlugin
... plugins: [ new ExtractTextPlugin('[name]-[contenthash].css'), + new webpack.optimize.CommonsChunkPlugin({ + name: 'runtime' + }), ], ...
case: pageA 移除 a.less 前
Hash: fc0f3a602209ca0adea9 Version: webpack 3.8.1 Time: 1182ms Asset Size Chunks Chunk Names pageB-ec1c1e788034e2312e56.js 316 bytes 0 [emitted] pageB pageA-cd16b75b434f1ff41442.js 315 bytes 1 [emitted] pageA runtime-3f77fc83f59d6c4208c4.js 529 kB 2 [emitted] [big] runtime pageA-8c3d50283e85cb98eafa5ed6a3432bab.css 56 bytes 1 [emitted] pageA pageB-64db1330bc88b15e8c5ae69a711f8179.css 59 bytes 0 [emitted] pageB runtime-f83ea969c4ec627cb92bea42f12b75d6.css 91 bytes 2 [emitted] runtime
case: pageA 移除 a.less 后
Hash: 8881467bf592ceb67696 Version: webpack 3.8.1 Time: 1185ms Asset Size Chunks Chunk Names pageB-8e3a2584840133ffc827.js 316 bytes 0 [emitted] pageB pageA-a5d2ad06fbaf6a0e42e0.js 190 bytes 1 [emitted] pageA runtime-f8bc79ce500737007969.js 529 kB 2 [emitted] [big] runtime pageB-64db1330bc88b15e8c5ae69a711f8179.css 59 bytes 0 [emitted] pageB runtime-f83ea969c4ec627cb92bea42f12b75d6.css 91 bytes 2 [emitted] runtime
此时我们再通过如下命令
$ git diff dist/pageB-8e3a2584840133ffc827.js dist/pageB-ec1c1e788034e2312e56.js
对 pageB 的脚本来进行对比
webpackJsonp([0],{ -/***/ 74: +/***/ 75: /***/ (function(module, exports, __webpack_require__) { "use strict"; console.log('bx'); -__webpack_require__(75); +__webpack_require__(76); __webpack_require__(27); __webpack_require__(28); /***/ }), -/***/ 75: +/***/ 76: /***/ (function(module, exports) { // removed by extract-text-webpack-plugin /***/ }) -},[74]); \ No newline at end of file +},[75]); \ No newline at end of file
发现模块的内容终于不再包含和 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
要放在最后!
... entry: { "pageA": "./a.js", "pageB": "./b.js", + "vendor": [ "lodash" ], }, ... plugins: [ new ExtractTextPlugin('[name]-[contenthash].css'), + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks: Infinity + }), new webpack.optimize.CommonsChunkPlugin({ name: 'runtime' }), ], ...
我们再对所有的变更进行回退。再来看看是否会满足我们的期望!
case: pageA 移除 a.less 前
Hash: 719ec2641ed362269d4e Version: webpack 3.8.1 Time: 4190ms Asset Size Chunks Chunk Names vendor-32e0dd05f48355cde3dd.js 523 kB 0 [emitted] [big] vendor pageB-204aff67bf5908c0939c.js 559 bytes 1 [emitted] pageB pageA-44af68ebd687b6c800f7.js 558 bytes 2 [emitted] pageA runtime-77e92c75831aa5a249a7.js 5.88 kB 3 [emitted] runtime pageA-3ebfe4559258be46a13401ec147e4012.css 147 bytes 2 [emitted] pageA pageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 1 [emitted] pageB
case: pageA 移除 a.less 后
Hash: 93ab4ab5c33423421e51 Version: webpack 3.8.1 Time: 4039ms Asset Size Chunks Chunk Names vendor-329a6b18e90435921ff8.js 523 kB 0 [emitted] [big] vendor pageB-96f40d170374a713b0ce.js 559 bytes 1 [emitted] pageB pageA-1d31b041a29dcde01cc5.js 433 bytes 2 [emitted] pageA runtime-f612a395e44e034757a4.js 5.88 kB 3 [emitted] runtime pageA-f83ea969c4ec627cb92bea42f12b75d6.css 91 bytes 2 [emitted] pageA pageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 1 [emitted] pageB
到此为止,合理利用 CommonsChunkPlugin
我们解决了命题 1
module id 是一个模块的唯一性标识,且该标识会出现在构建之后的代码中,如以下 pageB 脚本片段
/***/ 74:
/***/ (function(module, exports, __webpack_require__) {
"use strict";
console.log('bx');
__webpack_require__(75);
__webpack_require__(13);
__webpack_require__(15);
/***/ }),
模块的增减肯定或者引用权重的变更肯定会导致 id 的变更(这边对 id 如何进行分配不做展开讨论,如有兴趣可以以 webpack@1 中的 OccurrenceOrderPlugin 作为切入,该插件在 webpack@2 中被默认内置)。所以不难想象如果要解决这个问题,肯定是需要再找一个能保持唯一性的内容,并在构建期间进行 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 稳定
// 插件实现核心片段 apply(compiler) { compiler.plugin("compilation", (compilation) => { compilation.plugin("before-module-ids", (modules) => { modules.forEach((module) => { if(module.id === null && module.libIdent) { module.id = module.libIdent({ context: this.options.context || compiler.options.context }); } }); }); }); }
这部分内容,已经被 webpack 抽取为一个内置插件 NamedModulesPlugin
所以只需一小步在构建配置中添加该插件即可
... entry: { "pageA": "./a.js", "pageB": "./b.js", "vendor": [ "lodash" ], }, ... plugins: [ new ExtractTextPlugin('[name]-[contenthash].css'), + new webpack.NamedModulesPlugin(), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: Infinity }), new webpack.optimize.CommonsChunkPlugin({ name: 'runtime' }), ], ...
回滚之前所有的代码修改,我们再来做相应的比较
case: pageA 移除 a.less 前
Hash: 563971a30d909bbcb0db Version: webpack 3.8.1 Time: 1271ms Asset Size Chunks Chunk Names vendor-a5620db988a639410257.js 539 kB 0 [emitted] [big] vendor pageB-42b894ca482a061570ae.js 681 bytes 1 [emitted] pageB pageA-b7d7de62392f41af1f78.js 680 bytes 2 [emitted] pageA runtime-dc322ed118963cd2e12a.js 5.88 kB 3 [emitted] runtime pageA-3ebfe4559258be46a13401ec147e4012.css 147 bytes 2 [emitted] pageA pageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 1 [emitted] pageB
case: pageA 移除 a.less 后
Hash: 0d277f49f54159bc7286 Version: webpack 3.8.1 Time: 950ms Asset Size Chunks Chunk Names vendor-a5620db988a639410257.js 539 kB 0 [emitted] [big] vendor pageB-42b894ca482a061570ae.js 681 bytes 1 [emitted] pageB pageA-bedb93c1db950da4fea1.js 539 bytes 2 [emitted] pageA runtime-85b317d7b21588411828.js 5.88 kB 3 [emitted] runtime pageA-f83ea969c4ec627cb92bea42f12b75d6.css 91 bytes 2 [emitted] pageA pageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 1 [emitted] pageB
自此利用 NamedModulesPlugin
我们做到了 pageA 中的变更只引发了 pageA 的脚本、样式、和 runtime 的变更,而 vendor,pageB 的脚本和样式均未发生变更。
一窥 pageB 的代码片段
/***/ "./b.js": /***/ (function(module, exports, __webpack_require__) { "use strict"; console.log('bx'); __webpack_require__("./b.less"); __webpack_require__("./common.js"); __webpack_require__("./node_modules/[email protected]@lodash/lodash.js"); /***/ }),
确实模块的 id 被替换成了模块的路径。但是不得不规避的问题是,尺寸变大了,因为 id 数字 和 路径的字符数不是一个量级,以 vendor 为例,应用方案前后尺寸上增加了 16KB
。或许有同学已经想到,那我对路径做次 hash 然后取几位不就得了,是的没错,webpack 官方就是这么做的。NamedModulesPlugin
适合在开发环境,而在生产环境下请使用 HashedModuleIdsPlugin。
所以在生产环境下,为了获得最佳尺寸我们需要变更下构建的配置
... entry: { "pageA": "./a.js", "pageB": "./b.js", "vendor": [ "lodash" ], }, ... plugins: [ new ExtractTextPlugin('[name]-[contenthash].css'), - new webpack.NamedModulesPlugin(), + new webpack.HashedModuleIdsPlugin(), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: Infinity }), new webpack.optimize.CommonsChunkPlugin({ name: 'runtime' }), ], ...
Hash: 80871a9833e531391384 Version: webpack 3.8.1 Time: 1230ms Asset Size Chunks Chunk Names vendor-2e968166c755a7385f9b.js 524 kB 0 [emitted] [big] vendor pageB-68be4dda51b5b08538f2.js 595 bytes 1 [emitted] pageB pageA-a70b7fa4d67cb16cb1f7.js 461 bytes 2 [emitted] pageA runtime-6897b6cc7d074a5b2039.js 5.88 kB 3 [emitted] runtime pageA-f83ea969c4ec627cb92bea42f12b75d6.css 91 bytes 2 [emitted] pageA pageB-0651d43f16a9b34b4b38459143ac5dd8.css 150 bytes 1 [emitted] pageB
在生产环境下把 NamedModulesPlugin 替换为 HashedModuleIdsPlugin,在包的尺寸增加幅度上上达到了可接受的范围,以 vendor 为例,只增加了 1KB。
事情到此我以为可以结束了,直到我 diff 了一下 runtime 才发现持久化缓存似乎还可以继续深挖。
$ diff --git a/dist/runtime-85b317d7b21588411828.js b/dist/runtime-dc322ed118963cd2e12a.js
/******/ if (__webpack_require__.nc) { /******/ script.setAttribute("nonce", __webpack_require__.nc); /******/ } -/******/ script.src = __webpack_require__.p + "" + chunkId + "-" + {"0":"a5620db988a639410257","1":"42b894ca482a061570ae","2":"bedb93c1db950da4fea1"}[chunkId] + ".js"; +/******/ script.src = __webpack_require__.p + "" + chunkId + "-" + {"0":"a5620db988a639410257","1":"42b894ca482a061570ae","2":"b7d7de62392f41af1f78"}[chunkId] + ".js"; /******/ var timeout = setTimeout(onScriptComplete, 120000); /******/ script.onerror = script.onload = onScriptComplete; /******/ function onScriptComplete() {
我们发现在 3 个 entry 入口未改变的情况下,变更某个 entry chunk 的内容,对应 runtime 脚本的变更只是涉及到了 chunk id 的变更。基于 module id 的经验,自然想到了是不是有相应的唯一性内容来取代现有的 chunk id,因为数值型的 chunk id 总会存在不确定性。
所以至此问题又再次被拆分成两个命题:
接下来我们一个一个看
因为我们知道在 webpack 中 entry 其实是具有唯一性的,而 entry chunk 的 name 即来源于我们对 entry 名的设置。所以这里的问题变得很简单我们只需要把每个 chunk 对应的 id 指向到对应 chunk 的 name 即可。
在整个 webpack 执行过程中涉及 moudle id 的事件有
before-chunk-ids
-> optimize-chunk-ids
-> after-optimize-chunk-ids
所以我们只需要在 before-chunk-ids
这个时机内进行 chunk id 订正即可。
伪代码:
apply(compiler) { compiler.plugin("compilation", (compilation) => { compilation.plugin("before-chunk-ids", (chunks) => { chunks.forEach((chunk) => { if(chunk.id === null) { chunk.id = chunk.name; } }); }); }); }
非常简单。
在 webpack@2 时期作者把这个部分的实现引入到了官方插件,即 NamedChunksPlugin
。
所以在一般需求下我们只需要在构建配置中添加 NamedChunksPlugin
的插件即可。
... entry: { "pageA": "./a.js", "pageB": "./b.js", "vendor": [ "lodash" ], }, ... plugins: [ new ExtractTextPlugin('[name]-[contenthash].css'), new webpack.NamedModulesPlugin(), + new webpack.NamedChunksPlugin(), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: Infinity }), new webpack.optimize.CommonsChunkPlugin({ name: 'runtime' }), ], ...
runtime 的 diff
/******/ /******/ // objects to store loaded and loading chunks /******/ var installedChunks = { -/******/ 3: 0 +/******/ "runtime": 0 /******/ }; /******/ /******/ // The require function @@ -91,7 +91,7 @@ /******/ if (__webpack_require__.nc) { /******/ script.setAttribute("nonce", __webpack_require__.nc); /******/ } -/******/ script.src = __webpack_require__.p + "" + chunkId + "-" + {"0":"a5620db988a639410257","1":"42b894ca482a061570ae","2":"b7d7de62392f41af1f78"}[chunkId] + ".js"; +/******/ script.src = __webpack_require__.p + "" + chunkId + "-" + {"vendor":"45cd76029c7d91d6fc76","pageA":"0abd02f11fa4c29e99b3","pageB":"2b8c3672b02ff026db06"}[chunkId] + ".js"; /******/ var timeout = setTimeout(onScriptComplete, 120000); /******/ script.onerror = script.onload = onScriptComplete; /******/ function onScriptComplete() {
可以看到标示 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
做一些变更。
... entry: { "pageA": "./a.js", "pageB": "./b.js", "vendor": [ "lodash" ], }, ... plugins: [ new ExtractTextPlugin('[name]-[contenthash].css'), new webpack.NamedModulesPlugin(), - new webpack.NamedChunksPlugin(), + new webpack.NamedChunksPlugin((chunk) => { + if (chunk.name) { + return chunk.name; + } + return chunk.mapModules(m => path.relative(m.context, m.request)).join("_"); + }), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: Infinity }), new webpack.optimize.CommonsChunkPlugin({ name: 'runtime' }), ], ...
要做到持久化缓存需要做好以下几点:
[chunkhash]
对 extractTextPlugin 应用的的文件应用 [contenthash]
;CommonsChunkPlugin
合理抽出公共库 vendor
(包含社区工具库这些 如 lodash), 如果必要也可以抽取业务公共库 common
(公共部分的业务逻辑),以及 webpack的 runtime
;NamedModulesPlugin
来固化 module id,在生产环境下使用 HashedModuleIdsPlugin
来固化 module idNamedChunksPlugin
来固化 runtime 内以及在使用动态加载时分离出的 chunk 的 chunk id。