天天看點

Webpack 5 超詳細解讀(五)

41.代碼拆分方式

通過Webpack實作前端項目整體子產品化的優勢很明顯,但是它同樣存在一些弊端,那就是項目當中所有的代碼最終都會被打包到一起,試想一下,如果說應用非常複雜,子產品非常多的話,那打包結果就會特别的大,很多時候超過兩三兆也是非常常見的事情。而事實情況是,大多數時候在應用開始工作時,并不是所有的子產品都是必須要加載進來的,但是,這些子產品又被全部打包到一起,需要任何一個子產品,都必須得把整體加載下來過後才能使用。而應用一般又是運作在浏覽器端,這就意味着會浪費掉很多的流量和帶寬。

更為合理的方案就是把的打包結果按照一定的規則去分離到多個

bundle.js

當中,然後根據應用的運作需要,按需加載這些子產品,這樣的話就可以大大提高應用的響應速度以及它的運作效率。可能有人會想起來在一開始的時候說過Webpack就是把項目中散落的那些子產品合并到一起,進而去提高運作效率,那這裡又在說它應該把它分離開,這兩個說法是不是自相沖突?其實這并不是沖突,隻是物極必反而已,資源太大了也不行,太碎了更不行,項目中劃分的這種子產品的顆粒度一般都會非常的細,很多時候一個子產品隻是提供了一個小小的工具函數,它并不能形成一個完整的功能單元,如果不把這些散落的子產品合并到一起,就有可能再去運作一個小小的功能時,會加載非常多的子產品。而目前所主流的這種HTTP1.1協定,它本身就有很多缺陷,例如并不能同時對同一個域名下發起很多次的并行請求,而且每一次請求都會有一定的延遲,另外每次請求除了傳輸具體的内容以外,還會有額外的header請求頭和響應頭,當大量的這種請求的情況下,這些請求頭加在一起,也是很大的浪費。

綜上所述,子產品打包肯定是有必要的,不過像應用越來越大過後,要開始慢慢的學會變通。為了解決這樣的問題,Webpack支援一種分包的功能,也可以把這種功能稱之為代碼分割,它通過把子產品,按照所設計的一個規則打包到不同的bundle.js當中,進而去提高應用的響應速度,目前的Webpack去實作分包的方式主要有兩種:

  • 第一種就是根據業務去配置不同的打包入口,也就是會有同時多個打包入口同時打包,這時候就會輸出多個打包結果;
  • 第二種是多入口檔案,單獨打包依賴包的形式;
  • 第三種就是采用ES Module的動态導入的功能,去實作子產品的按需加載,這個時候Webpack會自動的把動态導入的這個子產品單獨輸出的一個bundle.js當中。

41.1 多入口檔案打包

多入口打包一般适用于傳統的“多頁”應用程式。最常見的劃分規則是一個頁面對應一個打包入口,對于不同頁面之間公共的部分再去提取到公共的結果中。

目錄結構

Webpack 5 超詳細解讀(五)

一般Webpack.config.js配置檔案中的entry屬性隻會一個檔案路徑(打包入口),如果需要配置多個打包入口,則需要将entry屬性定義成為一個對象(注意不是數組,如果是數組的話,那就是将多個檔案打包到一起,對于整個應用來講依然是一個入口)。一旦配置為多入口,輸出的檔案名也需要修改**“[name].bundle.js**”,[name]最終會被替換成入口的名稱,也就是index和album。

const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
const HtmlWebpackPlugin = require('html-Webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {
    index: './src/index.js',  // 多入口
    album: './src/album.js'
  },
  output: {
    filename: '[name].bundle.js'  // [name]占位符,最終被替換為入口名稱index和album
  },
  optimization: {
    splitChunks: {
      // 自動提取所有公共子產品到單獨 bundle
      chunks: 'all'
    }
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
    })
  ]
}
           

指令行運作yarn Webpack指令,打開dist目錄發現已經有兩個js檔案。

41.2 多入口依賴包單獨打包

多入口打包本身非常容易了解,也非常容易使用,但是它也存在一個小小的問題,就是在不同的打包入口當中,它一定會有那麼一些公共的部分,按照目前這種多入口的打包方式,不同的打包結果當中就會出現相同的子產品,例如在我們這裡index入口和album入口當中就共同使用了global.css和fetch.js這兩個公共的子產品,因為執行個體比較簡單,是以說重複的影響不會有那麼大,但是如果共同使用的是jQuery或者Vue這種體積比較大的子產品,那影響的話就會特别的大,是以說需要把這些公共的子產品去。提取到一個單獨的bundle.js當中,Webpack中實作公共子產品提取的方式也非常簡單,隻需要在優化配置當中去開啟一個叫splitChunks的一個功能就可以了,回到配置檔案當中,配置如下:

const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
const HtmlWebpackPlugin = require('html-Webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {
    index: './src/index.js',
    album: './src/album.js'
    // 或者使用下面的寫法
    // index: { import : './src/index.js', dependOn: 'shared' },
    // album: { import : './src/album.js', dependOn: 'shared' },
    // shared: ['jquery', 'lodash']
  },
  output: {
    filename: '[name].bundle.js'
  },
  optimization: {
    splitChunks: {
      // 自動提取所有公共子產品到單獨 bundle
      chunks: 'all'  // 表示會把所有的公共子產品都提取到單獨的bundle.js當中
    }
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']
    })
  ]
}
           

打開指令行運作yarn Webpack後發現,公共子產品的部分被打包進album~index.bundle.js中去了。

41.3 動态導入的形式打包

按需加載是開發浏覽器應用當中一個非常常見的需求,一般常說的按需加載指的是加載資料,這裡所說的按需加載指的是在應用運作過程中需要某個子產品時才去加載這個子產品,這種方式可以極大的節省帶寬和流量。Webpack支援使用動态導入的這種方式來去實作子產品的按需加載,而且所有動态導入的子產品都會被自動提取到單獨的bundle.js當中,進而實作分包,相比于多入口的方式,動态導入更為靈活,因為通過代碼的邏輯去控制,需不需要加載某個子產品,或者是時候加的某個子產品。而分包的目的中就有很重要的一點就是:讓子產品實作按需加載,進而去提高應用的響應速度。

具體來看如何使用,這裡已經提前設計好了一個可以發揮按需加載作用的場景,在這個頁面的主體區域,如果通路的是文章頁的話,得到的就是一個文章清單,如果通路的是相冊頁,顯示的就是相冊清單。

項目目錄:

Webpack 5 超詳細解讀(五)

動态導入使用的就是ESM标準當中的動态導入,在需要動态導入元件的地方,通過這個函數導入指定的路徑,這個方法傳回的就是一個promise,promise的方法當中就可以拿到子產品對象,由于網站是使用的預設導出,是以說這裡需要去解構子產品對象當中的default,然後把它放到post的這個變量當中,拿到這個成員過後,使用mainElement.appendChild(posts())建立頁面元素,album元件也是如此。完成以後再次回到浏覽器,此時頁面仍然可以正常工作的。

// import posts from './posts/posts'
// import album from './album/album'

