做者 Daniel 螞蟻金服·數據體驗技術團隊javascript
Webpack 4 發佈已經有一段時間了。Webpack 的版本號已經來到了 4.12.x。但由於 Webpack 官方尚未完成遷移指南,在文檔層面上還有所欠缺,大部分人對升級 Webpack 仍是一頭霧水。前端
不過 Webpack 的開發團隊已經寫了一些零散的文章,官網上也有了新版配置的文檔。社區中一些開發者也已經成功試水,升級到了 Webpack 4,而且總結成了博客。因此我也終於去了解了 Webpack 4 的具體狀況。如下就是我對遷移到 Webpack 4 的一些經驗。vue
本文的重點在:java
這裏以 Vue 官方的 Webpack 模板 vuejs-templates/webpack 爲例,說說 Webpack 4 以前,社區裏比較成熟的 Webpack 配置文件是怎樣組織的。node
大體的目錄結構是這樣的:react
+ build
+ config
+ src
複製代碼
在 build 目錄下有四個 webpack 的配置。分別是:webpack
這分別對應開發、生產和測試環境的配置。其中 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 通常須要作這些事情:
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 這個版本的 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 只有開發和生產兩種,測試環境應該是屬於開發階段。
在 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。
Webpack 4 下還有一個大改動,就是廢棄了 CommonsChunkPlugin,引入了 optimization.splitChunks
這個選項。
optimization.splitChunks
默認是不用設置的。若是 mode 是 production,那 Webpack 4 就會開啓 Code Splitting。
默認 Webpack 4 只會對按需加載的代碼作分割。若是咱們須要配置初始加載的代碼也加入到代碼分割中,能夠設置
splitChunks.chunks
爲'all'
。
Webpack 4 的 Code Splitting 最大的特色就是配置簡單(0配置起步),和__基於內置規則自動拆分__。內置的代碼切分的規則是這樣的:
簡單的說,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 這裏,基本的操做和 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 就能夠穩定下來。
這裏的 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('####', '@'),歡迎有志之士加入~