爲何用 Make 構建網站絕對不是一個好主意

本文直接聯動阮一峯blog的文章《使用 Make 構建網站》。javascript

這篇文章在描述javascript構建工具的弱點上,是牽強附會和誇大的。grunt和gulp的問題遠遠沒有文章所述那麼嚴重。html

而文中對make工具,僅有教程式的講述,而缺少對弱點和缺陷的討論。但恰恰原文的make教程就直接暴露了make的若干設計問題。此時若是沒有其餘觀點對make的弱點加以分析,無疑是不全面的。java

我以爲這篇文章從本質上來說,是錯誤和誤導性的,尤爲是對新手開發者而言。Make,包括標準的GNU make及其餘任何仿品,都不該看成爲Web項目的構建工具node

make:看似簡單實則簡陋

隱喻勝於明確

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

醜陋的「workaround」(臨時手段)

這裏說的例子是那個.PHONY僞文件。npm

make僅能處理實在的文件依賴文件的關係。但實際構建中,不免出現抽象、不含實體文件的任務,例如clean——人人須要,但不產出實在文件。這時就要把任務表述成文件,而後用.PHONY參數告知make哪些文件是假的。

用僞文件,把「任務」替代成「文件」,存在兩點明顯的問題:

  1. 「任務」與「文件」的命名空間直接產生衝突。
    任務佔用的名稱,就必須人工注意實在文件不能再用,不然必然產生麻煩。
  2. 增刪任務時,必須人工注意維護.PHONY列表。
    若是忘了把任務補到.PHONY中,構建過程就會產生無謂的空文件。空文件自己還不可怕,可怕的是若是沒有及時發現,就會形成一次構建以後不能再次構建,白白消耗調試時間。

而對於任何其餘構建方法來講,都根本不存在這個問題。全部其餘構建系統像看怪物同樣,用詭異的眼神鄙視着make。

make提出了僞文件這個東西,而且還在手冊中建議了「僞文件充當任務名」的用法,我相信make的開發者當初必定注意到了這個需求。可是任務(流程邏輯)和文件(內容存儲)畢竟是相關卻不一樣的兩件事,分開管理纔是必然的選擇。

我不清楚make的開發者是沒有想到這一點,仍是自認爲「借用過來‘文件依賴文件’的已有模型更加‘簡潔’」。但結果上看,這個模型的錯誤是本質性且不可修正的。這個實現懶惰、簡陋而不是所謂的「簡潔」,最後的結果也是後患大於收益。

再舉一個例子例子:UNIX聲稱「萬物皆文件」,到頭來還不是爲了避免同設備的邏輯,而保留了「塊文件」、「socket文件」之類的區別?

要替代就替代的聰明一些,把實在、重要的本質邏輯保留住。合理、明確,不迴避客觀區別的替代,和一時拼湊的「workaround」(臨時手段)是兩回事。後者一時使用尚可,但毫不應充看成爲軟件基礎的「萬靈藥」。

Makefile:介於語言和配置之間的「四不像」

考試:請僅用Makefile語法(不依賴shell特性)寫一個if/elseif/endif試試?

若是要用某種形式描述一個構建過程,其實:

  • 能夠表示成純粹的配置文件。不可獨立運行,不含任何實質的代碼,但絕對便於編寫、修改。
  • 能夠表述成真正的程序代碼。絕對靈活,全部語言特性隨便用。
  • 但通常都代碼和配置文件聯合使用,兼取二者之長。(即便是純代碼,其實數據和執行邏輯也會有必定的分離,而不是混在一塊兒搞成「意大利麪式編程」)

審查Makefile的本質設計,實際上是一種描述依賴關係配置文件,描述了「文件依賴文件」和「文件依賴shell代碼」兩種關聯。但恰恰Makefile也同時提供簡單的流程控制、賦值等語句,使得Makefile也是一種能夠控制流程走向的程序代碼

因此Makefile恰恰落在了配置代碼二者之間,既不是倒向一端,也不是二者的聯合,最後造成了一個「四不像」的混合品。做爲配置文件寫起來太費神,做爲程序代碼又太簡陋不夠用。

我想問:就從Makefile的設計上來看,那個被奉爲圭臬(事實上也確實很優秀)的「UNIX哲學」在哪裏?在哪裏?

shell:躲不開的雷區

符號勝於語義

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腳本,通常都須要:

  • 系統內有GNU工具鏈
  • 工具鏈的詳細用法(參數意義等),不能與編寫者產生嚴重的衝突,甚至不能進行形成deprecated(棄用)的升級
  • PATH等基本的環境變量正確
  • 但其餘的環境變量還不能與Makefile中用到的變量衝突

更可怕的是以上這些要素,基本上都是隱喻性的。沒有明確的版本控制手段去保證不說,甚至連確認都是不現實的。之前能用的腳本可能換個發行版、升級個系統,甚至於換個用戶就可能會發生問題。

咱們既然已經有了npm版本控制,更況且shell構建本質上也是調用基於node的工具,那咱們爲何還要去踩shell缺少版本控制這個坑?

shell調用js的性能問題

shell調用js,每個命令都須要啓動/中止node進程,而且各個工具是順序執行的。

而node構建工具,只須要使用同一個node進程,而且各個工具能夠異步啓停、並行運行。

這個效率區別是不須要具體比較的。

總結

「shell自動化」——邪道之路

從歷史上來看,shell原本就是爲了方便人類執行命令的小工具。然後人類發現了自動執行命令的便利性,從而將shell擴展成爲一門輕量的腳本語言,這個發展歷程是能夠理解的。

其實簡短的shell腳本也能夠大大方便人類的工做,是個好用的工具。但是一旦shell腳本龐大起來,shell不適合自動化運行和大型程序管理的各類硬傷就開始暴露:

  • 提倡使用特殊符號,過於強調語法簡短(首要問題)。
    這一點對於人類操做shell是優點,誰都想在命令行下少打幾個字。
    但若是用於長時間編寫、快速運行、長期維護的正式項目,shell的這個特色就會馬上表現爲難讀、難寫、難改的短板。
  • 缺乏大型項目所需的版本管理、面向對象等特性
  • 語言功能不足,例如缺少最基本的數值計算
  • 語言特性詭異,例如bash大多數狀況下對空白字符不敏感,可恰恰變量賦值的等號兩邊不許加空格
  • 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

相關文章
相關標籤/搜索