「Webpack」從0到1學會 code splitting

掘金引流終版.gif

構建專欄系列目錄入口javascript

焦傳鍇,微醫前端技術部平臺支撐組。不是吧阿 sir,又要兼容 IE?前端

1、前言

在默認的配置狀況下,咱們知道,webpack 會把全部代碼打包到一個 chunk 中,舉個例子當你的一個單頁面應用很大的時候,你可能就須要將每一個路由拆分到一個 chunk 中,這樣才方便咱們實現按需加載。java

代碼分離是 webpack 中最引人注目的特性之一。此特性可以把代碼分離到不一樣的 bundle 中,而後能夠按需加載或並行加載這些文件。代碼分離能夠用於獲取更小的 bundle,以及控制資源加載優先級,若是使用合理,會極大影響加載時間。node

2、關於代碼分割

接下來咱們會分別分析不一樣的代碼分隔方式帶來的打包差別,首先咱們的項目假設有這兩個簡單的文件👇react

index.jsjquery

import { mul } from './test'
import $ from 'jquery'

console.log($)
console.log(mul(2, 3))

複製代碼

test.jswebpack

import $ from 'jquery'

console.log($)

function mul(a, b) {
    return a * b
}

export { mul }

複製代碼

能夠看到如今他們兩者都依賴於 jquery 這個庫,而且相互之間也會有依賴。 image.png 當咱們在默認配置的狀況下進行打包,結果是這樣的👇,會把全部內容打包進一個 main bundle 內(324kbimage.png image.png 那麼咱們如何用最直接的方式從這個 bundle 中分離出其餘模塊呢?git

1. 多入口

webpack 配置中的 entry ,能夠設置爲多個,也就是說咱們能夠分別將 index 和 test 文件分別做爲入口:github

// entry: './src/index.js', 原來的單入口
/** 如今分別將它們做爲入口 */
entry:{
  index:'./src/index.js',
  test:'./src/test.js'
},
output: {
  filename: '[name].[hash:8].js',
  path: path.resolve(__dirname, './dist'),
},
複製代碼

這樣讓咱們看一下這樣打包後的結果: image.png 確實打包出了兩個文件!可是爲何兩個文件都有 320+kb 呢?不是說好拆分獲取更小的 bundle ?這是由於因爲兩者都引入了 jquery 而 webpack 從兩次入口進行打包分析的時候會每次都將依賴的模塊分別打包進去👇 image.pngweb

沒錯,這種配置的方式確實會帶來一些隱患以及不便:

  • 若是入口 chunk 之間包含一些重複的模塊,那些重複模塊都會被引入到各個 bundle 中。
  • 這種方法不夠靈活,而且不能動態地將核心應用程序邏輯中的代碼拆分出來。

那麼有沒有方式能夠既能夠將共同依賴的模塊進行打包分離,又不用進行繁瑣的手動配置入口的方式呢?那必然是有的。

2. SplitChunksPlugin

SplitChunks 是 webpack4 開始自帶的開箱即用的一個插件,他能夠將知足規則的 chunk 進行分離,也能夠自定義配置。在 webpack4 中用它取代了以前用來解決重複依賴的 CommonsChunkPlugin

讓咱們在咱們的 webpack 配置中加上一些配置:

entry: './src/index.js', // 這裏咱們改回單入口
/** 加上以下設置 */
optimization: {
  splitChunks: {
    chunks: 'all',
  },
},
複製代碼

打包後的結果如圖: image.png 能夠看到很明顯除了根據入口打包出的 main bundle 以外,還多出了一個名爲 vendors-node_modules_jquery_dist_jquery_js.xxxxx.js ,顯然這樣咱們將公用的 jquery 模塊就提取出來了。

接下來咱們來探究一下 SplitChunksPlugin 。 首先看下配置的默認值:

splitChunks: {
    // 表示選擇哪些 chunks 進行分割,可選值有:async,initial 和 all
    chunks: "async",
    // 表示新分離出的 chunk 必須大於等於 minSize,20000,約 20kb。
    minSize: 20000,
    // 經過確保拆分後剩餘的最小 chunk 體積超過限制來避免大小爲零的模塊,僅在剩餘單個 chunk 時生效
    minRemainingSize: 0,
    // 表示一個模塊至少應被 minChunks 個 chunk 所包含才能分割。默認爲 1。
    minChunks: 1,
    // 表示按需加載文件時,並行請求的最大數目。
    maxAsyncRequests: 30,
    // 表示加載入口文件時,並行請求的最大數目。
    maxInitialRequests: 30,
    // 強制執行拆分的體積閾值和其餘限制(minRemainingSize,maxAsyncRequests,maxInitialRequests)將被忽略
    enforceSizeThreshold: 50000,
    // cacheGroups 下能夠能夠配置多個組,每一個組根據 test 設置條件,符合 test 條件的模塊,就分配到該組。模塊能夠被多個組引用,但最終會根據 priority 來決定打包到哪一個組中。默認將全部來自 node_modules 目錄的模塊打包至 vendors 組,將兩個以上的 chunk 所共享的模塊打包至 default 組。
    cacheGroups: {
        defaultVendors: {
            test: /[\\/]node_modules[\\/]/,
            // 一個模塊能夠屬於多個緩存組。優化將優先考慮具備更高 priority(優先級)的緩存組。
            priority: -10,
            // 若是當前 chunk 包含已從主 bundle 中拆分出的模塊,則它將被重用
            reuseExistingChunk: true,
        },
   		  default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}
複製代碼

默認狀況下,SplitChunks 只會對異步調用的模塊進行分割(chunks: "async"),而且默認狀況下處理的 chunk 至少要有 20kb ,太小的模塊不會被包含進去。

補充一下,默認值會根據 mode 的配置不一樣有所變化,具體參見源碼👇:

const { splitChunks } = optimization;
if (splitChunks) {
  A(splitChunks, "defaultSizeTypes", () => ["javascript", "unknown"]);
  D(splitChunks, "hidePathInfo", production);
  D(splitChunks, "chunks", "async");
  D(splitChunks, "usedExports", optimization.usedExports === true);
  D(splitChunks, "minChunks", 1);
  F(splitChunks, "minSize", () => (production ? 20000 : 10000));
  F(splitChunks, "minRemainingSize", () => (development ? 0 : undefined));
  F(splitChunks, "enforceSizeThreshold", () => (production ? 50000 : 30000));
  F(splitChunks, "maxAsyncRequests", () => (production ? 30 : Infinity));
  F(splitChunks, "maxInitialRequests", () => (production ? 30 : Infinity));
  D(splitChunks, "automaticNameDelimiter", "-");
  const { cacheGroups } = splitChunks;
  F(cacheGroups, "default", () => ({
    idHint: "",
    reuseExistingChunk: true,
    minChunks: 2,
    priority: -20
  }));
  F(cacheGroups, "defaultVendors", () => ({
    idHint: "vendors",
    reuseExistingChunk: true,
    test: NODE_MODULES_REGEXP,
    priority: -10
  }));
}
複製代碼

cacheGroups 緩存組是施行分割的重中之重,他可使用來自 splitChunks.*任何選項,可是 test、priority 和 reuseExistingChunk 只能在緩存組級別上進行配置。默認配置中已經給咱們提供了 Vendors 組和一個 defalut 組,**Vendors **組中使用 test: /[\\/]node_modules[\\/]/ 匹配了 node_modules 中的全部符合規則的模塊。

Tip:當 webpack 處理文件路徑時,它們始終包含 Unix 系統中的 / 和 Windows 系統中的 \。這就是爲何在 {cacheGroup}.test 字段中使用 [\/] 來表示路徑分隔符的緣由。{cacheGroup}.test 中的 / 或 \ 會在跨平臺使用時產生問題。

綜上的配置,咱們即可以理解爲何咱們在打包中會產生出名爲 vendors-node_modules_jquery_dist_jquery_js.db47cc72.js 的文件了。若是你想要對名稱進行自定義的話,也可使用 splitChunks.name 屬性(每一個 cacheGroup 中均可以使用),這個屬性支持使用三種形式:

  1. boolean = false 設爲 false 將保持 chunk 的相同名稱,所以不會沒必要要地更更名稱。這是生產環境下構建的建議值。
  2. function (module, chunks, cacheGroupKey) => string 返回值要求是 string 類型,而且在 chunks 數組中每個 chunk 都有 chunk.namechunk.hash 屬性,舉個例子 👇
name(module, chunks, cacheGroupKey) {
  const moduleFileName = module
  .identifier()
  .split('/')
  .reduceRight((item) => item);
  const allChunksNames = chunks.map((item) => item.name).join('~');
  return `${cacheGroupKey}-${allChunksNames}-${moduleFileName}`;
},
複製代碼
  1. string 指定字符串或始終返回相同字符串的函數會將全部常見模塊和 vendor 合併爲一個 chunk。這可能會致使更大的初始下載量並減慢頁面加載速度

另外注意一下 splitChunks.maxAsyncRequestssplitChunks.maxInitialRequests 分別指的是按需加載時最大的並行請求數頁面初始渲染時候須要的最大並行請求數

在咱們的項目較大時,若是須要對某個依賴單獨拆包的話,能夠進行這樣的配置:

cacheGroups: {
  react: {
    name: 'react',
      test: /[\\/]node_modules[\\/](react)/,
      chunks: 'all',
      priority: -5,
  },
 },
複製代碼

這樣打包後就能夠拆分指定的包: image.png

更多配置詳見官網配置文檔

3. 動態 import

使用 import()語法 來實現動態導入也是咱們很是推薦的一種代碼分割的方式,咱們先來簡單修改一下咱們的 index.js ,再來看一下使用後打包的效果:

// import { mul } from './test'
import $ from 'jquery'

import('./test').then(({ mul }) => {
    console.log(mul(2,3))
})

console.log($)
// console.log(mul(2, 3))
複製代碼

能夠看到,經過 import() 語法導入的模塊在打包時會自動單獨進行打包 image.png

值得注意的是,這種語法還有一種很方便的「動態引用」的方式,他能夠加入一些適當的表達式,舉個例子,假設咱們須要加載適當的主題:

const themeType = getUserTheme();
import(`./themes/${themeType}`).then((module) => {
  // do sth aboout theme
});
複製代碼

這樣咱們就能夠「動態」加載咱們須要的異步模塊,實現的原理主要在於兩點:

  1. 至少須要包含模塊相關的路徑信息,打包能夠限定於一個特定的目錄或文件集。
  2. 根據路徑信息 webpack 在打包時會把 ./themes  中的全部文件打包進新的 chunk 中,以便須要時使用到。

4. 魔術註釋

在上述的 import() 語法中,咱們會發現打包自動生成的文件名並非咱們想要的,咱們如何才能本身控制打包的名稱呢?這裏就要引入咱們的魔術註釋(Magic Comments):

import(/* webpackChunkName: "my-chunk-name" */'./test')
複製代碼

經過這樣打包出來的文件: image.png

魔術註釋不只僅能夠幫咱們修改 chunk 名這麼簡單,他還能夠實現譬如預加載等功能,這裏舉個例子:

咱們經過但願在點擊按鈕時才加載咱們須要的模塊功能,代碼能夠這樣:

// index.js
document.querySelector('#btn').onclick = function () {
  import('./test').then(({ mul }) => {
    console.log(mul(2, 3));
  });
};
複製代碼
//test.js
function mul(a, b) {
  return a * b;
}
console.log('test 被加載了');
export { mul };
複製代碼

03-03.gif 能夠看到,在咱們點擊按鈕的同時確實加載了 test.js 的文件資源。可是若是這個模塊是一個很大的模塊,在點擊時進行加載可能會形成長時間 loading 等用戶體驗不是很好的效果,這個時候咱們可使用咱們的 /* webpackPrefetch: true */ 方式進行預獲取,來看下效果:

// index,js

document.querySelector('#btn').onclick = function () {
  import(/* webpackPrefetch: true */'./test').then(({ mul }) => {
    console.log(mul(2, 3));
  });
};
複製代碼

03-04.gif 能夠看到整個過程當中,在畫面初始加載的時候,test.js 的資源就已經被預先加載了,而在咱們點擊按鈕時,會從 (prefetch cache) 中讀取內容。這就是模塊預獲取的過程。另外咱們還有 /* webpackPreload: true */ 的方式進行預加載。

可是 prefetch 和 preload 聽起來感受差很少,實際上他們的加載時機等是徹底不一樣的:

  • preload chunk 會在父 chunk 加載時,以並行方式開始加載。prefetch chunk 會在父 chunk 加載結束後開始加載。
  • preload chunk 具備中等優先級,並當即下載。prefetch chunk 在瀏覽器閒置時下載。
  • preload chunk 會在父 chunk 中當即請求,用於當下時刻。prefetch chunk 會用於將來的某個時刻。

3、結尾

在最初有工程化打包思想時,咱們會考慮將多文件打包到一個文件內減小屢次的資源請求,隨着項目的愈來愈複雜,作項目優化時,咱們發現項目加載越久用戶體驗就越很差,因而又能夠經過代碼分割的方式去減小頁面初加載時的請求過大的資源體積。

本文中僅簡單介紹了經常使用的 webpack 代碼分割方式,可是在實際的項目中進行性能優化時,每每會有更加嚴苛的要求,但願能夠經過本文的介紹讓你們快速瞭解上手代碼分割的技巧與優點。

參考

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

Code Splitting - Webpack

相關文章
相關標籤/搜索