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 は
- node_modules は
これらの依存関係の解決方法を 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 活用例のサンプルなので、アプリ部分のショボさは無視してください。