Webpack 緩存

2023-05-17 16:14 更新

以上,我們使用 webpack 來打包我們的模塊化后的應(yīng)用程序,webpack 會(huì)生成一個(gè)可部署的 ?/dist? 目錄,然后把打包后的內(nèi)容放置在此目錄中。只要 ?/dist? 目錄中的內(nèi)容部署到 server 上,client(通常是瀏覽器)就能夠訪問此 server 的網(wǎng)站及其資源。而最后一步獲取資源是比較耗費(fèi)時(shí)間的,這就是為什么瀏覽器使用一種名為 緩存 的技術(shù)??梢酝ㄟ^命中緩存,以降低網(wǎng)絡(luò)流量,使網(wǎng)站加載速度更快,然而,如果我們在部署新版本時(shí)不更改資源的文件名,瀏覽器可能會(huì)認(rèn)為它沒有被更新,就會(huì)使用它的緩存版本。由于緩存的存在,當(dāng)你需要獲取新的代碼時(shí),就會(huì)顯得很棘手。

此指南的重點(diǎn)在于通過必要的配置,以確保 webpack 編譯生成的文件能夠被客戶端緩存,而在文件內(nèi)容變化后,能夠請求到新的文件。

輸出文件的文件名(output filename)

我們可以通過替換 ?output.filename? 中的 substitutions 設(shè)置,來定義輸出文件的名稱。webpack 提供了一種使用稱為 substitution(可替換模板字符串) 的方式,通過帶括號字符串來模板化文件名。其中,[contenthash] substitution 將根據(jù)資源內(nèi)容創(chuàng)建出唯一 hash。當(dāng)資源內(nèi)容發(fā)生變化時(shí),[contenthash] 也會(huì)發(fā)生變化。

這里使用 起步 中的示例和 管理輸出 中的 plugins 插件來作為項(xiàng)目基礎(chǔ),所以我們依然不必手動(dòng)地維護(hù) ?index.html? 文件:

project

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,
    },
  };

使用此配置,然后運(yùn)行我們的 build script npm run build,產(chǎn)生以下輸出:

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

可以看到,bundle 的名稱是它內(nèi)容(通過 hash)的映射。如果我們不做修改,然后再次運(yùn)行構(gòu)建,我們以為文件名會(huì)保持不變。然而,如果我們真的運(yùn)行,可能會(huì)發(fā)現(xiàn)情況并非如此:

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

這也是因?yàn)?webpack 在入口 chunk 中,包含了某些 boilerplate(引導(dǎo)模板),特別是 runtime 和 manifest。(譯注:boilerplate 指 webpack 運(yùn)行時(shí)的引導(dǎo)代碼)

提取引導(dǎo)模板(extracting boilerplate)

正如我們在 代碼分離 中所學(xué)到的,SplitChunksPlugin 可以用于將模塊分離到單獨(dú)的 bundle 中。webpack 還提供了一個(gè)優(yōu)化功能,可使用 optimization.runtimeChunk 選項(xiàng)將 runtime 代碼拆分為一個(gè)單獨(dú)的 chunk。將其設(shè)置為 single 來為所有 chunk 創(chuàng)建一個(gè) 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',
+   },
  };

再次構(gòu)建,然后查看提取出來的 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

將第三方庫(library)(例如 lodash 或 react)提取到單獨(dú)的 vendor chunk 文件中,是比較推薦的做法,這是因?yàn)?,它們很少像本地的源代碼那樣頻繁修改。因此通過實(shí)現(xiàn)以上步驟,利用 client 的長效緩存機(jī)制,命中緩存來消除請求,并減少向 server 獲取資源,同時(shí)還能保證 client 代碼和 server 代碼版本一致。 這可以通過使用 SplitChunksPlugin 示例 2 中演示的 SplitChunksPlugin 插件的 cacheGroups 選項(xiàng)來實(shí)現(xiàn)。我們在 optimization.splitChunks 添加如下 cacheGroups 參數(shù)并構(gòu)建:

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',
+         },
+       },
+     },
    },
  };

再次構(gòu)建,然后查看新的 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]
...

現(xiàn)在,我們可以看到 main 不再含有來自 node_modules 目錄的 vendor 代碼,并且體積減少到 240 bytes!

模塊標(biāo)識(shí)符(module identifier)

在項(xiàng)目中再添加一個(gè)模塊 print.js:

project

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 是由當(dāng)前 script 腳本 import 進(jìn)來的
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.onclick = Print.bind(null, 'Hello webpack!');

    return element;
  }

  document.body.appendChild(component());

再次運(yùn)行構(gòu)建,然后我們期望的是,只有 main bundle 的 hash 發(fā)生變化,然而……

...
                           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]
...

……我們可以看到這三個(gè)文件的 hash 都變化了。這是因?yàn)槊總€(gè) module.id 會(huì)默認(rèn)地基于解析順序(resolve order)進(jìn)行增量。也就是說,當(dāng)解析順序發(fā)生變化,ID 也會(huì)隨之改變。簡要概括:

  • main bundle 會(huì)隨著自身的新增內(nèi)容的修改,而發(fā)生變化。
  • vendor bundle 會(huì)隨著自身的 module.id 的變化,而發(fā)生變化。
  • manifest runtime 會(huì)因?yàn)楝F(xiàn)在包含一個(gè)新模塊的引用,而發(fā)生變化。

第一個(gè)和最后一個(gè)都是符合預(yù)期的行為,vendor hash 發(fā)生變化是我們要修復(fù)的。我們將 optimization.moduleIds 設(shè)置為 'deterministic':

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',
          },
        },
      },
    },
  };

現(xiàn)在,不論是否添加任何新的本地依賴,對于前后兩次構(gòu)建,vendor hash 都應(yīng)該保持一致:

...
                          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,臨時(shí)移除額外的依賴:

src/index.js

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

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

    // lodash 是由當(dāng)前 script 腳本 import 進(jìn)來的
    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());

最后,再次運(yùn)行我們的構(gòu)建:

...
                          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
...

我們可以看到,這兩次構(gòu)建中,vendor bundle 文件名稱,都是 55e79e5927a639d21a1b。

結(jié)論

緩存可能很復(fù)雜,但是從應(yīng)用程序或站點(diǎn)用戶可以獲得的收益來看,這值得付出努力。想要了解更多信息,請查看下面 進(jìn)一步閱讀 部分。

Further Reading


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號