NodeJS とかでファイル更新を監視してプロセスを再起動しつつ Browsersync でリロード

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 で出来そうです。watchexecnodemon で問題ありません。node-devts-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 とかで同じようなことをやっている例がでてきました。きめ細かにやるならその方が良いのかもしれませんが、自分専用なら雑にありもののツールを組み合わせてこういうのもアリじゃないでしょうか。