yanni4night

Webpack 的静态资源持久缓存

yanni4night · 2016-09-05翻译 · 2996阅读 原文链接

Webpack 是一个将你的所有 JavaScript、CSS 甚至图片这样的静态资源打包的好方法,但是为了在生产环境中更有效地使用生成的资源,应该考虑使用持久缓存。关于本话题的文献分散在不同的资源中,想搞对没那么容易。本文的目的是指导前端开发者完成所有的功能建设。


摘录

使用 webpack 开启静态资源的持久缓存:

  1. 使用 [chunkhash] 为每个文件增加一个内容相关的缓存清道夫;
  2. 使用编译统计在 HTML 中获取资源时取得文件名;
  3. 生成 JSON 格式的模块清单文件,并在 HTML 页面加载资源之前内联进去;
  4. 保证包含启动代码的入口块不会对于同样的依赖生成不同的哈希值;
  5. 开始收益!

问题

每次代码需要更新时,服务器必须重新部署,客户端也必须重新下载资源。因为从网络中获取资源会很慢,这显然非常低效。这就是为什么浏览器会缓存静态资源的原因。但是有一个缺陷:如果在部署新的版本中不修改文件名,浏览器会认为它没有更新,就会使用缓存中的版本。

可能告诉浏览器有更新的最简单方式是修改资源的文件名。在 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 插件。这里是使用示例:https://github.com/okonet/webpack-long-term-cache-demo/pull/3/files

根据 webpack 统计获取文件名

在开发模式下,在 HTML 中直接引用 JavaScript 文件:

`<script src="main.js">`</script>

因此,每次在生产环境中构建时,我们会得到不同的文件名。类似这样:

`<script src="main.155567618f4367cd1cb8.js">`</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()));
      });
    }
  ]
}

也可以使用 https://www.npmjs.com/package/webpack-manifest-plugin 或者 https://www.npmjs.com/package/assets-webpack-plugin 导出 JSON 文件。

在我们的配置下的 webpack-manifest-plugin 的一个输出看起来是:

{
  “main.js”: “main.155567618f4367cd1cb8.js”,
  “vendor.js”: “vendor.c2330c22cd2decb5da5a.js”
}

剩下的就依赖你的服务器设置了,但我相信非常简单。如果你使用 Rails,这是一篇最佳指南。或者你的应用不依赖于任何服务端渲染技术,生成一个单独的 index.html 就足够了,那么建议使用下面两个称赞的插件,https://github.com/ampedandwired/html-webpack-pluginhttps://github.com/szrenwei/inline-manifest-webpack-plugin,它们会显著地简化设置。

你会认为,到此为止了。然而,还没完。

确定性的哈希值

为了减少生成文件的体积,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 仓库

相关文章