const render = () => {
  const hash = window.location.hash || '#posts'
  console.log(hash)
  const mainElement = document.querySelector('.main')

  mainElement.innerHTML = ''

  if (hash === '#posts') {
    // mainElement.appendChild(posts())
    // 這個方法傳回的就是一個promise,promise的方法當中就可以拿到子產品對象,由于網站是使用的預設導出,是以說這裡需要去解構子產品對象當中的default,然後把它放到post的這個變量當中
    import('./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {
    // mainElement.appendChild(album())
    import('./album/album').then(({ default: album }) => {
      mainElement.appendChild(album())
    })
  }
}

render()

window.addEventListener('hashchange', render)
           

這時再回到開發工具當中,然後重新去運作打包,然後去看看此時打包的結果是什麼樣子的,打包結束,打開dist目錄,此時dist目錄下就會多出3個js檔案,那這三個js檔案,實際上就是由動态導入自動分包所産生的。這3個檔案的分别是剛剛導入的兩個子產品index.js/album.js,以及這兩個子產品當中公共子產品fetch.js。

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-1IMyFHpN-1669024724129)(http://5coder.cn/img/1668693735_0abaef9539abad0d157664b059ba6660.png)]

動态導入整個過程無需配置任何一個地方,隻需要按照ESM動态導入成員的方式去導入子產品就可以,内部會自動處理分包和按需加載,如果說你使用的是單頁應用開發架構,比如react或者Vue的話,在你項目當中的路由映射元件,就可以通過這種動态導入的方式實作按需加載。

42.splitchunks 配置

最初,chunks(以及内部導入的子產品)是通過内部 Webpack 圖譜中的父子關系關聯的。

CommonsChunkPlugin

曾被用來避免他們之間的重複依賴,但是不可能再做進一步的優化。

從 Webpack v4 開始,移除了

CommonsChunkPlugin

,取而代之的是

optimization.splitChunks

預設值

開箱即用的

SplitChunksPlugin

對于大部分使用者來說非常友好。

預設情況下,它隻會影響到按需加載的 chunks,因為修改 initial chunks 會影響到項目的 HTML 檔案中的腳本标簽。

Webpack 将根據以下條件自動拆分 chunks:

  • 新的 chunk 可以被共享,或者子產品來自于

    node_modules

    檔案夾
  • 新的 chunk 體積大于 20kb(在進行 min+gz 之前的體積)
  • 當按需加載 chunks 時,并行請求的最大數量小于或等于 30
  • 當加載初始化頁面時,并發請求的最大數量小于或等于 30

當嘗試滿足最後兩個條件時,最好使用較大的 chunks。

配置

Webpack 為希望對該功能進行更多控制的開發者提供了一組選項。

選擇了預設配置為了符合 Web 性能最佳實踐,但是項目的最佳政策可能有所不同。如果要更改配置,則應評估所做更改的影響,以確定有真正的收益。

optimization.splitChunks

下面這個配置對象代表

SplitChunksPlugin

的預設行為。

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};
           
當 Webpack 處理檔案路徑時,它們始終包含 Unix 系統中的

/

和 Windows 系統中的

\

。這就是為什麼在

{cacheGroup}.test

字段中使用

[\\/]

來表示路徑分隔符的原因。

{cacheGroup}.test

中的

/

\

會在跨平台使用時産生問題。
從 Webpack 5 開始,不再允許将 entry 名稱傳遞給

{cacheGroup}.test

或者為

{cacheGroup}.name

使用現有的 chunk 的名稱。

splitChunks.automaticNameDelimiter

string = '~'
           

預設情況下,Webpack 将使用 chunk 的來源和名稱生成名稱(例如

vendors~main.js

)。此選項使你可以指定用于生成名稱的分隔符。

splitChunks.chunks

string = 'async'` `function (chunk)
           

這表明将選擇哪些 chunk 進行優化。當提供一個字元串,有效值為

all

async

initial

。設定為

all

可能特别強大,因為這意味着 chunk 可以在異步和非異步 chunk 之間共享。

Note that it is applied to the fallback cache group as well (

splitChunks.fallbackCacheGroup.chunks

).

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      // include all types of chunks
      chunks: 'all',
    },
  },
};
           

或者,你也可以提供一個函數去做更多的控制。這個函數的傳回值将決定是否包含每一個 chunk。

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks(chunk) {
        // exclude `my-excluded-chunk`
        return chunk.name !== 'my-excluded-chunk';
      },
    },
  },
};
           

你可以将此配置與 HtmlWebpackPlugin 結合使用。它将為你注入所有生成的 vendor chunks。

splitChunks.maxAsyncRequests

number = 30
           

按需加載時的最大并行請求數。

splitChunks.maxInitialRequests

number = 30
           

入口點的最大并行請求數。

splitChunks.defaultSizeTypes

[string] = ['javascript', 'unknown']
           

Sets the size types which are used when a number is used for sizes.

splitChunks.minChunks

number = 1
           

拆分前必須共享子產品的最小 chunks 數。

splitChunks.hidePathInfo

boolean
           

為由 maxSize 分割的部分建立名稱時,阻止公開路徑資訊。

splitChunks.minSize

number = 20000` `{ [index: string]: number }
           

生成 chunk 的最小體積(以 bytes 為機關)。

splitChunks.minSizeReduction

number` `{ [index: string]: number }
           

生成 chunk 所需的主 chunk(bundle)的最小體積(以位元組為機關)縮減。這意味着如果分割成一個 chunk 并沒有減少主 chunk(bundle)的給定位元組數,它将不會被分割,即使它滿足

splitChunks.minSize

為了生成 chunk,

splitChunks.minSizeReduction

splitChunks.minSize

都需要被滿足。

splitChunks.enforceSizeThreshold

splitChunks.cacheGroups.{cacheGroup}.enforceSizeThreshold

number = 50000
           

強制執行拆分的體積門檻值和其他限制(minRemainingSize,maxAsyncRequests,maxInitialRequests)将被忽略。

splitChunks.minRemainingSize

splitChunks.cacheGroups.{cacheGroup}.minRemainingSize

number = 0
           

在 Webpack 5 中引入了

splitChunks.minRemainingSize

選項,通過確定拆分後剩餘的最小 chunk 體積超過限制來避免大小為零的子產品。 ‘development’ 模式 中預設為

。對于其他情況,

splitChunks.minRemainingSize

預設為

splitChunks.minSize

的值,是以除需要深度控制的極少數情況外,不需要手動指定它。

splitChunks.minRemainingSize

僅在剩餘單個 chunk 時生效。

splitChunks.layer

splitChunks.cacheGroups.{cacheGroup}.layer

RegExp` `string` `function
           

按子產品層将子產品配置設定給緩存組。

splitChunks.maxSize

number = 0
           

使用

maxSize

(每個緩存組

optimization.splitChunks.cacheGroups[x].maxSize

全局使用

optimization.splitChunks.maxSize

或對後備緩存組

optimization.splitChunks.fallbackCacheGroup.maxSize

使用)告訴 Webpack 嘗試将大于

maxSize

