webpack4 SplitChunks實現程式碼分隔詳解

DC_er發表於2019-05-22

程式碼均放在git倉庫

Webpack 4給我們帶來了一些改變。包括更快的打包速度,引入了SplitChunksPlugin外掛來取代(之前版本里的)CommonsChunksPlugin外掛。在這篇文章中,你將學習如何分割你的輸出程式碼,從而提升我們應用的效能。

SplitChunks外掛(webpack 4.x以前使用CommonsChunkPlugin)允許我們將公共依賴項提取到現有的entry chunk或全新的程式碼塊中。

程式碼分割的理念

首先搞明白: webpack裡的程式碼分割是個什麼鬼? 它允許你將一個檔案分割成多個檔案。如果使用的好,它能大幅提升你的應用的效能。其原因是基於瀏覽器會快取你的程式碼這一事實。每當你對某一檔案做點改變,訪問你站點的人們就要重新下載它。然而依賴卻很少變動。如果你將(這些依賴)分離成單獨的檔案,訪問者就無需多次重複下載它們了。

使用webpack生成一個或多個包含你原始碼最終版本的“打包好的檔案”(bundles),(概念上我們當作)它們由(一個一個的)chunks組成。

首先 webpack 總共提供了三種辦法來實現 Code Splitting,如下:

  • 入口配置:entry 入口使用多個入口檔案;
  • 抽取公有程式碼:使用 SplitChunks 抽取公有程式碼;
  • 動態載入 :動態載入一些程式碼。

這裡我們姑且只討論使用 SplitChunks 抽取公有程式碼。

splitChunks配置

在src目錄下建立三個檔案pageA.js、pageB.js和pageC.js。程式碼詳情見文章開頭git倉庫。

// src/pageA.js
var react = require('react');
var reactDom = require('react-dom');
var utility1 = require('../utils/utility1');
var utility2 = require('../utils/utility2');
new Vue();

module.exports = "pageA";
複製程式碼
// src/pageB.js
var react = require('react');
var reactDom = require('react-dom');
var utility2 = require('../utils/utility2');
var utility3 = require('../utils/utility3');

module.exports = "pageB";
複製程式碼
// src/pageC.js
var react = require('react');
var reactDom = require('react-dom');
var utility2 = require('../utils/utility2');
var utility3 = require('../utils/utility3');

module.exports = "pageC";
複製程式碼

入口檔案 && 出口檔案

entry: {
    pageA: "./src/pageA",    // 引用utility1.js  utility2.js
    pageB: "./src/pageB",    // 引用utility2.js  utility3.js
    pageC: "./src/pageC",   // 引用utility2.js  utility3.js
},
output: {
    path: path.join(__dirname, "dist"),
    filename: "[name].[hash:8].bundle.js"
},
複製程式碼

配置optimization

首先我們配置optimization如下:

