Webpack 是一个将你的所有 JavaScript、CSS 甚至图片这样的静态资源打包的好方法,但是为了在生产环境中更有效地使用生成的资源,应该考虑使用持久缓存。关于本话题的文献分散在不同的资源中,想搞对没那么容易。本文的目的是指导前端开发者完成所有的功能建设。
摘录
使用 webpack 开启静态资源的持久缓存:
- 使用 [chunkhash] 为每个文件增加一个内容相关的缓存清道夫;
- 使用编译统计在 HTML 中获取资源时取得文件名;
- 生成 JSON 格式的模块清单文件,并在 HTML 页面加载资源之前内联进去;
- 保证包含启动代码的入口块不会对于同样的依赖生成不同的哈希值;
- 开始收益!
问题
每次代码需要更新时,服务器必须重新部署,客户端也必须重新下载资源。因为从网络中获取资源会很慢,这显然非常低效。这就是为什么浏览器会缓存静态资源的原因。但是有一个缺陷:如果在部署新的版本中不修改文件名,浏览器会认为它没有更新,就会使用缓存中的版本。
可能告诉浏览器有更新的最简单方式是修改资源的文件名。在 webpack 之前的时代,我们一般在文件名后面追加参数,每次递增:
application.js?build=1
application.css?build=1
使用 webpack 就简单多了:每次构建时 webpack 都会生成一个唯一的哈希值,可用于组合到文件名中。下面的配置示例会生成2个在文件名中带哈希值的文件(每个都有入口):
// webpack.config.js
module.exports = {
entry: {
vendor: './src/vendor.js',
main: './src/index.js'
},
output: {
path: path.join(__dirname, 'build'),
filename: '[name].[hash].js'
}
};
使用这个配置运行 webpack 会生成下面的输出:
Hash: **55e783391098c2496a8f
**Version: webpack **1.10.1
**Time: **58**ms
**Asset** **Size** **Chunks** **Chunk Names
main.55e783391098c2496a8f.js** 1.43 kB **0** **[emitted]** main
**vendor.55e783391098c2496a8f.js** 1.43 kB **1** **[emitted]** vendor
[0] **./src/index.js** 46 bytes {**0**} **[built]**[0] **./src/vendor.js** 40 bytes {**1**} **[built]**
但是这种做法的问题是,每次构建,所有文件的的文件名都会被修改,客户端必须重新下载所有的代码。
并不是我们想要的,是吧?那么我们如何保证客户端总是获取到最新的版本,但不需要下载所有的资源?
为每个文件生成唯一的哈希值
如果文件内容不变,生成的文件名就不变会如何?比如说,依赖库文件以及其它不常变化的依赖之类的东西。
专业建议!
使用 CommonsChunkPlugin 区分你的依赖库和应用代码,显式创建一个依赖库的包,防止更新过多。
Webpack 允许根据文件内容生成哈希值。下面是新的配置:
// webpack.config.js
module.exports = {
...
output: {
...
filename: '[name].[chunkhash].js'
}
}
这个配置也会生成两个文件,但是在这个例子中,每个文件会独立地得到唯一的哈希值。
main.155567618f4367cd1cb8.js 1.43 kB 0 [emitted] main vendor.c2330c22cd2decb5da5a.js 1.43 kB 1 [emitted] vendor
专业建议!
不要在开发环境中使用 [chunkhash],因为它会增加编译时间。区分开发和生产环境的配置,使用 [name].js 应用于开发,使用 [name].[chunkhash].js 用于生产。
由于 Webpack 的一个问题,该生成哈希值的方法并不是确定的。保证哈希值是根据文件内容生成的,请使用 webpack-md5-hash 插件。这里是使用示例:github.com/okonet/webp…。
根据 webpack 统计获取文件名
在开发模式下,在 HTML 中直接引用 JavaScript 文件:
</script>
因此,每次在生产环境中构建时,我们会得到不同的文件名。类似这样:
</script>
为了在 HTML 中引用到正确的文件,我们需要知道构建的一些信息。可以使用一个简单的插件从 webpack 的编译统计中导出这些信息:
// webpack.config.js
module.exports = {
...
plugins: [
function() {
this.plugin("done", function(stats) {
require("fs").writeFileSync(
path.join(__dirname, "...", "stats.json"),
JSON.stringify(stats.toJson()));
});
}
]
}
也可以使用 www.npmjs.com/package/web… 或者 www.npmjs.com/package/ass… 导出 JSON 文件。
在我们的配置下的 webpack-manifest-plugin 的一个输出看起来是:
{
“main.js”: “main.155567618f4367cd1cb8.js”,
“vendor.js”: “vendor.c2330c22cd2decb5da5a.js”
}
剩下的就依赖你的服务器设置了,但我相信非常简单。如果你使用 Rails,这是一篇最佳指南。或者你的应用不依赖于任何服务端渲染技术,生成一个单独的 index.html 就足够了,那么建议使用下面两个称赞的插件, github.com/ampedandwir… 和 github.com/szrenwei/in…,它们会显著地简化设置。
你会认为,到此为止了。然而,还没完。
确定性的哈希值
为了减少生成文件的体积,webpack 使用了标识符而不是文件名。在编译期,标识符是生成的,对于于模块的文件名,并放置于叫做 chunk manifest 的 JavaScript 对象中。它(带着一些启动代码)被置于入口模块中,对于被 webpack 打包的代码来说极其关键。
问题与之前相同:当我们更改了代码的任何一部分,即使剩下的文件内容没有被修改,入口也会被更新以放入新的清单。这样反过来也就导致新的哈希值,影响了长期缓存。
为了修复这个问题,我们应该使用 chunk-manifest-webpack-plugin 插件来把清单导出到单独的 JSON 文件中。这是更新后的 webpack.config.js,它会在构建目录下创建 chunk-manifest.json 文件:
// webpack.config.js
var ChunkManifestPlugin = require('chunk-manifest-webpack-plugin');
module.exports = {
// 你的配置值
plugins: [
new ChunkManifestPlugin({
filename: "chunk-manifest.json",
manifestVariable: "webpackManifest"
})
]
};
因为我们从入口模块中移除了清单,现在我们要把它提供给 webpack。你也许在上面的例子中注意到了 manifestVariable 选项。这是 webpack 寻找清单 JSON 的全局变量,因此它必须在 HTML 中出现在打包文件的前面。把 JSON 的内容内联进 HTML 很简单。HTML 的 head 部分应该是这样的:
<html>
<head>
<script>
//<![CDATA[
window.webpackManifest = {"0":"main.3d038f325b02fdee5724.js","1":"1.c4116058de00860e5aa8.js"}
//]]>
</script>
</head>
<body>
</body>
</html>
第二个问题是 webpack 如何获取模块:默认地对于同样的依赖集合,模块在包中的顺序不是确定的。意思是:在两次构建之间,模块可能获取到不同的标识符,导致不同的内容,也就有了不同的哈希值。这是出现在 Github 上的 issue,建议使用 OccurenceOrderPlugin 来解决这个问题。
Webpack 2.0 已经修复了此问题,现在是 beta 阶段,如果你已经在使用了,就可以安全地移除 OccurenceOrderPlugin。
最后的 webpack.config.js:
var path = require('path');
var webpack = require('webpack');
var ManifestPlugin = require('webpack-manifest-plugin');
var ChunkManifestPlugin = require('chunk-manifest-webpack-plugin');
var WebpackMd5Hash = require('webpack-md5-hash');
module.exports = {
entry: {
vendor: './src/vendor.js',
main: './src/index.js'
},
output: {
path: path.join(__dirname, 'build'),
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "vendor",
minChunks: Infinity,
}),
new WebpackMd5Hash(),
new ManifestPlugin(),
new ChunkManifestPlugin({
filename: "chunk-manifest.json",
manifestVariable: "webpackManifest"
}),
new webpack.optimize.OccurenceOrderPlugin()
]
};
使用了这个配置,依赖包就不会更改哈希值,除非你修改了代码或依赖。下面是两次构建的输出,期间修改了 moduleB.js:
> webpack
Hash: 92670583f688a262fdad
Version: webpack 1.10.1
Time: 65ms
Asset Size Chunks Chunk Names
chunk-manifest.json 68 bytes [emitted]
vendor.6d107863983028982ef4.js 3.71 kB 0 [emitted] vendor
1.c4116058de00860e5aa8.js 107 bytes 1 [emitted]
main.5e17f4dff47bc1a007c0.js 373 bytes 2 [emitted] main
[0] ./src/index.js 186 bytes {2} [built]
[0] ./src/vendor.js 40 bytes {0} [built]
[1] ./src/moduleA.js 28 bytes {2} [built]
[2] ./src/moduleB.js 28 bytes {1} [built]
> webpack
Hash: a9ee1d1e46a538469d7f
Version: webpack 1.10.1
Time: 67ms
Asset Size Chunks Chunk Names
chunk-manifest.json 68 bytes [emitted]
vendor.6d107863983028982ef4.js 3.71 kB 0 [emitted] vendor
1.2883246944b1147092b1.js 107 bytes 1 [emitted]
main.5e17f4dff47bc1a007c0.js 373 bytes 2 [emitted] main
[0] ./src/index.js 186 bytes {2} [built]
[0] ./src/vendor.js 40 bytes {0} [built]
[1] ./src/moduleA.js 28 bytes {2} [built]
[2] ./src/moduleB.js 28 bytes {1} [built]
注意依赖包有相同的名字,正是我们所需要的!
结论
Webpack 模块化程序很高,有很多优化在默认下都没有开启。Webpack 提供的灵活性是的任何可以想到的设置成为可能,但是要记住长期缓存是一个常用的优化实践,我希望 webpack 的下一个版本能够默认地让这些事情更容易做到。这是本文中用到的例子的 Github 仓库。