webpack SplitChunksPlugin實用指南

wuzhengyan2015發表於2018-09-16

提到前端打包工具,毫無疑問想先到的是webpack。但是前端發展地很快,時不時會有新東西出現,打包工具這邊之前也出現parcel和rollup。各種工具的碰撞,相互汲取優點,促進技術的發展。

webpack4中支援了零配置的特性,同時對塊打包也做了優化,CommonsChunkPlugin已經被移除了,現在是使用optimization.splitChunks代替。

下面就開始介紹splitChunks的內容。

預設情況

首先webpack會根據下述條件自動進行程式碼塊分割:

  • 新程式碼塊可以被共享引用,或者這些模組都是來自node_modules資料夾裡面
  • 新程式碼塊大於30kb(min+gziped之前的體積)
  • 按需載入的程式碼塊,並行請求最大數量應該小於或者等於5
  • 初始載入的程式碼塊,並行請求最大數量應該小於或等於3

塊打包預設情況下只會影響按需載入模組,因為對初始塊也進行優化打包會影響HTML中的script標籤數,增加請求數。

接下來看些例子來理解預設情況的打包。

模組全部是同步引入

// indexA.js
import React from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'

console.log(_.join(['a', 'b'], '~'))

ReactDOM.render(
  <div>SplitChunk</div>,
  document.getElementById('root')
)
複製程式碼

webpack SplitChunksPlugin實用指南

預設情況只會影響按需載入模組,所以所有內容全部被打包到一起了。

有模組動態匯入

這裡首先使用符合 ECMAScript 提案 的 import() 語法

// indexA.js
import React from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'
import(/* webpackChunkName: "async-jquery" */ 'jquery').then(component => {
  console.log(component)
})

console.log(_.join(['a', 'b'], '~'))

ReactDOM.render(
  <div>SplitChunk</div>,
  document.getElementById('root')
)
複製程式碼

webpack SplitChunksPlugin實用指南

這裡jquery使用動態匯入,打包結果中可以看到jquery被單獨打包了

react按需載入

同樣的我們試要react按需載入,使用react-router提供的按需載入方案

AsyncModule模組按上面方案非同步載入Dashboard

import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter, Route} from 'react-router-dom'
import AsyncModule from './AsyncModule.jsx'
import _ from 'lodash'
import $ from 'jquery'

console.log(_.join(['a', 'b'], '~'))

ReactDOM.render(
  <div>
    <BrowserRouter>
      <Route path='/' component={AsyncModule} />
    </BrowserRouter>
  </div>,
  document.getElementById('root')
)
複製程式碼

webpack SplitChunksPlugin實用指南

從打包結果可以看到按需載入的模組被打包到0.js去了。

講完了webpack預設情況下對打包塊的優化,接下來看splitChunks配置項。

配置項

相關配置項:

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', 
      minSize: 30000,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~', 
      name: true,
      cacheGroups: {}
    }
  }
}
複製程式碼
  • chunks: 表示哪些程式碼需要優化,有三個可選值:initial(初始塊)、async(按需載入塊)、all(全部塊),預設為async
  • minSize: 表示在壓縮前的最小模組大小,預設為30000
  • minChunks: 表示被引用次數,預設為1
  • maxAsyncRequests: 按需載入時候最大的並行請求數,預設為5
  • maxInitialRequests: 一個入口最大的並行請求數,預設為3
  • automaticNameDelimiter: 命名連線符
  • name: 拆分出來塊的名字,預設由塊名和hash值自動生成
  • cacheGroups: 快取組。快取組的屬性除上面所有屬性外,還有test, priority, reuseExistingChunk
    • test: 用於控制哪些模組被這個快取組匹配到
    • priority: 快取組打包的先後優先順序
    • reuseExistingChunk: 如果當前程式碼塊包含的模組已經有了,就不在產生一個新的程式碼塊

配置項基本就上面這些,我們重點來看下chunks和cacheGroups。

chunks

chunks的取值是有initial, async, all。預設情況下是async,在本文第一部分已經介紹了它的表現,所以現在來看下其它兩個的表現。

initial, all模式會將所有來自node_modules的模組分配到一個叫vendors的快取組;所有重複引用至少兩次的程式碼,會被分配到default的快取組。

