webpack中分割代碼的三種方式


官網教程: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-pluginbundle-loaderpromise-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}`
);

webpackPrefetchwebpackPreload: prefetch是未來導航可能需要的資源,preload是當前導航也許需要的資源。當然,這需要瀏覽器支持,其實就是在瀏覽器的標籤里加入了rel="prefetch"或rel=“preload”

  • preload的資源是和父模塊平行加載的,而prefetch的資源則在父模塊加載完畢後開始加載。
  • preload的資源優先級中等,會立即下載。prefetch的資源會等到瀏覽器空閒時再下載。
  • preload的資源是父模塊立即需要的,prefetch的資源則是未來任意時間需要用到的。

下載和需求優先級上,preload > prefetch。

tips - 踩坑小提示

  1. webpackChunkName 魔法註釋爲什麼會失效?

檢查你的.babelrcwebpack.config.js,有沒有移除註釋的配置。魔法註釋,當然註釋得存在才能生效。所以.babelrc裏不能有comments: false,webpack的uglifyjs等插件中也不能設置comments: falseextractComments

  1. @babel/plugin-syntax-dynamic-import爲什麼失效?

你可能同時使用了@babel/plugin-syntax-dynamic-importdynamic-import-node

@babel/plugin-syntax-dynamic-importdynamic-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要大的多。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章