Rのつく財団入り口

ITエンジニア関連の様々な話題を書いているはずのブログです。

【感想】『実践Node.js入門 ―基礎・開発・運用』:Node.js周りを体系的に学ぼう

Node.jsの設計思想・動作から理解し、アプリケーション開発の実践的なスキルを身につける

 2023/1に出たばかりの本です。著者の伊藤康太さんは元Yahooのスペシャリスト認定制度の黒帯保持、有識者による一冊。タイトルは「Node.js入門」とありますがNode.js本体の話にとどまらずJavaScriptの文法の話、ExpressによるWebアプリにCLIツールにフロント/バックエンド両方の開発や運用...と、実質最新のフロントエンド・バックエンド全般を概観できる本となっています。384Pなのでそんなに分厚くはないですね。
 Node.jsは仕事でもインストールはしてるしたまに使うこともあるのですが、ふんいきでなんとなく使ってる感はずっとあったので、これを機会に体系的に押さえなおそうとじっくり拝読しました。

1 はじめてのNode.js

  • Chrome内蔵のV8JavaScriptエンジンを使ったJavaScriptランタイム。バックエンドのサーバーやフロントエンドのツールにも使えるように。
  • 2009年登場の黎明期を経て、2010年代前半から市民権獲得。
  • 非同期のイベント駆動型ランタイム:クライアントからの命令は非同期で実行される。JSは元からブラウザ上で非同期なイベント処理に適していたので、バックエンドでも書きやすい。
  • シングルプロセス・シングルスレッドで動作:プロセスが1つなので軽量。
  • イベントループとNon-Blicking I/O:ユーザーA,Bからリクエストが飛んできたときに、Aの分が終わるのを待たずにBの分を処理できる。イベントを細かく分割している。無限のイベントループがずっと動いているので、何もしないアイドル状態になりにくい。
  • 同時接続数が大まかに10万人を超えると重くなる「C10K問題」に対し、上の特徴で対応。(ただし近年は他のWebサーバーも問題なくなっている)

  • 2000年代後半のJS復権とともにプラグインやパッケージ管理が必要になってくる→Node.jsが持っていたnpmが解決。 沢山のファイルを完成版では1つのファイルにまとめてバンドル、minifyで最小化するタスクランナーが必要。初期はGruntやgulpなど。SPAフレームワーク群にも必要。→これらのバックにNode.jsが。

  • JavaScriptにはモジュール化の仕組みがなかったが、ES6からようやく諸々仕様が策定。古いブラウザでも動くようにトランスパイルするBabelが登場。モジュール化のES moduleが一般化する前にCommonJSの考え方もあり、バンドラーでBrowserifyやWebpackが登場。→これらのツール群もJavaScriptで実装、バックにはやはりNode.jsが。
  • SSR: Server Side Rendering SPAベースのアプリでも最初の1回は空のHTMLを返すのでなく、サーバーサイドで描画して表示してクローラーに引っかかるようにしたい。フロントエンドがJS実装ならバックエンドも同じJSでやったほうが良いので、実質Node.jsが必須になる。
  • BFF: Backend For Frontend SPAベースの画面に各種APIからいろいろ値を返すなら、複数APIを束ねる役割が必要だねという概念。これもフロントエンドエンジニアにとっての扱いやすさを考えるとNode.jsでやるのが合理的。

 最初は歴史を踏まえてNode.jsの役割を語っています。個別には知ってはいた話もあるのですが体系的に解説しているのでありがたい。改めてNode.js登場が2006年頃のJavaScript復権や、その後のフロントエンドの隆盛に大きな役割を果たしたのだなあと思います。
 Node Package Managerは最近は略称でなく正式名称がnpmぽいというのは初めて知りました。
 なお本書では解説の意図があってコードはJavaScriptになっていますが、複数人が関わるJavaScriptプロジェクトを始める時はもう最初からTypeScriptを推奨となっています。本も出揃ってきましたし最近はTSが整ってきた感がありますね。
 あまり意識していなかったのですが、ブラウザ上で動作するJavaScriptとNode.js上で動作するバックエンドのJavaScript微妙に違うとのこと。本書でしっかり押さえておかねば...

実践Node.js入門 - Node.js周りを体系的に学ぼう

