使用 Make 構建網站

阮一峰發表於2015-03-13

網站開發正變得越來越專業,涉及到各種各樣的工具和流程,迫切需要構建自動化。

所謂"構建自動化",就是指使用構建工具,自動實現"從原始碼到網頁"的開發流程。這有利於提高開發效率、改善程式碼質量。

本文介紹如何使用make命令,作為網站的構建工具。以下內容既是make語法的例項,也是網站構建的實戰教程。你完全可以將程式碼略作修改,複製到自己的專案。

(題圖:國家考古博物館,西班牙,攝於2014年8月)

一、Make的優點

首先解釋一下,為什麼要用Make。

目前,網站專案(尤其是Node.js專案)有三種構建方案。

我覺得,make是大型專案的首選方案。npm run可以認為是make的簡化形式,只適用於簡單專案,而Grunt、Gulp那樣的工具,有很多問題。

(1)外掛問題

Grunt和Gulp的操作,都由外掛完成。即使是檔案改名這樣簡單的任務,都要寫外掛,相當麻煩。而Make是直接呼叫命令列,根本不用擔心找不到外掛。

(2)相容性問題

外掛的版本,必須與Grunt和Gulp的版本匹配,還必須與對應的命令列程式匹配。比如,grunt-contrib-jshint外掛現在是0.11.0版,對應Grunt 0.4.5版和JSHint 2.6.0版。萬一Grunt和JSHint升級,而外掛沒有升級,就有可能出現相容性問題。Make是直接呼叫JSHint,不存在這個問題。

(3)語法問題

Grunt和Gulp都有自己的語法,並不容易學,尤其是Grunt,語法很羅嗦,很難一眼看出來程式碼的意圖。當然,make也不容易學,但它有複用性,學會了還可以用在其他場合。

(4)功能問題

make已經使用了幾十年,全世界無數的大專案都用它構建,早就證明非常可靠,各種情況都有辦法解決,前人累積的經驗和資料也非常豐富。相比之下,Grunt和Gulp的歷史都不長,使用範圍有限,目前還沒有出現它們能做、而make做不到的任務。

基於以上理由,我看好make。

二、常見的構建任務

下面是一些常見的網站構建任務。

  • 檢查語法
  • 編譯模板
  • 轉碼
  • 合併
  • 壓縮
  • 測試
  • 刪除

這些任務用到 JSHinthandlebarsCoffeeScriptuglifyjsmocha 等工具。對應的package.json檔案如下。


"devDependencies": {
    "coffee-script": "~1.9.1",
    "handlebars": "~3.0.0",
    "jshint": "^2.6.3",
    "mocha": "~2.2.1",
    "uglify-js": "~2.4.17"
}

我們來看看,Make 命令怎麼完成這些構建任務。

三、Makefile的通用配置

開始構建之前,要編寫Makefile檔案。它是make命令的配置檔案。所有任務的構建規則,都寫在這個檔案(參見《Make 命令教程》)。

首先,寫入兩行通用配置。


PATH  := node_modules/.bin:$(PATH)
SHELL := /bin/bash

上面程式碼的PATH和SHELL都是BASH變數。它們被重新賦值。

PATH變數重新賦值為,優先在 nodemodules/.bin 目錄尋找命令。這是因為(當前專案的)node模組,會在 nodemodules/.bin 目錄設定一個符號連結。PATH變數指向這個目錄以後,呼叫各種命令就不用寫路徑了。比如,呼叫JSHint,就不用寫 ~/node_modules/.bin/jshint ,只寫 jshint 就行了。

SHELL變數指定構建環境使用BASH。

四、檢查語法錯誤

第一個任務是,檢查原始碼有沒有語法錯誤。


js_files = $(shell find ./lib -name '*.js')

lint: $(js_files)
    jshint $?

上面程式碼中,shell函式呼叫find命令,找出lib目錄下所有js檔案,儲存在變數js_files。然後,就可以用jshint檢查這些檔案。

使用時呼叫下面的命令。


$ make lint

五、模板編譯

第二個任務是編譯模板。假定模板都在templates目錄,需要編譯為build目錄下的templates.js檔案。


