從實踐中尋找webpack4最優配置

連城發表於2019-03-04

筆者最近在準備給fle-cli升級到webpack4版本,覺得有必要將探索過程的經驗分享給大家,遂決定寫這篇文章。(不知道fle-cli看這裡

webpack4是大趨勢,升級是必然的,那麼為什麼現在才升級?

原因有以下幾個方面:

  • 剛釋出的版本還不穩定,潛在風險大,而目前版本已更新到4.8.3,基本處於穩定;
  • webpack社群工具未完全跟上節奏,好多工具都得自己搞,勞心勞力(其實主要就是懶哈哈);
  • webpack本身及社群工具存在或多或少的問題,未經時間沉澱,維護成本高。

然而現在,筆者認為以上這些已經成熟,是時候來一波升級了。

前言

本文不會講解webpack配置的每個細節點,因為這些官方文件都可以看到。筆者會挑一些難以理解的新概念、可能會碰到的問題,以及筆者總結下來的優化方案來分享,希望可以給大家帶來一些幫助。

配置

mode

mode是webpack4新增的引數選項,它的值有3個:development、production、none,能夠幫助我們載入一些預設配置,none即不載入預設配置。下面將對應的預設配置列出來供大家參考,以免重複配置。

development

注重提升程式碼構建速度和開發體驗

module.exports = {
  cache: true,
  devtools: "eval",
  plugins: [
    new webpack.NamedModulesPlugin(),
    new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") })
  ]
}
複製程式碼

prodution

提供程式碼優化,如壓縮、作用域提升等

var UglifyJsPlugin = require(`uglifyjs-webpack-plugin`)

module.exports = {
  plugins: [
    new UglifyJsPlugin(/* ... */),
    new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
    new webpack.optimize.ModuleConcatenationPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  ]
}
複製程式碼

optimization

這個選項也是webpack4新增的,主要是用來自定義一些優化打包策略。

minimizer

在production模式,該配置會預設為我們壓縮混淆程式碼,但這顯然滿足不了我們對於優化程式碼的訴求。下面筆者分享一套自身實踐總結下來的配置及解釋:


var UglifyJsPlugin = require(`uglifyjs-webpack-plugin`)
var OptimizeCssAssetsPlugin = require(`optimize-css-assets-webpack-plugin`)

module.exports = {
  optimization: {
    minimizer: [
      // 自定義js優化配置,將會覆蓋預設配置
      new UglifyJsPlugin({
        exclude: /.min.js$/, // 過濾掉以".min.js"結尾的檔案,我們認為這個字尾本身就是已經壓縮好的程式碼,沒必要進行二次壓縮
        cache: true,
        parallel: true, // 開啟並行壓縮,充分利用cpu
        sourceMap: false,
        extractComments: false, // 移除註釋
        uglifyOptions: {
          compress: {
            unused: true,
            warnings: false,
            drop_debugger: true
          },
          output: {
            comments: false
          }
        }
      }),
      // 用於優化css檔案
      new OptimizeCssAssetsPlugin({
        assetNameRegExp: /.css$/g,
        cssProcessorOptions: {
          safe: true,
          autoprefixer: { disable: true }, // 這裡是個大坑,稍後會提到
          mergeLonghand: false,
          discardComments: {
            removeAll: true // 移除註釋
          }
        },
        canPrint: true
      })
    ]
  }
}
複製程式碼

UglifyJsPlugin這款外掛相信大家也是經常用到,這裡不再多說,這裡的亮點是過濾掉本身已經是壓縮的js檔案,能夠提升我們的編譯效率以及避免二次混淆壓縮而造成的未知bug。

OptimizeCssAssetsPlugin這款外掛主要用來優化css檔案的輸出,預設使用cssnano,其優化策略主要包括:擯棄重複的樣式定義、砍掉樣式規則中多餘的引數、移除不需要的瀏覽器字首等,更多優化規則看這裡。前文我們提到這裡有個大坑,相信你已經察覺到了,沒錯,就是這貨把我們通過autoprefixer加好了字首給移除了。筆者查閱了許多資料,依舊沒有找到滿意的答案,沒辦法,只有硬著頭皮去原始碼中找答案了,於是便有了這段配置autoprefixer: { disable: true },禁用掉cssnano對於瀏覽器字首的處理。

runtimeChunk

分離出webpack編譯執行時的程式碼,也就是我們先前稱為manifest的程式碼塊,好處是方便我們做檔案的持久化快取。它可以設定多種型別的值,具體可以看這裡,其中single即將所有chunk的執行程式碼打包到一個檔案中,multiple就是給每一個chunk的執行程式碼打包一個檔案。

我們可以配合InlineManifestWebpackPlugin外掛將執行程式碼直接插入html檔案中,因為這段程式碼非常少,這樣做可以避免一次請求的開銷,但是新版外掛的配置和之前有些不太一樣,接下來詳細講解一下如何配置。

var HtmlWebpackPlugin = require(`html-webpack-plugin`)
var InlineManifestWebpackPlugin = require(`inline-manifest-webpack-plugin`)

module.exports = {
  entry: {
    app: `src/index.js`
  },
  optimization: {
    runtimeChunk: `single`
    // 等價於
    // runtimeChunk: {
    //   name: `runtime`
    // }
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: `fle-cli`,
      filename: `index.html`,
      template: `xxx`,
      inject: true,
      chunks: [`runtime`, `app`], // 將runtime插入html中
      chunksSortMode: `dependency`,
      minify: {/* */}
    }),
    new InlineManifestWebpackPlugin(`runtime`)
  ]
}
複製程式碼