個位元組的 chunk 分割成較小的部分。 這些較小的部分在體積上至少為

minSize

(僅次于

maxSize

)。 該算法是确定性的,對子產品的更改隻會産生局部影響。這樣,在使用長期緩存時就可以使用它并且不需要記錄。

maxSize

隻是一個提示,當子產品大于

maxSize

或者拆分不符合

minSize

時可能會被違反。

當 chunk 已經有一個名稱時,每個部分将獲得一個從該名稱派生的新名稱。 根據

optimization.splitChunks.hidePathInfo

的值,它将添加一個從第一個子產品名稱或其哈希值派生的密鑰。

maxSize

選項旨在與 HTTP/2 和長期緩存一起使用。它增加了請求數量以實作更好的緩存。它還可以用于減小檔案大小,以加快二次建構速度。

maxSize

maxInitialRequest/maxAsyncRequests

具有更高的優先級。實際優先級是

maxInitialRequest/maxAsyncRequests < maxSize < minSize

設定

maxSize

的值會同時設定

maxAsyncSize

maxInitialSize

的值。

splitChunks.maxAsyncSize

number
           

maxSize

一樣,

maxAsyncSize

可以為 cacheGroups(

splitChunks.cacheGroups.{cacheGroup}.maxAsyncSize

)或 fallback 緩存組(

splitChunks.fallbackCacheGroup.maxAsyncSize

)全局應用(

splitChunks.maxAsyncSize

maxAsyncSize

maxSize

的差別在于

maxAsyncSize

僅會影響按需加載 chunk。

splitChunks.maxInitialSize

number
           

maxSize

一樣,

maxInitialSize

可以對 cacheGroups(

splitChunks.cacheGroups.{cacheGroup}.maxInitialSize

)或 fallback 緩存組(

splitChunks.fallbackCacheGroup.maxInitialSize

)全局應用(splitChunks.maxInitialSize)。

maxInitialSize

maxSize

的差別在于

maxInitialSize

僅會影響初始加載 chunks。

splitChunks.name

boolean = false` `function (module, chunks, cacheGroupKey) => string` `string
           

每個 cacheGroup 也可以使用:

splitChunks.cacheGroups.{cacheGroup}.name

拆分 chunk 的名稱。設為

false

将保持 chunk 的相同名稱,是以不會不必要地更改名稱。這是生産環境下建構的建議值。

提供字元串或函數使你可以使用自定義名稱。指定字元串或始終傳回相同字元串的函數會将所有常見子產品和 vendor 合并為一個 chunk。這可能會導緻更大的初始下載下傳量并減慢頁面加載速度。

如果你選擇指定一個函數,則可能會發現

chunk.name

chunk.hash

屬性(其中

chunk

chunks

數組的一個元素)在選擇 chunk 名時特别有用。

如果

splitChunks.name

與 entry point 名稱比對,entry point 将被删除。

splitChunks.cacheGroups.{cacheGroup}.name

can be used to move modules into a chunk that is a parent of the source chunk. For example, use

name: "entry-name"

to move modules into the

entry-name

chunk. You can also use on demand named chunks, but you must be careful that the selected modules are only used under this chunk.

main.js

import _ from 'lodash';

console.log(_.join(['Hello', 'Webpack'], ' '));
           

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          // cacheGroupKey here is `commons` as the key of the cacheGroup
          name(module, chunks, cacheGroupKey) {
            const moduleFileName = module
              .identifier()
              .split('/')
              .reduceRight((item) => item);
            const allChunksNames = chunks.map((item) => item.name).join('~');
            return `${cacheGroupKey}-${allChunksNames}-${moduleFileName}`;
          },
          chunks: 'all',
        },
      },
    },
  },
};
           

使用以下

splitChunks

配置來運作 Webpack 也會輸出一組公用組,其下一個名稱為:

commons-main-lodash.js.e7519d2bb8777058fa27.js

(以哈希方式作為真實世界輸出示例)。

在為不同的拆分 chunk 配置設定相同的名稱時,所有 vendor 子產品都放在一個共享的 chunk 中,盡管不建議這樣做,因為這可能會導緻下載下傳更多代碼。

splitChunks.usedExports

splitChunks.cacheGroups{cacheGroup}.usedExports

boolean = true
           

弄清哪些 export 被子產品使用,以混淆 export 名稱,省略未使用的 export,并生成有效的代碼。 當它為

true

時:分析每個運作時使用的出口,當它為

"global"

時:分析所有運作時的全局 export 組合)。

splitChunks.cacheGroups

緩存組可以繼承和/或覆寫來自

splitChunks.*

的任何選項。但是

test

priority

reuseExistingChunk

隻能在緩存組級别上進行配置。将它們設定為

false

以禁用任何預設緩存組。

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        default: false,
      },
    },
  },
};
           

splitChunks.cacheGroups.{cacheGroup}.priority

number = -20
           

一個子產品可以屬于多個緩存組。優化将優先考慮具有更高

priority

(優先級)的緩存組。預設組的優先級為負,以允許自定義組獲得更高的優先級(自定義組的預設值為

)。

splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk

boolean = true
           

如果目前 chunk 包含已從主 bundle 中拆分出的子產品,則它将被重用,而不是生成新的子產品。這可能會影響 chunk 的結果檔案名。

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          reuseExistingChunk: true,
        },
      },
    },
  },
};
           

splitChunks.cacheGroups.{cacheGroup}.type

function` `RegExp` `string
           

允許按子產品類型将子產品配置設定給緩存組。

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        json: {
          type: 'json',
        },
      },
    },
  },
};
           

splitChunks.cacheGroups.test

splitChunks.cacheGroups.{cacheGroup}.test

function (module, { chunkGraph, moduleGraph }) => boolean` `RegExp` `string
           

控制此緩存組選擇的子產品。省略它會選擇所有子產品。它可以比對絕對子產品資源路徑或 chunk 名稱。比對 chunk 名稱時,将選擇 chunk 中的所有子產品。

{cacheGroup}.test

提供一個函數:

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        svgGroup: {
          test(module) {
            // `module.resource` contains the absolute path of the file on disk.
            // Note the usage of `path.sep` instead of / or \, for cross-platform compatibility.
            const path = require('path');
            return (
              module.resource &&
              module.resource.endsWith('.svg') &&
              module.resource.includes(`${path.sep}cacheable_svgs${path.sep}`)
            );
          },
        },
        byModuleTypeGroup: {
          test(module) {
            return module.type === 'javascript/auto';
          },
        },
      },
    },
  },
};
           

為了檢視

module

and

chunks

對象中可用的資訊,你可以在回調函數中放入

debugger;

語句。然後 以調試模式運作 Webpack 建構 檢查 Chromium DevTools 中的參數。

{cacheGroup}.test

提供

RegExp

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          // Note the usage of `[\\/]` as a path separator for cross-platform compatibility.
          test: /[\\/]node_modules[\\/]|vendor[\\/]analytics_provider|vendor[\\/]other_lib/,
        },
      },
    },
  },
};
           

splitChunks.cacheGroups.{cacheGroup}.filename

string` `function (pathData, assetInfo) => string
           

