Webpack 4 配置最佳實踐

做者 Daniel 螞蟻金服·數據體驗技術團隊javascript

Webpack 4 發佈已經有一段時間了。Webpack 的版本號已經來到了 4.12.x。但由於 Webpack 官方尚未完成遷移指南,在文檔層面上還有所欠缺,大部分人對升級 Webpack 仍是一頭霧水。前端

不過 Webpack 的開發團隊已經寫了一些零散的文章,官網上也有了新版配置的文檔。社區中一些開發者也已經成功試水,升級到了 Webpack 4,而且總結成了博客。因此我也終於去了解了 Webpack 4 的具體狀況。如下就是我對遷移到 Webpack 4 的一些經驗。vue

本文的重點在:java

  • Webpack 4 在配置上帶來了哪些便利?要遷移須要修改配置文件的哪些內容?
  • 以前的 Webpack 配置最佳實踐在 Webpack 4 這個版本,還適用嗎?

Webpack 4 以前的 Webpack 最佳實踐

這裏以 Vue 官方的 Webpack 模板 vuejs-templates/webpack 爲例,說說 Webpack 4 以前,社區裏比較成熟的 Webpack 配置文件是怎樣組織的。node

區分開發和生產環境

大體的目錄結構是這樣的:react

+ build
+ config
+ src

複製代碼

在 build 目錄下有四個 webpack 的配置。分別是:webpack

  • webpack.base.conf.js
  • webpack.dev.conf.js
  • webpack.prod.conf.js
  • webpack.test.conf.js

這分別對應開發、生產和測試環境的配置。其中 webpack.base.conf.js 是一些公共的配置項。咱們使用 webpack-merge 把這些公共配置項和環境特定的配置項 merge 起來,成爲一個完整的配置項。好比 webpack.dev.conf.js 中:git

'use strict'
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')

const devWebpackConfig = merge(baseWebpackConfig, {
   ...
})
複製代碼

這三個環境不只有一部分配置不一樣,更關鍵的是,每一個配置中用 webpack.DefinePlugin 向代碼注入了 NODE\_ENV 這個環境變量。github

這個變量在不一樣環境下有不一樣的值,好比 dev 環境下就是 development。這些環境變量的值是在 config 文件夾下的配置文件中定義的。Webpack 首先從配置文件中讀取這個值,而後注入。好比這樣:web

build/webpack.dev.js

plugins: [
  new webpack.DefinePlugin({
    'process.env': require('../config/dev.env.js')
  }),
]
複製代碼

config/dev.env.js

module.exports ={
  NODE_ENV: '"development"'
}
複製代碼

至於不一樣環境下環境變量具體的值,好比開發環境是 development,生產環境是 production,實際上是你們約定俗成的。

框架、庫的做者,或者是咱們的業務代碼裏,都會有一些根據環境作判斷,執行不一樣邏輯的代碼,好比這樣:

if (process.env.NODE_ENV !== 'production') {
  console.warn("error!")
}
複製代碼

這些代碼會在代碼壓縮的時候被預執行一次,而後若是條件表達式的值是 true,那這個 true 分支裏的內容就被移除了。這是一種編譯時的死代碼優化。這種區分不一樣的環境,並給環境變量設置不一樣的值的實踐,讓咱們開啓了編譯時按環境對代碼進行鍼對性優化的可能。

Code Splitting && Long-term caching

Code Splitting 通常須要作這些事情:

  • 爲 Vendor 單獨打包(Vendor 指第三方的庫或者公共的基礎組件,由於 Vendor 的變化比較少,單獨打包利於緩存)
  • 爲 Manifest (Webpack 的 Runtime 代碼)單獨打包
  • 爲不一樣入口的公共業務代碼打包(同理,也是爲了緩存和加載速度)
  • 爲異步加載的代碼打一個公共的包

Code Splitting 通常是經過配置 CommonsChunkPlugin 來完成的。一個典型的配置以下,分別爲 vendor、manifest 和 vendor-async 配置了 CommonsChunkPlugin。

new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),

    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),

    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),
複製代碼

CommonsChunkPlugin 的特色就是配置比較難懂,你們的配置每每是複製過來的,這些代碼基本上成了模板代碼(boilerplate)。若是 Code Splitting 的要求簡單倒好,若是有比較特殊的要求,好比把不一樣入口的 vendor 打不一樣的包,那就很難配置了。總的來講配置 Code Splitting 是一個比較痛苦的事情。

