官網教程:code-splitting
最近在開發一個新的功能,需要在某一條件下引入新包,而之前的場景則完全不需要。這種情況下,動態加載+代碼分割正可以派上用場。在動手之前,先看看官網是怎麼說的吧。
entry - 配置多入口、多頁面
在entry對象中寫入多個入口文件即可。
weboack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
這樣做的弊端主要由兩個。第一,如果有公共的代碼塊,那麼打包之後的兩個Bundle裏都會包含重複的模塊。第二,不夠靈活,不能被webpack通過splitChunks的規則靈活地優化。
因此,在entry裏直接寫多個入口的方式,適用於本身就是多入口的項目,而非一般的spa。可以通過代碼結構,用函數構造出合理的entry和相應的htmlwebpackplugin,來進行打包。
splitChunks - 智能抽取公共代碼
webpack還給出了智能分割代碼的功能:splitChunks
,可以用給定的策略去抽取公共代碼,避免重複。
webpack.config.js
optimization: {
minimizer: [
new UglifyJsPlugin({
exclude: /\.min\.js$/, // 過濾掉以".min.js"結尾的文件,這個後綴本身就是已經壓縮好的代碼,沒必要進行二次壓縮
cache: true,
parallel: true,
sourceMap: true,
extractComments: false,
uglifyOptions: {
output: {
// 移除註釋
comments: false,
},
},
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessorOptions: {
parser: safeParser,
autoprefixer: {
disable: true,
},
discardComments: {
removeAll: true, // 移除註釋
},
},
canPrint: true,
}),
],
splitChunks: {
minChunks: 3,
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
},
},
},
// 可取'single', 'multiple', default 爲false
// 此處等於 runtime的chunkname 即爲'runtime'
runtimeChunk: 'single',
}
具體配置寫在[optimization.splitChunks ](https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks)
中。此外還可以通過mini-css-extract-plugin
,bundle-loader
,promise-loader
等別的社區貢獻的工具進行代碼分割。
dynamic-import - 按需動態加載
語法分爲兩種,一種是import()
,另一種是webpack原來的老語法require.ensure
。注意使用import時,要配置對應的Babel插件。
main.js
async function lala() {
const { default: socketcluster } = await import(
/* webpackChunkName: "socket" */
/* webpackMode: "lazy" */
'socketcluster-client')
console.log(typeof socketcluster, socketcluster)
}
lala()
.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
]
],
"plugins": [
"@babel/plugin-transform-runtime",
"@babel/plugin-syntax-dynamic-import"
],
"env": {
"development": {
"presets": [
"@babel/preset-env"
]
}
}
}
webpack.config.js
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, '../dist'),
publicPath: '/',
chunkFilename: '[name].chunk.js',
},
注意main.js
裏的註釋,這是webpack的魔法註釋,可以讓chunkFileName
裏的name獲得註釋裏寫入的值。不過據說webpack5會採用更棒的方式來決定chunkFileName。
魔法註釋還包括一些其他的選項,比如:
import(
/* webpackInclude: /\.json$/ */
/* webpackExclude: /\.noimport\.json$/ */
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
/* webpackPrefetch: true */
/* webpackPreload: true */
`./locale/${language}`
);
webpackPrefetch
和webpackPreload
: prefetch是未來導航可能需要的資源,preload是當前導航也許需要的資源。當然,這需要瀏覽器支持,其實就是在瀏覽器的標籤里加入了rel="prefetch"或rel=“preload”。
- preload的資源是和父模塊平行加載的,而prefetch的資源則在父模塊加載完畢後開始加載。
- preload的資源優先級中等,會立即下載。prefetch的資源會等到瀏覽器空閒時再下載。
- preload的資源是父模塊立即需要的,prefetch的資源則是未來任意時間需要用到的。
下載和需求優先級上,preload > prefetch。
tips - 踩坑小提示
- webpackChunkName 魔法註釋爲什麼會失效?
檢查你的.babelrc
和webpack.config.js
,有沒有移除註釋的配置。魔法註釋,當然註釋得存在才能生效。所以.babelrc裏不能有comments: false
,webpack的uglifyjs等插件中也不能設置comments: false
和extractComments
。
- @babel/plugin-syntax-dynamic-import爲什麼失效?
你可能同時使用了@babel/plugin-syntax-dynamic-import
和 dynamic-import-node
。
@babel/plugin-syntax-dynamic-import
和 dynamic-import-node
在一定程度上是彼此衝突的。後者主要是給Node使用的,把import
語法轉譯爲一個被延遲的require()
。二者的區別已經被討論過。簡單來說,dynamic-import-node
,包括它的babel-7
版本,都是社區貢獻的插件。而@babel/plugin-syntax-dynamic-import
是babel官方出品的(看前綴那個@就知道啦),只是爲了讓babel能夠解析動態的import()
,需要配合別的打包工具如webpack,rollup或者原生實現一起使用。
避免過度優化!結局
經過打包和壓縮(uglify/terser)之後,動態引入部分的代碼大小爲40.2KB,app主體代碼爲896 KiB。如果說對於移動端,40KB的代碼還值得分離的話,那麼再經過gzip壓縮後,app主體代碼爲242KB,分離出的代碼大小隻有11KB左右,比起多發一個網絡請求,倒不如索性打包到一起了。
於是乎,雖然試驗了一下code-splitting,但是一番衡量後,最後還是回到了原點。不過這個過程還是蠻有意思的(勉強挽尊吧,哎)。
注意,gzip壓縮並不是在webpack打包這一步做的。實際上,webpack
雖然提供了這個選項,比如利用compression-webpack-plugin
,但多數情況下都不會選擇使用。這是因爲很多流行的靜態服務器主機如Surge/Netlify等都已經做了自動gzip靜態文件的事情。本項目中,服務端採用了express
,利用了[compress](https://github.com/expressjs/compression)
來進行gzip壓縮,減小response body
的大小。所以webpack直接打包出來的文件會比實際上線後,瀏覽器network
中顯示的文件size
要大的多。