Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于 webpack 的持久化缓存方案 #9

Open
soda-x opened this issue Nov 10, 2017 · 11 comments
Open

基于 webpack 的持久化缓存方案 #9

soda-x opened this issue Nov 10, 2017 · 11 comments
Labels

Comments

@soda-x
Copy link
Owner

soda-x commented Nov 10, 2017

如何基于 webpack 做持久化缓存似乎一直处于没有最佳实践的状态。网路上各式各样的文章很多,open 的 bug 反馈和建议成堆,很容易让人迷茫和心智崩溃。

作为开发者最大的诉求是:在 entry 内部内容未发生变更的情况下构建之后也能稳定不变。

TL;DR;

拉到最后看总结 XD

hash 的两种计算方式

想要做持久化缓存的首要一步是 hash,在 webpack 中提供了两种方式,hashchunkhash

在此或许有不少同学就这两者之间的差别就模糊了:

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 计算方式为 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 值保持一致,这对于所有资源的持久化缓存来说并没有深远的意义。

  • 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 尽可能的保持不变

module id 异动

我们来一个一个看。

命题1:如何把不相关的 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

命题2:如何能让 module id 尽可能的保持不变

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 方式
  • 找到时机进行 id 订正

找到替代数值型 module id 方式

直觉的第一反应肯定是路径,因为在一次构建中资源的路径肯定是唯一的,另外我们也可以非常庆幸在 webpack 中肯定在 resolve module 的环节中拿到资源的路径。

不过谈到路径,我们不得不担忧一下,windows 和 macos 下路径的 sep 是不一致的,如果我们把 id 生成这一块单独拿出来自己做了,会不会还要处理一大堆可能存在的差异性问题。带着这样的困惑我查阅了 webpack 的源码其中在 ContextModule#74ContextModule#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/_lodash@4.17.4@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 总会存在不确定性。

所以至此问题又再次被拆分成两个命题:

  • 找到替代现有 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) {
	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'
  }),
],
...

总结

要做到持久化缓存需要做好以下几点:

  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. 建议阅读一下全文,因为不看你很难明白为什么要如上这么做。
@Dcatfly
Copy link

Dcatfly commented Nov 12, 2017

感谢博主~我想问下有没有能实现output出来的文件,某些带hash,某些不带hash的解决方案?

@soda-x
Copy link
Owner Author

soda-x commented Nov 13, 2017

@Dcatfly 可以有方案,可以在 assets 资源输出磁盘之前的任何时机进行更名即可,写个 plugin 就好,不过选择时机你需要斟酌一下,比如 emit 那肯定是经过了 hash 的运算而造成计算上的浪费。

@Dcatfly
Copy link

Dcatfly commented Nov 13, 2017

@pigcan 好的,谢谢博主~这么一想确实是可以的,而且跟我现在做的事情是类似的。。😆

@NealST
Copy link

NealST commented Nov 23, 2017

非常好的文章,对利用webpack做持久化缓存有了更加深刻的认识,带着问题的探索模式值得学习

@suihanoooooo
Copy link

你好,问一下,关于chunk那块,你改了某一个entry文件的内容,并不只是chunkId变了啊,在runtime中的那个文件的hash(script中的引用)也变了啊,也会导致runtime的hash变化吧。

@soda-x
Copy link
Owner Author

soda-x commented Apr 11, 2018

@suihanoooooo runtime 我没记错的话最后的理想态应该是引入的 entry chunk 的 hash 值,和内容不相关,当然是不涉及 entry chunk 条目等变更

@dyf19118
Copy link

dyf19118 commented Jan 3, 2019

webpack4在async chunk这一块的处理上相较3有了不少变化,不妨针对4再重新探讨一下

@relign
Copy link

relign commented Jan 9, 2019

再次不经意间,拜读到了大佬的文章👍👍👍

@Rashomon511
Copy link

Rashomon511 commented Jul 3, 2019

很棒的文章,解决了我的很多疑惑

@Xing-Chuan
Copy link

探究精神很赞

@chwan97
Copy link

chwan97 commented Aug 3, 2021

2021年事情有什么变化了吗

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

9 participants