知海

缓存指南

webpackjsorg-main指南-教程

缓存指南

提示: 本指南中的示例源自入门指南输出管理代码分割

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

本指南重点介绍确保 webpack 编译生成的文件能够保持缓存(除非内容发生更改)所需的配置。

输出文件名

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

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

项目

diff 复制代码
webpack-demo
 ├── package.json
 ├── package-lock.json
 ├── webpack.config.js
 ├── /dist
 ├── /src
 │   └── index.js
 └── /node_modules

webpack.config.js

diff 复制代码
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   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,应产生以下输出:

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

正如您所看到的,bundle 的名称现在会反映其内容(通过哈希)。如果我们再次运行构建而不做任何更改,我们预计该文件名会保持不变。然而,如果我们再次运行它,可能会发现情况并非如此:

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

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

警告: 输出可能因你当前的 webpack 版本不同而有所差异。较新版本可能不会遇到与较旧版本相同的所有哈希问题,但我们仍然建议采取以下步骤以确保安全。

提取样板代码

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

webpack.config.js

diff 复制代码
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   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:

bash 复制代码
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.splitChunks 并配置 cacheGroups,然后构建:

webpack.config.js

diff 复制代码
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   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:

bash 复制代码
...
                          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

项目

diff 复制代码
webpack-demo
 ├── package.json
 ├── package-lock.json
 ├── webpack.config.js
 ├── /dist
 ├── /src
 │   ├── index.js
+│   └── print.js
 └── /node_modules

print.js

diff 复制代码
+ export default function print(text) {
+   console.log(text);
+ };

src/index.js

diff 复制代码
  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 的哈希会改变,然而......

bash 复制代码
...
                           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 的哈希。让我们使用 optimization.moduleIds 并设置 'deterministic' 选项:

webpack.config.js

diff 复制代码
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   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 哈希也应在多次构建之间保持一致:

bash 复制代码
...
                          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

diff 复制代码
  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());

最后再次运行构建:

bash 复制代码
...
                          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

结论

缓存可能很复杂,但为应用程序或网站用户带来的好处使这些努力值得。请参阅下面的_延伸阅读_部分以了解更多信息。

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。