2015 JS構建工具狀態

麥兜的坑發表於2015-10-13

因為將要開始的一個專案中會使用AngularJS,所以我最近一直在檢視一些JavaScript的構建工具。我需要一個構建工具來編譯、打包和壓縮我的指令碼檔案和樣式表。檢視這些工具的另外一個原因是 Visual Studio 2015 中將增加對 Grunt 和 Gulp 這類任務執行器的支援。

我的探索之旅開始於一個有用的腳手架工具,Yeoman為我建立的 Gruntfile.js 檔案。這個檔案預設的配置使用 SASS處理 CSS,然而我更傾向於使用LESS。所以,我開始深入地研究有什麼替代方案。當時,這個Grunt配置檔案被我認為是一個相當典型的構建配置檔案。它非常大,有超過450行程式碼。進一步深入發掘,我發現了一些不喜歡的東西。這導致我去評估其他的替代工具。從 Grunt 離開,我開始檢視下一個非常流行的構建工具 Gulp。接著,我研究了較為常見的 Broccoli.js 和 Brunch。我粗略地考察過名不見經傳的 Mimosa 和 Jake,但是在它們的網站上,我沒有發現任何引人注目的東西足以讓我優先考慮它們。這篇博文將詳細描述我的發現。

評判標準

在考察這些JavaScript構建工具時,我使用了特殊的視角並帶著一些期望。我是一個使用Visual Studio進行.NET網路應用開發多年的程式設計師。在這之前,我使用過Java和C++以及很多IDE。我使用過許多各種不同的構建工具,包括make、 msbuild、 rake、 NAnt 和 psake。這些使用經驗讓我更傾向於具有約定大於配置特性的優雅工具。對我來說,一個構建工具的存在讓我專注於我的專案工作,而構建工具應該在專案之外。我應該花最少的時間來配置和除錯我的構建。大多數IDE在這方面都是不錯的。你新增一個檔案,而IDE根據副檔名做正確的事情(唯一要做的只是偶爾設定一些選項)。

作為一個職業網路應用開發工程師,我期待有這樣一種專注於Web的構建工具。它能夠為眾多複雜的現代Web App功能提供支援。這正是Visual Studio走向衰敗的地方。這也是對構建工具來說有趣且獨一無二的需求。傳統的構建工具,比如Make,幾乎不會面臨這樣的情況。編譯一個C++應用基本上就是找到原始碼、編譯和連結。不會有比這更多的工作了。對於網路應用來說,有許多不同型別的原始檔,這些原始檔必須通過各自漫長的轉換和組合流程。而且,構建的結果不是一個簡單的可執行檔案,而是一個許多檔案的聚集。這使得清理檔案的工作非常重要。最後,開發工程師對構建工具的期望也與日俱增。以前,我們很高興在執行Make命令之後得到一個可執行檔案。而現如今,我們希望能觸發增量重構建的檔案監控和實時瀏覽器重載入的特性。

這裡有一個簡明的步驟、任務列表,這是我認為在大多數JavaScript網路應用構建過程中應該存在的。當然,這些要點會因為專案不同而不同,也會隨開發人員的喜好改變。

  • 轉換翻譯為JavaScript:CoffeScript、Dart、Bable、Traceur等。
  • JavaScript轉換:打包成模組或者ng註解等。
  • 構建、連線:將指令碼和樣式表檔案合併成多個檔案。
  • 壓縮:指令碼、樣式表和HTML。
  • 原始檔對映:指令碼和樣式表檔案都需要。
  • CSS前處理器:Less、 Sass、 Stylus等。
  • 樣式轉換:Autoprefixer、 PostCSS等。
  • 快取失敗:用hash值來重新命名檔案以避免不正確的快取。
  • 影像優化
  • 編譯模板:Mustache或者HandlebarsJS、 Underscore、 EJS、 Jade等。
  • 拷貝資源:HTML、 Fav圖示等。
  • 監控變更
  • 增量編譯
  • 清除構建:在開始構建前刪除所有檔案或者按需刪除。
  • 依賴注入:從捆綁的檔案中注入指令碼和樣式標籤。
  • 構建配置:分離開發、 測試和生產環境的配置,比如在開發構建中不用壓縮HTML檔案。
  • 服務:啟動開發伺服器。
  • 執行單元測試
  • 執行 JavaScript 和 CSS 檢測:jshint、 csslint等。
  • 依賴管理:使用npm和bower,browerfy等管理專案依賴。

發現

