Front End

[FE] splitChunks的contenthash为什么不

2019-04-09  本文已影响2人  何幻

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
调用了moduleupdateHash方法,去更新当前moduleHash对象。

moduleupdateHash位于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.jsvendors~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

上一篇 下一篇

猜你喜欢

热点阅读