2 JavaScript/Node.jsの文法

  • インストールは安定版LTSの最新バージョンがオススメ。偶数番号が安定版になる。現在v18。
  • JavaScriptの文法諸々。データ型でundefinedがその変数が未定義、nullはデータがないことを表す。
  • Object型は中に何でも持てるが、JSONは文字列、数値、配列、boolean、null、JSON Objectしか持てないのに注意。関数を持ったObjectをJSONに変換して戻すと一部消えたりする。
  • 継承は、例えば Array.prototype.myFunction = .. のように定義するとできる。class構文はシンタックスシュガー。
  • Node.jsを使ったバックエンド実装では、中に状態を持ったclassを使う設計は避けた方がよい。
  • 有名なthisの指す先が変わる挙動の話。アロー関数で解決する。
  • const c = [...a]; のように既存の配列やオブジェクトを展開するSpread構文をよく使う。元のオブジェクトとは違う新しい参照を持ったオブジェクトを作れる。(中にまたオブジェクトを持っていたら挙動が違う。
  • const [first, second, ...foo] = のようにオブジェクトからまとめて取れる分割代入も便利。Reactで出てくる。
  • ループで添字iが必要なら伝統のforループ、不要なら for...of が便利。

アロー関数は省略しまくると const doubleFunc = a = > a* 2; のように書けますが、本書の作者さんは(){}return文はむやみに省略しない派とのこと。僕もバックエンドが主なのでコードやコメントは冗長でもいいから他人や未来の自分向けにちゃんと書く派なので同意見なのですが、このへん世の中の流れや言語ごとの文化圏によって違いがありますね。
 ここで分割代入のコード例で console.log(foo); した結果が20,40,50になっていたのですが、30,40,50の間違いかな? と思いました。

3 Node.jsとモジュール

  • 歴史的経緯から、Node.jsは標準未策定の頃に採用したCommonJS(正確にはCommonJS modules)と、のちに標準になったECMAScript modules の両方を使っている。後者が言語としての標準だが普及にはまだまだ。
  • CommonJS:
    • コード中では export.(関数や変数) = .... で別ファイルからは require('./sample'); のようにして読み込む。
    • 変な別名はつけない方がよい。
    • module.exports = ....と書く方法もあるが、exportの方がおすすめ。
    • モジュールはシングルトンで読まれるので、中の変数を書き換えると他の場所で読み込んでも反映される。
    • 拡張子は *.js*.cjs がデフォルト。
  • ECMAScript modules:
    • 標準では *.mjs という拡張子を使う。
    • ドットがつかない export const hoge = 1 のようにしてエクスポート。
    • 別ファイルからは import {hoge} from './hoge.mjs'; のように拡張子まで書く。import * as も可能。
    • export default という変数名未指定でも使える記法もあるが、本書では混在させない方法をオススメ。
    • ボタン押下のイベントなどでimportしてthen で処理を書き、呼び出された時点で初めて読み込まれるDynamic imports機能もあるが、現状はバンドルでファイル名を減らす方が効果は高い。
  • package.jsontypeプロパティでどちらかに固定できる。アプリ開発はまだCommonJS、ライブラリなら両方に。
  • ブラウザの上で動く標準のJavaScriptにないNode.jsでの各種実装が「標準モジュール(Core API)」。ファイルアクセスなどなど。最近はrequire('node:fs'); のようにnode:接頭辞で区別することもある。今後は増えていく予定。
  • package.json"private": true; と書いておくと公開されないので安心。
  • npmで公開の全てのモジュールには適用されきってはいないが、セマンティックバージョニング(semver)が推奨なので参考に。
  • npm 以外の yarn や pnpm などのパッケージマネージャーをサポートしようという動きもある。

 時々遭遇することのある*.cjs*.mjsってそういうことだったのか...!と改めて整理になる章でした。今後はもうECMAScript modulesでシュッと統一、となれば良いのですが、CommonJSもまだまだ使われているとのこと。このへんJS文化圏はいつも発展の途中だなと。
 yarnは速いが結局npm本体も進化してるので結局npmのままで良いというような話も聞きましたが、他にもパッケージマネージャーでpnpmがあるのですね。いやはや広い世界です。

4 Node.jsにおける非同期処理(フロー制御)

  • イベントハンドリングがCalblack, Promise, async/await, EventEmitter/Stream の4種。可能ならasync/await, ストリーム処理のみEventEmitter/Streamに統一するのが本書お勧めの設計パターン。
  • Node.jsは常時イベントストリームが動いているので、同期処理でイベントループを停止させないことが重要。
  • 非同期処理は実はlibuvというライブラリから提供されている。V8エンジンで実装されているのが同期処理(JSON操作や配列など)
  • もっとも古くからある Callback: readFile(ファイル名, コールバック関数); 方式;
    • コールバック関数の中で次の関数とコールバック関数を書くので、ネストが深くなりがち。いわゆるコールバック地獄。
    • ループで順番に書き込むなどの処理が、処理終了の順番が思った通りに行かない場合がある。再帰処理するとよい。
    • Node.js提供のAPIは関数の最後の引数がCallback、そしてCallbackの第一引数がエラーオブジェクトという慣例あり。変数errのnullチェックとリターンが必須。またtry-catchで補足できないのも注意。
  • Callbackの弱点を解消したPromise
    • const PromiseFn = new Promise((resolve, reject) => { (正常時はresolve,失敗時はrejectを返すよう実装) });
    • そしてthen, catchで受ける。
    • ネストを防いでチェーンで書ける。しかしループや条件分岐になるとまだ苦しい。CallbackのPromise化も実はできる。
  • 推奨のasync/awaitシンタックスシュガーで同期的コードのように書ける。最推奨。
    • async function someFn() = { await Promiseを返す式 };
    • const someArrowFn = async () => { await Promiseを返す式 };
    • これらの関数を実行、.then((data) => {}) や.catch((err) => {}) でエラーハンドリングできる。
    • Promiseと組み合わせて並行処理も実はシンプルに書ける。
  • ストリーム処理のEventEmitter/Stream

    • 処理の開始や途中や終了、エラー時など様々なタイミングに別の処理を挟める。Core APIで実装。
    • EventEmitterクラスを拡張したクラスを定義、newして作り、.on('{イベント名}', {入れる処理}); でリスナーを設定
    • .emit('{イベント名}', {引数}); でイベントを発生。細切れの非同期イベントに対応できる。
    • このEventEmitterにデータを貯めるバッファのようなものがStream。HTTPサーバーを立てるコードでも使われている。データを少しづつ処理できる。
    • このStream処理ではtry-catchではエラーが捕捉できないのでエラー用イベントの定義が必要。
  • AsyncIterator : async/awaitとストリーム処理を一緒に扱える

    • for await (変数名 of iteratableな対象) { のようにして、大量データを少しづつ処理する非同期処理を同期コード的に書ける。
  • エラーハンドリングをまとめると、同期処理ならふつうのtry-catchasync/awaitでラップするの1通り。非同期処理はそれぞれ違う、となる。
  • 他の技術では1リクエスト=1プロセスだがNode.jsは複数リクエスト=1プロセスの割り当てになるので、プロセスがクラッシュすると全リクエストがエラーになるので注意。
  • Top-Level Await: v14.8.0から可能に。asyncな関数の中でなく一番外側でもawaitが書ける。ECMAScript modulesのみ。例えばHTTPサーバーの実装だとリクエスト待ち受けになりパフォーマンスは悪くなる場合もある。

 JavaScriptの入門書などでもたびたび登場するこのあたり、本格的に使い込まないとなかなか身につかずよく忘れてしまうのですが、本書では改めて整理されています。こうして一覧化されると改めてasync/awaitがだいぶ見やすくなったのだなあと分かります。
 ファイル処理やHTTPサーバーなどコード例も登場するのですが、バックエンドの他の言語に比べるとやはりかなり独特なところがありますね...注意が必要そうです。
 コラムにはNode.jsの進歩を加速させたio.jsや、async/awaitと名前が同じでややこしいけどそれより前に同じ問題を解決しようとしたasync.jsの話なども載っています。JavaScript周りはこういう四方山話が豊富なのでこういうのもありがたいですね。

5 CLIツールの開発

 Node.jsのパワーを活かして、ローカル上のMarkdownファイルを読み込んでHTMLに変換してファイルで出力するコマンドラインツールを作っていく章。難易度的にもちょうどよい題材かと思います。

  • まずはプロジェクトのフォルダを作って、>npm init -y でpackage.jsonを作る。
  • ファイル読み込みは標準モジュールのfs.readFileSync(....) で可能。CLIツールなので同期処理で良い。
  • コマンド引数はグローバル変数process.argv で取れる。配列の0,1,の先の2番目~が引数。
  • yargs モジュールが上位互換で引数処理諸々をよろしくやってくれる。オプションを表示してくれたり。
  • 処理が長くなってきたら1ファイル=1関数基本で lib/ の下に分割したり。ファイルパスは絶対パス注意。
  • .md->.html への変換は今回は marked モジュールを使う。marked({入力文字列}); と関数を呼ぶだけ。
  • リンターとしてはよく使われるESListを >npm install。package.jsonの中でルールを書いたりお勧め設定を読み込んだり。
  • そのままだと実行は > ./node_modules/.bin/eslint *.js とコマンドが長いので、package.jsonのscriptsプロパティにショートハンドを書いて便利に。./node_modules/.bin/ のパスを解決してくれる。>npm run で実行。
  • npmでESListを入れた上で、VSCode拡張機能「ESLint」を入れるとVSCode上でも実施できる。
  • ユニットテストは、実はNode.js標準モジュールにassertモジュールがあり、型まで含めたassert.strictEqual()やオブジェクトの中まで見てくれる assert.deepStrictEqual()などができる。
  • しかし前処理後処理などの高度な機能はないので、「テストランナー」を導入。Jestやmochaが有名。
  • testコマンドは特別扱いで、package.jsonにコマンドを書いておくと、>npm run test でなく > npm test で可能。
  • テストコードの置き場は、JavaScript文化圏では統一されていない。
    • tests ディレクトリにまとめる。コンテナ化の時に外したり
    • 同じ階層に*.test.jsを置く。相対パスが同じになるメリットあり
  • フォーマッターはPrettierが有名。

 他言語の経験はあるのでこの章は割とスラスラ読めてだいぶイメージが湧きました。npmに公開されているJS製CLIツールのモジュールも、根本的にはこんな感じで開発されているのですね。
 コラムで日時処理は昔はmoment.jsがあったが今日ではDays.jsやdate-fnsがお勧め...とあり、こういうこぼれ話も役に立ちます。
ちょっと分からなかったのはファイル分割でlibの下に切り出されたファイルでした。exportsしたい関数の外側で変数を宣言してるのですが、こういう変数は関数の中で定義するのがふつうじゃないでしょうか..JSやNode.js界隈だとお作法が違うのかな?

6 ExpressによるREST APIサーバー/Webサーバー

 指定されたURLへのアクセスでレスポンスやHTMLを返す、NoSQLのRedisからユーザーデータを返す、エラーハンドリングやテストもしていこう...という実戦的な章。

  • 実は標準モジュールのhttpでもAPIサーバーはすぐ書けるのだが、本格的なAPI/Webサーバー構築ならフレームワークを使う。今もExpressがデファクトスタンダード。ミニマムな作りでパフォーマンスを下げない。
    • ルーティング機能
    • ミドルウェアという各ルートに対応したコールバック関数 の2機能
    • 開発者側でさらにエラーハンドリングもしておけばOK。
  • ふつうにnpmでインストール、変数appに入れたら app.get({パス}, {ミドルウェア関数}, {ミドルウェア関数}...) でルーティング。
  • app.listen({ポート番号}, {コールバック関数}); で立ち上がる。
  • ミドルウェアは引数にreq,resを取り、複数動かす場合は第3引数のnextnext()で呼べばよい。
  • app.use({全ルート適用のミドルウェア関数}); のようにして共通ロギングなども可能。
  • Expressフレームワークが用意しているミドルウェアも幾つかある。

  • app.use((err, req,res, next)=> {500を返すとか}); のようにして全体のエラーをハンドリング。

  • asyncな関数が出す非同期エラーはキャッチできないので注意。
  • Redisとの接続には、ioredisというモジュールを使用。Redisオブジェクトをnewしてパラメーターで接続情報を渡す。
  • Node.jsではprocess.env.XXX環境変数が使える。
  • このオブジェクトはEventEmiitterを継承しているので一度だけ実行されるイベントハンドリングの.onceで起動処理のハンドラー関数を書けばよい。
  • 本書のサンプルではハンドラー内でPromise.allを使用して初期のuserデータを保存。ハンドラー内部でエラーが起きるとキャッチできないので、明確にtry-catchで処理する。
  • Redisから取得したデータは4章で出てきたAsyncIteratorの書き方で変換してレスポンスに返す。
  • ページングが必要な場合はURLパラメーターのoffsetに載せるのが一般的。Redisを呼ぶ関数の引数で使える。

  • Webサーバー側でHTMLを生成して返したい場合は、テンプレートエンジンとしてはejsが有名。

  • 普通にnpmでインストールし、サーバーのコードに app.set('view engine', 'ejs'); と書いて認識される。
  • レスポンスを返すミドルウェアの関数内で res.render({xxx.ejsの絶対パス}, {セットする値の集合}); のように書く。 慣例的に app_name/view/xxx.ejs の場所に書き、中身はHTML。<% ~ %> でJSが書けてそこに変数をセット。
  • 純粋なjsファイルなどは app_name/public/jsfuncs.js に配置することが多い。ルーティングではここを /static として設定、EJSの中のHTMLの<script>タグ内でも static/jsfuncs.js のように書く。

  • express-generatorという初期コードを生成してくれるライブラリがあるが、ファイル構成の考え方が若干古い。本書のスタイルではserver.jsという単一ファイルにルーティングをまとめる方法を推奨。

  • 設定ファイルの分割は昔はconfigモジュールというものが長く使われてきたが、近年はなくてもよくなりつつある。本書では単一のファイルに集約。

 DBなどを指すミドルウェアとは別の意味のミドルウェアは、PHPのLaravelフレームワークでもルーティング周りに出てくるので、へーと思いました。共通処理だけを指す訳ではなくNode.jsの文脈だとパスに対応して動く関数全体を指すのでしょうか? などとWikipediaを見たら

また別の意味として、Django、Laravel、ASP.NET CoreなどWebフレームワークの一部において、HttpリクエストからControllerに至るまで、およびControllerからHttpレスポンスを返すまでの共通処理を層状に追加する概念である。

とありました。なるほどなるほど。他の言語でのフレームワーク類を知っていればだいたい対応するところがあるので、この6章前半は他言語の方も理解できていくのではないでしょうか。
難しいと思ったのはところどころにasyncがついた非同期の関数が入ることですね。ハンドラー内部のエラーはキャッチできないそうですが、あれEventEmiitterの解説と違うような...(混乱中)

本書の例ではファイル構成は以下のようになるかと思います。

app_name/
+- public/
    +- index.js //HTML内で使うJS関数
+ handlers/
    +- user.js // ルーティングで/userに来たときのハンドラー関数
    +- user.test.js // ハンドラー関数に対応したテストコード
+ views/
    +- user.ejs // ルーティングで/userに来たときのレスポンスで返す画面
    +- index.ejs // デフォルトのレスポンスで返す画面
+ server.js // app.jsなどでも。Webサーバー起動、ルーティング、DB起動、初期データ投入など
+ config.js // 明言されていないが設定ファイルもルート直下のここ?
+ package.json // いつもの
+ package-lock.json // いつもの
  • テストランナーのJestにはjest.mockという機能で、読み込んでいるモジュールをモック化できる。本書の例ではRedisとのやりとりをモック化。
  • async/awaitとストリーム処理を一緒に扱えるAsyncIteratorの書き方をしているコードのモック化はちょっと高度になる。以下を活用して行う。
    • JSの仕様で*をつけると何度も値を返す関数が定義できるGenerator関数
    • JSの仕様でオブジェクト生成のように書くが===が真にならないSymbol
  • 失敗時のテスト
    • モック化されたクライアントにErrorを渡して、ハンドラーの関数を呼んでcatchに来ることで確認したり
    • Jestはexpectが何回呼ばれたかで判定するテストも書ける。これが重要になる場合も。
  • ハンドラーの単位とテストのしやすさの話
    • 本書のサンプルコードのgetUsers等の関数は引数req、戻り値が結果になっている。
    • 引数にres追加、これにステータスコードや検索結果を加える方法もあるが、依存が発生して後々大変になる話
    • ルーティングからはまずハンドラー関数自体を引数にしたエラーハンドリング用関数を呼ぶというテクも
    • エラーをクラス化するテクなどもある

Jestは様々な機能があって便利そうですが、モック周りも入ってくると本格的になってきてテストコードを書くのもけっこう難しそう...!と思いました。ちゃんと入門しないといけなさそうですね。
作者の方が様々な現場で試行錯誤して得られたのであろう、様々なテクニックも入っていて参考になります。このへんやはりNode.js開発で標準で唯一定まっている方法はないのですね。

  • Node.js製アプリをLinuxサーバーにデプロイする時の話
    • サーバーにもNode.jsを入れ、アプリをフォルダごと配置。
    • Linuxなら大抵最初から入っている起動処理やシステム管理を行う仕組み、sysmtemdをまず使うのがよい。
    • もっと本格的にプロセス管理が必要になったらnpmでインストールできる PM2, foreverを使う。
    • 新規リクエスト受付停止にしてから安全にシャットダウンするGraceful Shutdownのサンプルコード
    • シングルプロセス・シングルスレッドなので、プロセスごとに開けるファイル数設定のファイルディスクリプタは多めにしておく。
  • Docker上で動作させると、内部でプロセスはpid=1として起動する。Ndode.jsはpid=1として動作するよう設計されていない。結果としてCtrl+Cで止まってくれない。> docker run--init をつける方法がある。
  • デプロイ先がマルチコアでCPUが複数あると、Node.jsはシングルコア・シングルプロセスなのでリソースを活用しきれない。npmで入れるclusterというモジュールでマルチプロセス対応ができる。
    • CPUの数まで増やしたり、-1に留めたり、半分にとどめたり
    • クラウドのPaaS環境ではプラットフォーム側でスケールしてくれるので、そちらに任せた方がよい。
  • RDBに繋げる場合...
    • 最近の、Promiseのインターフェースに対応したモジュールを使う。
    • SQL部分を担うクエリービルダーではKnex.js
    • より進んだORMではSequelize、MongoDB用のmongose
    • TypeScriptではTypeORM, Prismaなど
    • プレーンなJavaScriptオブジェクトに変換して使うようになるとORMの意味が薄くなってしまったりもする。今後も移り変わりがありそう。

 沢山CPUを積んだマルチコアだとそれ用のモジュールがあったりするのか...なんて話は全く知りませんでした。浅い情報だとこのへん載ってないことも多いのでありがたいです。シングルプロセス・シングルスレッドならではの注意点がありそうです。

7 フロントエンド/バックエンドの開発

 なかなか長い充実の7章は、フロントエンドをReact、バックエンドをExpressで動くAPIサーバーとして、両方を同じリポジトリで実装していきます。

  • モノレポ(Monorepo): 複数のアプリやパッケージをひとつのリポジトリで管理。フロントとバックにも。マイクロサービス指向で細かく分割しすぎると影響範囲が広がってしまうため、モノレポにする手法がもある。
  • 他の管理ツールがLenaやbazelなど。今回取り上げるnpm workspacesはnpm v7でnpm自体に追加された機能、package.json内にworkspacesプロパティで管理したいパッケージを記述。今回はpackages/* とし、packages/frontend, backendの2つを管理。それぞれの下にまたpackage.jsonがある。

qiita.com

  • いにしえのjQueryスタイルではボタンクリックごとにイベントでHTML操作を記述したりしていたが、HTMLが表示だけでなく状態の責務も保持してしまい、複雑化すると管理が大変という弱点。ここでフロントエンドフレームワーク群が生まれた。
  • Reactによるフロントエンドの開発

    • 最初は > npx create-react-app packages/frontend でファイル一式が生成。
    • src/App.js がトップのReactコンポーネントで、モジュールはECMAScript modules 形式。
    • 昔はjQueryがブラウザ間の差異を吸収。その後BabelがES6→ES5への変換に。TypeScript言語もJavaScriptへの変換が必要。モジュールやライブラリの増加でコードを結合するためにビルド工程、全体をひとつにまとめるモジュールバンドラーが重要に。
    • これらの経緯があり2015年頃からビルドが必須に。React固有のJSXもビルドで変換される。create-react-appでは仕組みの中に隠蔽しているが、ビルドの中でトランスコンパイル、モジュールバンドラーが動いている。
    • Reactコンポーネント実装を進め、関数内で変数を持つように。描画ではバーチャルDOMを使い差分だけを再描画。繰り返し表示にはkeyが必要。
    • ボタンを押した際のhandleSubmit関数をコンポーネントの中に実装。素のJSのaddEventListener関数を使う方法だと、DOM要素がなくなったら消えてしまう。
    • 入力した文字列が再描画でも消えないように、ここでReact Hooks登場。useStateを使って保持。ここで新しい描画用配列を元の配列とは別に作りたい場合などに、...usersのSpread構文がよく出てくる。
  • React開発の応用

    • コンポーネントが大きくなってきたら分割。propsを渡すのにモダンJSの分割代入の記法を使ったり。
    • create-react-appには最初からホットリロード機能が入っているため、コード修正をすぐ反映してくれる。
    • バックエンド側も一緒に起動するときは両方がポート3000で被ると使えない。
    • Reactコンポーネント内からバックエンドを呼ぶ時は、今はfetch('{URIを書く}') で済む。返ってくるbodyはStearmオブジェクトなので、.json() という関数を呼べばよい。昔はXMLHttpRequest型だったが進化した。
  • 同一マシン上でフロントエンドとバックエンドを別のポート番号でWebサーバーとして起動していると、Cross-Origin Resource Sharing(CORS) が起こる問題アリ:
    • 開発サーバーではcreate-react-appにproxyオプションが内蔵されている。packaje.jsonにバックエンド側のポート番号(本書ではhttp://localhost:8000)を指定、fetchのコードでは /apiのようにドメインの後からだけの記述に。
    • 本番サーバーにフロントエンド部分を配信する場合は、Node.jsで動いているならhttp-proxy-middlewareを使うとよい。フロント配信用のコードをExpressフレームワークで実装、server.jsとして書いた基本コード内に記述。
    • 他、フロントエンド部分をnginxで置いてProxyサーバーにする方法もある。
    • 近年はフレームワークが自動的に吸収してくれていることもある。本書では別コラムで説明。
  • React Hooksの活用
    • コンポーネント呼び出し時に1回だけfetchしたい時は、useEffect({実行する関数}, []); ただし空配列にしてもReact v18~では2回実行されるので注意。
  • Client Side Routing

    • JavaScriptHistory API実装に伴い可能になってきた。画面リロードが伴わず、スマートフォンアプリの利用体験に近づける。
    • 自前実装も理論的には可能だが、ReactではReact Router(react-router-dom)を使うのが主流。
    • ルートで呼び出されるApp.jsのReactコンポーネントAppで、これをimport、<Router><Routes>~</Routes></Router>要素内にパスと呼び出す子供のコンポーネントを書いていく。
    • ルーティングを使っている場合はリンクは<a>要素で書くとブラウザの読み込みが発生、CSSなどのリソースをネットワークからもう一度取ってきてしまう。用意されている<Link to="">~</Link>を使うほうがよい。
  • フロントエンド部分のデプロイ

    • 本書の例では > npm run build -w package/frontend で本番用のものができる。
    • Expressフレームワークを使った配信用のコードもfrontend/内に格納。GETリクエストは/でなく /*index.htmlを返すようにしておく。
    • frontend/package.json 内に設定してコマンドで起動。
    • Reactコンポーネント部分を初回はサーバー側で描画する Server Side Renderingは、理論的には自作もできるがNode.jsの知識等々も必要。ラップしてくれるNext.jsに任せるのがお勧め。Vercel社の企業主導だがGoogleもサポートしている。

 本書の構成ではフロントエンド部分もNode.jsのサーバーで動かして、バックエンドもとはポート番号で分ける構成でやっています。2つあって慣れないうちはちょっと混乱しそうですね。

  • フロントエンド部分のテスト
    • 実行環境でNode.jsは必要、標準のテストランナーであるJestがcreate-react-appでは最初から入っている。
    • バックエンドのテストと違うのはDOMがあることだが、DOMをエミュレートしてくれるjsdomというライブラリを使う。
    • テストの中で共通して現れる描画の部分をカバーしてくれるtesting-library というモジュールがcreate-react-appには入っている。
    • バックエンドへのfetchのテストについては、fetch処理をmock化するか、mockサーバーを別に用意する2通りがある。testing-libraryがサポート。
    • mockサーバーでの方法は、*.test.jsGET /api/hoge が来たらこれを返すよ...というのを書き、その後にテスト本体も書く。
    • 本書の例ではUserコンポーネントがデータの取得と描画を両方受け持っていたので、描画はコンポーネント、データの保持・変更はCustom Hooksを使った別関数に分割。ファイルはsrc/Users.hook.jsとしている。それぞれをテストしやすくできる。
    • mockの利用は一長一短な所もある。
    • コンポーネントの責務としてのロジックと描画はなるべく分離、一つのテストでは片方だけをテスト、は常に正しい。
    • 描画されたHTMLを文字列で比較するスナップショットテストの手法もある。react-test-rendererを使用。無闇に採用すると手間が増えるだけのこともあるので注意。ロジックのテスト、コンポーネントのテストの方がよい。
    • ブラウザ操作自動化ではPupeteer、Chrome以外ならMicrosoftのPlaywrightがある。

 この7章がなかなか長く本格的なのですが、React Hooks含めた最新のReactでの基本的な開発諸々~デプロイ~テストまで、様々な知見が詰まっていて非常に充実しています。フロントとバックは別リポジトリで別々に開発するのが普通かと思ってたんですが、今はモノレポの手法もあるんですねー。コラムにもいろんなお役立ち情報があってありがたいです。

8 アプリケーションの運用と改善

  • Node.js自体も各種パッケージもなるべくバージョンアップする。> npm outdated で各パッケージがどれだけ古いか分かる。
  • >npm audit脆弱性チェック。>npm audit fix ですべて解決されるわけではないので注意。
  • 作成済みのモノレポなプロジェクトでも、後からworkspace(物理的にはフォルダ)を追加して共通部品を切り出したりできる。依存の方向は片方向に。
  • アプリ実運用に向けた注意として...
    • セキュリティ、パフォーマンスの観点からNode.js自体もなるべく最新のLTSを
    • ルーティングを通して飛んでくるリクエストは、複数同時処理がありえる。同じプロセスにアクセスされる。キャッシュはユーザー別にしないと問題に。
    • 巨大なJSONはparseに時間が掛かる。検索処理ならページングしておくなど。
    • リクエストごとに通るコード内では-Syncが関数名につく同期関数はなるべく避ける。そこでパフォーマンス低下。
    • でかいループは処理中にイベントループが低下するので避ける。
    • パフォーマンス計測でも各種ツール、工夫して計測。メモリリーク発見の手法もある。

 この8章は短いのですが実際の運用時の知見が詰まっています。ユーザー情報をキャッシュとして単に変数に入れておくと次のリクエストでアクセスできちゃったりするんですね。このへんかなり独特だなと思いました。他言語のバックエンド実装と同じ雰囲気でやっていると罠に嵌りそうです。

実践Node.js入門 - Node.js周りを体系的に学ぼう

まとめ:テーマ一式網羅、最新情報に基づいたNode.js周りを体系的に学べる本

 国産の本なので翻訳本特有のとっつきにくさもなく、文章も読みやすかったです。後書きによると作者さんもNode.jsを様々な案件で使い、慣れない頃は勉強会をやったり毎週泣きながらNode.js自体のソースを読んだり(ヒエッ...笑)したそうですが、そうした長年の経験に基づいた知見、独自の意見などもしっかり詰まっています。自分的には断片ごとには知っていたようなことも載っていたのですが、そうした断片が体系的に繋がり、知らなかったことが補完され、最近のアップデート分が上書き更新され、体系的に改めて整理できた感がありました。ちょうど勉強会でフロントエンド関連の話題をやろうと思っていたのでよいタイミングで読むことができました。
 Node.js自体にモダンJavaScriptにReactにExpressにDBアクセスにユニットテストに運用とテーマ一式全部載せ、オールラウンドで詰まったおトクな本です。現役フロントエンドエンジニアの方もこれから挑戦する方も、バックエンドの方も、十分に役に立つことと思います。

おまけ:Node.js関連書籍

本書から過去に遡っていくと...安心のオライリー『ハンズオンNode.js』が2020/11。500ページ以上のしっかりした本です。

初心者向けの様々な超入門本を出している掌田津耶乃さんの『Node.js超入門[第3版]』が2020/7。その前の第2版が2018年で僕も読んだような...まあエンジニアが仕事で使うにはちょっと浅いシリーズですね。バックエンドのFWはExpress、DBアクセスにsequelizeが出てきます。

同じ超入門シリーズで『Node.jsフレームワーク超入門』が2022/5で新しい本。バックエンドでExpress、TypeScript夜のORMにPrisma、TypeORM。他にFWでSails.js、Adonis.js、NestJS、Meteorという珍しいテーマもあります。この辺をつまみ食いするにはよいかもしれません。

『Node.jsデザインパターン 第2版』が2019/5。こちらもがっつりしたオライリー本、Node,js v11.9を対象にガッツリと内部に踏み込んだ中級者以上向けの本。

『入門Node.jsプログラミング』も2019/9。こちらも同じく海外本、基本は分かっている中級者以上向けに様々なプロジェクトと共に解説していく本。

『サーバサイドJavaScript Node.js入門 (アスキー書籍)』は遡って2014年、うーんNode.jsが徐々に市民権を得てきた頃の唯一の本ですね。さすがに今となっては古いですね。(僕も前に読もうと思ってやめたことがあります...)

Node.js自体を扱った本というのは意外と少なかった感がありますが最近揃ってきました。超入門シリーズよりもっと深い本、かつ海外本特有のとっつきにくさがない国産本、そしてエンジニアが仕事で使うレベルの各種テーマを総合的に網羅した最新の本...という立ち位置でも、本書『実践Node.js入門 ―基礎・開発・運用』はちょうど良い位置にフィットするのではないでしょうか。

 仕事で使うレベルのTypeScript自体の一冊というと2022/4に出たブルーベリー本こと『プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで』がド定番、もしくは2020/3のオライリー本の『プログラミングTypeScript ―スケールするJavaScriptアプリケーション開発』の2冊が有名。

そして最近は2023/2の『フロントエンド開発のためのセキュリティ入門 知らなかったでは済まされない脆弱性対策の必須知識』、2023/4に出る『フロントエンド開発のためのテスト入門 今からでも知っておきたい自動テスト戦略の必須知識』と今まではなかった個別テーマの本も出てきました。

JavaScript自体の本というと2020/4のJavaScript Primer 迷わないための入門書』が現場のエンジニアからも推奨されていましたが、その後オライリーのサイ本JavaScript 第7版』が2021/12に出たり、前の版も評価の高かったJavaScript本格入門が2023/2に『改訂3版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで』と遂にアップデートされました。

一昔前まではフロントエンドは変化が早いから本が出ない!なんて言われていましたが、こうしてみると商業書籍もだいぶ陣容が揃ってきましたね。2023年現在では、学ぶ環境が成熟してきたといえるでしょうか。