Webpack 缓存与独立打包实战:解决 Hash 失效与模块 ID 变更

作者:脚本小铺

时间:2026-10-22

浏览:423

本文深入解析 Webpack 打包中的缓存与代码分离问题。通过 CommonsChunkPlugin 提取第三方库,利用 chunkhash 生成唯一文件名以解决缓存更新难题。针对 manifest 导致库文件 Hash 频繁变动的缺陷,进一步分离 manifest 并引入 HashedModuleIdsPlugin 固定模块 ID,确保静态资源长效缓存且源码变更不影响库文件版本。

文章目录

Webpack 缓存与代码分离概述

本文深入探讨 Webpack 进阶配置中关于缓存机制与代码独立打包的具体应用,旨在提供实用的解决方案,帮助开发者优化构建流程。

首先回顾最基础的 Webpack 配置场景:


var path = require('path');
 
module.exports = {
 entry: './src/index.js',
 output: {
  filename: 'bundle.js',
  path: path.resolve(__dirname, 'dist')
 }
}

在入口文件 index.js 中引入了 lodash 库作为依赖:

src/index.js 源码如下:


import _ from 'lodash';
 
 function component() {
  var element = document.createElement('div');
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
 
  return element;
 }
 
 document.body.appendChild(component());

执行打包后,默认仅生成一个 bundle.js 文件。这种单一文件输出方式导致每次加载资源时,浏览器都必须重新下载包含 lodash 等未变动库的完整文件,造成极大的网络资源浪费和低效。

浏览器通常会对静态资源进行缓存,以避免每次访问都重新通过网络获取资源,从而提升页面加载速度。然而,这种机制也带来了新的挑战。

如果部署新版本时不更改资源文件名,浏览器会判定资源未更新,从而继续使用本地缓存的旧版本,导致用户无法获取最新代码。

使用 CommonsChunkPlugin 分离代码

因此,我们需要解决两个核心问题:第一,实现打包文件的分离;第二,有效解决浏览器缓存导致的更新滞后问题。


const path = require('path');
const webpack = require('webpack');
 
module.exports = {
 entry: {
  common: ['lodash'],
  app: './src/index.js'
 },
 output: {
  filename: '[name].[hash].js',
  path: path.resolve(__dirname, 'dist')
 },
 plugins: [
  new webpack.optimize.CommonsChunkPlugin({
   name: 'common' // 指代index.js引入的lodash库
  })
 ]
}

主要的配置变动如下:

  • 引入 CommonsChunkPlugin 插件,用于提取引入的第三方库并重命名,从而实现代码分离。
  • 在输出文件名中引入 hash 值,确保每次打包生成的文件名哈希值不同,以此解决浏览器缓存无法更新的问题。

最终结果:index.js 被打包为 app.[hash].js,而 index.js 引入的 lodash 库被打包为 common.[hash].js。这种方案虽然解决了浏览器缓存问题和静态资源与源代码的分离,但随之引发了新的问题。

第一次打包后的产物(注意 Asset 列下的文件名):

Webpack 缓存与代码分离概述

每次修改源代码并重新打包时,不仅 index 生成的 app.[hash].js 的 hash 值会发生变化,

Webpack 缓存与代码分离概述

common.[hash].js 的 hash 值以及与 app 相同的 hash 值也会随之改变(读者可自行测试:先执行一次 webpack 打包,修改 index.js 后再执行一次打包)。

这并非我们期望的结果。虽然源代码 hash 的改变解决了浏览器使用缓存版本的问题,但如果 common.js 的 hash 值也随之变化,浏览器仍需每次请求未发生改变的静态代码 common,这依然浪费了网络资源,效率低下。

注:本案例涉及多次打包,dist 目录中会生成大量垃圾文件。在实际生产环境中,通常会使用 CleanWebpackPlugin 插件来清理这些文件。

相关代码如下:

new CleanWebpackPlugin(['dist']) // 将其加入插件数组,用于在每次打包前清空打包文件夹下的历史文件。

利用 chunkhash 解决缓存更新问题

如果修改 index 后,仅生成的 app 的 hash 值发生变化,而 common 的 hash 值保持不变,则能达到我们的目的:既能缓存库文件,又能识别源文件的更改。 为此,我们进行如下配置:在 output 中将文件名格式 [name].[hash].js 改为 [name].[chunkhash].js,使每个文件生成基于内容的唯一 hash 值:


const path = require('path');
const webpack = require('webpack');
 
module.exports = {
 entry: {
  common: ['lodash'],
  app: './src/index.js'
 },
 output: {
  filename: '[name].[chunkhash].js',
  path: path.resolve(__dirname, 'dist')
 },
 plugins: [
  new CleanWebpackPlugin(['dist']),
  new webpack.optimize.CommonsChunkPlugin({
   name: 'common' // 指代index.js引入的lodash库
  })
 ]
}

(注意:不要在开发环境下使用 [chunkhash],因为这会增加编译时间。应将开发和生产模式的配置分开,在开发模式中使用 [name].js 的文件名,在生产模式中使用 [name].[chunkhash].js 文件名。因此,如果此时使用了热替换插件 HotModuleReplacementPlugin,将导致编译失败!)

配置完成后,执行 webpack 打包:

Webpack 缓存与代码分离概述

chunkhash 是根据文件内容生成的 hash,可见 app 与 common 生成的 hash 值已不相同(对比使用 [name].[hash].js 打包的结果)。

我们在 index.js 中随意进行修改,再次打包:

Webpack 缓存与代码分离概述

奇怪的是,虽然 common 与 app 生成了独立的 hash 值,但修改 index.js 后,common 的 hash 值依然发生了变化。

