Node.js 19のwatchモードとBrowsersyncを組み合わせて試してみた

ソフトウェアエンジニアの池澤です。
Node.js v19.0.0が2022/10/18にリリースされました。
v19にはwatchモードがexperimental(実験的)版として備わっています。watchモードというのは開発中のディレクトリ内のファイルに変更があったら、自動でNode.jsを再起動してくれるという開発支援機能です。(厳密にはwatchモードはv18.11.0から使用可能)

今回はそんなNode.jsのwatchモードを試してみます。

watchモードとBrowsersyncでクライアントもバックエンドも自動更新したい

watchモードはファイル変更があったらNode.jsを自動更新し、バックエンド開発を支援するものです。

以下Node v19リリースノート のwatch部分抜粋です。

node --watch (experimental)
An exciting recent feature addition to the runtime is support for running in ‘watch’ mode using the node --watch option. Running in ‘watch’ mode restarts the process when an imported file is changed.
$ node --watch index.js
This feature is available in v19.0.0 and v18.11.0+.
Contributed by Moshe Atlow in #44366

この機能の背景としてはNode.js ISSUEの中でnodemonのような機能が欲しいという声が1年ほど前から高まっていて、今回実験的にNode.jsコアに入れてみようということのようです。 watch機能のコミットログをざっくり見た感じではrequireにもimportにも対応しているようです。

今回はこれにBrowsersyncというクライアント開発支援機能も組み合わせて見ようと思います。Browsersyncはhtmlやjs等クライアント側のファイル変更があったらブラウザを自動リロードしてくれるモジュールです。
ではさっそくテストの準備に取り掛かります。

目的・内容について

やりたいこと

  • Node.js watchモードを動かしてみる
  • バックエンドのエントリーポイント(今回server.js)を変更保存して自動更新したい
    • (Node.jsを終了・起動し直す手間を省略したい)
  • Node.jsをwatchモード状態かつBrowsersyncも動作させ、jsやhtmlを変更保存時にブラウザ自動リロードさせたい
    • (watchモードとBrowsersyncを同時起動してちゃんと動くのか確認)

テストケース

  1. Node.js v19.0.0 watchモードなし
  2. Node.js v19.0.0 watchモードあり
  3. Node.js v19.0.0 watchモードあり & Browsersyncあり

ファイル構成

server.js         // Node.jsの起動やルーティング処理をするエントリーポイント
package.json
bs-config.js      // Brosersync設定
src/
┣━ index.html
┣━ app.js        // message.jsを読み込んでhtmlに書き込むだけのjs
┣━ message.js    // 1行メッセージをexportするだけ
┗━ index2.html

全実験結果

記事が結構長文になってしまったので先に実験結果を記載します。
以下の通りでした。

  • watchモードなし
    • サーバ側を変更・保存しても反映されない
    • Node.jsを一旦終了し再度起動する必要がある
  • watchモードあり
    • サーバ側を変更・保存すると変更が自動反映される
    • ブラウザのリロードは必要
  • watchモード&Browsersyncあり
    • フロントエンド側のファイルを変更・保存するとブラウザが自動リロードされ変更が自動反映される

【実験】watchモードなし

実験内容:

  1. Node.js v19をwatchモードなしで起動する
  2. エントリーポイント(server.js)で表示htmlを変更・保存する(index.html → index2.htmlへ変更)
  3. ブラウザ(localhost:3000)を更新する
  4. ブラウザでindex2.htmlの内容が表示されるかを確認する。

予想:

  • watchモードなしなのでエントリーポイント(server.js)を変更・保存してもブラウザ表示内容は反映されない。(Node.jsの再起動が必要)

watchモードなし実験開始

▼1. サーバ起動

$node server.js

▼2. ブラウザ表示内容確認(変更前)
ブラウザに変更前内容が表示される。

▼3. server.jsで表示するhtmlファイルを変更・保存

