本文基于 Webpack@4.28.3 版本
在现代前端工程中,为了更有效的利用浏览器和 CDN 的缓存,我们通常会给静态资源设置一个比较长的缓存时间(cache-control等),在有内容更新的时候,通过在静态资源路径中加入 hash 的方式来实现。具体可以参考这里: 大公司里怎样开发和部署前端代码?。
但是在 Webpack 中,实现可靠的长期缓存并不容易。在 github 上,关于这个问题的 issue 已经讨论了三年之久。在 Webpack 4 时代,我们终于看到了一点希望。
我们从一个最基础的 Webpack 配置开始:
1 | // webpack.config.js |
构建一下,得到如下结果:
1 | Asset Size Chunks Chunk Names |
看起来好像很完美。
在实际项目中,我们通常会选择将公共模块提取成一个单独的文件,在 Webpack 3 时代,我们一般会选用 CommonsChunkPlugin
,在 Webpack 4 中,我们则可以使用 splitChunks
来实现这一需求。这里我们将来自 node_modules 的模块提取成 vendors.js
:
1 | // webpack.config.js |
打包结果如下:
1 | Asset Size Chunks Chunk Names |
你可能已经注意到了,我们的 app 和 vendors 的 hash 是相同的。这样打包,对 app 模块的任何更改同时也会导致我们的 vendors 模块的 hash 失效。
要解决这个问题,我们必须在文件名中把 hash 改成 chunkhash
。这是因为 hash 会为我们构建的所有内容生成一个全局哈希,而 chunkhash
只会使用它自己的模块中的内容来生成哈希值。
修改 webpack 配置如下,再次构建,我们得到了两个不同的哈希值:
1 | // webpack.config.js |
1 | Asset Size Chunks Chunk Names |
现在,更改 app 模块中的内容, vendor 模块不会再受影响了(最新版本的 Webpack 中,不再需要为了 hash 的稳定单独提取出 runtime 了)。
我们来添加一行代码测试一下:
1 | // app.js |
打包之后,很完美:
1 | Asset Size Chunks Chunk Names |
随着项目的增长,我们不可避免的出现了更多的依赖:
1 | // app.js |
我们再来构建一次。在这次构建中,显然我们只希望 app.js 的 hash 值更新,但是事情总是不尽如人意的:1
2
3
4Asset Size Chunks Chunk Names
app.adcbdfaa.js 2.44 KiB 0 [emitted] app
index.html 319 bytes [emitted]
vendors.a7e68620.js 114 KiB 1 [emitted] vendors
尽管我们的 vendors 模块没有任何变化,它的 hash 还是又一次改变了。原因又是一个 Webpack 中的细节: Webpack 会为每个 chunk 按顺序给出一个依次增加的 chunk id,随着新依赖的增加,chunk 顺序也可能会发生变化,于是 chunk id 也会随之更新。
为了解决这个问题,我们引入了一个新的插件: NamedChunksPlugin
。这是在 Webpack 2.4 版本中加入的一项特性,借助它我们可以让模块有自己的名字,而不是冷冰冰的数字(Webpack 4 中, development mode下已默认开启)。
1 | // weback.config.js |
这样配置,Webpack 将使用唯一的 chunk 名称而不是其 id 来标识一个 chunk。
在最新版的 Webpack 中,我们也可以使用 optimization.chunkIds 来达成同样的效果:
1 | // weback.config.js |
我们在添加和不添加 hello.js 的情况下分别再构建一次,应该就可以看到,vendor 块的哈希是保持不变的了。
1 | // 不添加 hello.js |
好吧,并没有。
这是因为和 chunk id 类似,Webpack 同样会对 module 使用自增数字命名。类似地,我们可以使用 NamedModulesPlugin
或 HashedModuleIdsPlugin
来命名 module,从而使 hash 固定。其中 NamedModulesPlugin 使用 module 的路径来命名,生成的名字更可读,但是和使用 module 路径生成的4位(可能出现重复的情况下位数会增加) hash 命名 module 的 HashedModuleIdsPlugin 相比,会明显增大文件体积,适合用于开发环境,生产环境适合使用 HashedModuleIdsPlugin。
和 chunkIds 类似,在最新版本的 Webpack 中,我们也可以使用 optimization.moduleIds
来配置这一功能(development 模式下 optimization.namedModules
已默认开启)。
继续更新我们的 Webpack 配置:
1 | // webpack.config.js |
再次更改 app 模块的内容打包,我们可以看到,更新前后其余文件的 hash 是不变的了。
1 | // 不添加 hello.js |
为了首屏性能等需求,我们不可避免的需要在项目中使用异步模块。我们添加一个异步模块再次打包:
1 | // router.js |
1 | Asset Size Chunks Chunk Names |
可以看到 vendors 的 hash 又更新了。打开对应的文件,发现这样一段内容:
1 | window["webpackJsonp"] || []).push([[0], |
显然,对新加入的这个异步模块的命名失效了,又变成了从 0 开始的自增序列。
查看源码可以得知,NamedChunksPlugin
仅对有 name 的 chunk 有效,但是可以通过自定义 nameResolver
的方式来实现我们需要的功能:
1 | new webpack.NamedChunksPlugin(chunk => { |
还有一个更简单的方案则是使用 Webpack 的魔法注释来给异步模块命名:
1 | // router.js |
打包结果如下:
1 | Asset Size Chunks Chunk Names |
可以看到,vendors 的 hash 恢复到了之前的状态。
是不是感觉好像还缺点什么?
在生产环境中,我们通常会将 css 模块打包为独立文件。我们增加一个 css 模块来看一下:
1 | // app.js |
1 | // webpack.config.js |
改变一下 hello.css 的内容,打包两次,对比如下:
1 | Asset Size Chunks Chunk Names |
只修改了 CSS 文件,app.js 的 hash 值又变了。很容易理解,毕竟它们属于同一 chunk。
为了解决这个问题,Webpack 4.3 中引入了一个新的概念: contenthash
。
修改 Webpack 配置如下:
1 | // webpack.config.js |
再次打包对比更改 css 文件前后的变化,只有 css 文件的 hash 改变了。
1 | Asset Size Chunks Chunk Names |
有些场景下,我们需要从 CDN 引入一个模块,以 jQuery 为例,一般会如下配置:
1 | <!-- index.html --> |
1 | //webpack.config.js |
1 | // app.js |
和异步模块类似,NamedChunksPlugin 也不会对 externals 模块起作用,我们可以参考这里,使用 NameAllModulesPlugin
来做一些相关的配置,从而为它命名。
目前,Webpack 已经发布了 v5.0.0-alpha.3 版本。
在 Webpack 5 中,采用了全新的算法来生成 chunkIds 和 moduleIds,在打包后的文件大小和控制缓存之间有了一个更好的平衡。
在 Webpack 4 的更新说明中,开发团队提到了 Webpack 5 中会有开箱即用的长缓存配置,在这个 alpha 版本中,我们也看到了 cache: { type: "filesystem" }
这样的配置,然而,这个策略依然是实验性质的。希望在正式版本中能一劳永逸的解决这个问题。
https://medium.com/webpack/predictable-long-term-caching-with-webpack-d3eee1d3fa31
https://webpack.js.org/guides/caching/
https://zhuanlan.zhihu.com/p/38456425
https://segmentfault.com/a/1190000016355127
https://segmentfault.com/a/1190000015919928
https://github.com/pigcan/blog/issues/9