而 Long-term caching 策略是這樣的:給靜態文件一個很長的緩存過時時間,好比一年。而後在給文件名里加上一個 hash,每次構建時,當文件內容改變時,文件名中的 hash 也會改變。瀏覽器在根據文件名做爲文件的標識,因此當 hash 改變時,瀏覽器就會從新加載這個文件。

Webpack 的 Output 選項中能夠配置文件名的 hash,好比這樣:

output: {
  path: config.build.assetsRoot,
  filename: utils.assetsPath('js/[name].[chunkhash].js'),
  chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
複製代碼

Webpack 4 下的最佳實踐

Webpack 4 的變與不變

Webpack 4 這個版本的 API 有一些 breaking change,但不表明說這個版本就發生了翻天覆地的變化。其實變化的點只有幾個。並且只要你仔細瞭解了這些變化,你必定會拍手叫好。

遷移到 Webpack 4 也只須要檢查一下 checklist,看看這些點是否都覆蓋到了,就能夠了。

開發和生產環境的區分

Webpack 4 引入了 mode 這個選項。這個選項的值能夠是 development 或者 production。

設置了 mode 以後會把 process.env.NODE\_ENV 也設置爲 development 或者 production。而後在 production 模式下,會默認開啓 UglifyJsPlugin 等等一堆插件。

Webpack 4 支持零配置使用,能夠從命令行指定 entry 的位置,若是不指定,就是 src/index.js。mode 參數也能夠從命令行參數傳入。這樣一些經常使用的生產環境打包優化均可以直接啓用。

咱們須要注意,Webpack 4 的零配置是有限度的,若是要加上本身想加的插件,或者要加多個 entry,仍是須要一個配置文件。

雖然如此,Webpack 4 在各個方面都作了努力,努力讓零配置能夠作的事情更多。這種內置優化的方式使得咱們在項目起步的時候,能夠把主要精力放在業務開發上,等後期業務變複雜以後,才須要關注配置文件的編寫。

在 Webpack 4 推出 mode 這個選項以前,若是想要爲不一樣的開發環境打造不一樣的構建選項,咱們只能經過創建多個 Webpack 配置且分別設置不一樣的環境變量值這種方式。這也是社區裏的最佳實踐。

Webpack 4 推出的 mode 選項,實際上是一種對社區中最佳實踐的吸取。這種思路我是很贊同的。開源項目來自於社區,在社區中成長,從社區中吸取營養,而後回報社區,這是一個良性循環。最近我在不少前端項目中都看到了相似的趨勢。接下來要講的其餘幾個 Webpack 4 的特性也是和社區的反饋離不開的。

那麼上文中介紹的使用多個 Webpack 配置,以及手動環境變量注入的方式,是否在 Webpack 4 下就不適用了呢?其實否則。在Webpack 4 下,對於一個正經的項目,咱們依然須要多個不一樣的配置文件。若是咱們對爲測試環境的打包作一些特殊處理,咱們還須要在那個配置文件裏用 webpack.DefinePlugin 手動注入 NODE\_ENV 的值(好比 test)。

Webpack 4 下若是須要一個 test 環境,那 test 環境的 mode 也是 development。由於 mode 只有開發和生產兩種,測試環境應該是屬於開發階段。

第三方庫 build 的選擇

在 Webpack 3 時代,咱們須要在生產環境的的 Webpack 配置裏給第三方庫設置 alias,把這個庫的路徑設置爲 production build 文件的路徑。以此來引入生產版本的依賴。

好比這樣:

resolve: {
  extensions: [".js", ".vue", ".json"],
  alias: {
    vue$: "vue/dist/vue.runtime.min.js"
  }
},
複製代碼

在 Webpack 4 引入了 mode 以後,對於部分依賴,咱們能夠不用配置 alias,好比 React。React 的入口文件是這樣的:

'use strict';

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

複製代碼

這樣就實現了 0 配置自動選擇生產 build。

但大部分的第三庫並無作這個入口的環境判斷。因此這種狀況下咱們仍是須要手動配置 alias。

Code Splitting

Webpack 4 下還有一個大改動,就是廢棄了 CommonsChunkPlugin,引入了 optimization.splitChunks 這個選項。

optimization.splitChunks 默認是不用設置的。若是 mode 是 production,那 Webpack 4 就會開啓 Code Splitting。

默認 Webpack 4 只會對按需加載的代碼作分割。若是咱們須要配置初始加載的代碼也加入到代碼分割中,能夠設置 splitChunks.chunks'all'

Webpack 4 的 Code Splitting 最大的特色就是配置簡單(0配置起步),和__基於內置規則自動拆分__。內置的代碼切分的規則是這樣的:

  • 新 bundle 被兩個及以上模塊引用,或者來自 node_modules
  • 新 bundle 大於 30kb (壓縮以前)
  • 異步加載併發加載的 bundle 數不能大於 5 個
  • 初始加載的 bundle 數不能大於 3 個

簡單的說,Webpack 會把代碼中的公共模塊自動抽出來,變成一個包,前提是這個包大於 30kb,否則 Webpack 是不會抽出公共代碼的,由於增長一次請求的成本是不能忽視的。

具體的業務場景下,具體的拆分邏輯,能夠看 SplitChunksPlugin 的文檔以及 webpack 4: Code Splitting, chunk graph and the splitChunks optimization 這篇博客。這兩篇文章基本羅列了全部可能出現的狀況。

若是是普通的應用,Webpack 4 內置的規則就足夠了。

若是是特殊的需求,Webpack 4 的 optimization.splitChunks API也能夠知足。

splitChunks 有一個參數叫 cacheGroups,這個參數相似以前的 CommonChunks 實例。cacheGroups 裏每一個對象就是一個用戶定義的 chunk。

以前咱們講到,Webpack 4 內置有一套代碼分割的規則,那用戶也能夠自定義 cacheGroups,也就是自定義 chunk。那一個 module 應該被抽到哪一個 chunk 呢?這是由 cacheGroups 的抽取範圍控制的。每一個 cacheGroups 均可以定義本身抽取模塊的範圍,也就是哪些文件中的公共代碼會抽取到本身這個 chunk 中。不一樣的 cacheGroups 之間的模塊範圍若是有交集,咱們能夠用 priority 屬性控制優先級。Webpack 4 默認的抽取的優先級是最低的,因此模塊會優先被抽取到用戶的自定義 chunk 中。

splitChunksPlugin 提供了兩種控制 chunk 抽取模塊範圍的方式。一種是 test 屬性。這個屬性能夠傳入字符串、正則或者函數,全部的 module 都會去匹配 test 傳入的條件,若是條件符合,就被歸入這個 chunk 的備選模塊範圍。若是咱們傳入的條件是字符串或者正則,那匹配的流程是這樣的:首先匹配 module 的路徑,而後匹配 module 以前所在 chunk 的 name。

好比咱們想把全部 node_modules 中引入的模塊打包成一個模塊:

vendors1: {
    test: /[\\/]node_modules[\\/]/,
    name: 'vendor',
    chunks: 'all',
  }
複製代碼

由於從 node_modules 中加載的依賴路徑中都帶有 node_modules,因此這個正則會匹配全部從 node_modules 中加載的依賴。

test 屬性能夠以 module 爲單位控制 chunk 的抽取範圍,是一種細粒度比較小的方式。splitChunksPlugin 的第二種控制抽取模塊範圍的方式就是 chunks 屬性。chunks 能夠是字符串,好比 'all'|'async'|'initial',分別表明了所有 chunk,按需加載的 chunk 以及初始加載的 chunk。chunks 也能夠是一個函數,在這個函數裏咱們能夠拿到 chunk.name。這給了咱們經過入口來分割代碼的能力。這是一種細粒度比較大的方式,以 chunk 爲單位。

舉個例子,好比咱們有 a, b, c 三個入口。咱們但願 a,b 的公共代碼單獨打包爲 common。也就是說 c 的代碼不參與公共代碼的分割。

咱們能夠定義一個 cacheGroups,而後設置 chunks 屬性爲一個函數,這個函數負責過濾這個 cacheGroups 包含的 chunk 是哪些。示例代碼以下:

optimization: {
    splitChunks: {
      cacheGroups: {
        common: {
          chunks(chunk) {
            return chunk.name !== 'c';
          },
          name: 'common',
          minChunks: 2,
        },
      },
    },
  },
複製代碼

上面配置的意思就是:咱們想把 a,b 入口中的公共代碼單獨打包爲一個名爲 common 的 chunk。使用 chunk.name,咱們能夠輕鬆的完成這個需求。

在上面的狀況中,咱們知道 chunks 屬性能夠用來按入口切分幾組公共代碼。如今咱們來看一個稍微複雜一些的狀況:對不一樣分組入口中引入的 node_modules 中的依賴進行分組。

好比咱們有 a, b, c, d 四個入口。咱們但願 a,b 的依賴打包爲 vendor1,c, d 的依賴打包爲 vendor2。

這個需求要求咱們對入口和模塊都作過濾,因此咱們須要使用 test 屬性這個細粒度比較小的方式。咱們的思路就是,寫兩個 cacheGroup,一個 cacheGroup 的判斷條件是:若是 module 在 a 或者 b chunk 被引入,而且 module 的路徑包含 node\_modules,那這個 module 就應該被打包到 vendors1 中。 vendors2 同理。

vendors1: {
    test: module => {
      for (const chunk of module.chunksIterable) {
			if (chunk.name && /(a|b)/.test(chunk.name)) {
				if (module.nameForCondition() && /[\\/]node_modules[\\/]/.test(module.nameForCondition())) {
                 return true;
             }
			}
	   }
      return false;
    },
    minChunks: 2,
    name: 'vendors1',
    chunks: 'all',
  },
  vendors2: {
    test: module => {
      for (const chunk of module.chunksIterable) {
			if (chunk.name && /(c|d)/.test(chunk.name)) {
				if (module.nameForCondition() && /[\\/]node_modules[\\/]/.test(module.nameForCondition())) {
                 return true;
             }
			}
	   }
      return false;
    },
    minChunks: 2,
    name: 'vendors2',
    chunks: 'all',
  },
};

