本文深入解析 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 列下的文件名):

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

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 打包:

chunkhash 是根据文件内容生成的 hash,可见 app 与 common 生成的 hash 值已不相同(对比使用 [name].[hash].js 打包的结果)。
我们在 index.js 中随意进行修改,再次打包:

奇怪的是,虽然 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 打包后:

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

从这里可以发现!!我们修改了源代码,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
})
]
}
打包后:

这里发现对比上一张图片发现,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
})
]
}
打包发现,之前 [ ] 里都是数字,现在都是一些字符,

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

现在大功告成,common 的 hash 没有改变,而因为更变了内容 app 的 hash 改变了,这正是我们想要的结果。
参考资料:
webpack 文档 -- 缓存
webpack 独立打包与缓存处理
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。