所以我發現了什麼?在所有的構建工具中都存在一些共同的不足之處。其中之一就是,認為對 npm 和 Bower 包進行恢復的工作在構建範圍之外。與諸如Visual Studio這樣的IDE不同,IDE會在需要的時候自動執行 Nuget 進行包恢復,沒有一個JS構建工具試圖去做這件事。這通常沒法繞開,因為構建工具需要專案工程中的一個npm包,構建任務只有當這個包被還原後才會執行。許多構建工具的確有外掛支援 Bower 包還原。然而,我看到的大多數例子都是假定在構建開始前會先執行 npm install 命令然後是執行 bower install。他們甚至沒有用 npm Install 來觸發 bwoer install。另外一個不足是,所有構建工具對依賴的處理。這完全不是構建工具的問題,因為JavaScript生態系統一直在努力解決這個問題。顯而易見的是,npm 包在 Node.js 中工作良好 (儘管node_modules中建立重複包和深層目錄不在我的考慮範圍之內)。但是前端包並沒有被很好地考慮到。與程式碼庫不同,一個前端開發包中有許多不同語言寫成的檔案,每個檔案又有自己引用其他檔案的方法。除此之外,根據專案使用的工具箱,構建可能在編譯前或者編譯後需要依賴包。舉例來說,一些專案需要 Bootstrap 的 Less 檔案,但是其他的專案需要編譯好的 CSS 檔案。所以,如何表示和控制這些資原始檔,以一種什麼樣的方式融入專案中呢?如果你問我,我會說 Bower 不是解決方案(至少現在不是)。引用一個前端開發包需要變成和在.NET中引用一個程式集或者Nuget包一樣容易。現如今,配置包括依賴包的構建總是很痛苦。帶著對JavaScript構建工具共同的困境,我們現在輪流看看這些構建工具。

Grunt

對許多剛開始接觸JS構建工具的人來說,Grunt是一個不錯的開始。Grunt絕對是應用最廣泛的構建工具。然而,GruntJS.com 卻自豪的宣稱Grunt是“JavaScirpt任務執行器”,我也認為這是一個不錯的說明。Grunt幾乎不是一個合格的構建工具。更確切的說,它是一個序列執行命令列或者任務系統。每一個Grunt任務都是一個外掛,用來代表一個可能從命令列執行的工具。每一個外掛都在 Gruntfile 中定義了必須配置和可選擇配置。固定部分的命名意味著,你不能按自己的意願來組織或者命名這個部分。雖然外掛的配置有很多共性的部分,但是實踐中你必須強迫自己去閱讀每個外掛的文件,以弄清楚如何配置它。另一方面,這也意味著可被接受的文件經常被質量差、意義含糊不清的獨立外掛文件破壞。因為每個外掛基本上是一個命令列工具,所以它們都接受一些輸入檔案並有一些輸出檔案。為了把過長的構建管道連線起來,通常需要一個外掛去管理超出其範圍的一些中間臨時檔案。檔案監控和實時重新載入是手工配置一些任務,監控符合檔名規則的檔案是否被修改。伺服器端配置對我來說還是完全不透明的。增量構建必須通過手工配置 grunt-newer 來實現。實際上,這意味著對每個任務如何使用各種型別的收入檔案,以及和哪些臨時檔案需要被重新構建要有深刻的理解。所有這些手工配置都導致了巨大的、不易理解的配置檔案。如果你僅需要執行少量任務,Grunt對你來說是合適的。如果你需要一個真正的網路應用構建工具,我強烈建議你去別處看看。

Gulp

Gulp是比較熱門的新工具。它迅速獲得了大家的青睞是有原因的。它認識到並嘗試著去解決網路應用構建工具獨一無二的需求,即長構建管道。在Gulp中,所有的外掛以流的方式工作(好吧,時至今日依然有很多外掛不是以流的方式工作,這非常煩人)。這種流模式使得Gulp的任務可以連結在一起而不必擔心產生臨時檔案。Gulp鼓吹它的高效能是基於流模式和並行任務執行。是的,很多使用者已經抱怨並行任務執行讓人很困惑,並且有計劃去擴充套件配置以便獲得對並行任務的更多控制。個人觀點,我覺得並行任務執行非常的直觀,同時我覺得提出的解決方案比當下的問題更糟糕。

雖然流是一個構建工具吸引人的基礎,但是最終我還是發現了 Gulp 的不足之處。雖然核心配置是極小的,其目的是需要極少文件,但是我發現文件是不合適的。像 Grunt 一樣,它需要依賴那些配套文件很爛的外掛。相比Grunt,這種情況一定程度上由外掛使用的一致性而得到了緩解。Gulp 的作者們信仰固執軟體( opinionated software )和強配置軟體。通常情況下,我是很欣賞固執軟體的,但是在這種情況下,我必須反對作者的觀點。 他們認為一個已經支援流的工具不應該再被包裝成一個外掛,而應該直接使用。這就違背了構建中必須儘可能簡單配置的原則,因為使用者必須學習和適應工具獨一無二的 Gulp 系統 API。看一看由此引起的混亂,檢視在GitHub上關於 gulp-browserify 外掛問題的黑名單。可以看到,許多使用者試圖去找到 Browserify 的正確使用方法。我不認為有任何嘗試找到了官方提供的方法。但是請記住,即使這樣也沒有成功地在 Browerify 中支援流檔案。這個問題太過複雜以至於需要一整篇優秀但是冗長的博文來解釋。除此之外,對於那些不支援它的事物來說,流模式成為比較混亂的東西,比如清理。原始碼對映某些時候也很奇怪。我曾經試圖將外部原始碼對映與帶有 manifest 檔案的快取清理結合在一起。雖然最終的配置十分簡短,但花費了我幾個小時和適應額外的外掛才能工作。監控依然需要手工配置,增量構建的配置非常晦澀。Plethora的外掛。最後,當我自己不理解問題的時候,我發現了許多關於需要做額外的配置才能讓Gulp顯示有意義的錯誤訊息的討論。