原因在于:为了最小化生成文件的大小,Webpack 在编译期间使用标识符而非模块名称,生成标识符并映射到块文件名,然后放入一个名为 chunk manifest 的 JS 对象中。重点在于:当我们使用 CommonsChunkPlugin 分离代码时,被分离出来的代码(本文中的 lodash 库,被打包为 common)会默认被移动到 entry 中最后一个入口进行打包(第一个入口是 index.js)。重要的是,chunk manifest 将随着这些被分离出来的代码共同打包!!!

由于我们更改源代码后,不仅会更新 app 的 hash 值,还会生成新的映射。新的映射又会与资源代码一同打包,而由于 chunkhash 是根据内容生成 hash 的,那么加入了新映射对象 chunk manifest 的资源代码被打包后,hash 自然也会发生改变。这反过来,产生的新 hash 将使长效缓存失效。

分离 manifest 以稳定库文件 Hash

接下来我们需要做的就是将 manifest 分离出来。这里我们利用 CommonsChunkPlugin 一个较少有人知道的功能:能够在每次修改后的构建中将 manifest 提取出来。通过指定 entry 中未用到的名称,此插件会自动将我们需要的内容提取到单独的包中。

故再额外配置一个 CommonsChunkPlugin:


const path = require('path');
const webpack = require('webpack');
 
module.exports = {
 entry: {
  common: ['lodash'],
  app: './src/index.js'
 },
 output: {
  filename: '[name].[chunkhash].js',
  path: path.resolve(__dirname, 'dist')
 },
 plugins: [
  new CleanWebpackPlugin(['dist']),
  new webpack.optimize.CommonsChunkPlugin({
   name: 'common' // 指代index.js引入的lodash库
  }),
  new webpack.optimize.CommonsChunkPlugin({
   name: 'manifest' // 用于提取manifest
  })
 ]
}

Webpack 打包后:

Webpack 缓存与代码分离概述

从这里可以证明之前所说的 manifest 被打包进了 common!!!仔细看之前的图:common 的 Size 都是 547kb,到这里 common 大小是 541kb 而 manifest 大小正好为 5.85kb,加起来正好为 547kb。

然后我们修改 index.js 再次打包:

Webpack 缓存与代码分离概述

从这里可以发现!!我们修改了源代码,common 的 hash 值已经不再发生改变了!到这里可以达到我们不缓存源代码、缓存资源文件的目的了。

但是可别高兴得太早!!我们做了一个很小的修改,交换了 entry 中 app 和 common 的顺序(对比上一个代码段):


const path = require('path');
const webpack = require('webpack');
 
module.exports = {
 entry: {
  app: './src/index.js',
  common: ['lodash']
 },
 output: {
  filename: '[name].[chunkhash].js',
  path: path.resolve(__dirname, 'dist')
 },
 plugins: [
  new CleanWebpackPlugin(['dist']),
  new webpack.optimize.CommonsChunkPlugin({
   name: 'common' // 指代index.js引入的lodash库
  }),
  new webpack.optimize.CommonsChunkPlugin({
   name: 'manifest' // 用于提取manifest
  })
 ]
}

打包后:

Webpack 缓存与代码分离概述

这里发现对比上一张图片发现,common 的 hash 值又发生改变了!!而且根本没有更改 index.js 的内容,app 的 hash 也变了,只是换了一下顺序而已!

大家注意看本张图与上一张图的模块解析顺序([1],[2],[3]...之后所对应的模块)。发现上一张图,lodash 第一个解析,而现在 lodash 最后一个解析。

这就是 hash 更变的原因:这是因为每个 module.id 会基于默认的解析顺序 (resolve order) 进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变,所以 hash 值也会发生变化。

有人可能会决定,一般我们都不会更换 webpack.config.js 中 entry 的入口顺序,那么是否我就不会遇见这个问题了。答案是否定的,除非你能保证资源文件都写在 entry 的顶部。否则会出现这样的情况:

假如 entry 的顺序为: app -> common, 那么解析顺序为 index.js → lodash。 如果之后 index.js 引入了 print.js,那么解析顺序变为 index.js → print.js -> lodash。

以上,我们并没有在 entry 中更改入口顺序,解析的顺序还是会发生改变,common 的 hash 还是会发生变化,不能缓存。

使用 HashedModuleIdsPlugin 固定模块 ID

这里我们就引入一个新的组件:HashedModuleIdsPlugin:根据 hash 生成 ID(NamedModulesPlugin 也具有同样的效果,但是是根据路径名生成 ID,可读性更高,也由此编译时间会相对长一些)。 这样 module.id 就不会使用数字标识符,而使用 hash:


const path = require('path');
const webpack = require('webpack');
 
module.exports = {
 entry: {
  common: ['lodash'],
  app: './src/index.js'
 },
 output: {
  filename: '[name].[chunkhash].js',
  path: path.resolve(__dirname, 'dist')
 },
 plugins: [
  new CleanWebpackPlugin(['dist']),
  new webpack.HashedModuleIdsPlugin(), // 引入该插件
  new webpack.optimize.CommonsChunkPlugin({
   name: 'common' // 指代index.js引入的lodash库
  }),
  new webpack.optimize.CommonsChunkPlugin({
   name: 'manifest' // 用于提取manifest
  })
 ]
}

打包发现,之前 [ ] 里都是数字,现在都是一些字符,

Webpack 缓存与代码分离概述

接下来,我们再把 app 和 common 的顺序调换一下,并且随意修改 index.js,再次打包:

Webpack 缓存与代码分离概述

现在大功告成,common 的 hash 没有改变,而因为更变了内容 app 的 hash 改变了,这正是我们想要的结果。

参考资料:

webpack 文档 -- 缓存

webpack 独立打包与缓存处理

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章