複製代碼

Long-term caching

Long-term caching 這裏,基本的操做和 Webpack 3 是同樣的。不過 Webpack 3 的 Long-term caching 在操做的時候,有個小問題,這個問題是關於 chunk 內容和 hash 變化不一致的:

在公共代碼 Vendor 內容不變的狀況下,添加 entry,或者 external 依賴,或者異步模塊的時候,Vendor 的 hash 會改變

以前 Webpack 官方的專欄裏面有一篇文章講這個問題:Predictable long term caching with Webpack。給出了一個解決方案。

這個方案的核心就是,Webpack 內部維護了一個自增的 id,每一個 chunk 都有一個 id。因此當增長 entry 或者其餘類型 chunk 的時候,id 就會變化,致使內容沒有變化的 chunk 的 id 也發生了變化。

對此咱們的應對方案是,使用 webpack.NamedChunksPlugin 把 chunk id 變爲一個字符串標識符,這個字符包通常就是模塊的相對路徑。這樣模塊的 chunk id 就能夠穩定下來。

Screen Shot 2018-06-03 at 12.59.28 AM.png | left

這裏的 vendors1 就是 chunk id

HashedModuleIdsPlugin 的做用和 NamedChunksPlugin 是同樣的,只不過 HashedModuleIdsPlugin 把根據模塊相對路徑生成的 hash 做爲 chunk id,這樣 chunk id 會更短。所以在生產中更推薦用 HashedModuleIdsPlugin。

