🙄

Node.jsに本気で向き合う①基本概念を理解する

2022/02/05に公開

Node.jsを雰囲気で使っていたので、どこがどうなっているかをしっかり?理解しようと思いました。ざっと全体的な概念に触れつつ、業務でよく使う箇所についてまとめます。 長くなりそうなので「基本概念を理解する」、「コアモジュールを理解する」、「よく使うパターンについて理解する」の3つにわけます。今回は簡単にNode.jsの利点と、イベントまわりについて、あとはおまけ的にモジュールimportとconsole.log系についてまとめました。

そもそもNode.jsとは

フレームワーク…ではなく、JavaScriptの実行環境です。サーバーで動きます。シングルスレッド、イベントドリブン、ノンブロッキングI/Oあたりが特徴です。v8エンジンという実行エンジン(Google Chromeと同じ)で動きます。

何を知るとよいか

これは自分の考えですが、

  • Node.jsのコアモジュールをざっと把握している、よく使うモジュールは使い方がわかる
  • イベントとイベントループ取り扱いを理解している
  • よく使うパターン(デザパタ的な)を理解している
  • よく使うパターン(頻出のイディオム的な)の引き出しがある

みたいなことがわかっているとNode.jsがわかっている人な感じがします。(ついでにNode.jsで食っていこうと思うならJavaScriptの言語仕様とか、もっとマクロにWebシステムの設計ができるとか、そういうスキルも必要そう)

蛇足:モジュールについて

脱線しますが、Node.jsの話をするときにいつもJavaScriptのモジュールの話(CommonJSとかESModulesとか)にいつも躓くので概要が把握できるようなリンクを貼ります。

https://blog.ikeryo1182.com/javascript-modules/

簡単にまとめるとCommonJS(require使うやつ)、ESModules(import使うやつ)みたいなモジュールについての仕様です。このへんをいい感じにまとめるのがWebpackなどもモジュールバンドラーで、依存性解決したりJavaScriptにまとめてくれたりします。

モジュールの呼び出し

作る時は、以下のような特徴に注意するとよいでしょう。

  • モジュールは呼び出すフォルダ基準で相対パスを記述する(ルート基準ではない)
  • モジュールはキャッシュされるため、初めて使う時だけ初期化処理が走る

npmなどから持ってきて使う時は、ローカルにインストールするかグローバルにインストールするかは意識すべきです。package.jsonが肥大化してつらくなることが多いので、以下のような基準でグローバルにインストールするかを決めるとよいかもしれないです。

  • ドキュメントにあるインストールのコマンドに-gがついているものはグローバルに
  • どのプロジェクトでも使うもの、serverlessやeslintなどはグローバルに

Node.jsを知ろう

ドキュメントを読む

https://nodejs.org/api/
APIドキュメントにコアモジュールにどんなものがあるかが書いてあります。いくつか普段使っている内容についてドキュメントを読んでみます。

例えばProcessのドキュメントを読むと、

  • process.envで環境変数にアクセスできる
  • process.kill(pid)でプロセスを終了できる
  • process.nextTick(callback)でイベントループの次のtickのイベントキューにcallbackを登録できる

みたいなことがわかります。よく業務でも使うな…というところだけピックアップしてみました。ちなみにnextTickはイベントループガイドに説明があります。ざっと自分が使ったことのあるメソッドやイベントの説明を読んだり、目次からどんなクラスがあってそんなメソッドが定義されているかみるのはためになると思います。HTTPのドキュメントをよむと、http.Serverクラスを使えばwebサーバを自力で書けるな…とか、File systemのドキュメントをみるとfsはwriteStream以外にもいろんなメソッドがあるな、ファイル読む系はイベントループに乗っているから他の言語みたいにnext的な動作が見当たらないな…といったことがわかります。

とくにfs系をよく使うのと、processをいじる系をデバッグのときに使います。無理やりprocessを落としたりとか。

イベントループとは何か

処理開始後は以下のような処理がループします。

  • Timers:setTimeout()やsetInterval()の実行
  • pending calback:I/O完了処理、I/Oの例外処理
  • idle:nodejsの内部処理
  • poll:I/O処理
  • check:setImmediate()
  • close callbacks:I/O切断処理

流れとしては、

  • nextTickQueue、microTaskQueueなどのキューは上記のフェーズの合間に実行される(フェーズ実行前にキューの確認をされる)。nextTickQueueは- - Process.nextTick()、microTaskQueueはPromise.resolve.thenを実行する。(例えばsetTimeout()をすると、時間が経過するまでTimersを通っても無視される。)
    という感じです。

なお、遅延実行系のメソッドには以下のようなものがあります。
process.nextTick(callback)
Promise.recolve().then(callback)
global.setImmediate(callback)
global.setTimeout(callback,delay)
global.setInterval(callback,delay)

イベント

EventEmitterがイベントを司ります。例えばEventEmitter.on(name, listener)で発火時の処理を設定できますし、EventEmitter.off(name, listener)で削除、EventEmitter.emit(name, args)で発火することができます。自分でイベントを定義したいときにEventEmitterを継承したクラスを作って、メソッドを作ります。emitを中で書いて好きにイベントを発火させます。使用側はクラス.onを使ってイベントの発火を拾うことができます。

EventEmitterの使い方

  • on(eventName , listener ):第一引数に指定したイベントに紐づけて、listenerを登録
    • const eventEmitter = new EventEmitter();みたいにして
    • eventEmitter.on(・・・・でかく
    • 第二引数のコールバックは()=>と即時でかいても、関数つくってわたしても。イベント発火したあとに呼び出される
    • onたくさん作ってからemitすると、上からonが実行されていく
  • emit(eventName , [args]):argsを引数としてイベント発火
    • 同期呼び出し

細かい話

グローバルオブジェクト

__filenameだとかconsole、先ほど書いたprocesはグローバルオブジェクトです。実行中のディレクトリパスは確認するとAWSや何か別の環境で動かすときや、testを動かすときに嬉しいことがあります。相対パスで指定していたら読み込めないなんてことがあるので。まあパスエイリアスを

{
  "compilerOptions": {
    "baseUrl": ".", 
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

みたいに設定することもできますが。

ついでにプリントデバッグ

console.log系って結構使いますよね。これについてもまとめておきます。よく使うtipsとして、console.logではなくconsole.traceを使うとスタックトレースも表示される、とか、console.time()とconsole.timeEnd()で時間計測ができる、とかがあります。console.log(Json.stringify(hoge, null, 2));できれいにjsonを表示できるとかいうtipsもあります。

あとはバッチを作る時のデバッグでよく使うものとして、以下のような項目をlogに出力することがあります。

  • 実行時引数process.argv
  • 環境変数process.env
  • 実行中プログラムの場所global.__dirname
  • ワークディレクトリprocess.cwd()
  • 実行環境process.platform

コールバックをとる昔の関数をコールバック地獄から救う

util.promisify(fs.readFile)などとすると、async, awaitがつかえるようになります。

Discussion