冒頭
「理論を伴わない経験は盲目だが、経験を伴わない理論は知的遊戯に過ぎない」
カント
1章 イントロダクション
- Node.js の特徴
- 並行処理する
- 「コンビニ店員が弁当を温めながら次のお客をさばく」
- WebサーバーのI/O
- 従来はスレッドでやっていた
- 「客Aスレッド、客Bスレッド……」とマルチスレッドで切り替える
- 生成と切り替えコストが大きく、大量にリクエストをさばけない
- 「客Aスレッド、客Bスレッド……」とマルチスレッドで切り替える
- Node.js はイベントループ
- シングルスレッド
- タスクをキューに積んで順番に処理する
- I/O発生タイミングでタスク分割して、実行時に完了後のタスクを指定し、完了後には指定タスクがキューに追加されるようにする
- 「弁当をバーコードで読み取って電子レンジに入れるタスクと、温め終わった弁当を客に渡すタスク」に分割され、完了されたら後者がキューに追加される
- もともとJSにあった
- サーバーサイドに適用した
- もともとJSにあった
- タスクをキューに積んで順番に処理する
- シングルスレッド
- 従来はスレッドでやっていた
- WebサーバーのI/O
- 「コンビニ店員が弁当を温めながら次のお客をさばく」
- 並行処理する
- スモールコアとnpm
- 「コア(Node.jsそのものに付属する機能」は最小限に保つべきである」
- Node.js は TCP, HTTP, DNS やファイルシステムを提供する
- メンテコストが下がる
- 本質的な課題に集中できる
- 外側を束縛しない
- 本質的な課題に集中できる
- ユーザーランド(コア外)でのエコシステムの成長を促す
- npm
- メンテコストが下がる
- Node.js は TCP, HTTP, DNS やファイルシステムを提供する
- 「コア(Node.jsそのものに付属する機能」は最小限に保つべきである」
- モジュールシステム
- 取り外しできる
- UNIX哲学の影響
- 「一つのことを行い、またそれをうまくやるプログラムを書け」
- 再利用性に優れる
- 仕様の把握が容易
- テストしやすく堅牢
- 「標準入出力は普遍的インターフェイス」
- ストリームを扱う
- 高い拡張性
- 「一つのことを行い、またそれをうまくやるプログラムを書け」
- UNIX哲学の影響
- 取り外しできる
- ECMAScript標準
- TC39の仕様標準化プロセス
- 0(たたき台)
- 仕様へのインプット
- 1(提案)
- 課題と実行方法、実行時の課題を特定
- 2(ドラフト)
- 構文とセマンティクスを記述する
- 3(候補)
- 各環境での実装とフィードバックから洗練させる
- 4(完了)
- ECMAScript標準に取り込む準備が整った状態
- 0(たたき台)
- 年次でステージ4になった仕様をリリースする
- TC39でステージを調べれば最新の状況がわかる
- TC39の仕様標準化プロセス
- JSの知識
- オブジェクト
- プロパティ名を指定して取得
obj1.propA
obj1['propA']
- プロパティの追加
obj1.propC = 3
- プロパティの削除
delete obj1.propC
- 元オブジェクトを変更しない方法(イミュータブル)
- スプレッド構文でpropCを追加する
const obj2 = { ...obj1, propC: 3 }
- レスト構文でpropAを削除する
const { propA, ...obj3 } = obj2
- スプレッド構文でpropCを追加する
- プロパティ名を指定して取得
- 配列
- 指定した要素のインデックスを取得(なければ-1)
arr1.indexOf('bar')
- 要素が配列に含まれるかどうか
arr1.includes('bar')
- 結合(引数がなければ
,
)arr1.join('-')
- 末尾に要素を追加
arr1.push('a', 'b', 'c')
- 末尾の要素を削除
arr1.pop()
- 元オブジェクトを変更しない方法(イミュータブル)
- オブジェクトと同様
-
sort()
は破壊的メソッド -
forEach()
はmap()
と違って戻り値なし -
find()
は見つかった時点で反復処理を終了する
- 指定した要素のインデックスを取得(なければ-1)
- クラス
- privateなメンバーには先頭に
#
をつける
- privateなメンバーには先頭に
- 等価性
-
{ foo: 1 } === { foo: 1 }
はfalse
- 構造が同じだけの別オブジェクト
-
- CommonJSモジュール
-
require()
-
require()
は一度ロードしたモジュールをキャッシュする-
require.cache
で取得できるオブジェクトに保存されている-
delete require.cache[require.resolve('./cjs-math')]
でクリアできる
-
-
-
require()
の引数にはディレクトリも指定できる-
index.js
をロードする- まとめられる
-
- JSONファイルもロードできる
-
__filename
,__dirname
でファイル名、ディレクトリ名がわかる
-
-
- オブジェクト
2章 非同期プログラミング
// マルチスレッドでの並行処理
const 金額 = バーコードリーダー.読む(弁当)
const 温まった弁当 = 電子レンジ.チン(弁当)
レジ.会計する(金額)
商品を渡す(温まった弁当)
// シングルスレッドでの並行処理
const 金額 = バーコードリーダー.読む(弁当)
電子レンジ.チン(
弁当,
温まった弁当 => 商品を渡す(温まった弁当) // 処理完了後のタスクとしてコールバック関数を渡す
)
レジ.会計する(金額)
- マルチスレッドでの並行処理
- 「金額はすぐバーコードリーダーで読み取れるが、弁当を温めるのは時間がかかる」
- そこで一時停止してしまう
- ブロッキングI/O
- 別スレッドで変更処理する
- 「別の店員が次の客を接客する」
- スレッド切り替えはメモリ消費する
- スレッドごとに独立したスタックというメモリ領域を持つ
-
pthread_create
はデフォルトで2MB- 16GBメモリなら8,000スレッドが限界
- Webアプリなら同時接続数となる
- C10K問題
- 同時接続10,000クライアントを超えると急に遅くなる
- 16GBメモリなら8,000スレッドが限界
-
- スレッドごとに独立したスタックというメモリ領域を持つ
- コンテキストスイッチが頻繁に発生する
- スレッドセーフにする必要がある
- 更新操作の競合をロックで防ぐ
-
num = num + 1
- 互いに待ち合うデッドロックにもなりやすい
-
- 更新操作の競合をロックで防ぐ
- スレッド切り替えはメモリ消費する
- 「別の店員が次の客を接客する」
- そこで一時停止してしまう
- 「金額はすぐバーコードリーダーで読み取れるが、弁当を温めるのは時間がかかる」
- イベントループでの並行処理と非同期プログラミング
- シングルスレッド
- マルチスレッド固有の問題がない
- C10K問題の解消
- 処理完了後のタスクとしてコールバック関数を渡す
-
温まった弁当 => 商品を渡す(温まった弁当)
- 完了後のタスクが指定されているから次の行に進める
- ノンブロッキングI/O
- 非同期プログラミング
- 制御フローが複雑になりやすいデメリットもある
- 非同期プログラミング
- ノンブロッキングI/O
- 完了後のタスクが指定されているから次の行に進める
-
- CPU負荷の高い処理には適さない
- マルチスレッド固有の問題がない
- コールバック
- Promise
- then()の最後にcatch()を付けるとエラーを集約できる
- try...catch構文と似ている
- 平行実行
-
Promise.all
- 1つでもrejectedなら、結果を待たずにrejected
- 逐次実行の必要がなければ
Promise.all
の方が早い
-
Promise.race
- タイムアウト機能でよく使われる
-
- Promiseが適さないケース
- 内容を少しずつ読み取るAPI
- 3章EventEmitterとストリーム
- 内容を少しずつ読み取るAPI
-
util.promisify
で規約を満たさない関数も Promise を返すようにできる-
fs.readdir
やsetTimeout
-
- then()の最後にcatch()を付けるとエラーを集約できる
- ジェネレータ
-
function*
で始まる-
yield
がある-
generator.next()
でyield
ごとに実行される- イテレータプロトコル
-
value
とdone
を返す- イテラブル
[1, 2, 3][Symbol.iterator]()
- イテラブル
-
-
next()
が呼ばれたタイミングでyieldで値を返す- 値の生成を遅らせることができる
- 反復回数が膨大なとき
- 無限超のイテラブルを処理するとき
- 値の生成を遅らせることができる
- イテレータプロトコル
-
yield
をつけることで非同期処理を同期処理と同じように扱える
-
-
- ジェネレータ関数内の処理を一時停止、再開できる
-
next()
の引数を取得できる -
throw()
の引数を投げる
-
- async/await
- ジェネレータと同様のことが標準構文として提供された
- functionの後ろの「*」がfunction前の
async
に変わった - yieldが
await
になった
- functionの後ろの「*」がfunction前の
- await をつけると関数内の処理が一時停止する
- スレッド処理はブロックしない
- async関数は必ず Promiseインスタンスを返す
- ジェネレータと同様のことが標準構文として提供された
- シングルスレッド
3章 EventEmitterとストリーム
- 1回の要求で処理が複数の非同期処理はどうするか
- EventEmitter で実装する
-
const server = http.createServer()
- サーバオブジェクト(EventEmitterのインスタンス)
- EventEmitter は監視対象
- 監視役はリスナと呼ばれる
-
on()
メソッドにイベント名とコールバックを渡してリスナを登録している-
'request'
など
-
-
- 監視役はリスナと呼ばれる
- EventEmitter インスタンスの生成処理途中で同期的にイベントを発行してはいけない
- リスナは常に同期的に実行される
- P.112
- 11個以上のイベントリスナを登録すると警告表示
- メモリリークしがちだから
- リスナ登録するとガベージコレクトされない
- メモリリークしがちだから
- エラーハンドリング
- EventEmitterは
error
という名前のイベントでエラーを渡す-
on('error', err => console.log('errorイベント'))
- error イベントリスナがあればここに投げられる
- なければ unCaughtException になる
- error イベントリスナがあればここに投げられる
-
- EventEmitterは
- EventEmitter の利用法
- 2パターン
- 直接 new して使う
- 継承する
class FizzBuzzEventEmitter extends events.EventEmitter {}
- 2パターン
-
- ストリーム
- 一度に展開するとでかいファイルなどをストリームで読み込む
-
pipe()
で繋げられる- 継承してから
_read()
,_write()
,_transform()
などのの実装に注力すればいい
- 継承してから
- 完了時に
finish
が発行される-
on('finish', cb)
でリスナをつけておいたりする- 各種ストリームは EventEmitter
-
-
- 読み込みストリーム
- 書き込みストリーム
-
write()
メソッドの戻り値は「それ以上データを流せるか」を返す-
push()
も同じ - バックプレッシャと呼ばれる機能
- ストリームの下流が詰まっていると溢れてしまう
- 戻り値で伝える
- ストリームの下流が詰まっていると溢れてしまう
-
-
- 二重ストリーム
-
stream.Duplex
を継承して_read()
と_write()
を実装する- net モジュールの Socket クラスなど
- 外部とデータのやりとりをする
- net モジュールの Socket クラスなど
- 読み込んだデータを変換して下流に流す
-
- 変換ストリーム
-
createHash()
などで暗号化する
-
- 読み込みストリームの一時停止モードとフローイングモード
-
readStream.on('data', chunk => console.log(chunk))
-
data
イベントリスナを使うとフローイングモードになる- 読み込みストリームから自動的に読み込まれる
- 一時停止モードなら
read()
されない限り停止している- 読み込むタイミングを制御できる
- フローイングモードは制御できない
- 混ぜると予期せぬ挙動をする
- リスナを登録するより
pipe()
で繋げたほうがいい
- リスナを登録するより
- 混ぜると予期せぬ挙動をする
- フローイングモードは制御できない
- 読み込むタイミングを制御できる
- 一時停止モードなら
- 読み込みストリームから自動的に読み込まれる
-
-
- エラーハンドリング
-
pipe()
の代わりにstream.pipeline()
で連結する- ストリームのどこかでエラー発生すると最後のコールバックがエラーを引数にして実行される
-
promisify()
でtry catch
の非同期にもできる
-
- ストリームのどこかでエラー発生すると最後のコールバックがエラーを引数にして実行される
-
- ストリームの終了をハンドリングするなら
stream.finished()
- 一度に展開するとでかいファイルなどをストリームで読み込む
4章 マルチプロセス、マルチスレッド
- プロセスとスレッド
- 複数プロセスを起動する
- 同一プロセスの複数スレッドは独立しているがリソースは共有している
- 通信しやすい
- プロセス間通信(Inter Process Communication、 IPC)よりも
- 通信しやすい
- 同一プロセスの複数スレッドは独立しているがリソースは共有している
- マルチコアシステム
- 別々のコアで並列(parallel)実行できる
- マルチスレッド
- 「2人の店員がそれぞれのレジでそれぞれの客の対応を同時に行う」
- 並行かつ並列という状態もあり得る
- Node.jsのシングルスレッドでのイベントループによる並行(concurrent)動作
- 「1人の店員がある客の弁当を温めている間に次の客の対応を行う」
- 1コアしか使わない
- マルチコアでも並行動作のみ
- マルチコアのシステムだとシングルスレッドでは有効活用できない
- マルチプロセスで動かす必要がある
- cluster モジュールでマルチプロセス化する
- worker_threads モジュールを使う
- マルチプロセスで動かす必要がある
- マルチコアのシステムだとシングルスレッドでは有効活用できない
- マルチコアでも並行動作のみ
- マルチスレッド
- 別々のコアで並列(parallel)実行できる
- 複数プロセスを起動する
- cluster モジュールてマルチプロセス化
-
setupMaster()
で実行ファイルを指定する- CPUコアの数だけ
fork()
を実行する
- CPUコアの数だけ
- IPC(プロセス間通信)チャンネルを介して通信できる
-
process.on('message', <ハンドラ>)
で受信する -
process.send()
で送れる
-
- シリアライズ
- 構造化クローンアルゴリズム
-
cluster.setupMaster({ exec:
${__dirname}/web-app, serialization: 'advanced' })
- Dateや循環参照にも対応
-
- 構造化クローンアルゴリズム
- マルチスレッド
-
worker_threads
モジュールを使う- ポート共有できない
- CPU負荷の高い処理を並列化するときに使う
- 変数はスレッド間で共有されない
- 個別のイベントループを持つ
- 共有する場合は
SharedArrayBuffer
を使う- バイナリデータなので
TypedArray
でラップする-
Uint8Array
やInt32Array
-
- スレッド間の読み書きが競合する
- スレッドセーフに更新したい
-
Atomics.add()
などを使う
-
- スレッドセーフに更新したい
- バイナリデータなので
- 共有する場合は
- 個別のイベントループを持つ
- ポート共有できない
- スレッドプールの実装
-
- スレッド間通信とIPCの違い
- スレッド間通信は
postMessage()
で通信する- デフォルトで構造化クローンアルゴリズムを使う
- コピーせず値を直接他スレッドに渡せる
- メモリを共有できるから
-
ArrayBuffer
などを渡す
-
- メモリを共有できるから
- IPCは
send()
でJSONにシリアライズする
- スレッド間通信は
-
5章 HTTPサーバとHTTPクライアント
- httpモジュール
- APIは低レベルなので簡単ではない
- スモールコアの哲学
- ラップして使うことが多い
-
Express
など- ルーティング
- ミドルウェア
- 関数として実装される
-
app.get()
などの第二引数に渡していた関数
-
- 特定のパスやHTTPメソッドに対応するミドルウェア関数
- ルートハンドラと呼ばれる
- エラーハンドリングミドルウェア
-
Express
は引数の数で通常のミドルウェア関数とエラーハンドリングミドルウェア関数を見分けている-
next()
を使わなくても引数は4つ宣言する
-
-
- 静的ファイル
-
express.static()
ミドルウェア関数を使う
-
- リクエストボディのパース
-
express.json()
やexpress.urlencoded()
-
- 関数として実装される
- プロキシを介したHTTPリクエスト
- ロードバランサやリバースプロキシ
- HTTPリクエストはプロキシからのものとなってしまう
- 中継するときにヘッダを追加する
-
X-Forwarded-Host
- 元リクエストのHost
-
X-Forwarded-Proto
- 元リクエストのプロトコル
-
X-Forwarded-For
- 元リクエストのアクセス元IPアドレス
-
- 中継するときにヘッダを追加する
-
app.enable('trust proxy')
-
req.hostname
などに元リクエスト情報が入るようになる-
X-Forwarded-*
の情報
-
-
- HTTPリクエストはプロキシからのものとなってしまう
- ロードバランサやリバースプロキシ
-
404
や500
でも戻り値のPromiseはrejectではなくfulfilledになる-
response.ok
などを見てハンドリングする
-
-
- ラップして使うことが多い
- スモールコアの哲学
- APIは低レベルなので簡単ではない
- Node.jsプロジェクト
- npmパッケージとして開発する
-
npm init -y
でパッケージ初期化- パッケージのメタデータの
pakage.json
ができる
- パッケージのメタデータの
- ビルドやテスト開発時のみ使うなら
npm install -D
-
scripts
はnpm run <スクリプト名>
が使えるようになる-
test, start, restart, stop
ならnpm <スクリプト名>
で使える
-
-
- npmパッケージとして開発する
- ユニバーサルWebアプリケーション
- SSR/SSGとCSRによってサーバから配信するHTMLとクライアントサイドで構築するHTMLが共通の実装で描画されるアプリ
- Next.jsなど
-
useEffect()
部分はSSRされたHTMLの後に取得される
-
- Next.jsなど
- SSR/SSGとCSRによってサーバから配信するHTMLとクライアントサイドで構築するHTMLが共通の実装で描画されるアプリ
6章 リアルタイムWebアプリケーション
- リアルタイムWebアプリケーション
- リロードすることなく最新情報を同期させる
- ポーリング
- もっともシンプルな方法
- 一定間隔でAPIを叩く
-
setTimeout()
などで実装する - 無駄なリクエストの頻度と状態反映の遅延のトレードオフ
-
- 一定間隔でAPIを叩く
- もっともシンプルな方法
- ロングポーリング
- HTTPリクエストを受け取ったサーバがデータの更新を待ってレスポンスを返す
- SSE(Server Sent Events)
- 一度確立したHTTP接続を保持したまま更新されたらデータ送信する
-
Content-Type
はtext/event-stream
- クライアントは
EventSource
APIを使う-
data, id, event, retry
フィールドを含められる - DevToolsのNetworkのeventsで見れる
-
- クライアントは
-
- 一度確立したHTTP接続を保持したまま更新されたらデータ送信する
- WebSocket
- SSEとの違い
- サーバとクライアントが双方向で通信できる
- 一方向の通信でいい場合はSSEで良い
- プロトコルがHTTPではない
- サーバとクライアントが双方向で通信できる
- ハンドシェイクで始まる
- Node.jsコアだけでは難しい
- ライブラリを利用する
-
Socket.IO
- 最初はロングポーリングしてその後WebSocketにスイッチする
-
ws
- 必要最低限の機能
-
- ライブラリを利用する
- SSEとの違い
- ポーリング
- リロードすることなく最新情報を同期させる
7章 データストレージ
- 開放/閉鎖原則(open-closed principle、 OCP)
- データストレージに対する操作
- プロセス外だからI/O
- ノンブロッキングが原則
- 非同期であるべき
- ノンブロッキングが原則
- プロセス外だからI/O
-
require()
はJSONを自動的にパースする- だがキャッシュが残る
-
delete require.chache[require.resolve('./todos/1.json')]
で消す
-
- 同期的なのでブロッキングである
- アプリ実行中に変更されないJSONファイルを読み込むのに使うべき
- だがキャッシュが残る
- ファイル名やパスや拡張子を取得する
path.dirname(file)
path.basename(file)
path.extname(file)
- まとめて取得する
path.parse(file)
- パスの連結
path.join('path1', 'path2')
- 絶対パスを返す
-
path.resolve('path1', 'path2')
- 第二引数以降に
/
で始まるパスがあったらそれ以降だけを使って構築する
- 第二引数以降に
-
-
bind()
は this を固定する- this に依存するコードがエラーを引き起こさないようにする
-
apply()
も似ている-
apply(1, [2, 3])
のように引数を配列で渡せる
-
8章 ユニットテストとデバッグ
- テストダブル
- スパイ
- 引数や実行回数などを記録する
- スタブ
- 代用品として最低限の機能
- ハードコードされた値を返す関数など
- 代用品として最低限の機能
- モック
- 代用品への事前の期待が記述されていて検証機能を持つ
- スパイ
9章 デプロイ
- PM2
- Docker
- GCP
10章 パッケージ管理
- npm
-
npm init
- package.json の生成
- package.json
- name
-
@babel/core
という名前空間のようなスコープもつけられる- アカウント名
- 組織名
-
- version
- セマンティックバージョニングの v2.0.0の仕様を満たす
- メジャー.マイナー.バッチ
-
1.0.0-alpha.1
はプレリリース
- セマンティックバージョニングの v2.0.0の仕様を満たす
- main
- エントリポイントとなるファイルを指定する
- dependencies
-
^2.6.3
は 2.6.3 以上かつ 3.0.0 未満- 互換性のあるバージョン
-
>1.0.2 <=2.3.4
で上限下限を指定できる- 片方でも良い
-
~1.2.3
は>=1.2.3 <1.3.0
- パッチバージョンの更新の範囲内に限定したい
- 範囲指定せずにバージョン指定する
- dedupedされない
- require する場所によって別バージョンが読み込まれる
-
instanceof
が別になるため false を返す
-
- メモリ使用量の悪影響
- require する場所によって別バージョンが読み込まれる
- dedupedされない
-
-
npm ls --all
- 依存ツリーを表示できる
- package-lock.json
- 実際にインストールされたバージョンが入る
- リリース時でも完全に再現できる
-
npm ci
- CIでの利用を想定している
- package-lock.json に基づいてインストールする
- 元の node_modules は削除する
-
npm install
- 必要なものだけインストールする
- 元の node_modules は削除しない
- package-lock.json と矛盾するときは package.jsonを正年して更新する
- 元の node_modules は削除しない
- 必要なものだけインストールする
-
- リリース時でも完全に再現できる
- 実際にインストールされたバージョンが入る
- peerDependencies
- plugin-a と plugin-b で依存モジュールのメジャーバージョンが違うなど
- scripts
-
npm run
で実行できる - パッケージ公開前の処理などイベントでフックもできる
-
"prepublishOnly": "npm test"
など
-
-
- bin
- name
-
- Yarn
- yarn.lock と並列処理
- 当時 npm には package-lock.json がなかった
- yarn.lock と並列処理
11章 Node.jsとJavaScript標準
- Node.js
- イベントループによる並行処理
- ブラウザと共通言語
- ブラウザ環境と同じ仕様を満たすことで学習コストを下げる
- ECMAScript標準
- Web標準から一部導入
- WebAssembly
- ブラウザ環境と同じ仕様を満たすことで学習コストを下げる
- ESモジュール
- 常にstrictモード
-
'use strict'
は必要ない
-
- export と import
- キャッシュされる
- import文で指定したURL全体がキーとなる
-
import './esm-math.mjs?foo=1'
は別のキーとなる
-
- import文で指定したURL全体がキーとなる
- CommonJSでの
__filename, __dirname
はfileURLToPath(import.meta.url)
で参照する - CommonJSモジュールとESモジュールの識別
-
.cjs
はCommonJS -
.mjs
はESモジュール -
.js
はトップレベルのpackage.json
のtype値による- 同一または親階層に
package.json
がなければCommonJS - 指定なしはCommonJS
-
module
指定はESモジュール
- 同一または親階層に
-
- CommonJSモジュールとESモジュールの違い
- CommonJSモジュール
- 動的
- 同期的
- デフォルトで非strictモード
-
this
はmodule.exports
- ESモジュール
- 静的
- モジュールのパース段階で依存関係の解析ができる
-
export
,import
が特別な構文となっている- できないようになっていること
const fooOrBar = require(Math.random() < 0.5 ? 'foo' : 'bar')
- for文でexportsする
- できないようになっていること
- 非同期的
- 常にstrictモード
-
this
はundefind -
await
が予約語
- 静的
- CommonJSモジュール
- CommonJSモジュールとESモジュールの相互依存
-
import('./path/module.mjs').then(esm => {...
でCommonJSにESモジュールを動的インポートできる -
exports.a = 'a'
などのシンプルな値はCommonJSから名前付きインポートもできる
-
- 常にstrictモード
- WebAssembly
- WebAssemblyテキスト形式
- バイナリと双方向変換できる
- Wasmモジュール
- 通常のESモジュールと同様にインポートできる
- WebAssemblyテキスト形式
- JavaScriptとコンパイル
- Babel
-
- パース、 2. ASTに変換してから対象部分を変更する、 3. ASTからソースコードを作る
-
- Babel