缓存

因此,我们使用 webpack 打包我们的模块化应用程序,生成一个可部署的 /dist 目录。一旦 /dist 的内容部署到服务器,客户端(通常是浏览器)就会访问该服务器以获取网站及其资源。最后一步可能很耗时,这就是为什么浏览器使用一种称为缓存的技术。这使得网站加载速度更快,并减少了不必要的网络流量。然而,当您需要获取新代码时,这也会引起一些麻烦。

本指南重点介绍确保 webpack 编译生成的文件可以在内容未更改时保持缓存所需的配置。

输出文件名

我们可以使用 output.filename 替换符设置来定义输出文件的名称。Webpack 提供了一种使用方括号字符串(称为替换符)来模板化文件名的方法。[contenthash] 替换符将根据资源内容添加一个唯一的哈希值。当资源内容更改时,[contenthash] 也会随之更改。

让我们使用入门中的示例,并结合输出管理中的 plugins 来设置我们的项目,这样我们就无需手动维护 index.html 文件了。

项目

webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
|- /node_modules

webpack.config.js

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new HtmlWebpackPlugin({
-       title: 'Output Management',
+       title: 'Caching',
      }),
    ],
    output: {
-     filename: 'bundle.js',
+     filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

使用此配置运行我们的构建脚本 npm run build 应该会产生以下输出

...
                       Asset       Size  Chunks                    Chunk Names
main.7e2c49a622975ebd9b7e.js     544 kB       0  [emitted]  [big]  main
                  index.html  197 bytes          [emitted]
...

如您所见,bundle 的名称现在通过哈希反映了其内容。如果我们再次运行构建而没有任何更改,我们期望文件名保持不变。然而,如果我们再次运行它,我们可能会发现情况并非如此

...
                       Asset       Size  Chunks                    Chunk Names
main.205199ab45963f6a62ec.js     544 kB       0  [emitted]  [big]  main
                  index.html  197 bytes          [emitted]
...

这是因为 webpack 在入口 chunk 中包含了某些样板代码,特别是 runtime 和 manifest。

提取样板代码

正如我们在代码分离中了解到的,SplitChunksPlugin 可用于将模块分离到单独的 bundle 中。Webpack 提供了一个优化功能,可以使用 optimization.runtimeChunk 选项将运行时代码拆分到单独的 chunk 中。将其设置为 single 以创建所有 chunk 的单个 runtime bundle

webpack.config.js

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new HtmlWebpackPlugin({
      title: 'Caching',
      }),
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
+   optimization: {
+     runtimeChunk: 'single',
+   },
  };

让我们再次运行构建以查看提取的 runtime bundle

Hash: 82c9c385607b2150fab2
Version: webpack 4.12.0
Time: 3027ms
                          Asset       Size  Chunks             Chunk Names
runtime.cc17ae2a94ec771e9221.js   1.42 KiB       0  [emitted]  runtime
   main.e81de2cf758ada72f306.js   69.5 KiB       1  [emitted]  main
                     index.html  275 bytes          [emitted]
[1] (webpack)/buildin/module.js 497 bytes {1} [built]
[2] (webpack)/buildin/global.js 489 bytes {1} [built]
[3] ./src/index.js 309 bytes {1} [built]
    + 1 hidden module

将第三方库(例如 lodashreact)提取到单独的 vendor chunk 中也是一个好习惯,因为它们比我们的本地源代码更改的可能性更小。这一步将允许客户端从服务器请求更少的内容以保持最新。这可以通过使用SplitChunksPlugincacheGroups 选项来实现,如SplitChunksPlugin 示例 2 中所示。让我们使用以下参数添加 optimization.splitChunkscacheGroups 并进行构建

webpack.config.js

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new HtmlWebpackPlugin({
      title: 'Caching',
      }),
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
    optimization: {
      runtimeChunk: 'single',
+     splitChunks: {
+       cacheGroups: {
+         vendor: {
+           test: /[\\/]node_modules[\\/]/,
+           name: 'vendors',
+           chunks: 'all',
+         },
+       },
+     },
    },
  };

