NodeJS や Golang で Web アプリを作るとき、サーバサイドのコードを変更したときはプロセスの再起動が必要です。毎回手でやるのはさすがに煩雑なのでファイルの更新を監視して自動的に再起動してくれる系のツールがいろいろあります。
NodeJS なら下記などがよく使われるようです。
nodemon は実行コマンドを node
から変更できるので NodeJS 以外でも使おうと思えば使えます。
node-dev は require()
をフックすることで必要なファイルだけを監視、みたいな動作になるようです。なので監視対象のディレクトリやファイルの設定などしなくてもうまく動作します。その仕組的に NodeJS 以外では使えません。
ts-node-dev は node-dev の TypeScript 版です。TypeScript のコンパイラプロセスが毎回起動しなくて良いためリスタートが速いらしいです。
Golang なら oxequa/realize が鉄板なのでしょうか? ファイルの更新を監視して go run
をリスタートする以外にもいろいろ出来るようです。
うーん? Go Modules に対応していないので cosmtrek/air のほうが良いの? 単にファイルの更新を監視してプロセスを再起動するだけではないんですかね。。。
watchexec
この種のツール、NodeJS や Golang に特化することで便利になっているところもあるようなのですが、基本的にはファイルの更新を監視してプロセスをリスタートするだけのものなので、それならもっと汎用的なものを使ってもよいのでは、と思います。
普段ファイルの更新を監視してなんやかんやするときに watchexec を使っているのですが -r
オプションでプロセスのリスタートもできます。使い慣れているのでこれを使うことにします。
watchexec 自体は Homebrew で簡単にインストールできます。
brew install watchexec
次のようにすると src/
ディレクトリの *.ts
ファイルを監視して、更新があると ts-node
コマンドをリスタートしてくれます。
watchexec -r -w src/ -f '*.ts' -- env PORT=9876 ts-node -T src/app.ts
ファイル監視からのプロセスのリスタートはこれだけで十分です。
browser-sync
次に、サーバが HTML を返す系の Web アプリの場合、見た目の確認のためにいわゆる livereload も出来ると便利です。livereload にはいつも Browsersync を使ってます。便利です。
browser-sync start --port 3000 -p localhost:9876 --open -w -f '**/*.ts' -f '**/*.html' -f '**/*.css'
前述の watchexec と同時に実行すればブラウザで http://localhost:3000 を開けば Browsersync 経由で localhost:9876 でリッスンしている NodeJS のアプリにアクセスできます。
そして html や css を更新すると Browsersync でブラウザのリロード、サーバサイドのコードを更新したときは NodeJS のプロセスをリスタートした上で Browsersync でブラウザがリロードされ・・
ませんでした。サーバサイドのコードを更新したとき、NodeJS のプロセスが再起動しつつ Browsersync がブラウザで表示しているページをリロードするわけですが、NodeJS のプロセスが再起動してポートのリッスンが開始するより、ブラウザがリロードされる方が速いため、リロード後に Browsersync がプロキシ先のポートにアクセスできなくてエラーになります。
Makefile
試行錯誤のうえで出来上がったのがこれ。NodeJS のプロセスがリスタートするとき、NodeJS によってポートがリッスンされるのを待ってから Browsersync でリロードします。
APP_PORT = 9876 BS_PORT = 3000 .PHONY: all all: $(MAKE) -j watch bs .PHONY: app app: env PORT="$(APP_PORT)" ts-node -T src/app.ts .PHONY: watch watch: watchexec -r -w src/ -f '*.ts' -- $(MAKE) -j app reload .PHONY: bs bs: browser-sync start --port "$(BS_PORT)" -p localhost:"$(APP_PORT)" --open -w \ -f '**/*.html' \ -f '**/*.css' .PHONY: reload reload: @while ! nc -z localhost "$(APP_PORT)"; do sleep 1; done browser-sync reload
Makefile を使っていますがなにかをメイクするわけではなくタスクランナーとしてだけ使っています。タスクを直列に実行したり並列に実行したりの制御が Makefile なら簡単なので。。。
watchexec
からは $(MAKE) -j app reload
で、アプリを開始するタスクと、ページをリロードするタスクを同時に実行します。リロードのタスクはアプリのポートがリッスンを待ってから browser-sync reload
します。make
の -j
はタスクの並列数を指定するオプションです。値を省略しているので無制限になります。
また、これら2つを同時に実行するためにデフォルトのタスクで $(MAKE) -j watch bs
としています。
リッスンを開始したログをトリガにする版
試行錯誤中にできた、アプリが標準出力に出すログをトリガにする版(↑の方がスマートです)。
まず、アプリでリッスンを開始したときに適当なログを出力するようにしておきます。
const port = parseInt(process.env.PORT || '9876'); app.listen(port, () => { console.log(`Listening on :${port}`); });
そしてこのメッセージが標準出力に流れてきたのをトリガーに browser-sync reload
を実行します。
.PHONY: watch watch: watchexec -r -w src/ -f '*.ts' -- $(MAKE) app \ | tee /dev/stderr \ | grep --line-buffered '^Listening on :' \ | xargs -i browser-sync reload
watchexec
で実行したアプリの標準出力を tee
で /dev/stderr
に流しています。これはなくても動きますが、その場合アプリの標準出力の内容が失われます。
次に grep
で標準出力にポートがリッスンしたときのメッセージが流れるのを待ちます。--line-buffered
を付けて後段のパイプに行ごとにフラッシュされるようにします。
最後に xargs
で標準入力になにか来るたびに browser-sync reload
を実行します。-i
を付けてコマンドにに余計な引数が付与されないようにするとともに、標準入力から1行読むたびにコマンドが実行されるようにします。
npm だけで実現する
↑の方法は自分ひとりで使う分には良いですが、チームで使うのであれば NodeJS ならやっぱり npm だけで実行できたほうが良いでしょうか。
npm でも npm-run-all で並列実行は出来ます。ポートのリッスンを待つのは wait-on で出来そうです。watchexec
は nodemon で問題ありません。node-dev
や ts-node-dev
は実行コマンドを変更できないのでダメです。
次のようになりました。
{ "scripts": { "dev": "npm-run-all -p -r bs watch", "app": "ts-node -T src/app.ts", "watch": "nodemon -w src/ -e ts -x npm-run-all -- -p app wait-reload", "bs": "browser-sync start --port 3000 -p localhost:9876 --open -w -f \"**/*.ejs\" -f \"**/*.css\"", "wait-reload": "npm-run-all -s wait reload", "wait": "wait-on -v tcp:9876", "reload": "browser-sync reload" }
npm run dev
で開始できます。
npm run dev
さいごに
ググると gulp
とかで同じようなことをやっている例がでてきました。きめ細かにやるならその方が良いのかもしれませんが、自分専用なら雑にありもののツールを組み合わせてこういうのもアリじゃないでしょうか。