webpack长效缓存机制译
Posted 链家网技术团队
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了webpack长效缓存机制译相关的知识,希望对你有一定的参考价值。
如何正确的使用webpack长效缓存机制是一个始终没有得到答案的问题。
这个问题在Github上很受关注
它有162条评论
马上要到2周岁的生日了
过多的建议往往使事情变得更糟糕
还有一些在google中可以搜到
其实有一个简单的方式,就是使用RecordsPlugin
插件(它的相关文档有点少)。但这需要记录你的每一个构建文件。我不喜欢依赖于状态,因此尝试给这个问题寻找一个好的答案。
使用NamedModuleIds
使用NamedChunkIds
撒上一点魔法
然后再来一点
但是首先最重要的,是什么阻止了默认情况下未优化webpack构建的长效缓存呢?说来话长了,让我们开始吧。
我们将使用webpack建立一个小程序,然后渐渐丰富它,从而在这过程中遇到各种各样的问题。就像解决其他好的问题一样,我们将全部解决它们。
基础
这是我们最初未优化的webpack配置:
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
main: './src/foo',
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].[hash].js',
},
};
这是 foo.js
文件:
// foo.js
import preact from 'preact';
console.log(preact.toString());
构建一下,我们得到如下的webpack输出:
Asset Size Chunks Chunk Names
main.db3022283e4b37cce06b.js 23.6 kB 0 [emitted] main
[0] ./~/preact/dist/preact.js 20.5 kB {0} [built]
[1] ./src/foo.js 61 bytes {0} [built]
到目前为止看起来还不错。
Vendor chunks
可能你想要做的第一件事就是将preact
从主入口文件中移除,因为我们一般不会对它进行改动,所以我们在webpack配置中加入CommonsChunkPlugin
。
// webpack.config.js
// ...
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor']
})
]
// ...
再次构建一下,得到了一个类似的输出:
Asset Size Chunks Chunk Names
main.423221edd7eef26d646e.js 506 bytes 0 [emitted] main
vendor.423221edd7eef26d646e.js 26.7 kB 1 [emitted] vendor
[0] ./~/preact/dist/preact.js 20.5 kB {1} [built]
[1] ./src/foo.js 61 bytes {0} [built]
[2] multi preact 28 bytes {1} [built]
不要睡着,好玩的才刚刚开始!
正确的哈希
你可能已经发现了,在这里我们遇到了第一个问题。main和vendor块的哈希是同步的。对main文件的任何修改都会使原来的vendor无效掉。
为了修复这个问题,我们必须将文件名中的hash
转换为 chunkhash
。这是因为hash
为我们构建的所有静态资源生成一些全局hash,而 chunkhash
只会为它块内的资源生成hash。
// webpack.config.js
// ...
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].[chunkhash].js',
},
// ...
再次运行构建,现在我们看到两个不同的哈希。
Asset Size Chunks Chunk Names
main.edc22f71759cbe5336ae.js 528 bytes 0 [emitted] main
vendor.27f1230219fd2a606a54.js 26.7 kB 1 [emitted] vendor
[0] ./~/preact/dist/preact.js 20.5 kB {1} [built]
[1] ./src/foo.js 83 bytes {0} [built]
[2] multi preact 28 bytes {1} [built]
非常棒!
运行时问题
现在在main中做任何修改应该不会影响到vendor
了吧。让我们添加一行新的代码来测试一下:
// foo.js
// ...
console.log(preact.toString());
console.log("hello world");
再次构建,然而,我们看到所有的事情都白费了,同样的问题依然出现了:
Asset Size Chunks Chunk Names
main.91022729b32987083f0d.js 506 bytes 0 [emitted] main
vendor.0da51f051fcf235d7027.js 26.7 kB 1 [emitted] vendor
[0] ./~/preact/dist/preact.js 20.5 kB {1} [built]
[1] ./src/foo.js 61 bytes {0} [built]
[2] multi preact 28 bytes {1} [built]
可是这是为什么呢?问题的详细答案在这里:
因为Webpack在把 preact
从main
块中提取出来的同时,也将Webpack的运行时代码提取了出来。运行时代码是Webpack的一部分,用来解析模块、处理异步加载等。如果深入它的代码,在其中能看到我们主模块哈希的一个索引:
// somewhere in the vendor.0da51f051fcf235d7027.js
// ...
chunkId + "." + {"0":"91022729b32987083f0d"}[chunkId]
// ...
幸运的是我们可以解决这个问题。如果你添加一个CommonsChunkPlugin
,用一个入口文件名称中不存在的新名字,Webpack将生成一个以该名字命名的块,并提取运行时代码放入其中。听起来有没有很神奇?
// webpack.config.js
// ...
plugins: [
// ...
new webpack.optimize.CommonsChunkPlugin({
name: ['runtime']
})
]
挥动着魔法的翅膀~再次构建,现在是这样的:
Asset Size Chunks Chunk Names
vendor.634878b098e5c599febd.js 20.7 kB 0 [emitted] vendor
main.d59c6ff3126e3943c563.js 538 bytes 1 [emitted] main
runtime.25ce0c546aab71f8eac5.js 5.92 kB 2 [emitted] runtime
[0] ./~/preact/dist/preact.js 20.5 kB {0} [built]
[1] ./src/foo.js 93 bytes {1} [built]
[2] multi preact 28 bytes {0} [built]
现在更改main块中的内容将只会改变runtime和main块的哈希,vendor 块的哈希将保持不变。
添加更多的依赖
然而,故事还没有结束。随着项目丰富起来,我们来添加更多的依赖。
// foo.js
import bar from './bar';
// ...
再次构建,我们希望只有runtime和main块改变。但是,你应该猜到了,事实却并不是这样:
Asset Size Chunks Chunk Names
main.cec87b856171489c2719.js 811 bytes 0 [emitted] main
vendor.73db375ed475c718163f.js 20.7 kB 1 [emitted] vendor
runtime.93b41beba42ebff23af0.js 5.92 kB 2 [emitted] runtime
[0] ./~/preact/dist/preact.js 20.5 kB {1} [built]
[1] ./src/bar.js 23 bytes {0} [built]
[2] ./src/foo.js 118 bytes {0} [built]
[3] multi preact 28 bytes {1} [built]
虽然 vendor块中并没有任何更改,它的哈希却再一次改变了。这又是有原因的。每一个块都有一个数字的chunk id。这些chunk id是按顺序生成的。随着新的重要的代码添加,这顺序是会改变的,从而chunks ids也会随着改变。
命名你的chunk
加入 NamedChunksPlugin
。它是在Webpack 2.4版本中新添加的,允许在块中使用名字而不是数字。
// weback.config.js
// ...
plugins: [
new webpack.NamedChunksPlugin(),
// ...
// ...
这将使用唯一的块名称而不是其id来标识一个块。
我们在添加和不添加bar.js
两种情况下分别再次构建,希望看到 vendor块的哈希保持不变。然而,结果并不是这样。分别观察两次构建后vendor块的代码,能够看到:
// old vendor build
// ...
/***/ }),
/* 1 */,
/* 2 */
/***/ (function(module, exports, __webpack_require__) {
// ...
// vendor build with new import
// ...
/***/ }),
/* 1 */,
/* 2 */
/* 3 */
/***/ (function(module, exports, __webpack_require__) {
命名你的模块
由于一些原因,Webpack将存在的所有模块的id都加进了vendor块里面去。我们暂且不要太在乎为什么。幸运的是有一个简单的解决方案。加入NamedModulesPlugin
。
// webpack.config.js
// ...
plugins: [
new webpack.NamedModulesPlugin(),
// ...
// ...
它和命名块非常形似。它用一个唯一的路径去将我们的请求映射到一个模块,而不是用数字 id。
多亏了这个变化,现在vendor哈希将始终保持不变。
// output without bar.js
Asset Size Chunks Chunk Names
main.5f15e6808c8037c8bdbc.js 628 bytes main [emitted] main
runtime.72ef2fc7d9df236c7f1c.js 5.94 kB runtime [emitted] runtime
vendor.73c86187abcdf9fd7b18.js 20.7 kB vendor [emitted] vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[0] multi preact 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 121 bytes {main} [built]
// output with bar.js
Asset Size Chunks Chunk Names
main.47b747115cd1c2c24b93.js 901 bytes main [emitted] main
runtime.8d94ab27ee79b53aa9a2.js 5.94 kB runtime [emitted] runtime
vendor.73c86187abcdf9fd7b18.js 20.7 kB vendor [emitted] vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/bar.js] ./src/bar.js 23 bytes {main} [built]
[0] multi preact 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 118 bytes {main} [built]
异步
随着我们应用程序的成长,它的体积越来越庞大。为了防止一次加载所有代码,我们使用一些异步分割点将其分解。首先,我们添加一个:
// foo.js
import('./async-bar').then( a => console.log(a));
// ...
Asset Size Chunks Chunk Names
0.9110a255e8cbd547adc7.js 311 bytes 0 [emitted]
...
而不久之后又有一个异步依赖:
// foo.js
import('./async-bar').then( a => console.log(a));
import('./async-baz').then( a => console.log(a));
// ...
Asset Size Chunks Chunk Names
0.612f1fa751a3287cf615.js 311 bytes 0 [emitted]
1.9606a5eadde08d70c763.js 311 bytes 1 [emitted]
...
WTF WEBPACK。为什么我的异步块的命名突然变化了?为什么他们再次编号?我认为将缓存无效是很难的,但你却总是在将它们无效掉:(。
事实上,NamedChunkPlugin
只处理具有名称的块。我们的异步块并没有名称。愚蠢懒惰的OSS开发者。
我们来解决这个问题。
NamedChunksPlugin
接收一个参数。此参数必须是一个函数,接收chunk作为其自身参数并且必须为它返回一个id。我们将插件更改为如下所示:
// 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("_");
}),
]
再次构建,我们现在可以根据需要添加多个异步块。以前添加的将保持其名称和hash不变:
Asset Size Chunks Chunk Names
async-bar.js.06896d922ee7bb8af159.js 324 bytes async-bar.js [emitted]
async-baz.js.fff77d118cec24487e5d.js 324 bytes async-baz.js [emitted]
main.16bb304a9926477df558.js 1.18 kB main [emitted] main
runtime.8f75ffddb5ac5820d4a9.js 6.01 kB runtime [emitted] runtime
vendor.73c86187abcdf9fd7b18.js 20.7 kB vendor [emitted] vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/async-bar.js] ./src/async-bar.js 41 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 41 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 23 bytes {main} [built]
[0] multi preact 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 218 bytes {main} [built]
旁注:请站在美学角度随意更改这些异步块的名称。我太懒了
现在一切都OK了吗?
如果我说并没有呢!
外部依赖
这是一个至今都没有死的事情,我们必须支持它。并且我们也可能在应用程序中真正使用到它。有些东西,我们不想加载它两次,并且想从全局上下文中得到它。但是,作为好的开发人员,我们也希望使依赖性显而易见。所以我们将jQuery定义为外部依赖。
// webpack.config.js
// ...
externals: {
jquery: 'jQuery'
},
// ...
然后呢,有哪里可能会出现问题吗?好吧,所有的。
// before:
Asset Size Chunks Chunk Names
async-bar.js.06896d922ee7bb8af159.js 324 bytes async-bar.js [emitted]
async-baz.js.fff77d118cec24487e5d.js 324 bytes async-baz.js [emitted]
main.e4704e563c227ee88d16.js 1.21 kB main [emitted] main
runtime.6add1d75139c2dd504b1.js 6.01 kB runtime [emitted] runtime
vendor.73c86187abcdf9fd7b18.js 20.7 kB vendor [emitted] vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/async-bar.js] ./src/async-bar.js 41 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 41 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 23 bytes {main} [built]
[0] multi preact 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 250 bytes {main} [built]
// after:
async-bar.js.06896d922ee7bb8af159.js 324 bytes async-bar.js [emitted]
async-baz.js.fff77d118cec24487e5d.js 324 bytes async-baz.js [emitted]
main.f8b6bbf7315113f5b167.js 1.48 kB main [emitted] main
runtime.5544c7f42d217300ca94.js 6.01 kB runtime [emitted] runtime
vendor.3614a776703bbb63977c.js 20.7 kB vendor [emitted] vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/async-bar.js] ./src/async-bar.js 41 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 41 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 23 bytes {main} [built]
[1] multi preact 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 247 bytes {main} [built]
好吧。。谢谢jQuery
,干掉了我的vendor块?什么鬼?!
事实证明,就像chunks一样, NamedModulesPlugin
也只适用于普通模块。也就是说,外部模块从 multi preact
模块中偷走了 id 0
。让我们一劳永逸地解决这个问题。
给每人一个名字
除了普通的模块,还有一堆Webpack使用的其他模块。然而并不是所有的都被NamedModulesPlugin
覆盖。因此,我们需要自己处理(也许我应该把它做成一个包?*):
我们将其添加到我们的Webpack配置中:
// webpack.config.js
plugins: [
// ...
{
apply(compiler) {
compiler.plugin("compilation", (compilation) => {
compilation.plugin("before-module-ids", (modules) => {
modules.forEach((module) => {
if (module.id !== null) {
return;
}
module.id = module.identifier();
});
});
});
}
}
现在在npm(https://www.npmjs.com/package/name-all-modules-plugin),是可用的哦。
这其实与NamedModulesPlugin
本身非常相似,不同的是,它为没有id
的模块调用module#identifier
方法。
再次构建,我们确实获得了一个新的vendor缓存,但这一次是稳定的了,无论你添加多少个外部模块!
Asset Size Chunks Chunk
async-bar.js.06896d922ee7bb8af159.js 324 bytes async-bar.js [emitted]
async-baz.js.fff77d118cec24487e5d.js 324 bytes async-baz.js [emitted]
main.8fad02c2ce285a92402a.js 1.52 kB main [emitted] main
runtime.a1ba8a5ef1cf9f245e87.js 6.01 kB runtime [emitted] runtime
vendor.12a52011916cc3cb208c.js 20.8 kB vendor [emitted] vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/async-bar.js] ./src/async-bar.js 41 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 41 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 23 bytes {main} [built]
[./src/foo.js] ./src/foo.js 247 bytes {main} [built]
[multi preact] multi preact 28 bytes {vendor} [built]
正如你所看到的multi preact
的id,从[0]
变为[multi preact]
。
就是这样了?不,还有最后一步!
添加更多的入口点
应用程序进一步扩大,我们需要有第二个入口。所以我们添加一个新的入口点,并将其添加到我们的Webpack配置中:
// webpack.config.js
// ...
entry: {
main: './src/foo',
other: './src/foo-two',
vendor: ['preact']
},
// ...
它的内容是这样的:
// foo-two.js
import bar from './bar';
import preact from 'preact';
import('./async-baz').then( a => console.log(a));
console.log(preact.toString() + "hello world again and again");
再次构建,使我们无语了(╯°□°)╯(┻━┻。
现在构建后是这样:
Asset Size Chunks Chunk Names
async-bar.js.06896d922ee7bb8af159.js 324 bytes async-bar.js [emitted]
async-baz.js.fff77d118cec24487e5d.js 324 bytes async-baz.js [emitted]
main.8407140a96b58ecd9a36.js 1.32 kB main [emitted] main
other.e86bb03c185f1e6bfc33.js 863 bytes other [emitted] other
runtime.da520a6c3b0dab33c4dc.js 6.05 kB runtime [emitted] runtime
vendor.c0d706bb45788764e72e.js 21 kB vendor [emitted] vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/async-bar.js] ./src/async-bar.js 41 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 41 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 23 bytes {vendor} [built]
[./src/foo-two.js] ./src/foo-two.js 168 bytes {other} [built]
[./src/foo.js] ./src/foo.js 247 bytes {main} [built]
[multi preact] multi preact 28 bytes {vendor} [built]
+ 1 hidden modules
一切都变了。vendor,main文件,一切,但为什么?
事实证明,我们的vendor块不仅取决于我们指定的内容,还包括了两个入口点里面的共用的内容。虽然有时可能需要共用我们自己定义的常用模块,但这不应该归到vendor块中去。现在,我们根本就不想要这样。那么,添加minChunks: Infinity
到我们的vendor公共块告诉Webpack我们真的只想要我们在条目中指定的内容:
// webpack.config.js
plugins: [
//...
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity
}),
// ...
如果我们再次构建:
Asset Size Chunks Chunk Names
async-bar.js.06896d922ee7bb8af159.js 324 bytes async-bar.js [emitted]
async-baz.js.fff77d118cec24487e5d.js 324 bytes async-baz.js [emitted]
main.8fad02c2ce285a92402a.js 1.52 kB main [emitted] main
other.db28c745f0f45fa4d8ed.js 1.06 kB other [emitted] other
runtime.506eb2c73a3b28891b05.js 6.05 kB runtime [emitted] runtime
vendor.12a52011916cc3cb208c.js 20.8 kB vendor [emitted] vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/async-bar.js] ./src/async-bar.js 41 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 41 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 23 bytes {main} {other} [built]
[./src/foo-two.js] ./src/foo-two.js 168 bytes {other} [built]
[./src/foo.js] ./src/foo.js 247 bytes {main} [built]
[multi preact] multi preact 28 bytes {vendor} [built]
得到了我们想要的结果。最终我们的Webpack配置看起来是这样的:
const path = require('path');
const webpack = require('webpack');
const NameAllModulesPlugin = require('name-all-modules-plugin');
module.exports = {
entry: {
main: './src/foo',
other: './src/foo-two',
vendor: ['preact']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].[chunkhash].js',
},
externals: {
jquery: 'jQuery'
},
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.NamedChunksPlugin((chunk) => {
if (chunk.name) {
return chunk.name;
}
return chunk.modules.map(m => path.relative(m.context, m.request)).join("_");
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
}),
new NameAllModulesPlugin(),
]
}
以上。至少这包含了我能够想到的所有边界情况。我希望这有帮助。如果您仍然发现有奇怪之处,请随时发表评论,我将尽力为他们寻求解决方案。
现在尽情去现实世界中享受吧。请停止使用奇怪的弊大于利的哈希插件吧!
最后一个重要的注意事项:您现在可能不得不关注缓存失效的问题。由于Webpack可能允许插件在chunkhash
计算后更改静态资源,然而这些插件可能无法正确更新chunkhash
,这可能导致静态资源保持其哈希不变,尽管实际上资源更改了。我个人没有碰到过这种情况,但注意一下总是好的。
就说这么多了!祝好!:)
以上是关于webpack长效缓存机制译的主要内容,如果未能解决你的问题,请参考以下文章
干货从构建进程间缓存设计 谈 Webpack5 优化和工作原理