We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
前段时间发布一个基于webpack的前端工程时,对生成的增量发布文件列表感到迷惑。
假设业务有两个页面A、B,其webpack入口分别为{a,b}.js。
{a,b}.js
本次发布仅对a.js进行了修改,理想的发布的文件列表应该为:
a.js
a.html
a.[hash].js
但实际上却是:
b.html
b.[hash].js
vendor.[hash].js
迷惑不解,于是花了点时间去探索webpack生成文件哈希值的奥秘。
探索webpack caching策略之前,首先要明白配合HTTP缓存的前端代码部署发布策略。推荐阅读:
大公司里怎样开发和部署前端代码?
读完后就会明白,现阶段比较成熟的持久化缓存方案就是使静态资源的文件名包含其内容的hash值,并在静态资源服务器配置HTTP缓存规则。基于此可用做到增量发布及很好地利用HTTP缓存能力。
那么webpack提供了什么配置来影响生成的文件名哈希值呢?
output.filename可以指定构建结果输出的文件名,其中提供了三个关于哈希值的占位符:
[hash]
[chunkhash]
[contenthash]
Tips: 可以通过[hash:16]指定哈希值的长度 可以通过output.hashDigestLength指定默认哈希值的长度
[hash:16]
output.hashDigestLength
补充: 可能有读者不理解什么是chunk, asset, module,请查看官方文档
那么问题来了:
接下来以一个典型的实践Demo来探索下:
文中所有demo都可以在Github地址找到,基于:
webpack v4.25.1
Node v8.16.0
Mac OS 10.14.5
以下demo基于常见的代码拆包优化,它会
vendor.xxx.js
common.xxx.js
配置为:
// webpack.config.js // ... optimization: { splitChunks: { cacheGroups: { vendor: { test: /node_modules/, chunks: 'all', name: 'vendor' }, common: { test: /.js$/, name: 'common', minChunks: 2 } } } } // ...
假设目前仅有一个页面入口a.js,使用[hash]的占位符进行打包输出:
// a.js import 'lodash';
// webpack.config.js module.exports = { entry: { a: './src/pages/a.js' }, output: { filename: '[name].[hash].js' // ... }, };
打包结果为:
Asset Size Chunks Chunk Names a.ce71fc0e3be0265f3764.js 1.47 KiB 0 [emitted] a vendor.ce71fc0e3be0265f3764.js 69.8 KiB 1 [emitted] vendor
很奇怪,vendor与a文件的hash值竟然是一致的,当我随意在a.js加上一行代码后,vendor的文件名也随之变化了,无法有效利用HTTP缓存,这显然不是我们想要的。
vendor
a
为了使vendor和a能拥有不同的hash值,我们将[hash]改为[chunkhash]使之文件hash值根据自身chunk来计算。
// webpack.config.js module.exports = { output: { filename: '[name].[chunkhash].js' // ... }, };
运行结果为:
Asset Size Chunks Chunk Names a.b4a6f84d92c2cd8e68e2.js 1.47 KiB 0 [emitted] a vendor.1f01c1b51cfa1b8560ea.js 69.8 KiB 1 [emitted] vendor
效果不错,如果任意修改a.js,那么vendor文件应该不会改变,然而事实却不是这样。 如果对a.js模块的修改,导致原有抽离到vendor的modules的id有所变化的话:
// a.js import '../common/util'; import 'lodash';
Asset Size Chunks Chunk Names a.c90b7ef837be6067c4db.js 1.51 KiB 0 [emitted] a vendor.67cb27b8f3bcb258a6b8.js 69.8 KiB 1 [emitted] vendor
其实对比前后的vendor结果只是webpack生成的runtime代码(也就是module的id)轻微的不一致而已:
关于webpack runtime的细节可以自行google
为了解决runtime的影响,我们根据webpack的caching文档中指出可以使用使用配置抽离runtime代码:
// webpack.config.js // ... optimization: { runtimeChunk: 'single', // ...
Asset Size Chunks Chunk Names a.025a679cc53a17ae5e20.js 112 bytes 0 [emitted] a runtime.761a672aee47740d7b60.js 1.42 KiB 1 [emitted] runtime vendor.e0a3e1f04b6066968af9.js 69.8 KiB 2 [emitted] vendor
重复在a.js添加util依赖后,再次编译结果为:
util
Asset Size Chunks Chunk Names a.757fd6c1c697ab9fa7ea.js 152 bytes 0 [emitted] a runtime.761a672aee47740d7b60.js 1.42 KiB 1 [emitted] runtime vendor.42d139ff34a38fa36e79.js 69.8 KiB 2 [emitted] vendor
很遗憾,看了下runtime.xxx.js的内容,只是将runtime顶部的代码进行了抽离而已(所以前后都没有变化),modules的id变化还是使得vendor的hash值发生了变化。
runtime.xxx.js
解决方案可以通过optimization.moduleIds配置来告诉webpack使用文件路径的hash值来作为module的id,而不是自增的id。这样将vendor的相关的id固定下来,就不会改变hash值了。
// webpack.config.js // ... optimization: { moduleIds: 'hashed', // ...
Asset Size Chunks Chunk Names a.39f663c39a00b0a0d3ed.js 127 bytes 0 [emitted] a runtime.761a672aee47740d7b60.js 1.42 KiB 1 [emitted] runtime vendor.43f6930938e9626aee08.js 69.8 KiB 2 [emitted] vendor
Asset Size Chunks Chunk Names a.0fea31ce608e3a986832.js 177 bytes 0 [emitted] a runtime.761a672aee47740d7b60.js 1.42 KiB 1 [emitted] runtime vendor.43f6930938e9626aee08.js 69.8 KiB 2 [emitted] vendor
到此,js文件hash值的行为已经符合我们的预期了。但真实的项目往往会更为复杂,接下来我们增加CSS模块。
按照官方指引我们添加了CSS的处理:
// webpack.config.js const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // ... module: { rules: [ { test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader, }, 'css-loader', ], }, ], }, plugins: [ new MiniCssExtractPlugin({ // 注意这里为contenthash,因为官方文档说明了其他两个是不管用的 filename: '[name].[contenthash].css' }), ] // ...
先在上一次基础上打包,结果为:
Asset Size Chunks Chunk Names a.4f71dbbe92135e074143.js 177 bytes 0 [emitted] a runtime.526e924c24f031d6c058.js 1.42 KiB 1 [emitted] runtime vendor.43f6930938e9626aee08.js 69.8 KiB 2 [emitted] vendor
对比了a.4f71dbbe92135e074143.js与a.0fea31ce608e3a986832.js,runtime.526e924c24f031d6c058.js与runtime.761a672aee47740d7b60.js 可以发现在打包内容完全不变的情况下,改变了webpack配置也会使chunkhash改变(猜测a与runtime chunk的某些配置属性变化了,后续可以更加深入chunkhash的生成原理,这里对于实际场景来说,项目稳定后改变构建的场景是非常少的)。
a.4f71dbbe92135e074143.js
a.0fea31ce608e3a986832.js
runtime.526e924c24f031d6c058.js
runtime.761a672aee47740d7b60.js
然后我们简单的添加a.css并在a.js依赖进来:
a.css
// a.css * { margin: 0; padding: 0; }
// a.js // ... import './a.css';
打包后结果为:
Asset Size Chunks Chunk Names a.a2e96b7b9cacef0a192b.js 212 bytes 0 [emitted] a a.ad59b1cfef38946db95f.css 33 bytes 0 [emitted] a runtime.526e924c24f031d6c058.js 1.42 KiB 1 [emitted] runtime vendor.43f6930938e9626aee08.js 69.8 KiB 2 [emitted] vendor
现在我们将a.css作些简单改变,再打包一次,预期只有a.css的hash值会改变,结果a.js的hash值(因为chunkhash是基于整个chunk的内容的)也变了:
Asset Size Chunks Chunk Names a.d8e08571019679820f98.css 63 bytes 0 [emitted] a a.fd7d396d5654ca01204b.js 212 bytes 0 [emitted] a runtime.526e924c24f031d6c058.js 1.42 KiB 1 [emitted] runtime vendor.43f6930938e9626aee08.js 69.8 KiB 2 [emitted] vendor
这是我们不希望看到的,官方的issue提供了解决方案。
将webpack配置稍微修改下:
// webpack.config.js // ... output: { filename: '[name].[contenthash].js', // ...
更改a.css的前后两次的运行结果对比为:
Asset Size Chunks Chunk Names a.0d13a673fbf7e0fb82c9.js 212 bytes 0 [emitted] a a.a99fa96ecdccdf2c269e.css 34 bytes 0 [emitted] a runtime.ffaf452731b773b5b33e.js 1.42 KiB 1 [emitted] runtime vendor.94725a7abb26f12e6126.js 69.8 KiB 2 [emitted] vendor
Asset Size Chunks Chunk Names a.0d13a673fbf7e0fb82c9.js 212 bytes 0 [emitted] a a.d8e08571019679820f98.css 63 bytes 0 [emitted] a runtime.ffaf452731b773b5b33e.js 1.42 KiB 1 [emitted] runtime vendor.94725a7abb26f12e6126.js 69.8 KiB 2 [emitted] vendor
只改变了a.css的hash值,完美。
接下来我们新增一个页面入口b.js,来探索下页面之间会不会有影响:
b.js
// webpack.config.js // ... entry: { // ... b: './src/pages/b.js', }, optimization: { // ... splitChunks: { minSize: 1, // 为了cacheGroups.common可拆包成功 cacheGroups: { vendor: { // ... priority: 2, }, common: { test: /\.js$/, chunks: 'all', name: 'common', priority: 1, } } } }, // ...
这里新增了一个公共的chunk common,尽可能贴近生产环境。尝试不同模块的修改都符合预期,很棒:
common
a.xxx.css
common/util.js
b.xxx.js
当页面功能变得越来越复杂臃肿时,特别是SPA,我们常常会通过dynamic import的方式,拆分异步模块来解决:
// a.js // ... import('../async/a.async').then((a) => a.log());
// a.async.js export const log = () => console.log('a async')
对比前后两次的构建结果,发现变更的不仅是a.js的hash值,runtime.xxx.js的hash值也因为新增了部分异步加载的runtime代码变化了:
Asset Size Chunks Chunk Names a.a99fa96ecdccdf2c269e.css 34 bytes 3 [emitted] a a.f27987a0484118d434b9.js 174 bytes 3 [emitted] a b.a99fa96ecdccdf2c269e.css 34 bytes 4 [emitted] b b.eb62980a9e5aa5b97832.js 226 bytes 4 [emitted] b common.584c299f230dbafc55e8.js 100 bytes 0 [emitted] common runtime.ffaf452731b773b5b33e.js 1.42 KiB 1 [emitted] runtime vendor.94725a7abb26f12e6126.js 69.8 KiB 2 [emitted] vendor
Asset Size Chunks Chunk Names 5.a8b36218e4d816632f48.js 171 bytes 5 [emitted] a.a21c9b5ef7c674164ee4.js 224 bytes 3 [emitted] a a.a99fa96ecdccdf2c269e.css 34 bytes 3 [emitted] a b.a99fa96ecdccdf2c269e.css 34 bytes 4 [emitted] b b.eb62980a9e5aa5b97832.js 226 bytes 4 [emitted] b common.584c299f230dbafc55e8.js 100 bytes 0 [emitted] common runtime.0eba89b4f3878dd2ec40.js 2.18 KiB 1 [emitted] runtime vendor.94725a7abb26f12e6126.js 69.8 KiB 2 [emitted] vendor
同样的在a.js中再增加一个a2.async.js,结果runtime.xxx.js还是因为新增了chunk信息变更了:
a2.async.js
Asset Size Chunks Chunk Names 5.a8b36218e4d816632f48.js 171 bytes 5 [emitted] 6.17a8c61193ab8db1915d.js 172 bytes 6 [emitted] a.a99fa96ecdccdf2c269e.css 34 bytes 3 [emitted] a a.cf4d753b92b883744875.js 274 bytes 3 [emitted] a b.a99fa96ecdccdf2c269e.css 34 bytes 4 [emitted] b b.eb62980a9e5aa5b97832.js 226 bytes 4 [emitted] b common.584c299f230dbafc55e8.js 100 bytes 0 [emitted] common runtime.6b834e964b97efa45c8d.js 2.21 KiB 1 [emitted] runtime vendor.94725a7abb26f12e6126.js 69.8 KiB 2 [emitted] vendor
这种页面A的改动引起页面B需要加载变更的runtime的方式不科学。实际上更改起来也很简单,只需要在配置即可:
runtime
// webpack.config.js // ... optimization: { runtimeChunk: true, // ...
改动之后验证就没有问题了。更改了hash的只有a.xxx.js和runtime~a.xxx.js。 但细心的同学可能发现了,异步加载的chunk文件名又变成了自增id的模式,尽管我尝试将配置改为:
a.xxx.js
runtime~a.xxx.js
// webpack.config.js // ... optimization: { // ... chunkIds: 'named', // ...
也没有作用。因为webpack.NamedChunkPlugin只处理那些有名字的chunk,而异步加载的chunk是默认没有名字的。解决方式有两种:
webpack.NamedChunkPlugin
// webpack.config.js // ... plugins: [ new webpack.NamedChunksPlugin((chunk) => { if (chunk.name) { return chunk.name; } return [...chunk._modules].map(m => path.relative(m.context, m.request)).join("_"); }), // ...
// a.js // ... import(/* webpackChunkName: "a_async" */'../async/a.async').then((a) => a.log()); import(/* webpackChunkName: "a2_async" */'../async/a2.async').then((a2) => a2.log());
至此已经基本实现webpack4的可预测持久化缓存了,Perfect!
本文探索了基于webpack4的长效缓存实践经验,对于更新迭代频繁的实际业务来说,运用这些优化经验能节省不少的网络流量成本开支,且用户能获得更好的体验。
true
runtime chunk
hashed
The text was updated successfully, but these errors were encountered:
No branches or pull requests
前段时间发布一个基于webpack的前端工程时,对生成的增量发布文件列表感到迷惑。
假设业务有两个页面A、B,其webpack入口分别为
{a,b}.js
。本次发布仅对
a.js
进行了修改,理想的发布的文件列表应该为:a.html
a.[hash].js
但实际上却是:
a.html
b.html
a.[hash].js
b.[hash].js
vendor.[hash].js
迷惑不解,于是花了点时间去探索webpack生成文件哈希值的奥秘。
前言
探索webpack caching策略之前,首先要明白配合HTTP缓存的前端代码部署发布策略。推荐阅读:
大公司里怎样开发和部署前端代码?
读完后就会明白,现阶段比较成熟的持久化缓存方案就是使静态资源的文件名包含其内容的hash值,并在静态资源服务器配置HTTP缓存规则。基于此可用做到增量发布及很好地利用HTTP缓存能力。
那么webpack提供了什么配置来影响生成的文件名哈希值呢?
output.filename
output.filename可以指定构建结果输出的文件名,其中提供了三个关于哈希值的占位符:
[hash]
:The hash of the module identifier基于每次构建的compilation对象的所有内容计算而来
[chunkhash]
:The hash of the chunk content基于每一个chunk根据自身的内容计算而来
[contenthash]
:The hash of the content of a file, which is different for each asset基于每一个生成文件(asset)内容计算而来
那么问题来了:
接下来以一个典型的实践Demo来探索下:
文中所有demo都可以在Github地址找到,基于:
webpack v4.25.1
Node v8.16.0
Mac OS 10.14.5
场景及实践
以下demo基于常见的代码拆包优化,它会
vendor.xxx.js
common.xxx.js
配置为:
[hash]
假设目前仅有一个页面入口
a.js
,使用[hash]
的占位符进行打包输出:打包结果为:
很奇怪,
vendor
与a
文件的hash值竟然是一致的,当我随意在a.js
加上一行代码后,vendor
的文件名也随之变化了,无法有效利用HTTP缓存,这显然不是我们想要的。[chunkhash]
为了使
vendor
和a
能拥有不同的hash值,我们将[hash]
改为[chunkhash]
使之文件hash值根据自身chunk来计算。运行结果为:
效果不错,如果任意修改
a.js
,那么vendor
文件应该不会改变,然而事实却不是这样。如果对
a.js
模块的修改,导致原有抽离到vendor
的modules的id有所变化的话:其实对比前后的
vendor
结果只是webpack生成的runtime代码(也就是module的id)轻微的不一致而已:optimization.runtimeChunk
为了解决runtime的影响,我们根据webpack的caching文档中指出可以使用使用配置抽离runtime代码:
重复在
a.js
添加util
依赖后,再次编译结果为:很遗憾,看了下
runtime.xxx.js
的内容,只是将runtime顶部的代码进行了抽离而已(所以前后都没有变化),modules的id变化还是使得vendor
的hash值发生了变化。optimization.moduleIds
解决方案可以通过optimization.moduleIds配置来告诉webpack使用文件路径的hash值来作为module的id,而不是自增的id。这样将vendor的相关的id固定下来,就不会改变hash值了。
重复在
a.js
添加util
依赖后,再次编译结果为:到此,js文件hash值的行为已经符合我们的预期了。但真实的项目往往会更为复杂,接下来我们增加CSS模块。
处理CSS
按照官方指引我们添加了CSS的处理:
先在上一次基础上打包,结果为:
对比了
a.4f71dbbe92135e074143.js
与a.0fea31ce608e3a986832.js
,runtime.526e924c24f031d6c058.js
与runtime.761a672aee47740d7b60.js
可以发现在打包内容完全不变的情况下,改变了webpack配置也会使chunkhash改变(猜测a与runtime chunk的某些配置属性变化了,后续可以更加深入chunkhash的生成原理,这里对于实际场景来说,项目稳定后改变构建的场景是非常少的)。
然后我们简单的添加
a.css
并在a.js
依赖进来:打包后结果为:
现在我们将
a.css
作些简单改变,再打包一次,预期只有a.css
的hash值会改变,结果a.js
的hash值(因为chunkhash是基于整个chunk的内容的)也变了:这是我们不希望看到的,官方的issue提供了解决方案。
[contenthash]
将webpack配置稍微修改下:
更改
a.css
的前后两次的运行结果对比为:只改变了
a.css
的hash值,完美。增加页面入口
接下来我们新增一个页面入口
b.js
,来探索下页面之间会不会有影响:这里新增了一个公共的chunk
common
,尽可能贴近生产环境。尝试不同模块的修改都符合预期,很棒:a.css
-> 仅变化a.xxx.css
common/util.js
-> 仅变化common.xxx.js
b.js
(引入多一个模块) -> 仅变化b.xxx.js
增加异步加载
当页面功能变得越来越复杂臃肿时,特别是SPA,我们常常会通过dynamic import的方式,拆分异步模块来解决:
对比前后两次的构建结果,发现变更的不仅是
a.js
的hash值,runtime.xxx.js
的hash值也因为新增了部分异步加载的runtime代码变化了:同样的在
a.js
中再增加一个a2.async.js
,结果runtime.xxx.js
还是因为新增了chunk信息变更了:这种页面A的改动引起页面B需要加载变更的
runtime
的方式不科学。实际上更改起来也很简单,只需要在配置即可:改动之后验证就没有问题了。更改了hash的只有
a.xxx.js
和runtime~a.xxx.js
。但细心的同学可能发现了,异步加载的chunk文件名又变成了自增id的模式,尽管我尝试将配置改为:
也没有作用。因为
webpack.NamedChunkPlugin
只处理那些有名字的chunk,而异步加载的chunk是默认没有名字的。解决方式有两种:至此已经基本实现webpack4的可预测持久化缓存了,Perfect!
总结
本文探索了基于webpack4的长效缓存实践经验,对于更新迭代频繁的实际业务来说,运用这些优化经验能节省不少的网络流量成本开支,且用户能获得更好的体验。
[contenthash]
占位符[contenthash]
占位符true
为每一个入口抽离runtime chunk
hashed
稳定module Id防止影响到公共chunk参考链接
The text was updated successfully, but these errors were encountered: