使用HashedModuleIdsPlugin和NamedChunksPlugin
代码目录结构如下

webpack.config.js如下
const path = require("path");
const CleanWebpackPlugin = require("clean-webpack-plugin");
module.exports = {
context: __dirname,
mode: "production",
entry: {
app: "./src/app.js"
},
output: {
filename: "[name].[contenthash].js",
chunkFilename: "[name].[contenthash].js",
path: path.resolve(__dirname, "./dist"),
publicPath: "./"
},
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, "./src"),
use: [
{
loader: "babel-loader"
}
]
}
]
},
plugins: [
// 每次构建时先清理dist目录
new CleanWebpackPlugin()
],
optimization: {
runtimeChunk: 'single',
minimize: false
}
};
app.js如下
import { fn as util1 } from "./util/util1";
import(/* webpackChunkName: "chunk2" */ "./chunk/chunk2");
util1();
构建结果如下

app.b905381137b0b2fc6a4e.js如下
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],[
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
// CONCATENATED MODULE: ./src/util/util1.js
function fn() {
console.log("util1");
}
// CONCATENATED MODULE: ./src/app.js
__webpack_require__.e(/* import() | chunk2 */ 1).then(__webpack_require__.bind(null, 1));
fn();
/***/ })
],[[0,2]]]);
__webpack_require__.e(/* import() | chunk2 */ 1)这里的1表示的chunk id,__webpack_require__.bind(null, 1)这里的1表示的是module id
chunk2.ee8ca10baa406153084d.js如下
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],[
/* 0 */,
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fn", function() { return fn; });
function fn() {
console.log("chunk2");
}
/***/ })
]]);
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],这里的1表示该chunk的id,/* 0 */,/* 1 */这里的1表示该chunk的模块id
现在,我们在app.js中再异步引入chunk1.js(注意:要在chunk2之前引入),如下
import { fn as util1 } from "./util/util1";
import(/* webpackChunkName: "chunk1" */ "./chunk/chunk1");
import(/* webpackChunkName: "chunk2" */ "./chunk/chunk2");
util1();
再次构建,结果如下

可以看到chunk2.js的内容未改,之前的chunkhash是ee8ca10baa406153084d,但现在的chunkhash却变成了babf6b6949e15e038987。这表示chunk2.js构建后的内容肯定变化了。
app.141c8d345ce1da6f485c.js如下
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],[
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
// CONCATENATED MODULE: ./src/util/util1.js
function fn() {
console.log("util1");
}
// CONCATENATED MODULE: ./src/app.js
__webpack_require__.e(/* import() | chunk1 */ 1).then(__webpack_require__.bind(null, 1));
__webpack_require__.e(/* import() | chunk2 */ 2).then(__webpack_require__.bind(null, 2));
fn();
/***/ })
],[[0,3]]]);
chunk2.babf6b6949e15e038987.js如下
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[2],{
/***/ 2:
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fn", function() { return fn; });
function fn() {
console.log("chunk2");
}
/***/ })
}]);
从上可以看出chunk2.js的模块id由原来的1变成目前的2了,所以内容变化导致了chunkhash变化。原因就是app.js中在chunk2.js之前异步引入了chunk1.js,而webpack构建时默认的模块id为全局递增的数字,所以导致了chunk2.js的模块id变化。要解决该问题,我们可以使用HashedModuleIdsPlugin插件,该插件使用构建后的模块内容生成hash值来作为模块id。(webapck@4.16.0以上设置optimization.moduleIds:'hashed'也可以达到同样的效果)
webpack.config.js修改成如下
const path = require("path");
const CleanWebpackPlugin = require("clean-webpack-plugin");
module.exports = {
context: __dirname,
mode: "production",
entry: {
app: "./src/app.js"
},
output: {
filename: "[name].[contenthash].js",
chunkFilename: "[name].[contenthash].js",
path: path.resolve(__dirname, "./dist"),
publicPath: "./"
},
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, "./src"),
use: [
{
loader: "babel-loader"
}
]
}
]
},
plugins: [
// 每次构建时先清理dist目录
new CleanWebpackPlugin(),
new webpack.HashedModuleIdsPlugin({
hashDigestLength: 10
})
],
optimization: {
runtimeChunk: 'single',
minimize: false
}
};
当app.js中未引入chunk1.js时,构建结果如下

app.e24ea5a17cb2a4aad451.js如下
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
/***/ "ERIhSrEdfs":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
// CONCATENATED MODULE: ./src/util/util1.js
function fn() {
console.log("util1");
}
// CONCATENATED MODULE: ./src/app.js
__webpack_require__.e(/* import() | chunk2 */ 1).then(__webpack_require__.bind(null, "Dpna+w6KQe"));
fn();
/***/ })
},[["ERIhSrEdfs",2]]]);
chunk2.bfc12f57e231357cecb2.js如下
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],{
/***/ "Dpna+w6KQe":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fn", function() { return fn; });
function fn() {
console.log("chunk2");
}
/***/ })
}]);
我们可以看到,chunk2.js的模块id变成了Dpna+w6KQe,chunk id还是1
当app.js中安装上述方法引入chunk1.js时,构建结果如下

