本文直接聯動阮一峯blog的文章《使用 Make 構建網站》。javascript
這篇文章在描述javascript構建工具的弱點上,是牽強附會和誇大的。grunt和gulp的問題遠遠沒有文章所述那麼嚴重。html
而文中對make工具,僅有教程式的講述,而缺少對弱點和缺陷的討論。但恰恰原文的make教程就直接暴露了make的若干設計問題。此時若是沒有其餘觀點對make的弱點加以分析,無疑是不全面的。java
我以爲這篇文章從本質上來說,是錯誤和誤導性的,尤爲是對新手開發者而言。Make,包括標準的GNU make及其餘任何仿品,都不該看成爲Web項目的構建工具。node
Makefile
使用大量的隱喻來表述實在的語法意義。python
舉一個最簡單的例子:無參數的make
命令,執行Makefile定義的第一個任務。這就是一個很是很差的語法。這形成了咱們修改Makefile時必須自行記憶:「任務是否排在首位是不同的」。jquery
而這一點在大多數語言中都不存在——例如類的方法寫成什麼順序均可以。在grunt和gulp構建系統中,也都使用default
任務,明確規定無參時默認(default)的行爲。符合語義,無需額外思考(Don't make me think)。web
就連Python這樣的通用語言,都把用__name__
魔術變量標明主流程,做爲編寫腳本的一種建議實踐:正則表達式
def fun1(): pass def fun2(): pass if __name__ == "__main__": fun1(); fun2()
實在的意義,就應當用實在的語法寫清楚,這沒有任何能夠退讓的餘地。隱喻賽過明確,這是make脫不了的原罪。shell
這裏說的例子是那個.PHONY
僞文件。npm
make僅能處理實在的文件依賴文件的關係。但實際構建中,不免出現抽象、不含實體文件的任務,例如clean——人人須要,但不產出實在文件。這時就要把任務表述成文件,而後用
.PHONY
參數告知make哪些文件是假的。
用僞文件,把「任務」替代成「文件」,存在兩點明顯的問題:
.PHONY
列表。.PHONY
中,構建過程就會產生無謂的空文件。空文件自己還不可怕,可怕的是若是沒有及時發現,就會形成一次構建以後不能再次構建,白白消耗調試時間。而對於任何其餘構建方法來講,都根本不存在這個問題。全部其餘構建系統像看怪物同樣,用詭異的眼神鄙視着make。
make提出了僞文件這個東西,而且還在手冊中建議了「僞文件充當任務名」的用法,我相信make的開發者當初必定注意到了這個需求。可是任務(流程邏輯)和文件(內容存儲)畢竟是相關卻不一樣的兩件事,分開管理纔是必然的選擇。
我不清楚make的開發者是沒有想到這一點,仍是自認爲「借用過來‘文件依賴文件’的已有模型更加‘簡潔’」。但結果上看,這個模型的錯誤是本質性且不可修正的。這個實現懶惰、簡陋而不是所謂的「簡潔」,最後的結果也是後患大於收益。
再舉一個例子例子:UNIX聲稱「萬物皆文件」,到頭來還不是爲了避免同設備的邏輯,而保留了「塊文件」、「socket文件」之類的區別?
要替代就替代的聰明一些,把實在、重要的本質邏輯保留住。合理、明確,不迴避客觀區別的替代,和一時拼湊的「workaround」(臨時手段)是兩回事。後者一時使用尚可,但毫不應充看成爲軟件基礎的「萬靈藥」。
考試:請僅用Makefile
語法(不依賴shell特性)寫一個if/elseif/endif試試?
若是要用某種形式描述一個構建過程,其實:
審查Makefile
的本質設計,實際上是一種描述依賴關係的配置文件,描述了「文件依賴文件」和「文件依賴shell代碼」兩種關聯。但恰恰Makefile
也同時提供簡單的流程控制、賦值等語句,使得Makefile
也是一種能夠控制流程走向的程序代碼。
因此Makefile
恰恰落在了配置和代碼二者之間,既不是倒向一端,也不是二者的聯合,最後造成了一個「四不像」的混合品。做爲配置文件寫起來太費神,做爲程序代碼又太簡陋不夠用。
我想問:就從Makefile
的設計上來看,那個被奉爲圭臬(事實上也確實很優秀)的「UNIX哲學」在哪裏?在哪裏?
shell使用各類符號來表達語義,難讀難寫。也就比那個正則表達式簡單點很少。
如下兩段構建腳本,你願意讀、寫或改哪個?
lib_bundle := build/lib.min.js libraries := node_modules/jquery/dist/jquery.js \ node_modules/underscore/underscore.js \ $(lib_bundle): $(libraries) uglifyjs -cmo $@ $^ # What the heck does "c m o @ ^ $" means ???
var gulp = require('gulp'); var concat = require('gulp-concat'); var uglify = require('gulp-uglify'); gulp.task('lib:bundle', function () { return gulp.src(['node_modules/jquery/dist/jquery.js', 'node_modules/underscore/underscore.js']) .pipe(uglify()) .pipe(concat('lib.min.js')) .pipe(gulp.dest('build/')); });
shell是一個嚴重依賴系統環境的工具。一個make可以正確調用shell腳本,通常都須要:
Makefile
中用到的變量衝突更可怕的是以上這些要素,基本上都是隱喻性的。沒有明確的版本控制手段去保證不說,甚至連確認都是不現實的。之前能用的腳本可能換個發行版、升級個系統,甚至於換個用戶就可能會發生問題。
咱們既然已經有了npm版本控制,更況且shell構建本質上也是調用基於node的工具,那咱們爲何還要去踩shell缺少版本控制這個坑?
shell調用js,每個命令都須要啓動/中止node進程,而且各個工具是順序執行的。
而node構建工具,只須要使用同一個node進程,而且各個工具能夠異步啓停、並行運行。
這個效率區別是不須要具體比較的。
從歷史上來看,shell原本就是爲了方便人類執行命令的小工具。然後人類發現了自動執行命令的便利性,從而將shell擴展成爲一門輕量的腳本語言,這個發展歷程是能夠理解的。
其實簡短的shell腳本也能夠大大方便人類的工做,是個好用的工具。但是一旦shell腳本龐大起來,shell不適合自動化運行和大型程序管理的各類硬傷就開始暴露:
[
。shell從本質上,是方便人類手打命令的終端軟件,而不是可靠的自動化工具。本質如此,未來就會一直如此。shell就是shell,也一直只會是shell,不該當賦予其過深的責任和負擔。
除非①沒有更好的選擇②工做實在太少太簡單,不然永遠不要在正式項目中依賴shell自動化。
必須認可:shell與make工具備很多「坑」,但一旦調試良好,它們確實可以穩定運行。而且工程師們常常會產生這種心態:解決的問題越難,填平的「坑」越多,最後成功時的成就感就越強。
這是一個思惟陷阱。這個陷阱中用過程的複雜度替代了需求的複雜度,從而容易讓人錯誤的評判和看待本身的工做。
可工程畢竟不是智力題。實際需求才是惟一的,只有需求自己的複雜度才須要尊重。代碼只不過是完成任務的一種副產品。代碼量越少越好,代碼引入的額外複雜度越低越好,代碼維護起來越容易越好。至於代碼自己解決了多少難題,適配了其餘工具多少的「坑」,通常都不值一提。
作黑客自有作黑客的合適場景,就如同業餘時間作點智力題實際上是個不錯的愛好。但實際工程環境下,請老老實實作工程師,使用簡單的工具解決同等的問題,不要炫技。
請使用npm的構建工具。我推薦目前(成文時)仍處於測試狀態的Gulp 4。
Gulp 4最讚揚的地方是引入了簡潔明確的語法,規定命令之間的串並行關係。今後能夠把任意形狀的加權森林(權值表明執行順序)簡單地表示成Gulp的代碼:
gulp.task('default', gulp.series('clean', 'build', 'deploy')) gulp.task('clean', gulp.parallel('clean:a', 'clean:b')) gulp.task('build', gulp.parallel('less', 'uglify')) gulp.task('deploy', gulp.series('revision', 'copy'))
Gulp 4入門請通讀《Gulp 4.0 前瞻》這篇文章,以及Gulp 4源代碼目錄中的全部recipes
(參考代碼),很是容易。
Gulp也有Gulp、Node和JavaScript的麻煩(例如並行代碼的編寫不良通常不會報錯),但起碼在Web構建這個環境下,比shell值得擁有。
若是你真的須要一些命令行的工具,那也應該捨棄shell這一層,在js、python等正常語言環境下調用它們。命令行工具是不須要shell的,啓動子進程而且傳遞argc、argv的參數纔是本質。
原創發表在 SegmentFault.com 博客,轉載請遵照 SegmentFault 相關規定(見頁腳),做者爲沙渺 sha@miao.im。