// indexA.js
import './Dashboard.jsx'

// indexB.js
import './Dashboard.jsx'

// Dashboard.jsx
import React from 'react'
複製程式碼
// webpack.config.js
splitChunks: {
  chunks: 'initial'
}
複製程式碼

webpack SplitChunksPlugin實用指南

打包表現正如上面所述,產生了兩個程式碼塊vendors, default。

可以通過配置optimization.splitChunks.cacheGroups.default: false禁用default快取組。

// webpack.config.js
splitChunks: {
  chunks: 'initial',
  cacheGroups: {
    default: false
  }
}
複製程式碼

webpack SplitChunksPlugin實用指南

至於all和initial的差別,可以看下這篇文章Webpack 4 — Mysterious SplitChunks Plugin(要科學上網)

裡面有提到initial模式下會分開優化打包非同步和非非同步模組。而all會把非同步和非非同步同時進行優化打包。也就是說moduleA在indexA中非同步引入,indexB中同步引入,initial下moduleA會出現在兩個打包塊中,而all只會出現一個。

cacheGroups

使用cacheGroups可以自定義配置打包塊。

// indexA.js
import React from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'
import $ from 'jquery'

// indexB.js
import React from 'react'
import ReactDOM from 'react-dom'
import('lodash')
import $ from 'jquery'

// webpack.config.js
optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          name: 'commons',
          chunks: 'initial',
          minChunks: 2
        }
      }
    }
  }
複製程式碼

webpack SplitChunksPlugin實用指南

根據開頭介紹webapck分割條件,一些公共模組被打包進了commons,自定義打包塊的優先順序是0,所以現在公共模組會被打包進commons,而不是上述提到的預設打包塊vendors(優先順序為負)。

但是這邊為什麼lodash為什麼沒打包在一起呢?可以回顧下initial和all的區別。接下來實驗下all的效果。

// indexA, indexB同上
// webpack.config.js
optimization: {
    splitChunks: {
        cacheGroups: {
            commons: {
                name: 'commons',
                chunks: 'all',
                minChunks: 2
            }
        }
    }
}
複製程式碼

webpack SplitChunksPlugin實用指南

結果在預期中,lodash被打包在一起了。

提取第三方庫

最後看下之前CommonsChunkPlugin常用的分離部分第三方庫功能。這邊你可以想一下怎麼操作。

上面已經提到了設定chunks: initial || all都可以提取出第三方庫。但是它是把所有第三庫提取出來,所以我們在只提取react和react-dom的情況下,需要自定義一個cacheGroup。

// indexA.js
import React from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'
import $ from 'jquery'

// webpack.config.js
entry: {
    indexA: path.join(__dirname, 'src/indexA.js')
},
optimization: {
    splitChunks: {
        chunks: 'all',
        cacheGroups: {
            vendors: {
                test: /react/,
                name: 'vendors'
            }
        }
    }
}
複製程式碼

webpack SplitChunksPlugin實用指南

我們去重寫了vendors打包塊,只打包匹配react的模組,所以達到了之前CommonsChunkPlugin的功能。

或者

// index.jsx
import React from 'react'
import ReactDOM from 'react-dom'

// webpack.config.js
entry: {
    indexA: path.join(__dirname, 'src/indexA.js'),
    vendor: ["react", "react-dom"]
},
optimization: {
    splitChunks: {
        cacheGroups: {
            vendor: {
                name: "vendor",
                chunks: "initial"
            }
        }
    }
}
複製程式碼

webpack SplitChunksPlugin實用指南

webpack 打包入口增加一個vendor入口,裡面包括所有需要另外打包出來的庫,然後在cacheGroups設定這個打包塊chunks: initial || all,也能把indexA和vendor中重複的庫提取到vendor打包塊中。

optimization.runtimeChunk

最後提一下runtimeChunk,通過optimization.runtimeChunk: true選項,webpack會新增一個只包含執行時(runtime)額外程式碼塊到每一個入口。(譯註:這個需要看場景使用,會導致每個入口都載入多一份執行時程式碼)

總結

webpack4的splitChunks功能是比較強大的,不過推薦還是使用預設模式,或者提取一下第三方庫。

參考材料

相關文章