這段配置會產生一個叫做runtime的程式碼塊,和老版本不同的是,我們並不需要在html模版中新增<%= htmlWebpackPlugin.files.webpackManifest %>,只需將runtime加入chunks即可。這裡有一個點要注意,InlineManifestWebpackPlugin外掛的順序一定要在HtmlWebpackPlugin之後,否則會導致編譯失敗。

splitChunks

終於要講到重頭戲了,也是筆者個人認為最難以理解的一個配置項。webpack4移除了CommonsChunkPlugin外掛,取而代之的是splitChunks。

我們先來看下預設配置:

splitChunks: {
  chunks: "async",
  minSize: 30000,
  minChunks: 1,
  maxAsyncRequests: 5,
  maxInitialRequests: 3,
  automaticNameDelimiter: `~`,
  name: true,
  cacheGroups: {
    vendors: {
      test: /[\/]node_modules[\/]/,
      priority: -10
    },
    default: {
      minChunks: 2,
      priority: -20,
      reuseExistingChunk: true
    }
  }
}
複製程式碼

預設配置只會作用於非同步載入的程式碼塊,它限制了分離檔案的最小體積,即30KB(注意這個體積是壓縮之前的),這個是前提條件,然後它有兩個分組:屬於node_modules模組,或者被至少2個入口檔案引用,它才會被打包成獨立的檔案。

為什麼要限制最小體積呢?因為webpack認為小於30KB的程式碼塊分離出來,還要額外消耗一次請求去載入它,成本太高,當然這個值也不是隨便意淫出來的,而是經過大量的實踐總結得到的,筆者個人認為這是一個非常好策略。

maxAsyncRequests(最大的非同步請求數)和maxInitialRequests(最大的初始請求數)這兩個引數則是為了限制程式碼塊劃分的過於細緻,導致大量的檔案請求。

但是隻分離非同步程式碼塊顯然滿足不了我們的需求,因此接下來筆者分享一套相對來說比較優雅的分離打包配置:

splitChunks: {
  cacheGroups: {
    vendors: {
      test: /[\/]node_modules[\/]/,
      name: `vendors`,
      minSize: 30000,
      minChunks: 1,
      chunks: `initial`,
      priority: 1 // 該配置項是設定處理的優先順序,數值越大越優先處理
    },
    commons: {
      test: /[\/]src[\/]common[\/]/,
      name: `commons`,
      minSize: 30000,
      minChunks: 3,
      chunks: `initial`,
      priority: -1,
      reuseExistingChunk: true // 這個配置允許我們使用已經存在的程式碼塊
    }
  }
}
複製程式碼

首先是將node_modules的模組分離出來,這點就不再累述了。非同步載入的模組將會繼承預設配置,這裡我們就不需要二次配置了。

第二點是分離出共享模組,筆者認為一個優雅的專案結構,其公共程式碼(或者稱為可複用的程式碼)應該是放置於同一個根目錄下的,基於這點我們可以將src/common中的公用程式碼提取出來。

當然你還可以有另外一種選擇,將字尾為.js且使用次數超過3次的檔案提取出來,但是筆者不建議這個做,因為這不利於持久化快取,新增或刪除檔案都有可能影響到使用次數,從而導致原先的公共檔案失效。

文末

原先還想著講一下css外掛部分的配置,限於篇幅,本文就不再進行講解說明了,感興趣的小哥哥小姐姐可以在這裡翻看原始碼:webpack4-test

順便在這裡推薦一款好用的全域性通用腳手架fle-cli:旨在幫助我們從複雜繁瑣的編譯配置中解放出來,全身心地投入業務開發中,提高開發效率;同時它也是真正意義上的全域性腳手架,區別於市面上其他的全域性腳手架,它不會在專案工程中生成各種編譯配置檔案,也不會給你安裝一系列編譯的依賴包,這意味著你的專案工程可以非常乾淨純粹。

相關文章