這篇文章說還講到,webpack.NamedChunksPlugin 只能對普通的 Webpack 模塊起做用,異步模塊,external 模塊是不會起做用的。

異步模塊能夠在 import 的時候加上 chunkName 的註釋,好比這樣:import(/* webpackChunkName: "lodash" */ 'lodash').then() 這樣就有 Name 了

因此咱們須要再使用一個插件:name-all-modules-plugin

這個插件中用到一些老的 API,Webpack 4 會發出警告,這個 pr 有新的版本,不過做者不必定會 merge。咱們使用的時候能夠直接 copy 這個插件的代碼到咱們的 Webpack 配置裏面。

作了這些工做以後,咱們的 Vendor 的 ChunkId 就不再會發生不應發生的變化了。

總結

Webpack 4 的改變主要是對社區中最佳實踐的吸取。Webpack 4 經過新的 API 大大提高了 Code Splitting 的體驗。但 Long-term caching 中 Vendor hash 的問題仍是沒有解決,須要手動配置。本文主要介紹的就是 Webpack 配置最佳實踐在 Webpack 3.x 和 4.x 背景下的異同。但願對讀者的 Webpack 4 項目的配置文件組織有所幫助。

另外,推薦 SURVIVEJS - WEBPACK 這個在線教程。這個教程總結了 Webpack 在實際開發中的實踐,而且把材料更新到了最新的 Webpack 4。

對咱們團隊感興趣的能夠關注專欄,關注github或者發送簡歷至'tao.qit####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~

原文地址:github.com/ProtoTeam/b…

相關文章
相關標籤/搜索