app.2514e42725d363c97b03.js如下
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
/***/ "ERIhSrEdfs":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
// CONCATENATED MODULE: ./src/util/util1.js
function fn() {
console.log("util1");
}
// CONCATENATED MODULE: ./src/app.js
__webpack_require__.e(/* import() | chunk1 */ 1).then(__webpack_require__.bind(null, "rnZY9ZqZXi"));
__webpack_require__.e(/* import() | chunk2 */ 2).then(__webpack_require__.bind(null, "Dpna+w6KQe"));
fn();
/***/ })
},[["ERIhSrEdfs",3]]]);
chunk2.f56f43fbe0b9ad016842.js如下
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[2],{
/***/ "Dpna+w6KQe":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fn", function() { return fn; });
function fn() {
console.log("chunk2");
}
/***/ })
}]);
看到这里,有的童鞋可能会有疑问了,为什么应用了HashedModuleIdsPlugin插件后chunk2.js的chunkhash还是变了呢?这时候我们就要回归事物的本源了,chunkhash是根据构建后的内容来生成的,既然chunkhash变化了,那一定是生成的内容发生了变化。说到这里,细心的童鞋应该发现了,虽然构建后的模块id没有变化,但是chunk id是变化了的,由原来的1变成了2。所以这里又要告诉大家,webpack构建时chunk id生成的机制跟模块id的机制差不多,都是全局递增的数字。有时候我们增加或者删除了某个chunk,也会导致其他的chunk内容发生变化。这时候我们可以使用NamedChunksPlugin插件来解决该问题。(webapck@4.16.0以上设置optimization.chunkIds:'named'也可以达到同样的效果)
webpack.config.js修改成如下
const path = require("path");
const crypto = require("crypto");
const md5 = crypto.createHash("md5");
const CleanWebpackPlugin = require("clean-webpack-plugin");
module.exports = {
context: __dirname,
mode: "production",
entry: {
app: "./src/app.js"
},
output: {
filename: "[name].[contenthash].js",
chunkFilename: "[name].[contenthash].js",
path: path.resolve(__dirname, "./dist"),
publicPath: "./"
},
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, "./src"),
use: [
{
loader: "babel-loader"
}
]
}
]
},
plugins: [
// 每次构建时先清理dist目录
new CleanWebpackPlugin(),
new webpack.HashedModuleIdsPlugin({
hashDigestLength: 10
}),
/**
* 默认的chunk id为全局递增的数字,一旦增加或者删除了某个chunk,可能会导致其他chunk的id也跟着变化(因为chunk代码包含了chunk id,所以同时也会导致chunk的内容变化)
* 而NamedChunksPlugin默认只处理命名了的chunk,所以这里要自定义chunk id生成函数
* https://github.com/webpack/webpack/blob/master/lib/NamedChunksPlugin.js
*/
new webpack.NamedChunksPlugin(chunk => {
if (chunk.name) {
return chunk.name
}
return md5
.update(Array.from(chunk.modulesIterable, m => m.id).join('_'))
.digest('hex')
.substr(0, 8)
})
],
optimization: {
runtimeChunk: 'single',
minimize: false
}
};
当app.js中未引入chunk1.js时,构建结果如下

app.665e0e9c6c74049f3738.js如下
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["app"],{
/***/ "ERIhSrEdfs":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
// CONCATENATED MODULE: ./src/util/util1.js
function fn() {
console.log("util1");
}
// CONCATENATED MODULE: ./src/app.js
__webpack_require__.e(/* import() | chunk2 */ "chunk2").then(__webpack_require__.bind(null, "Dpna+w6KQe"));
fn();
/***/ })
},[["ERIhSrEdfs","runtime"]]]);
chunk2.cec1ac47e32bfd2572f6.js如下
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["chunk2"],{
/***/ "Dpna+w6KQe":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fn", function() { return fn; });
function fn() {
console.log("chunk2");
}
/***/ })
}]);
当app.js中安装上述方法引入chunk1.js时,构建结果如下

chunk2.cec1ac47e32bfd2572f6.js如下
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["chunk2"],{
/***/ "Dpna+w6KQe":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fn", function() { return fn; });
function fn() {
console.log("chunk2");
}
/***/ })
}]);
可以看到chunk2.js的chunk id不再是变化的数字,而始终是我们命名的chunk2。
综上可知,通过应用webpack.HashedModuleIdsPlugin和webpack.NamedChunksPlugin,当文件名使用chunkhash命名时,我们可以确保只要chunk的内容没有变化,构建出来的文件一定不会变化。
其实webpack.NamedModulesPlugin也可以实现webpack.HashedModuleIdsPlugin类似的功能,不过这个插件会把模块的路径当作模块id,因为通常模块路径都会很长,所以这个插件一般用在开发模式中,方便调试。
其他建议:
- 将webpack的runtime代码抽取成单独的模块,这样某个chunk的路径改了,只会影响runtime chunk的文件名
- 将node_modules中的模块尽量抽取成单独的模块,因为这些模块通常不会变化,其他的代码修改不会导致这些模块重新加载
- chunk文件名采用[contenthash]占位符而不是[chunkhash],因为[chunkhash]计算时包括了被抽离出去的内容(比如css),如果css内容改了也会导致[chunkhash]变化,而[contenthash]就不会变化