僅在初始 chunk 時才允許覆寫檔案名。 也可以在

output.filename

中使用所有占位符。

也可以在

splitChunks.filename

中全局設定此選項,但是不建議這樣做,如果

splitChunks.chunks

未設定為

'initial'

,則可能會導緻錯誤。避免全局設定。

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          filename: '[name].bundle.js',
        },
      },
    },
  },
};
           

若為函數,則:

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          filename: (pathData) => {
            // Use pathData object for generating filename string based on your requirements
            return `${pathData.chunk.name}-bundle.js`;
          },
        },
      },
    },
  },
};
           

通過提供以檔案名開頭的路徑

'js/vendor/bundle.js'

,可以建立檔案夾結構。

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          filename: 'js/[name]/bundle.js',
        },
      },
    },
  },
};
           

splitChunks.cacheGroups.{cacheGroup}.enforce

boolean = false
           

告訴 Webpack 忽略

splitChunks.minSize

splitChunks.minChunks

splitChunks.maxAsyncRequests

splitChunks.maxInitialRequests

選項,并始終為此緩存組建立 chunk。

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          enforce: true,
        },
      },
    },
  },
};
           

splitChunks.cacheGroups.{cacheGroup}.idHint

string
           

設定 chunk id 的提示。 它将被添加到 chunk 的檔案名中。

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          idHint: 'vendors',
        },
      },
    },
  },
};
           

Examples

Defaults: Example 1

// index.js

import('./a'); // dynamic import
// a.js
import 'react';

//...
           

結果: 将建立一個單獨的包含

react

的 chunk。在導入調用中,此 chunk 并行加載到包含

./a

的原始 chunk 中。

為什麼:

  • 條件1:chunk 包含來自

    node_modules

    的子產品
  • 條件2:

    react

    大于 30kb
  • 條件3:導入調用中的并行請求數為 2
  • 條件4:在初始頁面加載時不影響請求

這背後的原因是什麼?

react

可能不會像你的應用程式代碼那樣頻繁地更改。通過将其移動到單獨的 chunk 中,可以将該 chunk 與應用程式代碼分開進行緩存(假設你使用的是 chunkhash,records,Cache-Control 或其他長期緩存方法)。

Defaults: Example 2

// entry.js

// dynamic imports
import('./a');
import('./b');
// a.js
import './helpers'; // helpers is 40kb in size

//...
// b.js
import './helpers';
import './more-helpers'; // more-helpers is also 40kb in size

//...
           

結果: 将建立一個單獨的 chunk,其中包含

./helpers

及其所有依賴項。在導入調用時,此 chunk 與原始 chunks 并行加載。

為什麼:

  • 條件1:chunk 在兩個導入調用之間共享
  • 條件2:

    helpers

    大于 30kb
  • 條件3:導入調用中的并行請求數為 2
  • 條件4:在初始頁面加載時不影響請求

helpers

的内容放入每個 chunk 中将導緻其代碼被下載下傳兩次。通過使用單獨的塊,這隻會發生一次。我們會進行額外的請求,這可以視為一種折衷。這就是為什麼最小體積為 30kb 的原因。

Split Chunks: Example 1

建立一個

commons

chunk,其中包括入口(entry points)之間所有共享的代碼。

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          name: 'commons',
          chunks: 'initial',
          minChunks: 2,
        },
      },
    },
  },
};
           

此配置可以擴大你的初始 bundles,建議在不需要立即使用子產品時使用動态導入。

Split Chunks: Example 2

建立一個

vendors

chunk,其中包括整個應用程式中

node_modules

的所有代碼。

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  },
};
           

這可能會導緻包含所有外部程式包的較大 chunk。建議僅包括你的核心架構和實用程式,并動态加載其餘依賴項。

Split Chunks: Example 3

建立一個

custom vendor

chunk,其中包含與

RegExp

比對的某些

node_modules

包。

Webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'vendor',
          chunks: 'all',
        },
      },
    },
  },
};
           

這将導緻将

react

react-dom

分成一個單獨的 chunk。 如果你不确定 chunk 中包含哪些包,請參考 Bundle Analysis 部分以擷取詳細資訊。

43.import 動态導入配置

Webpack打包過程中利用動态導入的方式對代碼進行拆包。之前使用

import './title'

的同步的方式進行導入,可以選擇splitChunks選項進行配置。

Webpack 5 超詳細解讀(五)

此時,我們更改導入的方式為異步導入,即使用

import('./title')

的方式進行導入,并且将splitChunks配置項删除,觀察打包後的結果。

Webpack 5 超詳細解讀(五)

可以看到打包後的結果為

198.bundle.js

檔案。這是Webpack自身就會配置好的屬性,無需進行其他配置。基于這個特點,對其周邊進行補充。

44.1 chunkIds

根據官網介紹,

chunkIds

有幾個配置的值,這裡隻針對

natural

named

deterministic

進行測試。

optimization.chunkIds

boolean = false` `string: 'natural' | 'named' | 'size' | 'total-size' | 'deterministic'
           
告知 Webpack 當選擇子產品 id 時需要使用哪種算法。将

optimization.chunkIds

設定為

false

會告知 Webpack 沒有任何内置的算法會被使用,但自定義的算法會由插件提供。

optimization.chunkIds

的預設值是

false

  • 如果環境是開發環境,那麼

    optimization.chunkIds

    會被設定成

    'named'

    ,但當在生産環境中時,它會被設定成

    'deterministic'

  • 如果上述的條件都不符合,

    optimization.chunkIds

    會被預設設定為

    'natural'

下述選項字元串值均為被支援:
選項值 描述

'natural'

按使用順序的數字 id。

'named'

對調試更友好的可讀的 id。

'deterministic'

在不同的編譯中不變的短數字 id。有益于長期緩存。在生産模式中會預設開啟。

'size'

專注于讓初始下載下傳包大小更小的數字 id。

'total-size'

專注于讓總下載下傳包大小更小的數字 id。

44.1.1 natural

按使用順序的數字 id。一般不使用,在多個導入的過程中,例如同時導入了

title.js

a.js

,打包過後會生成

1.bundle.js

2.bundle.js

,但是當我們不再需要

title.js

時,再次進行打包,會生成

1.bundle.js

,這是浏覽器就會存在緩存問題。

打包結果:

Webpack 5 超詳細解讀(五)

44.1.2 named

