webpack长效缓存注意事项

1,605 阅读4分钟

使用HashedModuleIdsPluginNamedChunksPlugin

代码目录结构如下

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.HashedModuleIdsPluginwebpack.NamedChunksPlugin,当文件名使用chunkhash命名时,我们可以确保只要chunk的内容没有变化,构建出来的文件一定不会变化。

其实webpack.NamedModulesPlugin也可以实现webpack.HashedModuleIdsPlugin类似的功能,不过这个插件会把模块的路径当作模块id,因为通常模块路径都会很长,所以这个插件一般用在开发模式中,方便调试。

其他建议:

  • 将webpack的runtime代码抽取成单独的模块,这样某个chunk的路径改了,只会影响runtime chunk的文件名
  • 将node_modules中的模块尽量抽取成单独的模块,因为这些模块通常不会变化,其他的代码修改不会导致这些模块重新加载
  • chunk文件名采用[contenthash]占位符而不是[chunkhash],因为[chunkhash]计算时包括了被抽离出去的内容(比如css),如果css内容改了也会导致[chunkhash]变化,而[contenthash]就不会变化