Broccoli

Broccoli.js是一個較新的構建工具,已經在工作中使用過一段時間了。它目前對我還沒有太大的吸引力,但是希望 ember 命令列工具能帶來改觀。我真的很喜歡這個工具,開發者考慮到了一個JavaScript構建工具所面臨的大多數問題。通過自帶檔案監控和增量構建特性,Broccoli可以自己決定監控哪些檔案,並且只在需要重新構建的時候重新構建。它通過一套構建在檔案樹核心抽象上的複雜快取系統來實現這個特性。樹結構是Broccoli 對 Gulp 流模式的回應,並可以輕易實現連結。為了簡化系統,Broccoli 並不像 Gulp 一樣並行執行任務,並宣稱這不是必要的,因為它們的效能一直不錯。我強烈推薦你去看看作者的博文“與其他構建工具的比較” (章節5)。

儘管擁有這些優異的特性,Broccoli還處於beta階段,不是很成熟。它在文件方面比較欠缺。說明檔案中直接說明對Window的支援不夠穩定。事實上不支援原始碼對映是我最頭疼的地方。它看起來還沒有為自己偉大的時代做好準備,但是我真的很喜歡看到它成熟並最終替代掉 Grunt 和 Gulp 的一天。

Brunch

Brunch顯然已經存在多時,但還是很低調、神祕。Brunch主要依賴配置中的約定,並假設你的構建管道包括了我上面說的幾乎所有的步驟。當你加入一個外掛,被認為加入到了管道中合理的位置,而需要很少甚至不需要任何配置。這意味著一個配置檔案在使用 Grunt 時需要600多行程式碼,使用 Gulp 的時候只需要150多行,如果使用Brunch的話,最多需要20行。Brunch 使用 CoffeeScript 編寫,它的所有示例也用CoffeeScript編寫,但是轉化為JavaScrpt並不難。它天生就支援監控和增量編譯而且不需要任何的配置。Brunch可以通過不同的配置直接進行開發vs、生產構建(其他構建配置也很容易設定)。它甚至可以自動包含JavaScript模組(可配置)。

終上所述,你也許覺得我們找到了解決JavaScript構建之道。然而,總會有問題存在。不考慮文件和手冊的長度,我還是發現自己希望能找到更清晰的解釋和更多的示例。我很吃驚的發現,沒有辦法去清除已經完成的構建。希望因為我發現了這個問題會把它修復。我還在嘗試找到正確的方式使用 Brunch 執行單元測試。對我來說,真正的問題是Node和Bower包管理。因為 Brunch 試圖更深入的管理你的程式碼,甚至將JavaScript程式碼包裝成模組,但這不能很好的與包一起工作。他們宣稱對 Bower 提供支援,但是沒有很好的說明文件而且似乎結果也不對。引起這一切的也許是 Brunch 在早期廣泛地使用了 NPM 和 Bower,現在他們正努力嘗試解決這些問題。同時,要準備好應對 Brunch 和 Browerify 的拙劣設計的補丁。最後,對於那些專注於約定大於配置系統的普遍現象,如果你已經偏移設定好的路線,也許會有麻煩。舉例來說,如果我試圖編譯引用這些影像的模版,還在不斷嘗試如何正確地將快取失敗應用於像影像這樣的資源。

總結

我對JavaScript構建工具在2015的發展態勢表示失望。它們都很不成熟,而且沒有從其他環境和平臺的構建工具長期發展的歷史中得到啟示。另外,這些工具的作者沒有好好坐下來去真正地理解所遇到的問題,從而修復問題和確保他們的工具是容易使用的。只有其中的兩個,Brunch 和 Mimosa似乎設定了目標,要使絕大多數構建變得十分容易並且不需要繁冗的配置。

所以,我會推薦哪個構建工具呢?個人觀點,我還在使用 Brunch 以便確認它能否為我的專案工作。它應該現在處於我的候選列表首位(如果你喜歡約定大於配置方式的 Brunch,你也可以看看 Mimosa)。然而,如果你不介意大量笨拙的配置,你也許會挑選 Gulp 甚至 Grunt,因為它們有更強的社群支援和更多的外掛(雖然我還沒有在尋找我需要的 Brunch 外掛方面遇到困難)。我真的對 Broccoli 的發展有很大興趣,也希望它能在未來變成一個可行的選擇。最後的回答,似乎當今沒有一個符合要求的工具存在。一旦包依賴被正確的移除,我想我們會發現現存的所有工具都沒有了意義。當然,沒人知道到達這個狀態需要多久的時間。

相關文章