對調試更友好的可讀的 id。

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-1WiAFHds-1669024724134)(http://5coder.cn/img/1668724505_b01c65ada36e259e5eed23f863e6d506.png)]

這裡很明确的知道src_title_js.bundle.js是title.js打包生成後的結果。對于開發階段沒有任何影響,但是對于生産階段,就會并不需要進行任何調試,就不需要更好的閱讀。

44.1.3 deterministic

在不同的編譯中不變的短數字 id。有益于長期緩存。在生産模式中會預設開啟。
Webpack 5 超詳細解讀(五)

在設定

chunkIds

deterministic

時,發現就回到了最初的狀态

198.bundle.js

,因為這是

Webpack5

中預設提供的。

44.2 chunkFilename

在動态導入中,還可以配置chunkFilename選項,對打包的結果進行重命名檔案名。

Webpack 5 超詳細解讀(五)

此時

js/chunk_[name].js

中的name與

js/chunk_[name]_[id].js

中的id指向的都是198。可以使用魔法注釋的功能對打包結果的檔案名進行重置。

Webpack 5 超詳細解讀(五)

這樣就可以很好的識别某個打封包件對應的源檔案。

44.runtimeChunk 優化配置

針對

Webpack

中的

optimization

的優化過程中,還有一個

runtimeChunk

的配置。

optimization.runtimeChunk

object` `string` `boolean
           

optimization.runtimeChunk

設定為

true

'multiple'

,會為每個入口添加一個隻含有 runtime 的額外 chunk。此配置的别名如下:

Webpack.config.js

module.exports = {
//...
optimization: {
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
},
};
           

"single"

會建立一個在所有生成 chunk 之間共享的運作時檔案。此設定是如下設定的别名:

Webpack.config.js

module.exports = {
//...
optimization: {
runtimeChunk: {
name: 'runtime',
},
},
};
           
通過将

optimization.runtimeChunk

設定為

object

,對象中可以設定隻有

name

屬性,其中屬性值可以是名稱或者傳回名稱的函數,用于為 runtime chunks 命名。

預設值是

false

:每個入口 chunk 中直接嵌入 runtime。

Warning

對于每個 runtime chunk,導入的子產品會被分别初始化,是以如果你在同一個頁面中引用多個入口起點,請注意此行為。你或許應該将其設定為

single

,或者使用其他隻有一個 runtime 執行個體的配置。

Webpack.config.js

module.exports = {
//...
optimization: {
runtimeChunk: {
name: (entrypoint) => `runtimechunk~${entrypoint.name}`,
},
},
};
           

runtimeChunk,直覺翻譯是運作時的chunk檔案,其作用是啥呢,通過調研了解了一波,在此記錄下。

44.1 何為運作時代碼?

形如

import('abc').then(res=>{})

這種異步加載的代碼,在Webpack中即為運作時代碼。在VueCli工程中常見的異步加載路由即為runtime代碼。

{
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* WebpackChunkName: "about" */ '../views/About.vue')
    // component: About
  }
           

44.2 搭建工程測試功效

1、搭建簡單的vue項目,使用vuecli建立一個隻需要router的項目,腳手架預設路由配置了一個異步加載的about路由,如上圖所示

2、不設定runtimeChunk時,檢視打封包件,此時不需要做任何操作,因為其預設是false,直接yarn build,此時生成的主代碼檔案的hash值為

7d50fa23

Webpack 5 超詳細解讀(五)

3、接着改變about.vue檔案的内容,再次build,檢視打包結果,發現app檔案的hash值發生了變化。

Webpack 5 超詳細解讀(五)
設定runtimeChunk是将包含

chunks 映射關系

的 list單獨從 app.js裡提取出來,因為每一個 chunk 的 id 基本都是基于内容 hash 出來的,是以每次改動都會影響它,如果不将它提取出來的話,等于app.js每次都會改變。緩存就失效了。設定runtimeChunk之後,Webpack就會生成一個個runtime~xxx.js的檔案。

然後每次更改所謂的運作時代碼檔案時,打包建構時app.js的hash值是不會改變的。如果每次項目更新都會更改app.js的hash值,那麼使用者端浏覽器每次都需要重新加載變化的app.js,如果項目大切優化分包沒做好的話會導緻第一次加載很耗時,導緻使用者體驗變差。現在設定了runtimeChunk,就解決了這樣的問題。是以

這樣做的目的是避免檔案的頻繁變更導緻浏覽器緩存失效,是以其是更好的利用緩存。提升使用者體驗。

4、建立vue.config.js,配置runtimeChunk,第一次打包,然後修改about,在打包一次,檢視2次打包之後app檔案的hash值的變化。

// vue.config.js
module.exports = {
  productionSourceMap: false,
  configureWebpack: {
     runtimeChunk: true
  }
}
           
Webpack 5 超詳細解讀(五)

通過截圖看到2次打包生成的app檔案的hash值沒有改變。和上面說的作用一緻。

44.3 你以為這就完了?

1、檢視下runtime~xxx.js檔案内容:

function a(e){return i.p+"js/"+({about:"about"}[e]||e)+"."+{about:"3cc6fa76"}[e]+".js"}f
           

發現檔案很小,且就是加載chunk的依賴關系的檔案。雖然每次建構後app的hash沒有改變,但是runtime~xxx.js會變啊。每次重新建構上線後,浏覽器每次都需要重新請求它,它的 http 耗時遠大于它的執行時間了,是以建議不要将它單獨拆包,而是将它内聯到我們的 index.html 之中。這邊我們使用script-ext-html-Webpack-plugin來實作。(也可使用html-Webpack-inline-source-plugin,其不會删除runtime檔案。)

// vue.config.js
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-Webpack-plugin')
module.exports = {
  productionSourceMap: false,
  configureWebpack: {
    optimization: {
      runtimeChunk: true
    },
    plugins: [
      new ScriptExtHtmlWebpackPlugin({
        inline: /runtime~.+\.js$/  //正則比對runtime檔案名
      })
    ]
  },
  chainWebpack: config => {
    config.plugin('preload')
      .tap(args => {
        args[0].fileBlacklist.push(/runtime~.+\.js$/) //正則比對runtime檔案名,去除該檔案的preload
        return args
      })
  }
}
           

重新打包,檢視index.html檔案

<!DOCTYPE html>
<html lang=en>

<head>
    <meta charset=utf-8>
    <meta http-equiv=X-UA-Compatible content="IE=edge">
    <meta name=viewport content="width=device-width,initial-scale=1">
    <link rel=icon href=/favicon.ico>
    <title>runtime-chunk</title>
    <link href=/js/about.cccc71df.js rel=prefetch>
    <link href=/css/app.b087a504.css rel=preload as=style>
    <link href=/js/app.9f1ba6f7.js rel=preload as=script>
    <link href=/css/app.b087a504.css rel=stylesheet>
</head>

