如何使用 splitChunks 精細控制代碼分割

背景

前端小夥伴都知道,爲了下降包大小,常常會把依賴的前端模塊獨立打包,好比把 vuevue-router 打到一個單獨的包 vendor 中。另外,常會將存在多個路由的複雜頁面的每一個頁面都單獨打一個包,只有訪問某個頁面的時候,再去下載該頁面的js包,以此來加快首頁的渲染。html

不管是 react 仍是 vue 都提供了完善的工具,幫咱們屏蔽了繁瑣的配置工做。當咱們對代碼進行構建時,已經自動幫咱們完成了代碼的拆分工做。前端

因此,不少小夥伴並不知道背後到底發生了什麼事。至於爲何這麼拆分,到底如何控制代碼的拆分,更是一頭霧水了。vue

問題測驗

講解開始以前,你們先看一個問題。若是你已經知道問題的答案,並且明白爲何,就沒必要往下閱讀了。若是不知道答案或者知道答案,但不知道緣由。那麼,強烈建議閱讀本文。node

// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: { app: "./src/index.js" },
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "dist")
  },
  optimization: {
    splitChunks: {
      chunks: "all"
    }
  },
  plugins: [
    new HtmlWebpackPlugin()
  ]
};
複製代碼
// index.js

import "vue"
import(/*webpackChunkName: 'a' */ "./a");
import(/*webpackChunkName: 'b' */ "./b");
複製代碼
// a.js

import "vue-router";
import "./someModule"; // 模塊大小大於30kb
複製代碼
// b.js

import "vuex";
import "./someModule"; // 模塊大小大於30kb
複製代碼
// someModule.js
// 該模塊大小超過30kb

// ...
複製代碼

代碼分割的三種方式

webpack 中如下三種常見的代碼分割方式:react

  • 入口起點:使用 entry 配置手動地分離代碼。
  • 動態導入:經過模塊的內聯函數調用來分離代碼。
  • 防止重複:使用 splitChunks 去重和分離 chunk。 第一種方式,很簡單,只須要在 entry 裏配置多個入口便可:
entry: { app: "./index.js", app1: "./index1.js" }
複製代碼

第二種方式,就是在代碼中自動將使用 import() 加載的模塊分離成獨立的包:webpack

//...

import("./a");

//...

複製代碼

第三種方式,是使用 splitChunks 插件,配置分離規則,而後 webpack 自動將知足規則的 chunk 分離。一切都是自動完成的。web

前兩種拆分方式,很容易理解。本文主要針對第三種方式進行討論。vue-router

splitChunks 代碼拆分

splitChunks 默認配置

splitChunks: {
    // 表示選擇哪些 chunks 進行分割,可選值有:async,initial和all
    chunks: "async",
    // 表示新分離出的chunk必須大於等於minSize,默認爲30000,約30kb。
    minSize: 30000,
    // 表示一個模塊至少應被minChunks個chunk所包含才能分割。默認爲1。
    minChunks: 1,
    // 表示按需加載文件時,並行請求的最大數目。默認爲5。
    maxAsyncRequests: 5,
    // 表示加載入口文件時,並行請求的最大數目。默認爲3。
    maxInitialRequests: 3,
    // 表示拆分出的chunk的名稱鏈接符。默認爲~。如chunk~vendors.js
    automaticNameDelimiter: '~',
    // 設置chunk的文件名。默認爲true。當爲true時,splitChunks基於chunk和cacheGroups的key自動命名。
    name: true,
    // cacheGroups 下能夠能夠配置多個組,每一個組根據test設置條件,符合test條件的模塊,就分配到該組。模塊能夠被多個組引用,但最終會根據priority來決定打包到哪一個組中。默認將全部來自 node_modules目錄的模塊打包至vendors組,將兩個以上的chunk所共享的模塊打包至default組。
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        },
        // 
    default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}
複製代碼

