如今對於每一個前端工程師來說,webpack 已經成為了一項基礎技能,它基本上包辦了本地開發、編譯壓縮、效能優化的所有工作,從這個角度上來說,webpack 確實是偉大的,它的誕生意味著一整套工程化體系開始普及,並且慢慢統一了前端自動構建的讓前端開發徹底告別了之前的刀耕火種時代。現在 webpack 之於前端開發,正如同 gcc/g++ 之於 C/C++,是一個你無論如何都繞不開的工具。
但是,即使它如此偉大,也有一個巨大的問題,那就是 webpack 實在是太難用了!!!
我從多年前的 webpack 1.0 時代就一直在用它,現在也不能說完全掌握了它,很多時候真的讓我產生了懷疑,究竟是因為我的能力不足,還是因為 webpack 自身的設計就太難用?隨著我接觸到越來越多的前端專案,聽到越來越多的吐槽,我也越發地相信,是 webpack 自身的問題,導致它變得如此複雜又難用。
舉個簡單的例子,一個 vue-cli 生成的最簡單的腳手架專案,開發、構建相關的檔案就有 14 個之多,程式碼超過 800 行,而真實的專案只會比這個更多:
所以,既然這篇文章的標題是《webpack 為什麼這麼難用?》,那我們就好好在這裡分析一下,webpack 難用的根本原因。
一、文件極其不完善
是的,這就是第一位的原因。
我作為參加過 webpack 中文文件翻譯的人,真的想說 webpack 即使經過了這麼多年的不斷迭代,如今的文件依然還是是一坨那啥。作為一個開源專案,設計好不好、易用性怎麼樣、擴充套件性怎麼樣這些問題都是仁者見仁智者見智的,但文件寫得很爛這一點上,真的沒有任何可以開脫的理由。
對於使用者的不友好
比如,webpack 的外掛體系可以說是 webpack 最核心的一部分功能了,基本上一個專案的構建中,大部分任務都是由各種外掛完成的。然而,官方文件上對於外掛的介紹只有寥寥幾句話:webpack · Plugins,甚至推薦你直接去看 webpack 的原始碼:
更糟的是,現有的文件裡(包括 webpack 一些外掛的文件也是),大部分內容都是在告訴你 “你這樣做就可以了”,而沒有解釋 “你為什麼需要這麼做” 以及 “你這麼做了會有哪些後果”。
比如,在 target 配置上,官方文件裡列舉了你可以構建到哪些 target,如 node
、node-webkit
、electron-main
,但都只是簡單的一句話帶過:
想知道 target 為 electron-main
時,和瀏覽器環境的打包有什麼不同?對不起,官方文件不想告訴你,看原始碼或者去 stackoverflow 上搜吧。
官方文件語焉不詳的直接後果就是,當你遇到了任何問題,你都沒辦法在文件裡得到直接的回答,而是需要看無數的程式碼、github issue、stackoverflow、部落格文章,然後在自己的專案裡反反覆覆試了好多次,才能大致解決問題。而這種所謂的“解決問題”,一般都是個人經驗性的,意味著其它任何一個人想要解決這個問題,都要重複一遍這個流程,時間成本大量上升。
這就是為什麼使用 webpack 的時候,經常會出現下面的哲學三問:
- 這是 webpack 的問題嗎?
- 我要怎麼解決這個問題?
- 咦我是怎麼解決的?
對於開發者的不友好
我們要如何開發一個 webpack 的外掛?
官方文件裡確實寫了一些關於如何開發外掛的指南。但這份指南也只有 60 分剛及格的水平,它確實向你介紹了 webpack 外掛的基礎範例、基本概念以及一些 API,但當你讀完這份簡短的文件後想自己真的去開發一個外掛時,你會發現文件裡講的東西真的遠遠不夠。
我們不妨來看看現在 webpack 生態裡那些成熟的外掛是怎麼寫的,以 html-webpack-plugin 為例,這是一個廣泛用於生成 html 檔案的外掛。在它的原始碼裡你會發現,它引用了五個 webpack 內部自帶的外掛(原始碼在這裡):
var NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
var NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
var LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
var LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
var SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
複製程式碼
嗯哼?這五個外掛都是用來幹什麼的?
官方文件上對內建的外掛一個字都沒有提及,是的,甚至連 Plugins 這裡都沒有。官方的 wiki 上倒是寫了,但真的真的太簡略了,而且看起來很久沒更新了。
再看另外一個同樣常用的 uglifyjs-webpack-plugin,它倒是沒依賴 webpack 的內建外掛,不過也引用了 webpack 內部的兩個檔案:
import RequestShortener from 'webpack/lib/RequestShortener';
import ModuleFilenameHelpers from 'webpack/lib/ModuleFilenameHelpers';
複製程式碼
文件裡同樣沒有對這兩個檔案做任何介紹。令人欣慰(?)的是,這兩個檔案從檔名上看,起碼是方法庫(實際上也確實是),使用起來不會太複雜。
換句話說,如果你想給 webpack 寫一個廣為人知的外掛,你就必須深入瞭解 webpack 的全部,這一點我不反對,畢竟 webpack 開發者和 webpack 使用者在能力的要求上有高低之分。但即使是有經驗的開發者,遇到一個文件如此不完善的開源專案,也是很吃力的。很多能幫助開發者的東西本應該正大光明地寫在文件和指南里,而不是隱藏在原始碼裡。
二、過重的外掛體系
外掛體系是 webpack 的核心,事實上,webpack 的大部分功能都是通過內部外掛或者第三方外掛來完成的。可以說,webpack 的生態就是建立在眾多外掛之上的。
但外掛體系也同樣有很多問題。
外掛數量問題
先問一個問題,一個通過 webpack 構建的專案需要多少外掛?
還是以一個標準的 vue-cli 生成的腳手架專案為例,一共有 7 個第三方外掛:
"copy-webpack-plugin": "^4.0.1",
"extract-text-webpack-plugin": "^3.0.0",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"webpack-bundle-analyzer": "^2.9.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"uglifyjs-webpack-plugin": "^1.1.1",
複製程式碼
以及 7 個 webpack 內建外掛:
HashedModuleIdsPlugin
ModuleConcatenationPlugin
CommonsChunkPlugin
DefinePlugin
HotModuleReplacementPlugin
NamedModulesPlugin
NoEmitOnErrorsPlugin
總共 14 個外掛,我們按照平均一個外掛含有 2-3 個配置項(這已經是往低了算了)來計算,14 個外掛就有 30 多項配置,這已經是一個現代 webpack 開發、構建使用的很基礎的配置了,真實的專案只會比這個更多。
要注意到,30 多個配置項帶來的複雜程度是遠勝於 30 行程式碼的。 因為配置項已經具有了比較高的抽象性,一項配置包含的副作用是要遠大於一行程式碼的。比如下面是常常用於提取公共模組的 CommonsChunkPlugin
的配置:
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
})
複製程式碼
如果你不是一個 webpack 老手的話,看到這 4 項配置肯定是一臉懵逼的:
name
該填什麼?隨便命個名就好嗎?async
是什麼?非同步模組?那為什麼是個字串?children
是個啥?為什麼不是Array
而是個boolean
?minChunks
這個數字是什麼?chunk
又是什麼?
然後你就去看了 CommonsChunkPlugin 的文件,十五分鐘艱難的閱讀之後,你會發現這四項配置都不簡單,每一項的更改會給構建帶來很大的影響。
然而壞訊息是,像這樣的配置在專案裡整整有 30 多處!
所以我每次改一個專案的構建時,基本都是這樣的:
面向配置的外掛
在討論這個話題之前,先回答兩個問題:
-
webpack 的外掛先後順序會影響構建結果嗎?
-
如果外掛順序不同,會影響哪些東西?
實際上,這兩個問題我找遍了官方文件,也沒有提到外掛的順序會影響哪些東西,stackoverflow 上倒是找到了一個問題:Webpack: Does the order of plugins matter?
所以回答就是:外掛的順序有影響,但作用不明。
其實問題不止在外掛的順序先後上,就連一個外掛到底對構建產生了哪些影響,我們也很難得知,除非你極其熟悉這個外掛或者就是這個外掛的作者。為什麼會這樣?根本原因就是,webpack 的外掛是面向配置的,而不是程式導向的
什麼叫程式導向?如果你知道或者使用過 gulp 這個自動化工具的話,應該會記得 gulp 管道的概念,即從源頭那裡得到源資料(js/css/html 原始碼、圖片、字型等等),然後資料通過一個又一個組合起來的管道,最後輸出成為構建的結果。寫成虛擬碼的話,大概是這樣:
gulp.src('某些原始檔')
.pipe(處理一)
.pipe(處理二)
.pipe(處理三)
.dest('構建結果')
複製程式碼
這種管道化,或者說程式導向的構建,非常容易 debug 或者修改,因為它構建的每一步過程,都整齊的按照順序展示給你看了。想要修改其中任何一步的心智負擔是很低的,因為它的處理機制非常純函式。
然而如果是 webpack 的話,就類似這樣:
{
plugins: [
外掛一,
外掛二,
外掛三
]
}
複製程式碼
這裡,外掛一二三是完全面向配置的,沒有告訴你任何執行順序,它們可能會在 webpack 構建的每個時間點觸發,你只能從它們的功能上大致猜出它是在哪個時間點工作的。這就是為什麼修改一些 webpack 的配置,就像要解開一條放在包裡很久的耳機線一樣,麻煩又鬧心。
當然,這種配置化的外掛也是有好處的,配置化代表了高整合度,當你只有 1-3 個外掛時,維護這些配置的心智負擔是可以接受的,並且比維護程式導向的配置更加方便。但當外掛數量超過這個值的時候,構建的複雜程度就會呈指數式上升,我們之前就已經提到了,一個現代的 webpack 專案起碼會有 14 個以上的外掛以及至少 30 多項配置,這種情況下,程式導向就會好於面向配置,這就是為什麼我一直覺得 gulp + webpack 才是正確解決方案的原因。
當然還是要說一句,gulp 和 webpack 並不能直接比較,前者是一個 task runner,而後者是一個 module bundler,它們兩者之間都有一些相互不可替代的功能。
三、配置化是銀彈嗎?
在日常業務中,特別是大公司的一些運營性質的業務裡,我們常常會看到 “某某業務已經實現完全配置化” 這樣的字眼,在這個語境裡,配置化代表了低維護成本、高靈活性、高封裝性。
在技術的世界裡,配置化也同樣是個好東西,很多工具都會宣稱自己是完全配置化的,只要你的專案里加入一個配置檔案,那麼這個工具就可以幫你做很多很多的事情,babel、eslint、stylelint,還有本文討論的 webpack 都是如此。
所以配置化是不是就是所有工具進化的終點了呢?它是不是能解決所有的問題呢?
軟體工程上有一句耳熟能詳的話:“沒有銀彈”,指的是複雜的軟體工程問題無法靠簡單的答案來解決。在前端工程構建這個問題上,也同樣不例外。
如何解決前端工程的構建?webpack 給出的答案是:通過 webpack + loader + plugin,讓一切資源構建可配置。 這在它誕生的那個時代看來,是非常厲害的,一份簡單的配置檔案就幫你搞定了所有資源構建的問題。
但是當時間的推移,一個前端專案的構建變得越來越複雜,webpack 的配置也越來越多,維護起來越來越難,這個時候,也就慢慢誕生了諸如 create-react-app、vue-cli 這樣的腳手架工具,在 webpack 的基礎上進一步封裝,來幫你自動生成 webpack 的配置。這個時候,webpack 更多地變成了一個“底層”工具,而這些腳手架才是你實際上的“構建工具”,或者說,這些腳手架提供的配置,才是你真正的構建配置。
為什麼會這樣?
這個問題的根源在於,webpack 現在提供的配置的封裝性已經不夠了,它面對一個如今複雜得多的大型前端工程,僅有的配置已經沒辦法像幾年前那樣為我們遮蔽掉大部分的構建細節了,所以在它的基礎上誕生了如此多的腳手架工具幫我們進一步封裝複雜性。
所以我們現在可以回答這一段的標題了:配置化是解決複雜度的銀彈嗎?當然不是,因為配置會隨著複雜度的提升,而也逐漸變得複雜,維護越來越難,直到超過某個臨界值,就會需要在它的基礎上進一步封裝,產生新的配置化。
四、前端工程構建的未來
正如我在上一章所說的,隨著複雜度的上升,需要不斷地封裝複雜性,以讓維護配置的心智成本降到可以接受的程度。而在前端構建工具上,截止到趨勢也正是如此:
- 前端的遠古時代我們不需要構建,因為這時的前端專案還很簡單,原始的 html/js/css 就足以應付需求,手工處理這些資源方便又快捷。
- 隨著前端的複雜化,手工處理的效率越來越低,grunt、gulp 這樣的自動化工具就誕生了,它們遮蔽掉了很多資源處理的細節問題,讓資源的處理可以自動完成。
- 隨著構建流程越來越多、資源種類越來越多、ECMAScript 的語言特性愈加複雜、開始區分開發/測試/生產環境等等因素,gulpfile/grunt 這樣的工具已經不能滿足我們的需求,我們需要的是一整套完整的配置化的構建方案,而 webpack 就是這樣一種方案。
- 隨著 webpack 配置越來越複雜,維護成本也越來越高,於是誕生了很多腳手架工具,幫你生成 webpack 的配置,封裝起 webpack 的複雜性。
那麼未來的下一代前端構建工具是怎樣的呢?
現在廣泛使用的這些腳手架工具,終究依賴的是 webpack,我們實際上需要的是整合度更高、封裝性更高(甚至零配置)的構建工具。更詳細地說,下一代前端構建工具,必然會有下面的某些特性:
- 內建的功能更多,比如自帶 babel、dev-server、HMR、sourceMap 等等功能;
- 配置更少,甚至零配置;
- 更低成本區分開發、測試、生產環境;
- 效能更好,整合冗長的構建流程,支援多核 CPU 等;
- 對於新型模組的支援:非同步模組、WebAssembly 模組等。
事實上,這也就是部分 webpack 4.0 將會有的新特性,以及前段時間看到的 parcel 也具有其中的某些特點(雖然它現在看起來還很不成熟)。未來這樣的構建工具只會越來越多。
總結
這篇文章很久之前就在構思了,只是近期在工作上集中遇到了很多 webpack 的坑,讓我徹底有動力來吐槽一下它的種種不是。
webpack 為什麼這麼難用?本文給出的答案濃縮起來就是兩點:
- 文件不完善,導致使用者和開發者遇到問題都很難下手;
- 專案需要使用的外掛數量太多,且面向配置,導致維護成本指數級上升。
這些問題未來會有改善嗎?當然。其實,這篇文章其實有標題黨的嫌疑,更準確的標題應該是:
《現在的 webpack 為什麼這麼難用?》
因為這篇文章裡提到的問題,都會在 webpack 4.0 中得到改善。
額……至於它的文件嘛……算了不提了不提了 O__O "…