<body><noscript><strong>We're sorry but runtime-chunk doesn't work properly without JavaScript enabled. Please enable it
            to continue.</strong></noscript>
    <div id=app></div>
    <script>(function (e) { function r(r) { for (var n, a, i = r[0], c = r[1], l = r[2], f = 0, s = []; f < i.length; f++)a = i[f], Object.prototype.hasOwnProperty.call(o, a) && o[a] && s.push(o[a][0]), o[a] = 0; for (n in c) Object.prototype.hasOwnProperty.call(c, n) && (e[n] = c[n]); p && p(r); while (s.length) s.shift()(); return u.push.apply(u, l || []), t() } function t() { for (var e, r = 0; r < u.length; r++) { for (var t = u[r], n = !0, a = 1; a < t.length; a++) { var c = t[a]; 0 !== o[c] && (n = !1) } n && (u.splice(r--, 1), e = i(i.s = t[0])) } return e } var n = {}, o = { "runtime~app": 0 }, u = []; function a(e) { return i.p + "js/" + ({ about: "about" }[e] || e) + "." + { about: "cccc71df" }[e] + ".js" } function i(r) { if (n[r]) return n[r].exports; var t = n[r] = { i: r, l: !1, exports: {} }; return e[r].call(t.exports, t, t.exports, i), t.l = !0, t.exports } i.e = function (e) { var r = [], t = o[e]; if (0 !== t) if (t) r.push(t[2]); else { var n = new Promise((function (r, n) { t = o[e] = [r, n] })); r.push(t[2] = n); var u, c = document.createElement("script"); c.charset = "utf-8", c.timeout = 120, i.nc && c.setAttribute("nonce", i.nc), c.src = a(e); var l = new Error; u = function (r) { c.onerror = c.onload = null, clearTimeout(f); var t = o[e]; if (0 !== t) { if (t) { var n = r && ("load" === r.type ? "missing" : r.type), u = r && r.target && r.target.src; l.message = "Loading chunk " + e + " failed.\n(" + n + ": " + u + ")", l.name = "ChunkLoadError", l.type = n, l.request = u, t[1](l) } o[e] = void 0 } }; var f = setTimeout((function () { u({ type: "timeout", target: c }) }), 12e4); c.onerror = c.onload = u, document.head.appendChild(c) } return Promise.all(r) }, i.m = e, i.c = n, i.d = function (e, r, t) { i.o(e, r) || Object.defineProperty(e, r, { enumerable: !0, get: t }) }, i.r = function (e) { "undefined" !== typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 }) }, i.t = function (e, r) { if (1 & r && (e = i(e)), 8 & r) return e; if (4 & r && "object" === typeof e && e && e.__esModule) return e; var t = Object.create(null); if (i.r(t), Object.defineProperty(t, "default", { enumerable: !0, value: e }), 2 & r && "string" != typeof e) for (var n in e) i.d(t, n, function (r) { return e[r] }.bind(null, n)); return t }, i.n = function (e) { var r = e && e.__esModule ? function () { return e["default"] } : function () { return e }; return i.d(r, "a", r), r }, i.o = function (e, r) { return Object.prototype.hasOwnProperty.call(e, r) }, i.p = "/", i.oe = function (e) { throw console.error(e), e }; var c = window["WebpackJsonp"] = window["WebpackJsonp"] || [], l = c.push.bind(c); c.push = r, c = c.slice(); for (var f = 0; f < c.length; f++)r(c[f]); var p = l; t() })([]);</script>
    <script src=/js/chunk-vendors.1e5c55d3.js></script>
    <script src=/js/app.9f1ba6f7.js></script>
</body>
</html>
           

index.html中已經沒有對runtime~xxx.js的引用了,而是直接将其代碼寫入到了index.html中,故不會在請求檔案,減少http請求。

runtimeChunk作用是為了線上更新版本時,充分利用浏覽器緩存,使使用者感覺的影響到最低。

45.代碼懶加載

https://www.jianshu.com/p/6fc86fa8ee81

子產品懶加載本身與Webpack沒有關系,Webpack可以讓懶加載的子產品代碼打包到單獨的檔案中,實作真正的按需加載。Webpack會自動對異步代碼進行分割。

示例代碼如下:

function getComponent() {
    return import(/* WebpackChunkName: "lodash" */ 'lodash').then(({default: _})=>{
        var element = document.createElement('div')
        element.innerHTML = _.join(['a','b'],'-')
        return element
    })
}

document.addEventListener('click', ()=>{
    getComponent().then(element => {
        document.body.appendChild(element)
    })
})
           

需要配置

@babel/preset-env

"useBuiltIns": "usage"

{
   "presets": [
     ["@babel/preset-env",{
       "targets": {
          "chrome": "67"
        },
        "useBuiltIns": "usage",
        "corejs": "3"
     }
     ],
     "@babel/preset-react"
   ],
    "plugins": [
      "@babel/plugin-syntax-dynamic-import"
    ]
}
           

執行打包指令,打包後的檔案如下:

Webpack 5 超詳細解讀(五)

生成了vendors~lodash.js檔案。

浏覽器打開打包後的html檔案,檢視Network如下:

Webpack 5 超詳細解讀(五)

點選後才會加載vendors~lodash.js

Webpack 5 超詳細解讀(五)

實作了子產品按需加載。

異步函數的方式:

async function getComponent(){
    const { default: _} = await import(/* WebpackChunkName: "lodash" */ 'lodash')
    const element = document.createElement('div')
    element.innerHTML = _.join(['a','b'],'-')
    return element
}


document.addEventListener('click', ()=>{
    getComponent().then(element => {
        document.body.appendChild(element)
    })
})
           

46.prefetch 與 preload

Webpack v4.6.0+ 增加了對預擷取和預加載的支援。

在聲明 import 時,使用下面這些内置指令,可以讓 Webpack 輸出 “resource hint(資源提示)”,來告知浏覽器:

  • prefetch(預擷取):将來某些導航下可能需要的資源
  • preload(預加載):目前導航下可能需要資源

下面這個 prefetch 的簡單示例中,有一個

HomePage

元件,其内部渲染一個

LoginButton

元件,然後在點選後按需加載

LoginModal

元件。

LoginButton.js

//...
import(/* WebpackPrefetch: true */ './path/to/LoginModal.js');
           

這會生成

<link rel="prefetch" href="login-modal-chunk.js" target="_blank" rel="external nofollow" >

并追加到頁面頭部,訓示着浏覽器在閑置時間預取

login-modal-chunk.js

檔案。

Tips:隻要父 chunk 完成加載,Webpack 就會添加 prefetch hint(預取提示)。

與 prefetch 指令相比,preload 指令有許多不同之處:

  • preload chunk 會在父 chunk 加載時,以并行方式開始加載。prefetch chunk 會在父 chunk 加載結束後開始加載。
  • preload chunk 具有中等優先級,并立即下載下傳。prefetch chunk 在浏覽器閑置時下載下傳。
  • preload chunk 會在父 chunk 中立即請求,用于當下時刻。prefetch chunk 會用于未來的某個時刻。
  • 浏覽器支援程度不同。

下面這個簡單的 preload 示例中,有一個

Component

,依賴于一個較大的 library,是以應該将其分離到一個獨立的 chunk 中。

我們假想這裡的圖表元件

ChartComponent

元件需要依賴一個體積巨大的

ChartingLibrary

庫。它會在渲染時顯示一個

LoadingIndicator(加載進度條)

元件,然後立即按需導入

ChartingLibrary

ChartComponent.js

//...
import(/* WebpackPreload: true */ 'ChartingLibrary');
           

在頁面中使用

ChartComponent

時,在請求 ChartComponent.js 的同時,還會通過

<link rel="preload">

請求 charting-library-chunk。假定 page-chunk 體積比 charting-library-chunk 更小,也更快地被加載完成,頁面此時就會顯示

LoadingIndicator(加載進度條)

,等到

charting-library-chunk

請求完成,LoadingIndicator 元件才消失。這将會使得加載時間能夠更短一點,因為隻進行單次往返,而不是兩次往返。尤其是在高延遲環境下。