build/templates.js: templates/*.handlebars
    mkdir -p $(dir [email protected])
    handlebars templates/*.handlebars > [email protected]

template: build/templates.js

上面程式碼檢視build目錄是否存在,如果不存在就新建一個。dir函式用於取出構建目標的路徑名(build),內建變數[email protected]代表構建目標(build/templates.js)。

使用時呼叫下面的命令。


$ make template

六、Coffee指令碼轉碼

第三個任務是,將CofferScript指令碼轉為JavaScript指令碼。


source_files := $(wildcard lib/*.coffee)
build_files  := $(source_files:lib/%.coffee=build/%.js)

build/%.js: lib/%.coffee
    coffee -co $(dir [email protected]) $<

coffee: $(build_files)

上面程式碼中,首先獲取所有的Coffee指令碼檔案,存放在變數sourcefiles,函式wildcard用來擴充套件萬用字元。然後,將變數sourcefiles中的coffee檔名,替換成js檔名,即 lib/x.coffee 替換成 build/x.js 。

使用時呼叫下面的命令。


$ make coffee

七、合併檔案

使用cat命令,合併多個檔案。



JS_FILES := $(wildcard build/*.js)
OUTPUT := build/bundle.js

concat: $(JS_FILES)
    cat $^ > $(OUTPUT)

使用時呼叫下面的命令。


$ make concat

八、壓縮JavaScript指令碼

將所有JavaScript指令碼,壓縮為build目錄下的app.js。


app_bundle := build/app.js

$(app_bundle): $(build_files) $(template_js)
    uglifyjs -cmo [email protected] $^

min: $(app_bundle)

使用時呼叫下面的命令。


$ make min

還有另一種寫法,可以另行指定壓縮工具。


UGLIFY ?= uglify

$(app_bundle): $(build_files) $(template_js)
    $(UGLIFY) -cmo [email protected] $^

上面程式碼將壓縮工具uglify放在變數UGLIFY。注意,變數的賦值符是 ?= ,表示這個變數可以被命令列引數覆蓋。

呼叫時這樣寫。


$ make UGLIFY=node_modules/.bin/jsmin min

上面程式碼,將jsmin命令給變數UGLIFY,壓縮時就會使用jsmin命令。

九、刪除臨時檔案

構建結束前,刪除所有臨時檔案。


clean:
    rm -rf build

使用時呼叫下面的命令。


$ make clean

十、測試

假定測試工具是mocha,所有測試用例放在test目錄下。


test: $(app_bundle) $(test_js)
    mocha

當指令碼和測試用例都存在,上面程式碼就會執行mocha。

使用時呼叫下面的命令。


$ make test

十一、多工執行

構建過程需要一次性執行多個任務,可以指定一個多工目標。


build: template concat min clean

上面程式碼將build指定為執行模板編譯、檔案合併、指令碼壓縮、刪除臨時檔案四個任務。

使用時呼叫下面的命令。


$ make build

如果這行規則在Makefile的最前面,執行時可以省略目標名。


$ make

通常情況下,make一次執行一個任務。如果任務都是獨立的,互相沒有依賴關係,可以用引數 -j 指定同時執行多個任務。


$ make -j build

十二、宣告偽檔案

最後,為了防止目標名與現有檔案衝突,顯式宣告哪些目標是偽檔案。


.PHONY: lint template coffee concat min test clean build

十三、Makefile檔案示例

下面是兩個簡單的Makefile檔案,用來補充make命令的其他構建任務。

例項一。


PROJECT = "My Fancy Node.js project"

all: install test server

test: ;@echo "Testing ${PROJECT}....."; \
    export NODE_PATH=.; \
    ./node_modules/mocha/bin/mocha;

install: ;@echo "Installing ${PROJECT}....."; \
    npm install

update: ;@echo "Updating ${PROJECT}....."; \
    git pull --rebase; \
    npm install

clean : ;
    rm -rf node_modules

.PHONY: test server install clean update

例項二。


all: build-js build-css

build-js: 
  browserify -t brfs src/app.js > site/app.js

build-css:
  stylus src/style.styl > site/style.css

.PHONY build-js build-css

十四、參考連結

(完)

相關文章