optimization: {
    splitChunks: {
      chunks: "all",
  },
複製程式碼

執行npm run build打包命令之後,檢視dist目錄

image.png

可以發現,打包出來的除了三個page檔案,還存在一個vendors~pageA~pageB~pageC.[hash].bundle.js檔案(此檔案中儲存了pageA、pageB、pageC和node_modules中共有的size大於30KB的檔案)。事實上這全靠了配置中本身預設固有一個cacheGroups的配置項:

splitChunks: {
    chunks: "all",
    cacheGroups: {
      vendors: {
        test: /[\\/]node_modules[\\/]/,  // 匹配node_modules目錄下的檔案
        priority: -10   // 優先順序配置項
      },
      default: {
        minChunks: 2,
        priority: -20,   // 優先順序配置項
        reuseExistingChunk: true
      }
    }
  }
複製程式碼

在預設設定中,

  • 會將 node_mudules 資料夾中的模組打包進一個叫 vendors的bundle中,
  • 所有引用超過兩次的模組分配到 default bundle 中 更可以通過 priority 來設定優先順序。

引數說明如下:

  • chunks:表示從哪些chunks裡面抽取程式碼,除了三個可選字串值 initial、async、all 之外,還可以通過函式來過濾所需的 chunks;
  • minSize:表示抽取出來的檔案在壓縮前的最小大小,預設為 30000
  • maxSize:表示抽取出來的檔案在壓縮前的最大大小,預設為 0,表示不限制最大大小;
  • minChunks:表示被引用次數,預設為1;上述配置commons中minChunks為2,表示將被多次引用的程式碼抽離成commons。

值得注意的是,如果沒有修改minSize屬性的話,而且被公用的程式碼(假設是utilities.js)size小於30KB的話,它就不會分割成一個單獨的檔案。在真實情形下,這是合理的,因為(如分割)並不能帶來效能確實的提升,反而使得瀏覽器多了一次對utilities.js的請求,而這個utilities.js又是如此之小(不划算)。

  • maxAsyncRequests:最大的按需(非同步)載入次數,預設為 5;
  • maxInitialRequests:最大的初始化載入次數,預設為 3;
  • automaticNameDelimiter:抽取出來的檔案的自動生成名字的分割符,預設為 ~;
  • name:抽取出來檔案的名字,預設為 true,表示自動生成檔名;
  • cacheGroups: 快取組。(這才是配置的關鍵)

快取組會繼承splitChunks的配置,但是test、priorty和reuseExistingChunk只能用於配置快取組。cacheGroups是一個物件,按上述介紹的鍵值對方式來配置即可,值代表對應的選項。除此之外,所有上面列出的選擇都是可以用在快取組裡的:chunks, minSize, minChunks, maxAsyncRequests, maxInitialRequests, name。可以通過optimization.splitChunks.cacheGroups.default: false禁用default快取組。預設快取組的優先順序(priotity)是負數,因此所有自定義快取組都可以有比它更高優先順序(譯註:更高優先順序的快取組可以優先打包所選擇的模組)(預設自定義快取組優先順序為0)

現在我們再重新來看一下pageA、pageB、pageC三個js檔案,這三個檔案中都引入了utility2.js檔案,但是此檔案size很明顯小於30KB,所以這部分公用程式碼並沒有分割出來。如果想要分割出來很簡單,只需要:

optimization: {
    splitChunks: {
      chunks: "all",
      minSize: 0
    }
},
複製程式碼

執行npm run build打包命令之後,檢視dist目錄

image.png

顯然多了一個pageA~pageB~pageC.[hash].bundle.js檔案。檢視檔案可得知此檔案中儲存了utility2.js中的程式碼。如下圖所示(藉助於webpack-bundle-analyzer外掛,詳情文章末尾附錄)。

image.png

上圖可以看出,React相關程式碼均放在了vendors~pageA~pageB~pageC.[hash].bundle.js檔案中,如果我們想要抽離出React程式碼,應該怎麼做吶?

splitChunks: {
      chunks: "all",
      cacheGroups: {
        commons: {
          chunks: "initial",
          minChunks: 2,
          name: "commons",
          maxInitialRequests: 5,
          minSize: 0, // 預設是30kb,minSize設定為0之後
                            // 多次引用的utility1.js和utility2.js會被壓縮到commons中
        },
        reactBase: {
          test: (module) => {
            return /react|redux|prop-types/.test(module.context);
          }, // 直接使用 test 來做路徑匹配,抽離react相關程式碼
          chunks: "initial",
          name: "reactBase",
          priority: 10,
        }
    }
},
複製程式碼

run build之後如下圖所示。

image.png

附錄

我們再安裝一個 webpack-bundle-analyzer,這個外掛會清晰的展示出打包後的各個bundle所依賴的模組:

npm i webpack-bundle-analyzer -D
複製程式碼

引入:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
複製程式碼

使用,在plugins陣列中新增即可:

new BundleAnalyzerPlugin()
複製程式碼

相關文章