以上配置,歸納以下4個條件:vuex

  1. 模塊在代碼中被複用或者來自 node_modules 文件夾
  2. 模塊的體積大於等於30kb(壓縮以前)
  3. 當按需加載 chunks 時,並行請求的最大數量不能超過5
  4. 初始頁面加載時,並行請求的最大數量不能超過將3
// index.js

import("./a");

// ...
複製代碼
// a.js

import "vue";

// ...
複製代碼

以上代碼,在默認配置下的構建結果以下:bash

圖1

緣由分析

  • index.js 做爲入口文件,屬於入口起點手動配置分割代碼的狀況,所以會獨立打包。(app.js)
  • a.js 經過 import() 進行加載,屬於動態導入的狀況,所以會獨立打出一個包。(1.js)
  • vue 來自 node_modules 目錄,而且大於30kb;將其從 a.js 拆出後,與 a.js 並行加載,並行加載的請求數爲2,未超過默認的5;vue 拆分後,並行加載的入口文件並沒有增長,未超過默認的3。vue 也符合 splitChunks 的拆分條件,單獨打了一個包(2.js)

理解 chunks

chunks 用以告訴 splitChunks 的做用對象,其可選值有 asyncinitialall。默認值是 async,也就是默認只選取異步加載的chunk進行代碼拆分。這個咱們在開頭的例子裏已經驗證。這裏咱們經過兩個例子來看一下當chunks的值爲 initialall 時,打包結果如何。 首先將chunks值改成 initial

chunks: "initial"
複製代碼

構建結果以下:

圖2

緣由分析:

chunks 值爲 initial 時,splitChunks 的做用範圍變成了非異步加載的初始 chunk,例如咱們的 index.js 就是初始化的時候就存在的chunk。而 vue 模塊是在異步加載的chunk a.js 中引入的,因此並不會被分離出來。

chunks 仍使用 initial, 咱們對 index.jsa.js 稍做修改:

// index.js
import 'vue'
import('./a')
複製代碼
// a.js
console.log('a')
複製代碼

構建結果以下:

圖3

緣由分析:

vueindex.js 直接被引入,而 index.js 是初始chunk,因此分離出來打到了 vendors~app.js 中。

能不能讓 splitChunks 既處理初始chunk也處理異步chunk呢?答案是能夠,只須要將 chunks 改成 all

chunks: "all"
複製代碼

index.jsa.js 稍做修改:

// index.js
import 'vue-router'
import('./a')
複製代碼
// a.js
import 'vue'
console.log('a')
複製代碼

構建結果以下:

圖4

緣由分析:

chunks 值爲 all 時,splitChunks 的處理範圍包括了初始chunk和異步chunk兩種場景,所以 index.js 中的 vue-router 被分拆到了 vendors~app.js 中,而異步加載的chunk a.js 中的 vue 被分拆到了 3.js 中。推薦在開發中將 chunks 設置爲 all

理解 maxInitialRequests

maxIntialRequests 表示 splitChunks 在拆分chunk後,頁面中須要請求的初始chunk數量不超過指定的值。所謂初始chunk,指的是頁面渲染時,一開始就須要下載的js,區別於在頁面加載完成後,經過異步加載的js。

splitChunks 作如下修改,其餘使用默認配置:

chunks: 'initial',
maxInitialRequests: 1
複製代碼

對 index.js 稍做修改:

// index.js
import 'vue'
複製代碼

構建結果以下:

圖5

緣由分析:

由於 maxInitialRequests 爲1,若是 vueindex.js 中拆出的話,新建立的chunk做爲初始chunk index.js 的前置依賴,是須要在頁面初始化的時候就先請求的。那麼初始化時的請求數變成了2,所以不知足拆分條件,因此 splitChunks 沒有對 index.js 進行拆分。

理解 maxAsyncRequests

maxInitialRequests 相對,maxAsyncRequests 表示 splitChunks 在拆分chunk後,並行加載的異步 chunk 數不超過指定的值。