让我们再次运行构建以查看我们新的 vendor bundle

...
                          Asset       Size  Chunks             Chunk Names
runtime.cc17ae2a94ec771e9221.js   1.42 KiB       0  [emitted]  runtime
vendors.a42c3ca0d742766d7a28.js   69.4 KiB       1  [emitted]  vendors
   main.abf44fedb7d11d4312d7.js  240 bytes       2  [emitted]  main
                     index.html  353 bytes          [emitted]
...

我们现在可以看到我们的 main bundle 不再包含来自 node_modules 目录的 vendor 代码,并且大小已减少到 240 bytes

模块标识符

让我们向项目中添加另一个模块 print.js

项目

webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
+ |- print.js
|- /node_modules

print.js

+ export default function print(text) {
+   console.log(text);
+ };

src/index.js

  import _ from 'lodash';
+ import Print from './print';

  function component() {
    const element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.onclick = Print.bind(null, 'Hello webpack!');

    return element;
  }

  document.body.appendChild(component());

再次运行构建,我们期望只有 main bundle 的哈希值会改变,然而...

...
                           Asset       Size  Chunks                    Chunk Names
  runtime.1400d5af64fc1b7b3a45.js    5.85 kB      0  [emitted]         runtime
  vendor.a7561fb0e9a071baadb9.js     541 kB       1  [emitted]  [big]  vendor
    main.b746e3eb72875af2caa9.js    1.22 kB       2  [emitted]         main
                      index.html  352 bytes          [emitted]
...

...我们看到三者都改变了。这是因为每个 module.id 默认根据解析顺序递增。这意味着当解析顺序改变时,ID 也会改变。总结一下

  • main bundle 因其新内容而改变。
  • vendor bundle 因其 module.id 改变而改变。
  • 并且,runtime bundle 改变是因为它现在包含对新模块的引用。

第一个和最后一个是预期的,我们需要修复的是 vendor 哈希。让我们使用带有 'deterministic' 选项的 optimization.moduleIds

webpack.config.js

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Caching',
      }),
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
    optimization: {
+     moduleIds: 'deterministic',
      runtimeChunk: 'single',
      splitChunks: {
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all',
          },
        },
      },
    },
  };

现在,尽管有任何新的本地依赖,我们的 vendor 哈希在构建之间都应该保持一致

...
                          Asset       Size  Chunks             Chunk Names
   main.216e852f60c8829c2289.js  340 bytes       0  [emitted]  main
vendors.55e79e5927a639d21a1b.js   69.5 KiB       1  [emitted]  vendors
runtime.725a1a51ede5ae0cfde0.js   1.42 KiB       2  [emitted]  runtime
                     index.html  353 bytes          [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.216e852f60c8829c2289.js
...

然后让我们修改 src/index.js 暂时删除该额外依赖

src/index.js

  import _ from 'lodash';
- import Print from './print';
+ // import Print from './print';

  function component() {
    const element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-   element.onclick = Print.bind(null, 'Hello webpack!');
+   // element.onclick = Print.bind(null, 'Hello webpack!');

    return element;
  }

  document.body.appendChild(component());

最后再次运行我们的构建

...
                          Asset       Size  Chunks             Chunk Names
   main.ad717f2466ce655fff5c.js  274 bytes       0  [emitted]  main
vendors.55e79e5927a639d21a1b.js   69.5 KiB       1  [emitted]  vendors
runtime.725a1a51ede5ae0cfde0.js   1.42 KiB       2  [emitted]  runtime
                     index.html  353 bytes          [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.ad717f2466ce655fff5c.js
...

我们可以看到两次构建在 vendor bundle 的文件名中都产生了 55e79e5927a639d21a1b

总结

缓存可能很复杂,但它对应用程序或网站用户的好处使其值得付出努力。请参阅下面的延伸阅读部分了解更多信息。

延伸阅读

12 贡献者

okonetjouni-kantolaskipjackdannycjonesfadysamirsadekafontcurosavagesaiprasad2595EugeneHlushkoAnayaDesignaholznersnitin315