Grunt や gulp のかわりに Make も使ってみよう

フロントエンド開発のタスクランナーとして Grunt や gulp、npm run-script なんかを使ってきたが、今は Make を使っている。フロントエンド分野ではあまり馴染みのないツールかもしれないが、必要十分な機能性と高い表現力のバランスの良さを実感し、一巡辿ってゴールにたどり着いた感がある。もっと流行ってほしい。

Make は Makefile に定義したルールにしたがってビルドプロセスを実行する。しかし Makefile には独特な表現が多く、$@ とかのマクロはググりようがなくてちょっとしんどい。とはいえ、いきなり高度な使い方をしようと思わなければ簡単なので、以下を参考に導入してみてほしい。

STEP 1. コマンドのエイリアスを書く

基本はただのエイリアスです。

JavaScript をビルドする例

次の Makefile があるディレクトリで make build コマンドを実行すれば Browserify でのビルドが実行される。

build:
    node_modules/.bin/browserify src/main.js -o dist/bundle.js

なお build というタスク名的な部分のことを ターゲット という。

ミニファイしたければ Uglify にパイプ、もしくは uglifyify トランスフォームすればいい。

build:
    node_modules/.bin/browserify src/main.js | \
    node_modules/.bin/uglifyjs -o dist/bundle.js

ターゲットの部分には生成したいファイル名、例えば dist/bundle.js などを指定できる。そうした場合の Makefile は以下のように、コマンドは make dist/bundle.js となり、自然言語的に理解しやすくなる。

dist/bundle.js:
    @node_modules/.bin/browserify src/main.js | \
    node_modules/.bin/uglifyjs -o dist/bundle.js

なおコマンド部のはじめに @ をつけてみたが、これは実行するコマンドを標準出力しないための記号。コマンドを確認したければつけないままで OK。

JavaScript をインクリメンタルビルドする例

watchify がそれ自身でインクリメンタルビルドする機能を持っているので、これを叩くだけ。

watch-js:
    @node_modules/.bin/watchify src/main.js -o src/bundle.js -v -d

STEP 2. 依存ターゲットを書く

上の Makefile は本当にただのエイリアスに過ぎないので、次は 依存ターゲット を導入する。

dist/bundle.js を生成するまでに必要な依存関係をリスト化してみると、

  • dist/bundle.js は
    • browserifyuglifyjs コマンドが使用できなければ出力できない。つまり Browserify や Uglify などの Node モジュールが格納されている node_modules ディレクトリの存在に依存 している。
    • dist/bundle.js は、 dist ディレクトリがないと出力できない。つまり dist ディレクトリの存在に依存 している。
  • node_modules は
    • package.json ファイルを元に npm install コマンドによって作られる。つまり package.json ファイルの存在 に依存している。

これらの依存関係の解決方法を Makefile で表現すると次のようになる。

dist/bundle.js: node_modules dist
    @node_modules/.bin/browserify src/main.js | \
    node_modules/.bin/uglifyjs -o dist/bundle.js

dist:
    @mkdir -p dist

node_modules: package.json
    @npm install

このとき make dist/bundle.js を実行すると、必要な依存を自動的に解決してくれる。つまり、初めに node_modules の解決のため npm install が実行され、次に dist の解決のため mkdir -p dist が実行される。最後に dist/bundle.js が生成される。つまり make dist/bundle.js コマンド一つを実行すれば、他のコマンドを覚えたり実行せずにビルドできる。

しかもターゲットと依存ターゲットのファイルのタイムスタンプを比較し、更新が必要なければコマンドはスキップされる。例えば package.json よりも node_modules が新しければ、node_modules の更新は不要なので npm install は実行されない。

STEP 3. 他の継続プロセスを並列実行する

ウェブサーバ越しに動作・表示確認するために http-server を起動したいとする。さらに RESTful API モックサーバ json-server を起動したいとする。とりあえずそれぞれのプロセスの起動のためのターゲットを定義すると以下のようになる。

run-dev-server: node_modules
    @node_modules/.bin/http-server

run-api-mock-server: node_modules
    @node_modules/.bin/json-server --watch db.json

watch-js:
    @node_modules/.bin/watchify src/main.js -o src/bundle.js -v -d

これらのプロセスは並列に実行したいもの。そんな時は Make の j オプションでパラレル実行できるので、make -j run-dev-server run-api-mock-server watch-js のようなコマンドを実行するといい。とはいえこのコマンドを毎回叩くのは面倒なので、これを更に Makefile に定義しておく。

watch:
    @make -j run-dev-server run-api-mock-server watch-js

こうすれば、make watch すれば、3つのプロセスがパラレル実行される。仮にエラーでどれかのタスクが停止しても、Make のプロセスを止めればすべてのプロセスが止まるので、バックグラウンドでプロセスが残り続ける心配もない。npm run-script などで & 区切りで実行すると、バックグラウンドプロセスが残りやすいので、これは便利。

STEP 4. 変数とかマクロとか関数とか使う

ここまでの内容で十分に便利に使えるが、Makefile らしさを出すために以降では簡単なマクロを使う例を紹介する。ただししんどくなってきたら本末転倒なのでやめよう。

はじめのほうに書いた次の Makefile は、

dist/bundle.js:
    @node_modules/.bin/browserify src/main.js | \
    node_modules/.bin/uglifyjs -o dist/bundle.js

次の様に書き換えることができる。

SRC       = src
DIST      = dist
MAIN_JS   = $(SRC)/main.js
BUNDLE_JS = $(DIST)/bundle.js

$(BUNDLE_JS):
    @node_modules/.bin/browserify $(MAIN_JS) | \
    node_modules/.bin/uglifyjs -o $@

変数定義 VAR=foo と参照 $(VAR)、更にターゲット名を表すマクロ $@ を導入した。

(番外) STEP 5. Windows 対応

Windows よくわからないけど、MinGW とか使ってほしい。コマンドプロンプトでは諦めてほしい。こんな感じで /\ に置換する関数を使うといけたりもする。諦めてほしい

ifdef SystemRoot
    fixPath = $(subst /,\,$1)
else
    fixPath = $1
endif

$(call fixPath,dist/bundle.js):
    @$(call fixPath,node_modules/.bin/browserify src/main.js -o dist/bundle.js)

Grunt, gulp, npm run-script との比較

  • Grunt 遅い、タスク定義が面倒。
  • gulp 早いけどタスク定義が面倒。
  • Grunt も gulp も、プラグイン化が必要。バージョンアップ追従のタイムラグや、中にはメンテナンスされなくなるものもあるので、プラグイン化されたものではなく生で使えるに越したことはない。
  • npm run-script は表現力が足りない。簡単なタスクならいいが、マルチラインやコメントが書けないので複雑なタスクは無理。
  • シェルスクリプトもいいけど、一貫したプラクティスを提供する Make のほうが乗っかりやすい。

サンプル

https://github.com/keik/frontend-with-make

git clone https://github.com/keik/frontend-with-make.git して make すれば依存パッケージのインストールやら何やらができるはず。Makefile 活用例のサンプルなので、アプリ部分のショボさは無視してください。