Code Splitting 代碼分包 / 代碼分割
webpack默認打包依然存在一些弊端。
就是所有代碼最終都會被打包到一起(一個bundle文件中)。
如果項目中代碼比較複雜,模塊比較多,打包結果就會很大(bundle體積過大),很容易超過2、3MB。
而實際情況是,項目啓動時,並不是每個模塊都需要加載進來。
但是這些模塊又被全部打包到一起,需要任何一個模塊都必須整體加載進來後才能使用。
項目運行在瀏覽器端,這樣就會浪費掉很多的流量和帶寬。
更爲合理的方案,就是「分包」「按需加載」
- 分包:將打包結果,按照一定的規則,分離到多個bundle當中。
- 按需加載:根據應用的運行需要,按需加載這些模塊。
這樣就會大大提高項目的響應速度和運行效率。
webpack 合併 與 分離
前面說webpack就是將代碼中散落的模塊合併到一起,從而提高運行效率。
現在又要求將它們分離開,這是「物極必反」的結果。資源太大不行,太碎了也不行。
項目中劃分模塊的顆粒度一般非常的細。
很多時候一個模塊只是提供了一個小小的工具函數,並不能形成一個完整的功能單元。
如何不將這些散落的模塊合併到一起,在運行一個小小的功能時,就會加載很多的模塊。
而當前HTTP1.1版本有很多缺陷,例如:
- 同域並行請求限制:不能同時對同一個域名發起多次請求
- 每次請求都會有一定的延遲
- 每次請求除了傳輸具體的內容外,還會有額外的請求頭和響應頭,當有大量請求時,這些請求頭和響應頭加在一起,也是很大的浪費
綜上所述,模塊打包(合併)是有必要的。
不過在應用越來越大之後,也要慢慢學會變通。
Code Splitting
爲了解決打包文件過大的問題,webpack支持「代碼分包」的功能,也可以稱爲「代碼分割」。
它通過把打包的模塊按照我們設計的規則打包到不同的bundle當中,從而提高應用的響應速度。
目前webpack實現 「分包」的方式主要有兩種:
- 多入口打包
- 根據業務配置不同的打包入口
- 配置多個入口同時打包,最終輸出多個打包結果
- 動態導入
- 採用ESM的動態導入的功能
import()
,實現模塊的按需加載。 - webpack會將這些動態導入的模塊,單獨輸出到一個bundle當中。
- 採用ESM的動態導入的功能
多入口打包 Multi Entry
多入口打包,一般適用於傳統的多頁應用程序。
最常見的劃分規則就是:
- 一個頁面對應一個打包入口。
- 對於不同頁面公共的部分,單獨提取到一個公共的結果中。
webpack配置文件的entry屬性配置類型:
- 字符串:一個入口文件
- 字符串數組:多個入口文件打包到一起,相當於一個入口打包
- 對象:多個入口文件分別打包
- key:入口的名稱
- value:入口對應的文件路徑
每個打包入口會形成一個獨立的chunk,入口名稱就是這個chunk的name(默認是main)。
一旦配置爲多入口,輸出的文件名也需要修改:
使用[name]佔位符,動態輸出文件名,[name]最終替換爲入口的名稱,即entry的key。
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
index: './src/index.js',
album: './src/album.js',
},
output: {
// 使用[name]佔位符,動態輸出文件名
// [name]最終替換爲入口的名稱,即entry的key
filename: '[name].bundle.js',
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/index.html',
}),
new HtmlWebpackPlugin({
filename: 'album.html',
template: './src/album.html',
}),
],
}
此時打包會輸出兩個html和兩個bundle文件,但是發現html文件中將兩個bundle文件全部引用了。
這是因爲html-webpack-plugin插件會自動生成一個注入所有打包結果的html。
如果需要指定輸出的html所使用的bundle,可以使用插件的chunks
屬性配置:
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/index.html',
chunks: ['index'],
}),
new HtmlWebpackPlugin({
filename: 'album.html',
template: './src/album.html',
chunks: ['album'],
}),
每個打包入口,都會形成一個獨立的chunk
。
而插件的chunks
屬性,通過chunk
的名稱,指定需要注入哪些chunk
。
多入口打包缺陷:
不同入口中肯定會有公共的模塊,根據現在多入口打包的方式,就會出現在不同的打包結果中,會有相同的模塊出現。
例如上面的index.js和album.js都導入了一個公共模塊或css模塊。
解決方案:
將這些公共的模塊單獨提取輸出到一個公共的bundle中。
提取公共模塊 Split Chunks
webpack可以在optimization優化配置中開啓splitChunks功能,實現提取公共模塊打包。
optimization: {
splitChunks: {
// all 表示將所有的公共模塊都提取到單獨的bundle當中
chunks: 'all'
}
}
打包後,就會多生成一個album~index.bundle.js
文件。
它存放的就是index.js和album.js中公共的模塊。
動態導入 Dynamic Imports
「按需加載」是開發瀏覽器應用中常見的需求。
一般指的按需加載數據。
這裏的按需加載指的是:應用運行過程中,需要哪個模塊時,纔會加載那個模塊。
這個方式可以極大的節省帶寬和流量。
webpack支持使用動態導入的方式,實現模塊的按需加載。
而且所有動態導入的模塊,都會被自動分包(形成一個單獨的chunk,提取到單獨的bundle當中)。
相比多入口方式,動態導入更爲靈活。
可以通過代碼的邏輯控制是否需要加載某個模塊,或何時加載。
使用ESM的import()
方法導入指定的模塊,它返回一個promise。
例如:
// 入口文件 /src/index.js
const render = (query) => {
if (query == '#posts') {
import('./posts/posts')
.then(({default: posts}) => {
console.log(posts())
})
} else if (query == 'album') {
import('./album/album')
.then(({default: album}) => {
console.log(album())
})
}
}
// PS:假設posts和album模塊中都使用了相同的公共模塊
打包會多生成3個文件,文件名爲[number].bundle.js
。
分別是posts和album模塊,另一個是從二者提取出來的公共模塊。
這三個文件就是有動態導入、自動分包生成的。
整個過程不需要配置任何地方,例如不需要配置optimization.splitChunks就可以實現提供公共模塊。
只需要按照ESM動態導入成員的方式導入模塊即可。
webpack內部會自動處理分包、按需加載以及提取公共模塊。
如果使用的是單頁應用開發框架(如vue,react),項目中的路由映射組件,就可以通過這種動態導入的方式,實現按需加載。
魔法註釋 Magic Comments
默認通過動態導入產生的bundle文件,它的名稱是一個序號,文件名爲[number].bundle.js
。
可以通過webpack特有的魔法註釋,給它們定義名稱。
具體使用就是,在import()
的參數位置(前後都可以),添加一個特定格式的行內註釋:
// 格式:/*webpackChunkName:'<name>'*/
import(/* webpackChunkName: 'posts' */'./posts/posts').then(() => {})
import('./posts/posts'/* webpackChunkName: 'album' */).then(() => {})
生成文件:
- posts.bundle.js
- album.bundle.js
- album~posts.bundle.js 提供公共模塊的文件也同步變化
如果多個模塊使用的相同的chunkName,那它們最終會被打包到一起,自然不需要提取公共模塊,最終只會生成一個文件。
藉助這個特點,就可以根據情況,靈活組織動態導入的模塊所輸出的文件。