原创翻译,转载请注明出处。
原文地址:https://webpack.js.org/guides/caching/
为了使webpack生成的静态资源能长时间的缓存:
- 使用[chunkhash],为每一个文件添加一个基于内容的cache-buster。
- 把webpack的对象清单提取到单独的文件里。
- 确保在依赖群没有发生变化的时候,包含启动引导指令代码的入口点代码块不会更改哈希值。
- 使用编译器统计来获得在HTML里请求的资源的名称。
- 生成代码块的清单JSON,并且在加载资源之前把它写入到HTML页面里。
问题
每一次我们的代码需要更新的时候,它们都需要在服务器上再次发布,然后由所有的客户端再次下载。这很明显非常低效因为通过网络获取资源将可能会很慢。这就是浏览器缓存静态文件的原因。
这种处理方式有个问题:如果我们部署新版本的时候没有更改资源文件名,浏览器会认为它没有被更新,所以客户端会得到一个缓存的版本。
告诉浏览器去下载新版本的一个简单的做法就是更改资源名称。在webpack之前,我们经常在文件名上加一个构建号作为参数,然后累加:
application.js?build=1 application.css?build=1
在webpack里会更容易。每一次webpack编译的时候都会生成一个独一无二的哈希值,把它放在output的占位符里,来构成文件名。下面的配置示例会生成2个文件名里包含哈希值的文件(每个入口文件生成一个):
// webpack.config.js const path =require("path"); module.exports ={ entry:{ vendor:"./src/vendor.js", main:"./src/index.js" }, output:{ path: path.join(__dirname,"build"), filename:"[name].[hash].js" } };
用这个配置运行webpack时会产生下面的输出:
Hash: 2a6c1fee4b5b0d2c9285 Version: webpack 2.2.0 Time: 62ms Asset Size Chunks Chunk Names vendor.2a6c1fee4b5b0d2c9285.js 2.58 kB 0 [emitted] vendor main.2a6c1fee4b5b0d2c9285.js 2.57 kB 1 [emitted] main [0] ./src/index.js 63 bytes {1}[built] [1] ./src/vendor.js 63 bytes {0}[built]
这里有个问题,任何一个文件更新之后的编译,会更新所有文件名,客户端不得不再次下载所有代码。我们怎么能确保客户端不用通过下载所有资源就总会得到最新版本的资源?
为每一个文件生成唯一的哈希值
如果文件的内容在两次编译之间没有发生变化,我们生成了同样的文件名将会怎样呢?例如,当依赖没有更新,只有应用代码被更新了,那么它将不需要再次下载vendor文件。
Webpack通过把占位符[hash]替换成[chunkhash],允许你根据内容来生成哈希值。这里是更新后的配置:
module.exports = { /*...*/ output: { /*...*/ - filename: "[name].[hash].js" + filename: "[name].[chunkhash].js" } };
这个配置也会生成两个文件,但是在这里,每一个文件都回得到属于它们自己的唯一哈希值。
Hash: cfba4af36e2b11ef15db Version: webpack 2.2.0 Time: 66ms Asset Size Chunks Chunk Names vendor.50cfb8f89ce2262e5325.js 2.58 kB 0 [emitted] vendor main.70b594fe8b07bcedaa98.js 2.57 kB 1 [emitted] main [0] ./src/index.js 63 bytes {1}[built] [1] ./src/vendor.js 63 bytes {0}[built]
从webpack编译统计信息里得到文件名一览
在开发模式时,你只需在HTML文件里引用以入口点名字命名的文件。
<script src="vendor.js"></script> <script src="main.js"></script>
但是,每次编译产品时,我们会得到不同的文件名。有时它们看起来像这样:
<script src="vendor.50cfb8f89ce2262e5325.js"></script> <script src="main.70b594fe8b07bcedaa98.js"></script>
为了在HTML里引用正确的文件,我们需要得到编译信息。使用下面的插件就可以从webpack编译统计信息里提取出来。
// webpack.config.js const path =require("path"); module.exports ={ /*...*/ plugins:[ function(){ this.plugin("done",function(stats){ require("fs").writeFileSync( path.join(__dirname,"build","stats.json"), JSON.stringify(stats.toJson())); }); } ] };
从下面插件里选取一个来导出JSON文件:
- https://www.npmjs.com/package/webpack-manifest-plugin
- https://www.npmjs.com/package/assets-webpack-plugin
在配置里使用WebpackManifestPlugin时的输出例子:
{ "main.js":"main.155567618f4367cd1cb8.js", "vendor.js":"vendor.c2330c22cd2decb5da5a.js" }
确定的哈希值
为了让生成文件最小化,webpack用识别码取代模块名称。在编译的时候,生成识别码,映射到代码块的文件名,然后被放到一个叫代码块清单的JavaScript对象里。为了生成保留在整个编译期里的识别码,webpack提供了NamedModulesPlugin(推荐开发使用)和HashedModuleIdsPlugin插件(推荐产品使用)。
代码块清单然后被(连同引导指令代码,运行代码)放到入口代码块里,它对用webpack打包代码的运行至关重要。
和之前的问题一样:不管何时我们更改代码的任何部分,即使其他内容没有被改过,它将更新入口代码块,包含新的清单信息。这样的话就会生成一个新的哈希值,也就不能长期缓存了。
为了解决这个,我们应该使用ChunkManifestWebpackPlugin插件,它会把清单信息提取到一个独立的JSON文件里。在webpack运行时代码里,通过一个变量代替代码块清单信息。但是我们可以做的更好;通过使用CommonsChunkPlugin插件把运行时代码提取到独立的入口文件。下面是更新后的webpack.config.js,将会在编译路径下生成清单信息文件和运行时代码文件。
// webpack.config.js var ChunkManifestPlugin =require("chunk-manifest-webpack-plugin"); module.exports ={ /*...*/ plugins:[ /*...*/ newwebpack.optimize.CommonsChunkPlugin({ name:["vendor","manifest"],// vendor libs + extracted manifest minChunks:Infinity, }), /*...*/ newChunkManifestPlugin({ filename:"chunk-manifest.json", manifestVariable:"webpackManifest" }) ] };
我们把清单信息从入口代码块移走之后,下一步我们需要做的是把它提供给webpack。上面例子里的manifestVariable配置定义了一个全局变量的名字,webpack用它来查找保存清单信息的JSON文件。它应该定义在HTML请求代码包之前。通过在HTML文件里内嵌JSON的内容来实现。我们的HTML文件的头部看起来像这样:
<html> <head> <script> //<![CDATA[ window.webpackManifest = {"0":"main.5f020f80c23aa50ebedf.js","1":"vendor.81adc64d405c8b218485.js"} //]]> </script> </head> <body> </body> </html>
最后,文件的哈希值是基于文件的内容。我们使用webpack-chunk-hash或者webpack-md5-hash来实现。
最终webpack.config.js应该是这样:
var path =require("path"); var webpack =require("webpack"); var ChunkManifestPlugin =require("chunk-manifest-webpack-plugin"); var WebpackChunkHash =require("webpack-chunk-hash"); module.exports ={ entry:{ vendor:"./src/vendor.js",// vendor reference file(s) main:"./src/index.js"// application code }, output:{ path: path.join(__dirname,"build"), filename:"[name].[chunkhash].js", chunkFilename:"[name].[chunkhash].js" }, plugins:[ newwebpack.optimize.CommonsChunkPlugin({ name:["vendor","manifest"],// vendor libs + extracted manifest minChunks:Infinity, }), newwebpack.HashedModuleIdsPlugin(), newWebpackChunkHash(), newChunkManifestPlugin({ filename:"chunk-manifest.json", manifestVariable:"webpackManifest" }) ] };
用这个配置,第三方库的代码块将不会改变哈希值,除非你修改了它们的代码或者依赖。下面是moduleB.js在两次编译之间被修改之后的输出:
> node_modules/.bin/webpack Hash: f0ae5bf7c6a1fd3b2127 Version: webpack 2.2.0 Time: 102ms Asset Size Chunks Chunk Names main.9ebe4bf7d99ffc17e75f.js 509 bytes 0, 2 [emitted] main vendor.81adc64d405c8b218485.js 159 bytes 1, 2 [emitted] vendor chunk-manifest.json 73 bytes [emitted] manifest.d41d8cd98f00b204e980.js 5.56 kB 2 [emitted] manifest
> node_modules/.bin/webpack Hash: b5fb8e138b039ab515f3 Version: webpack 2.2.0 Time: 87ms Asset Size Chunks Chunk Names main.5f020f80c23aa50ebedf.js 521 bytes 0, 2 [emitted] main vendor.81adc64d405c8b218485.js 159 bytes 1, 2 [emitted] vendor chunk-manifest.json 73 bytes [emitted] manifest.d41d8cd98f00b204e980.js 5.56 kB 2 [emitted] manifest
注意,第三方库的代码块文件名不变,清单信息文件也是一样(如果我们把清单信息提取成清单信息代码块的话)
内嵌清单信息
内嵌代码块清单信息和webpack运行时代码(防止想定外的HTTP请求),取决与你的服务器设置。
参考文献
https://medium.com/@okonetchnikov/long-term-caching-of-static-assets-with-webpack-1ecb139adb95#.vtwnssps4
https://gist.github.com/sokra/ff1b0290282bfa2c037bdb6dcca1a7aa
https://github.com/webpack/webpack/issues/1315
https://github.com/webpack/webpack.js.org/issues/652
https://presentations.survivejs.com/advanced-webpack/
-- End --