近年来,单页应用(SPA)已经成为前端开发的主流,而 Webpack 作为一个强大的模块打包工具,也在这个领域大放异彩。在使用 Webpack4 构建 SPA 应用时,很多开发者会遇到一个棘手的问题——chunkHash 值不变,导致浏览器缓存失效,影响应用性能。本文将分享如何解决这个问题,以及一些相关的技术细节。
chunkHash 是什么?
在 Webpack 的构建中,chunkHash 是一个重要的概念。它是根据 chunk 的内容计算出来的哈希值,用于区分不同的 chunk。举个例子,我们可以通过以下配置让 Webpack 生成带有 chunkHash 的文件名:
output: { filename: '[name].[chunkHash].js' }
这样,每次构建时,Webpack 会自动根据文件内容生成唯一的 chunkHash 值,并将其作为文件名的一部分。
然而,在构建 SPA 应用时,我们往往会发现一个棘手的问题——chunkHash 值不变。这意味着,即使我们修改了代码,重新构建应用,但由于某些原因(后文会详细介绍),新生成的 chunk 文件名仍然和旧的一样。这就导致了浏览器缓存的失效,每次访问页面都需要重新请求这些 chunk 文件,大大降低了应用性能。
那么,如何解决这个问题呢?
解决方案
在绝大多数情况下,chunkHash 值不变的原因是因为 Webpack 认为 chunk 内容没有发生变化,因此不重新生成新的 chunk 文件。这往往是出于效率考虑,但也可能会导致我们上述的问题。因此,我们需要让 Webpack 强制重新生成新的 chunk 文件,即使它们的内容没有变化。这可以通过以下方式实现:
方案一:使用 NamedChunksPlugin 插件
我们可以通过在 Webpack 配置中使用 NamedChunksPlugin 插件,来给每个 chunk 设置一个唯一的名称,这样就能确保每次构建时都会生成新的 chunk 文件。具体配置如下:
// javascriptcn.com 代码示例 const { NamedChunksPlugin } = require('webpack'); module.exports = { output: { filename: '[name].[chunkHash].js' }, plugins: [ new NamedChunksPlugin(chunk => { if (chunk.name) { return chunk.name; } const hash = require('hash-sum'); const joinedHash = hash(Array.from(chunk.modulesIterable, m => m.id).join('_')); return `chunk-${joinedHash}`; }) ] };
这里的代码主要作用是生成一个唯一的 chunk 名称,如果原本的 chunk 有 name 属性,则直接使用它。否则,自动生成一个唯一的名称,以保证每次构建时都能生成新的文件。
方案二:使用 HashedModuleIdsPlugin 插件
除了 NamedChunksPlugin 插件,我们还可以使用另一个插件——HashedModuleIdsPlugin 来实现相同的效果。它的原理是给每个模块生成一个 id,然后根据这个 id 计算出一个唯一的 hash 值来替代 chunkHash。
// javascriptcn.com 代码示例 const { HashedModuleIdsPlugin } = require('webpack'); module.exports = { output: { filename: '[name].[chunkHash].js' }, plugins: [ new HashedModuleIdsPlugin() ] };
这个插件会替代 chunkHash,所以我们需要使用 [name].[chunkHash].js
的文件名格式来确保文件名的唯一性。
注意事项
虽然上述两种方案可以解决 chunkHash 值不变的问题,但我们还需要注意以下几点细节:
使用 MiniCssExtractPlugin 时的注意事项
当我们使用 MiniCssExtractPlugin 插件提取 CSS 时,Webpack4 会默认将 CSS 中的文件名替换为 chunkHash 值,以实现浏览器缓存。但这会导致与上述方案冲突,因为每个 CSS 文件的 chunkHash 值都是相同的,因此即使我们重新构建了应用,CSS 文件也不会更新。
为了解决这个问题,我们可以通过在 MiniCssExtractPlugin 的 options 中配置 ignoreOrder: true 来禁用默认的 chunkHash 值,然后加上我们自己的 hash 值,在 CSS 文件中手动替换掉文件名和 url 中的 hash。具体方法可以参考如下代码:
// javascriptcn.com 代码示例 const { HashedModuleIdsPlugin } = require('webpack'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { output: { filename: '[name].[chunkHash].js' }, module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader' ] } ] }, plugins: [ new HashedModuleIdsPlugin(), new MiniCssExtractPlugin({ filename: '[name].[contentHash].css', ignoreOrder: true }) ] };
上面的代码中,我们通过将 MiniCssExtractPlugin 的 filename 设置为 [name].[contentHash].css
,使用 contentHash 来代替 chunkHash,这样就与我们之前的方案不会冲突。然后,在 CSS 文件中手动替换掉文件名和 url 中的 hash,代码示例如下:
/* index.css */ body { background: url(../images/bg.jpg?hash=[contentHash]); }
使用动态 import 时的注意事项
如果我们使用了 Webpack4 的动态 import 功能来异步加载模块,那么也需要注意一个细节。由于动态 import 是异步加载的,它可能会在其他模块已经加载完毕的情况下才被加载。因此,在动态 import 中使用 chunkHash 时要格外小心,因为它可能并不是最新的。
为了解决这个问题,我们可以使用 ImportedOnDemandPlugin 插件来实现。这个插件会在动态 import 导入的模块中注入一个占位符,等到这个模块被加载时,再动态地替换占位符为正确的 chunkHash 值。使用方法如下:
首先安装这个插件:
npm i -D webpack-imported-on-demand
然后在 Webpack 配置中使用:
// javascriptcn.com 代码示例 const ImportedOnDemandPlugin = require('webpack-imported-on-demand').default; module.exports = { output: { filename: '[name].[chunkHash].js' }, plugins: [ new ImportedOnDemandPlugin({ generate: moduleInfo => { return moduleInfo.namedChunkGroup.split('_')[1]; } }) ] };
这里的代码中,我们使用了一个 generate 回调函数,它会在插件注入占位符时被调用。这里我们将使用动态 import 导入的模块的 chunkHash 作为占位符的初始值,然后在真正加载这个模块时,通过回调函数计算出正确的 chunkHash 值,并再次替换占位符。
总结
本文介绍了 Webpack4 中如何解决 chunkHash 值不变的问题,并详细介绍了两种常用的解决方案。同时,还针对使用 MiniCssExtractPlugin 和动态 import 时的注意事项进行了说明。希望这篇文章能够对大家在构建 SPA 应用时有所帮助。
示例代码
下面是完整的 Webpack 配置示例代码,包括上述方案和注意事项的实现:
// javascriptcn.com 代码示例 const { NamedChunksPlugin } = require('webpack'); const { HashedModuleIdsPlugin } = require('webpack'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const ImportedOnDemandPlugin = require('webpack-imported-on-demand').default; module.exports = { output: { filename: '[name].[chunkHash].js' }, module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader' ] } ] }, plugins: [ new NamedChunksPlugin(chunk => { if (chunk.name) { return chunk.name; } const hash = require('hash-sum'); const joinedHash = hash(Array.from(chunk.modulesIterable, m => m.id).join('_')); return `chunk-${joinedHash}`; }), new HashedModuleIdsPlugin(), new MiniCssExtractPlugin({ filename: '[name].[contentHash].css', ignoreOrder: true }), new ImportedOnDemandPlugin({ generate: moduleInfo => { return moduleInfo.namedChunkGroup.split('_')[1]; } }) ] };
来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/65316b777d4982a6eb31c18e