Tips:不正确地使用

WebpackPreload

會有損性能,請謹慎使用。

有時你需要自己控制預加載。例如,任何動态導入的預加載都可以通過異步腳本完成。這在流式伺服器端渲染的情況下很有用。

const lazyComp = () =>
  import('DynamicComponent').catch((error) => {
    // 在發生錯誤時做一些處理
    // 例如,我們可以在網絡錯誤的情況下重試請求
  });
           

如果在 Webpack 開始加載該腳本之前腳本加載失敗(如果該腳本不在頁面上,Webpack 隻是建立一個 script 标簽來加載其代碼),則該 catch 處理程式将不會啟動,直到 chunkLoadTimeout 未通過。此行為可能是意料之外的。但這是可以解釋的 - Webpack 不能抛出任何錯誤,因為 Webpack 不知道那個腳本失敗了。Webpack 将在錯誤發生後立即将 onerror 處理腳本添加到 script 中。

為了避免上述問題,你可以添加自己的 onerror 處理腳本,将會在錯誤發生時移除該 script。

<script
  src="https://example.com/dist/dynamicComponent.js"
  async
  onerror="this.remove()"
></script>
           

在這種情況下,錯誤的 script 将被删除。Webpack 将建立自己的 script,并且任何錯誤都将被處理而沒有任何逾時。

47.第三方擴充設定 CDN

47.1 什麼是CDN

傳送門

cdn全稱是内容分發網絡。其目的是讓使用者能夠更快速的得到請求的資料。簡單來講,cdn就是用來加速的,他能讓使用者就近通路資料,這樣就更更快的擷取到需要的資料。舉個例子,現在伺服器在北京,深圳的使用者想要擷取伺服器上的資料就需要跨越一個很遠的距離,這顯然就比北京的使用者通路北京的伺服器速度要慢。但是現在我們在深圳建立一個cdn伺服器,上面緩存住一些資料,深圳使用者通路時先通路這個cdn伺服器,如果伺服器上有使用者請求的資料就可以直接傳回,這樣速度就大大的提升了。

Webpack 5 超詳細解讀(五)

cdn的整個工作過程

Webpack 5 超詳細解讀(五)

47.2 如何設定CDN

Webpack

中我們引入一個不想被打包的第三方包,可能是由于該包的體積過大或者其他原因,這對與

Webpack

打包來說是有優勢的,因為減少第三包的打包會提高

Webpack

打包的速度。比如在實際使用中,我們使用到了

lodash

第三方包,我們又沒有自己的CDN伺服器,這是就需要借助别人的

CDN

伺服器進行對該包的引入(一般是官方的

CDN

服務)。

47.2.1 有自己的CDN伺服器

如果有自己的CDN伺服器,我們可以在Webpack配置檔案中的

output

中設定

publicPath

目錄,其中寫入

CDN

的伺服器位址,如下:

Webpack 5 超詳細解讀(五)

這樣在打包過後,打開index.htm可以看到,我們對所有的資源都會從該CDN伺服器下查找。

Webpack 5 超詳細解讀(五)

47.2.2 使用第三方資源官方的CDN服務

在Webpack官網中,可以看到,設定

externals

屬性,可以選擇我們不需要打包的第三方資源,具體配置如下:

防止将某些

import

的包(package)打包到 bundle 中,而是在運作時(runtime)再去從外部擷取這些擴充依賴(external dependencies)。

例如,從 CDN 引入 jQuery,而不是把它打包:

index.html

<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous"
></script>
           

Webpack.config.js

module.exports = {
  //...
  externals: {
    jquery: 'jQuery',
  },
};
           

這樣就剝離了那些不需要改動的依賴子產品,換句話,下面展示的代碼還可以正常運作:

import $ from 'jquery';

$('.my-element').animate(/* ... */);
           

上面

Webpack.config.js

externals

下指定的屬性名稱

jquery

表示

import $ from 'jquery'

中的子產品

jquery

應該從打包産物中排除。 為了替換這個子產品,

jQuery

值将用于檢索全局

jQuery

變量,因為預設的外部庫類型是

var

,請參閱 externalsType。

雖然我們在上面展示了一個使用外部全局變量的示例,但實際上可以以以下任何形式使用外部變量:全局變量、CommonJS、AMD、ES2015 子產品,在 externalsType 中檢視更多資訊。

48.打包 Dll 庫

在Webpack4往後或者Webpack5,本身其打包的速度已經足夠優化,是以在高版本Vue腳手架、React腳手架中已經移除了DLL庫的使用。 但是從打包内容的多少以及打包的速度上來講,如果使用了DLL庫,它的确可以提高建構速度。

48.1 DLL庫是什麼

DllPlugin

DllReferencePlugin

用某種方法實作了拆分 bundles,同時還大幅度提升了建構的速度。“DLL” 一詞代表微軟最初引入的動态連結庫(有一些東西可以進行共享,共享的東西可以提前準備好,将其變為一個庫。将來在不同的項目中,對其進行使用的時候,隻需要将該庫導入即可)。

48.2 打包DLL庫

這裡已React和React Dom為例。

項目目錄以及package.json

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-nqA1uxSP-1669024724151)(http://5coder.cn/img/1668918984_0dcd78e5d8d2ae6a4a558f111765fa19.png)]

Webpack.config.js

const path = require('path')
const Webpack = require('Webpack')
const TerserPlugin = require('terser-Webpack-plugin')

module.exports = {
  mode: "production",
  entry: {
    react: ['react', 'react-dom']
  },
  output: {
    path: path.resolve(__dirname, 'dll'),
    filename: 'dll_[name].js',
    library: 'dll_[name]'
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        extractComments: false
      }),
    ],
  },
  plugins: [
    new Webpack.DllPlugin({
      name: 'dll_[name]',
      path: path.resolve(__dirname, './dll/[name].manifest.json')
    })
  ]
}
           

執行yarn dll後,發現dll目錄中生成兩個檔案

dll_react

.js和

react.manifest.json

。其中在其他項目中使用該

dll

庫時,會先引入

react.manifest.json

檔案,根據其中的引用路徑,再對應找到

js

檔案進行打包。

Webpack 5 超詳細解讀(五)

react.manifest.json

{
  "name": "dll_react",
  "content": {
    "./node_modules/react/index.js": {
      "id": 294,
      "buildMeta": {
        "exportsType": "dynamic",
        "defaultObject": "redirect"
      },
      "exports": [
        "Children",
        "Component",
        "Fragment",
        "Profiler",
        "PureComponent",
        "StrictMode",
        "Suspense",
        "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED",
        "cloneElement",
        "createContext",
        "createElement",
        "createFactory",
        "createRef",
        "forwardRef",
        "isValidElement",
        "lazy",
        "memo",
        "useCallback",
        "useContext",
        "useDebugValue",
        "useEffect",
        "useImperativeHandle",
        "useLayoutEffect",
        "useMemo",
        "useReducer",
        "useRef",
        "useState",
        "version"
      ]
    },
    "./node_modules/react-dom/index.js": {
      "id": 935,
      "buildMeta": {
        "exportsType": "dynamic",
        "defaultObject": "redirect"
      },
      "exports": [
        "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED",
        "createPortal",
        "findDOMNode",
        "flushSync",
        "hydrate",
        "render",
        "unmountComponentAtNode",
        "unstable_batchedUpdates",
        "unstable_createPortal",
        "unstable_renderSubtreeIntoContainer",
        "version"
      ]
    }
  }
}
           