function routeSetting(req, res) {
  // const htmFile = './src/index.html';
  const htmFile = './src/index2.html';

▼4. ブラウザをリロード
これで結果を見ます。

▼5. ブラウザ表示内容確認
変更・保存後のブラウザ表示内容。特に変化なしでした。

▼ 6. 結果
watchモードなしでの実験結果は以下となりました。

  • server.jsを変更・保存しても変化なし

予想通りの結果となりました。
なお、Node.jsを一旦終了し再度起動し直したら変更後のindex2.htmlが表示されました。これも予想通りです。

【実験】watchモードあり

実験内容:

  1. Node.js v19をwatchモードありで起動する
  2. エントリーポイント(server.js)で表示htmlを変更・保存する(index.html → index2.htmlへ変更)
  3. ブラウザ(localhost:3000)を更新する
  4. ブラウザでindex2.htmlの内容が表示されるかを確認する。
  5. jsファイルを変更して反映確認

予想:

  • watchモードありではエントリーポイント(server.js)を変更・保存してブラウザリロードすると、html読み込み先変更が反映されブラウザ表示内容が変わる。(watchモードがあるのでNode.jsの再起動が不要)
  • js内容(message.js)を変更してブラウザリロードすると変更内容が反映される。

watchモードあり実験開始

▼1. サーバ起動

$node --watch server.js

ターミナルにまだ実験的機能なのでいつ変更になるかわからないという注意メッセージも表示されました。
ExperimentalWarning: Watch mode is an experimental feature. This feature could change at any time

▼2. ブラウザ表示内容確認(変更前)
ブラウザに変更前内容が表示される。

▼3. server.jsで表示するhtmlファイルを変更・保存

function routeSetting(req, res) {
  // const htmFile = './src/index.html';
  const htmFile = './src/index2.html';

ターミナルを見るとRestarting 'server.js'と表示されました。

▼4. ブラウザをリロード
これで結果を見ます。

▼5. ブラウザ表示内容確認
変更・保存後のブラウザ表示内容。

▼6. message.jsのテキスト変更・保存
index.htmlに戻って、importされているファイルの更新が反映されるかテストです。

export const message = " importしたmessage.js内容:////テキスト変更しました!////";

▼ 7. ブラウザをリロードしてjs反映確認
ブラウザをリロードします。すると変更したjs内容が表示されました。

▼ 8. 結果
watchモードありでの実験結果は以下となりました。

  • server.jsを変更・保存すると変更が自動反映される
  • フロントエンド側のjsを変更・保存してブラウザリロードすると変更が反映される

予想通りの結果です。 nodemonと同等の機能が外部モジュールなしで使えるようですね。これは便利に使えそうです。

【実験】watchモードあり&Browsersyncあり

実験内容:

  1. Node.js v19をwatchモードありかつBrowsersyncありで並列起動する
  2. Browsersyncが出力するlocalhost:4000の表示をまず確認する
  3. message.jsをを変更・保存する
  4. ブラウザ自動リロードおよび内容反映するか確認

予想:

  • message.jsを変更・保存すると自動でブラウザリロードされ、変更内容がブラウザに表示される。

watchモードあり&Browsersyncあり実験開始

▼1. サーバ起動
Node.jsの起動と共にBrowsersyncもパラレル(並列処理)起動します。

$node --watch server.js & npx browser-sync start --config bs-config.js

▼2. ブラウザ表示内容確認(変更前)
ブラウザに変更前内容が表示される。
なおlocalhost:3000側のブラウザ表示は【実験】watchモードあり > 2. ブラウザ表示内容確認(変更前) と同様です。

▼3. message.jsのテキスト変更・保存
importされているファイルの更新が反映されるかテストです。

export const message = " importしたmessage.js内容:////テキスト変更しました!////";

▼4. message.js変更保存後
変更後はターミナルに[Browsersync] Reloading Browsers...が表示されます。

localhost:4000 ページもブラウザ自動リロードが走りました。
そして変更したmessage.js内容が表示されました。

▼ 5. 結果
watchモードあり&Browsersyncありでの実験結果は以下となりました。

  • message.jsを変更・保存するとBrowsersyncでブラウザ自動リロードされ変更が反映される

こちらも予想通りの結果が出ました。
watchモードがまだexperimental(実験的)版ということもあり、モジュール連携ができるか不明でしたがBrowsersyncとは無事連携できるようで良かったです。


【実験上の注意】既存ポートおよびwatchモードの初期化

ポート内の既存プロセスを初期化

今回はport:3000と4000を使いました。 このポートが既に何らかのプロセスで使用されている場合は
Error: listen EADDRINUSE: address already in use
のようにNode.jsでエラーとなることがあります。
そのため以下の通りポート3000、4000の初期化を行ってください。
(参照:Qiita > ポートを使用してるプロセスを調べてkillする覚書き

port:3000および4000ポートで使用中のプロセスを調べてPIDを確認:

$lsof -i :3000
$lsof -i :4000

PID番号をコピペしてプロセスをkillする:

$kill -9 [PID番号]

watchモードの既存プロセスを初期化

テストで何度も起動しているとNode.jsのwatchモードプロセスが複数立ち上がった状態になることがあります。 この状態でエントリーポイントファイル(今回のserver.js)を更新するとport:3000のプロセスを消しても
Error: listen EADDRINUSE: address already in use
のエラーが発生する場合があります。その場合は以下で既存のnode --watchプロセスも事前にkillしましょう。

プロセス一覧を表示してnode --watchのものを探す:

ps

プロセス一覧の中から node --watch となっているプロセスのPIDをコピペしてkillする:

$kill -9 [PID番号]

ソースコード

設定系

package.json:
Browsersyncモジュールを読み込んでいるだけの内容です。

{
  "devDependencies": {
    "browser-sync": "^2.27.10"
  }
}

bs-config.js:
Browsersyncの設定ファイルです。

  • filesにてsrcディレクトリ配下の.js、.htmlの更新を監視する設定です。
  • proxyにてNode.jsの出力する localhost:3000 をラップします。
  • portにてBrowsersyncの表示先ポートを設定。自動更新結果が表示されます。
module.exports = {
    "files": [
        "src/*.js",
        "src/*.html"
    ],
    "proxy": "localhost:3000",
    "port": 4000
};

バックエンド・フロントエンド用

server.js:
htmlページを表示するためのNode.js サーバ側処理です。依存関係をシンプルにしたいのでexpress等は使っていません。(参照:MDN > フレームワークなしの Node.js サーバ

  • Node.jsの起動やルーティング処理をするエントリーポイントファイルです。
  • htmlやjsファイルを読み込んで localhost:3000 に出力します。
  • htmlとjsの拡張子からmimeタイプを判別してContent-Type指定しています。
  • watchモード動作状態で読み込みhtmlファイル(index.html | index2.html)を切り替えるテストを想定しています。
const http = require("http");
const fs = require("fs");
const path = require("path");

const mimeTypes = {
    '.html': 'text/html',
    '.js': 'text/javascript'
};

function routeSetting(req, res) {
  const htmFile = './src/index.html';
  // const htmFile = './src/index2.html';

  const filePath = req.url == '/' ? htmFile : './src' + req.url;
  const extName = String(path.extname(filePath)).toLowerCase();
  const contentType = mimeTypes[extName] || 'application/octet-stream';

  fs.readFile(filePath, function(error, content) {
    if(error){
      res.writeHead(500, {'Content-Type': 'text/html'});
      res.end('Sorry, error: '+error.code);
    }else{
      res.writeHead(200, { 'Content-Type': contentType+';charset=utf-8' });
      res.write(content);
      res.end();
    }
  });
}

http.createServer((req,res)=>{
  routeSetting(req,res);
}).listen(3000,()=>{
  console.log("Listening on localhost port 3000 -> (http://localhost:3000)");
});

index.html:

  • server.jsから表示されるhtmlです。
  • jsファイルの変更時にBrowsersyncの自動更新をテストするためapp.jsを読み込んでいます。
<!DOCTYPE html>
<html>
  <body style="background:#eee8aa;">
    <h1>node.js 19でのwatch検証</h1>
    <div id="result">---</div>
    <script src="./app.js" type="module"></script>
  </body>
</html>

app.js:

  • index.htmlで読み込まれるjsファイルです。
  • importファイルの変更時にBrowsersyncの自動更新をテストするためmessage.jsを読み込んでいます。
  • message.jsのテキスト内容をindex.htmlのid=result内に表示します。
import {message} from "./message.js";
document.getElementById("result").innerText = message;

message.js:

  • app.jsでimportされるjsファイルです。
  • テキストをexportしているだけの1行のみコードです。
export const message = " importしたmessage.js内容:";

index2.html:

  • server.jsから表示されるhtmlです。
  • server.jsで読み込みhtmlファイルの切り替えを検証するためのファイルです。
  • テキスト表示のみのシンプル内容です。
<!DOCTYPE html>
<html>
  <body>
    <h1>index2が表示されています</h1>
  </body>
</html>

nodenvで最新のバージョンが出てこない時は?

Node.jsのversion 19.0.0のインストールについてです。
私の場合はMac のbrew経由でnodenvを使ってNode.jsのバージョン切り替えをしています。
(Node.jsやnodenvのインストール方法についてはnodenv#installationをご参照ください。)

nodenvで最新のversionが出てこないことがあります。
そんな時は次の方法を試して見てください。

Homebrewとnodenv自体を更新したい場合

brewとnodenv自体を更新したい場合は次のコマンドを実行します。
参照:nodenv > Upgrading with Homebrew

$ brew upgrade nodenv node-build

nodenvで利用可能なNode.js versionリストを最新に更新したい場合

nodenvはNode.jsのversion取得をnode-buildで行っています。
よくあるのがbrewやnodenvは最新にしたのに、それでもNode.jsの最新版やLTS版が
$nodenv install --list
をしても出てこないケースです。

この場合はnode-buildに対して以下のコマンドを実行すると取得できることが多いです。
参照:nodenv > Updating the list of available Node versions

$ cd ~/.nodenv/plugins/node-build
$ git pull

Browsersyncのインストール設定

Browsersyncをインストールするには以下のコマンドを実行します。
参照: Browsersync

$npm install --save-dev browser-sync

まとめ

Node.js watchモードを検証して無事バックエンド側もフロントエンド側(Browsersync使用)も自動更新されることが分かりました。 watchモードを使ってNode.js側で変更の追跡をしてくれるなら、これまでnodemon等に頼ってきた部分も減り、モジュール依存が軽くなる効果もありそうです。

watchモードはまだ実験的リリースに過ぎませんが、Node.js運営側でISSUEの声を聞きコア機能へ反映しようとしてくれる姿勢はありがたいですし、オープンソース文化の良さを感じました。
これからも新しい機能のリリースを見て行きたいと思います。