splitChunks 作如下修改,其餘使用默認配置:

maxAsyncRequests: 1
複製代碼

index.js 稍做修改:

// index.js
import('./a')
複製代碼
// a.js
import 'vue'
console.log('a')
複製代碼

構建結果以下:

圖6

緣由分析: 由於 maxAsyncRequests 爲1,因爲 a.js 是經過 import() 異步加載的,此時並行的異步請求數是1。若是將 vuea.js 中拆出的話,拆出的包也將成爲一個異步請求chunk。這樣的話,當異步請求 a.js 的時候,並行請求數有2個。所以,不知足拆分條件,因此 splitChunks 沒有對 a.js 進行拆分。

理解 minChunks

minChunks 表示一個模塊至少應被指定個數的 chunk 所共享才能分割。默認爲1。

splitChunks 作如下修改,其餘使用默認配置:

chunks: 'all',
minChunks: 2
複製代碼

index.js 稍做修改:

// index.js
import 'vue'
複製代碼

構建結果以下:

圖7

緣由分析:

由於 minChunks 爲 2,因此只有當 vue 至少被2個 chunk 所共享時,纔會被拆分出來。

思考題

請問以下代碼,構建結果是什麼?

chunks: 'all',
minChunks: 2
複製代碼
// index.js
import 'vue'
import './a'
複製代碼
// a.js
import 'vue'
console.log('a')
複製代碼

理解 cache groups

cacheGroups 繼承 splitChunks 裏的全部屬性的值,如 chunksminSizeminChunksmaxAsyncRequestsmaxInitialRequestsautomaticNameDelimitername ,咱們還能夠在 cacheGroups 中從新賦值,覆蓋 splitChunks 的值。另外,還有一些屬性只能在 cacheGroups 中使用:testpriorityreuseExistingChunk

經過 cacheGroups,咱們能夠定義自定義 chunk 組,經過 test 條件對模塊進行過濾,符合條件的模塊分配到相同的組。

cacheGroups 有兩個默認的組,一個是 vendors,將全部來自 node_modules 目錄的模塊;一個 default,包含了由兩個以上的 chunk 所共享的模塊。

前面的例子中,你可能注意到了怎麼有的拆分出的chunk名字這麼奇怪,例如 vendors~app(默認由 cacheGroups 中組的 key + 源chunk名組成)。咱們看一下如何自定義拆分出的chunk名。

首先找到該chunk所屬的分組,該例爲 vendors 分組,做以下修改,其餘使用默認配置:

chunks:'all',
cacheGroups: {
    vendors: {
      test: /[\\/]node_modules[\\/]/,
      name: "customName",
      priority: -10
    }
}
複製代碼

對 index.js 稍做修改:

// index.js
import 'vue'
複製代碼

構建結果以下:

圖8

緣由分析:

vue 來自 node_modules 目錄,被分配到了默認的 vendors 組中,若是不指定 name 的話,會使用默認的chunk名,這裏咱們指定了 name,所以最終的chunk名爲customName

模塊還能夠分配到多個不一樣的組,但最終會根據 priority 優先級決定打包到哪一個 chunk。

新增一個分組:

chunks:'all',
cacheGroups: {
    vendors: {
      test: /[\\/]node_modules[\\/]/,
      name: "customName",
      priority: -10
    },
    customGroup: {
      test: /[\\/]node_modules[\\/]/,
      name: "customName1",
      priority: 0
    }
}
複製代碼

構建結果:

圖9

緣由分析:

雖然 vendorscustomGroup 這個兩個組的條件都符合,但因爲後者的優先級更高,因此最終將 vue 打包到了 customName1.js 中。

總結

講解到這裏,想必你對 webpack 如何進行代碼分割有了深入地理解了。對於文章開頭的問題,能夠給出你的答案了吧?

關注咱們

公衆號@前端論道
相關文章
相關標籤/搜索