[FE] splitChunks的contenthash为什么不
1. 背景
webpack内置了SplitChunksPlugin,可用来将公共模块提取到一个单独的文件中。
下面来写一个最简的例子。
1.1 初始化项目
$ npm init -f
$ npm i -D @babel/core@7.1.2 \
@babel/preset-env@7.1.0 \
babel-loader@8.0.4 \
webpack@4.20.2 \
webpack-cli@3.1.2
$ npm i -S moment@2.24.0
1.2 文件操作
(1)新增 src/index.js
require('moment');
(2)新增 webpack.config.js
const path = require('path');
module.exports = {
entry: {
index: path.resolve(__dirname, 'src/index.js'),
},
output: {
path: path.resolve(__dirname, 'dist/'),
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].js',
},
module: {
rules: [
{ test: /\.js$/, use: { loader: 'babel-loader', query: { presets: ['@babel/preset-env'] } } },
]
},
optimization: {
splitChunks: {
chunks: 'all',
}
}
};
(3)修改 package.json
添加 build npm scripts,
{
...
"scripts": {
...
"build": "webpack"
},
...
}
(4)最终目录结构如下
.
├── package.json
├── src
│ └── index.js
└── webpack.config.js
1.3 构建
$ npm run build
结果dist目录中会生成两个文件,
index.dc697561.js
vendors~index.e6c2b0b2.js
目录结构如下,
.
├── dist
│ ├── index.dc697561.js
│ └── vendors~index.e6c2b0b2.js
├── package.json
├── src
│ └── index.js
└── webpack.config.js
moment 模块的内容被提取到了 vendors~index.e6c2b0b2.js 中。
webpack.config.js中 splitChunks
参数的默认值,可以参考这里,optimization.splitChunks
2. hash
我们来看一下这两个文件的hash值是怎么得来的。
通过使用这篇文章介绍的办法,使用vscode调试npm scripts。
2.1 文件名
我们在 webpack/lib/Compilation.js 第2343行打个断点,
file = this.getPath(filenameTemplate, fileManifest.pathOptions);
可以看到这个 file
就最终写到磁盘中的文件名。
2.2 hash对象
可以看到file
的值,实际上来自于
fileManifest.pathOptions.chunk.contentHash.javascript
接着就需要追溯一下,什么地方生成这个 contentHash
了。
全局搜索一下wepback源码,
会发现对于contentHash.javascript
赋值发生在,webpack/lib/JavascriptModulesPlugin.js 第151行
compilation.hooks.contentHash.tap("JavascriptModulesPlugin", chunk => {
...
const hash = createHash(hashFunction);
...
hash.update(`${chunk.id} `);
hash.update(chunk.ids ? chunk.ids.join(",") : "");
template.updateHashForChunk(
hash,
chunk,
compilation.moduleTemplates.javascript,
compilation.dependencyTemplates
);
for (const m of chunk.modulesIterable) {
if (typeof m.source === "function") {
hash.update(m.hash);
}
}
chunk.contentHash.javascript = hash
.digest(hashDigest)
.substr(0, hashDigestLength);
});
webpack在设置会新建一个hash
对象,然后不断的更新hash
,
最终再调用 hash.digest
方法得到hash值。
2.3 子模块的hash
指的注意的是上图的chunk.modulesIterable
,
webpack会使用子模块的hash值,更新hash
对象。
所以子模块的hash值,对目标文件的文件名hash是有影响的。
那么子模块的hash值又是怎么计算出来的呢?
经过一些搜索,我们找到webpack是在 webpack/lib/Compilation.js 第2237行 对module.hash
赋值的。
createHash() {
...
const modules = this.modules;
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash);
module.hash = moduleHash.digest(hashDigest);
...
}
...
}
其中比较重要的步骤是,module.updateHash
,
调用了module
的updateHash
方法,去更新当前moduleHash
对象。
module
的updateHash
位于webpack/lib/NormalModule.js 第537行
它使用了module
的_buildHash
去更新hash
对象。
2.4 _buildHash
NormalModule对 _buildHash
赋值,位于webpack/lib/NormalModule.js 第393行
_initBuildHash(compilation) {
const hash = createHash(compilation.outputOptions.hashFunction);
if (this._source) {
hash.update("source");
this._source.updateHash(hash);
}
hash.update("meta");
hash.update(JSON.stringify(this.buildMeta));
this._buildHash = hash.digest("hex");
}
它新建了一个hash
对象,然后用_source.updateHash
去更新,
我们看到这里直接用文件内容更新了
hash
对象。
2.5 小结
(1)目标文件名,来源于 chunk.contentHash.javascript
(2)chunk.contentHash.javascript
取决于chunk中每个模块的 hash
值
(3)模块的hash
值由它的子模块计算得来
(4)而子模块的hash
值,与该模块的_buildHash
有关
(5)_buildHash
是根据文件内容进行计算的
3. splitChunk的contenthash
一般而言,node_modules
中的文件内容在短时间内是不会发生变化的,
因此,以上示例中vendors~index.xxxxxx.js
,hash是不会变化的。
可以试验下,上述示例中,我们多次构建的产物hash值是稳定的。
index.dc697561.js
vendors~index.e6c2b0b2.js
index.dc697561.js
vendors~index.e6c2b0b2.js
然而不幸的是,
如果有node_modules中的模块,引用了自己的package.json
,那就出问题了。
3.1 问题复现
(1)安装antd,react和react-dom
$ cnpm i -S antd@3.16.2 \
react@16.8.6 \
react-dom@16.8.6
(3)修改src/index.js
require('antd');
(3)把 node_modules 删掉
$ trash node_modules
其中,trash 可将文件移到回收站
(4)使用cnpm安装依赖,并构建
$ cnpm i
$ cnpm run build
其中,cnpm 是淘宝的npm镜像。
(5)重复(3)和(4)
.
├── dist
│ ├── index.5fab8c33.js
│ └── vendors~index.7e7b6645.js
├── package-lock.json
├── package.json
├── src
│ └── index.js
└── webpack.config.js
.
├── dist
│ ├── index.5fab8c33.js
│ └── vendors~index.3652f376.js
├── package-lock.json
├── package.json
├── src
│ └── index.js
└── webpack.config.js
可以看到 index.5fab8c33.js
的 hash 是没有发生变化的,
vendors~index.7e7b6645.js
,vendors~index.3652f376.js
hash变了。
然而,比对一下这两个 vendors 文件,却发现内容又是一致的。
3.2 原因
我们打开node_modules/_antd@3.16.2@antd/es/version/index.js
,
import { version } from '../../package.json';
export default version;
它引用了node_modules/_antd@3.16.2@antd/package.json
,
{
...
"__npminstall_done": "Tue Apr 09 2019 15:08:52 GMT+0800 (GMT+08:00)",
...
}
可以看到,与npm不同的是,
cnpm安装的模块,package.json中会多一个 __npminstall_done
字段。
这个时间戳导致了 package.json 的文件内容,每次安装依赖都是不同的,
我们知道 contenthash 是跟文件内容有关的,
所以引用了 package.json,就会导致 hash不稳。
3.3 验证
我们每次安装完依赖,
将node_modules/_antd@3.16.2@antd/es/version/index.js
中的内容都注释掉,
// import { version } from '../../package.json';
// export default version;
然后再进行构建,
$ cnpm run build
就会发现vendors文件的hash值不会发生变化了,
.
├── dist
│ ├── index.5fab8c33.js
│ └── vendors~index.f38b29bd.js
├── package-lock.json
├── package.json
├── src
│ └── index.js
└── webpack.config.js
.
├── dist
│ ├── index.5fab8c33.js
│ └── vendors~index.f38b29bd.js
├── package-lock.json
├── package.json
├── src
│ └── index.js
└── webpack.config.js
其中,index.5fab8c33.js
的hash值跟上文相同,
vendors~index.f38b29bd.js
虽然与上文不同,但是不会发生变化了。
参考
SplitChunksPlugin
moment
optimization.splitChunks
使用vscode调试npm scripts
wepback 4.20.2
cnpm
antd 3.16.2