49.使用 Dll 庫

目錄結構

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-JMSUhuNy-1669024724153)(http://5coder.cn/img/1668920895_3af3c0f0ea9e322dcd02a0c0a7fa20b3.png)]

Webpack.config.js

const resolveApp = require('./paths')
const HtmlWebpackPlugin = require('html-Webpack-plugin')
const { merge } = require('Webpack-merge')
const TerserPlugin = require('terser-Webpack-plugin')
const Webpack = require('Webpack')
const AddAssetHtmlPlugin = require('add-asset-html-Webpack-plugin')

// 導入其它的配置
const prodConfig = require('./Webpack.prod')
const devConfig = require('./Webpack.dev')

// 定義對象儲存 base 配置資訊
const commonConfig = {
  entry: {
    index: './src/index.js'
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        extractComments: false
      })
    ],
    runtimeChunk: false,
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      maxSize: 20000,
      minChunks: 1,
      cacheGroups: {
        reactVendors: {
          test: /[\\/]node_modules[\\/]/,
          filename: 'js/[name].vendor.js'
        }
      }
    }
  },
  resolve: {
    extensions: ['.js', '.json', '.wasm', '.jsx', '.ts', '.vue'],
    alias: {
      '@': resolveApp('./src')
    }
  },
  output: {
    filename: 'js/[name].[contenthash:8]._bundle.js',
    path: resolveApp('./dist'),

  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              esModule: false
            }
          },
          'postcss-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      },
      {
        test: /\.(png|svg|gif|jpe?g)$/,
        type: 'asset',
        generator: {
          filename: "img/[name].[hash:4][ext]"
        },
        parser: {
          dataUrlCondition: {
            maxSize: 30 * 1024
          }
        }
      },
      {
        test: /\.(ttf|woff2?)$/,
        type: 'asset/resource',
        generator: {
          filename: 'font/[name].[hash:3][ext]'
        }
      },
      {
        test: /\.jsx?$/,
        use: ['babel-loader']
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'copyWebpackPlugin',
      template: './public/index.html'
    }),
    new Webpack.DllReferencePlugin({
      context: resolveApp('./'),
      manifest: resolveApp('./dll/react.manifest.json')
    }),
    new AddAssetHtmlPlugin({
      outputPath: 'js',
      filepath: resolveApp('./dll/dll_react.js')
    })
  ]
}

module.exports = (env) => {
  const isProduction = env.production

  process.env.NODE_ENV = isProduction ? 'production' : 'development'

  // 依據目前的打包模式來合并配置
  const config = isProduction ? prodConfig : devConfig

  const mergeConfig = merge(commonConfig, config)

  return mergeConfig
}
           
Webpack 5 超詳細解讀(五)

打包後的index.html

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-Kk36w3rU-1669024724157)(http://5coder.cn/img/1668921259_41811dba52c9da64fdd5486cc1d94224.png)]

Webpack 5 超詳細解讀(五)

50.CSS 抽離和壓縮

CSS抽離和壓縮

Webpack

中,如果正常在

js

中引入

css

檔案樣式,在

Webpack

打包時會将改

css

檔案也打包進入

js

bundle

中。我們希望在

js

中引入的

css

樣式檔案單獨抽離出來并且打包和壓縮,這裡需要使用

Webpack

提供的

MiniCssExtractPlugin

來實作。

代碼目錄

Webpack 5 超詳細解讀(五)

由于将css單獨抽離打包的需求在開發階段并不需要,且不适合,是以需要區分環境進行使用。在webpack.common.js和webpack。prod.js中分别進行單獨配置。

const resolveApp = require('./paths')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { merge } = require('webpack-merge')
const TerserPlugin = require("terser-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin")

// 導入其它的配置
const prodConfig = require('./webpack.prod')
const devConfig = require('./webpack.dev')

// 定義對象儲存 base 配置資訊
const commonConfig = (isProduction) => {
  return {
    entry: {
      index: './src/index.js'
    },
    resolve: {
      extensions: [".js", ".json", '.ts', '.jsx', '.vue'],
      alias: {
        '@': resolveApp('./src')
      }
    },
    output: {
      filename: 'js/[name].[contenthash:8].bundle.js',
      path: resolveApp('./dist'),
    },
    optimization: {
      runtimeChunk: true,
      minimizer: [
        new TerserPlugin({
          extractComments: false,
        }),
      ]
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
            {
              loader: 'css-loader',
              options: {
                importLoaders: 1,
                esModule: false
              }
            },
            'postcss-loader'
          ]
        },
        {
          test: /\.less$/,
          use: [
            'style-loader',
            'css-loader',
            'postcss-loader',
            'less-loader'
          ]
        },
        {
          test: /\.(png|svg|gif|jpe?g)$/,
          type: 'asset',
          generator: {
            filename: "img/[name].[hash:4][ext]"
          },
          parser: {
            dataUrlCondition: {
              maxSize: 30 * 1024
            }
          }
        },
        {
          test: /\.(ttf|woff2?)$/,
          type: 'asset/resource',
          generator: {
            filename: 'font/[name].[hash:3][ext]'
          }
        },
        {
          test: /\.jsx?$/,
          use: ['babel-loader']
        }
      ]
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'copyWebpackPlugin',
        template: './public/index.html'
      })
    ]
  }
}

module.exports = (env) => {
  const isProduction = env.production

  process.env.NODE_ENV = isProduction ? 'production' : 'development'

  // 依據目前的打包模式來合并配置
  const config = isProduction ? prodConfig : devConfig

  const mergeConfig = merge(commonConfig(isProduction), config)

  return mergeConfig
}
           

在webpack.common.js中将配置檔案作為function的導出資料使用,可以使用傳參的方式來判斷目前的環境(生産或者開發)。

Webpack 5 超詳細解讀(五)

在webpack.prodd.js中

const CopyWebpackPlugin = require('copy-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")

module.exports = {
  mode: 'production',
  optimization: {
    minimizer: [
      new CssMinimizerPlugin()
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: 'public',
          globOptions: {
            ignore: ['**/index.html']
          }
        }
      ]
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].[hash:8].css'
    })
  ]
}
           

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-Mmewl0lZ-1669024724160)(http://5coder.cn/img/1668934227_b7c50ebe030466eb472723cd56921e9f.png)]

執行yarn build打包後,發現在dist目錄中單獨抽離出來了css目錄及檔案。并且在使用yarn serve開發環境時,樣式檔案也可以正常加載。

并且使用新的插件

css-minimizer-webpack-plugin

對css檔案進行壓縮。

Webpack 5 超詳細解讀(五)

[MiniCssExtractPlugin官方文檔](

繼續閱讀