冒頭
「理論を伴わない経験は盲目だが、経験を伴わない理論は知的遊戯に過ぎない」
カント
1章 イントロダクション
Node.js の特徴
並行処理する
「コンビニ店員が弁当を温めながら次のお客をさばく」
WebサーバーのI/O
従来はスレッドでやっていた
「客Aスレッド、客Bスレッド……」とマルチスレッドで切り替える
生成と切り替えコストが大きく、大量にリクエストをさばけない
Node.js はイベントループ
シングルスレッド
タスクをキューに積んで順番に処理する
I/O発生タイミングでタスク分割して、実行時に完了後のタスクを指定し、完了後には指定タスクがキューに追加されるようにする
「弁当をバーコードで読み取って電子レンジに入れるタスクと、温め終わった弁当を客に渡すタスク」に分割され、完了されたら後者がキューに追加される
もともとJSにあった
サーバーサイドに適用した
スモールコアとnpm
「コア(Node.jsそのものに付属する機能」は最小限に保つべきである」
Node.js は TCP, HTTP, DNS やファイルシステムを提供する
メンテコストが下がる
本質的な課題に集中できる
外側を束縛しない
ユーザーランド(コア外)でのエコシステムの成長を促す
npm
モジュールシステム
取り外しできる
UNIX哲学の影響
「一つのことを行い、またそれをうまくやるプログラムを書け」
再利用性に優れる
仕様の把握が容易
テストしやすく堅牢
「標準入出力は普遍的インターフェイス」
ストリームを扱う
高い拡張性
ECMAScript標準
TC39の仕様標準化プロセス
0(たたき台)
仕様へのインプット
1(提案)
課題と実行方法、実行時の課題を特定
2(ドラフト)
構文とセマンティクスを記述する
3(候補)
各環境での実装とフィードバックから洗練させる
4(完了)
ECMAScript標準に取り込む準備が整った状態
年次でステージ4になった仕様をリリースする
TC39でステージを調べれば最新の状況がわかる
JSの知識
オブジェクト
プロパティ名を指定して取得
obj1.propA
obj1['propA']
プロパティの追加
obj1.propC = 3
プロパティの削除
delete obj1.propC
元オブジェクトを変更しない方法(イミュータブル)
スプレッド構文でpropCを追加する
const obj2 = { ...obj1, propC: 3 }
レスト構文でpropAを削除する
const { propA, ...obj3 } = obj2
配列
指定した要素のインデックスを取得(なければ-1)
arr1.indexOf('bar')
要素が配列に含まれるかどうか
arr1.includes('bar')
結合(引数がなければ,)
arr1.join('-')
末尾に要素を追加
arr1.push('a', 'b', 'c')
末尾の要素を削除
arr1.pop()
元オブジェクトを変更しない方法(イミュータブル)
オブジェクトと同様
sort()は破壊的メソッド
forEach()はmap()と違って戻り値なし
find()は見つかった時点で反復処理を終了する
クラス
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クライアントを超えると急に遅くなる
コンテキストスイッチが頻繁に発生する
スレッドセーフにする必要がある
更新操作の競合をロックで防ぐ
num = num + 1
互いに待ち合うデッドロックにもなりやすい
イベントループでの並行処理と非同期プログラミング
シングルスレッド
マルチスレッド固有の問題がない
C10K問題の解消
処理完了後のタスクとしてコールバック関数を渡す
温まった弁当 => 商品を渡す(温まった弁当)
完了後のタスクが指定されているから次の行に進める
ノンブロッキングI/O
非同期プログラミング
制御フローが複雑になりやすいデメリットもある
CPU負荷の高い処理には適さない
コールバック
map()は同期的に実行される
非同期処理だけではない
Node.jsの非同期処理の実装規約
コールバックはパラメータの最後にある
コールバックの最初のパラメータはエラー、2つ目以降のパラメータは処理の結果
setTimeout()は規約を守っていない
ブラウザのJSのAPIに由来しているから
イベントループにはフェーズがある
コールバックヘル
asyncFunc1(input).then(asyncFunk2).then...で防ぐ
Promise
then()の最後にcatch()を付けるとエラーを集約できる
try...catch構文と似ている
平行実行
Promise.all
1つでもrejectedなら、結果を待たずにrejected
逐次実行の必要がなければPromise.allの方が早い
Promise.race
タイムアウト機能でよく使われる
Promiseが適さないケース
内容を少しずつ読み取るAPI
3章EventEmitterとストリーム
util.promisifyで規約を満たさない関数も Promise を返すようにできる
fs.readdirやsetTimeout
ジェネレータ
function*で始まる
yieldがある
generator.next()でyieldごとに実行される
イテレータプロトコル
valueとdoneを返す
イテラブル
[1, 2, 3][Symbol.iterator]()
next()が呼ばれたタイミングでyieldで値を返す
値の生成を遅らせることができる
反復回数が膨大なとき
無限超のイテラブルを処理するとき
yieldをつけることで非同期処理を同期処理と同じように扱える
ジェネレータ関数内の処理を一時停止、再開できる
next()の引数を取得できる
throw()の引数を投げる
async/await
ジェネレータと同様のことが標準構文として提供された
functionの後ろの「*」がfunction前のasyncに変わった
yieldがawaitになった
await をつけると関数内の処理が一時停止する
スレッド処理はブロックしない
async関数は必ず Promiseインスタンスを返す
3章 EventEmitterとストリーム
1回の要求で処理が複数の非同期処理はどうするか
Webサーバの処理
Promiseは一度 settled になったらそれ以降変化しない
Observer パターンで実現する
監視対象(Subject)に発生したイベントが監視役(Observer)に逐一通知される
EventEmitter で実装する
const server = http.createServer()
サーバオブジェクト(EventEmitterのインスタンス)
EventEmitter は監視対象
監視役はリスナと呼ばれる
on()メソッドにイベント名とコールバックを渡してリスナを登録している
'request'など
EventEmitter インスタンスの生成処理途中で同期的にイベントを発行してはいけない
リスナは常に同期的に実行される
P.112
11個以上のイベントリスナを登録すると警告表示
メモリリークしがちだから
リスナ登録するとガベージコレクトされない
エラーハンドリング
EventEmitterはerrorという名前のイベントでエラーを渡す
on('error', err => console.log('errorイベント'))
error イベントリスナがあればここに投げられる
なければ unCaughtException になる
EventEmitter の利用法
2パターン
直接 new して使う
継承する
class FizzBuzzEventEmitter extends events.EventEmitter {}
ストリーム
一度に展開するとでかいファイルなどをストリームで読み込む
pipe()で繋げられる
継承してから_read(),_write(),_transform()などのの実装に注力すればいい
完了時にfinishが発行される
on('finish', cb)でリスナをつけておいたりする
各種ストリームは EventEmitter
読み込みストリーム
書き込みストリーム
write()メソッドの戻り値は「それ以上データを流せるか」を返す
push()も同じ
バックプレッシャと呼ばれる機能
ストリームの下流が詰まっていると溢れてしまう
戻り値で伝える
二重ストリーム
stream.Duplexを継承して_read()と_write()を実装する
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 モジュールを使う
cluster モジュールてマルチプロセス化
setupMaster()で実行ファイルを指定する
CPUコアの数だけfork()を実行する
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
X-Forwarded-Proto
X-Forwarded-For
app.enable('trust proxy')
req.hostnameなどに元リクエスト情報が入るようになる
X-Forwarded-*の情報
404や500でも戻り値のPromiseはrejectではなくfulfilledになる
response.okなどを見てハンドリングする
Node.jsプロジェクト
npmパッケージとして開発する
npm init -yでパッケージ初期化
パッケージのメタデータのpakage.jsonができる
ビルドやテスト開発時のみ使うならnpm install -D
scriptsはnpm run <スクリプト名>が使えるようになる
test, start, restart, stopならnpm <スクリプト名>で使える
ユニバーサルWebアプリケーション
SSR/SSGとCSRによってサーバから配信するHTMLとクライアントサイドで構築するHTMLが共通の実装で描画されるアプリ
Next.jsなど
useEffect()部分はSSRされたHTMLの後に取得される
6章 リアルタイムWebアプリケーション
リアルタイムWebアプリケーション
リロードすることなく最新情報を同期させる
ポーリング
もっともシンプルな方法
一定間隔でAPIを叩く
setTimeout()などで実装する
無駄なリクエストの頻度と状態反映の遅延のトレードオフ
ロングポーリング
HTTPリクエストを受け取ったサーバがデータの更新を待ってレスポンスを返す
SSE(Server Sent Events)
一度確立したHTTP接続を保持したまま更新されたらデータ送信する
Content-Typeはtext/event-stream
クライアントはEventSourceAPIを使う
data, id, event, retryフィールドを含められる
DevToolsのNetworkのeventsで見れる
WebSocket
SSEとの違い
サーバとクライアントが双方向で通信できる
一方向の通信でいい場合はSSEで良い
プロトコルがHTTPではない
ハンドシェイクで始まる
Node.jsコアだけでは難しい
ライブラリを利用する
Socket.IO
最初はロングポーリングしてその後WebSocketにスイッチする
ws
必要最低限の機能
7章 データストレージ
開放/閉鎖原則(open-closed principle、 OCP)
簡単に拡張できるが、拡張するときのコード修正は少ない
require(./${process.env.npm_lifecycle_event})
npm run file-stystemでfile-system.jsを読み込める
app.js の変更なくデータストレージを切り替えられる
データストレージに対する操作
プロセス外だから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はプレリリース
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 を返す
メモリ使用量の悪影響
npm ls --all
依存ツリーを表示できる
package-lock.json
実際にインストールされたバージョンが入る
リリース時でも完全に再現できる
npm ci
CIでの利用を想定している
package-lock.json に基づいてインストールする
元の node_modules は削除する
npm install
必要なものだけインストールする
元の node_modules は削除しない
package-lock.json と矛盾するときは package.jsonを正年して更新する
peerDependencies
plugin-a と plugin-b で依存モジュールのメジャーバージョンが違うなど
scripts
npm runで実行できる
パッケージ公開前の処理などイベントでフックもできる
"prepublishOnly": "npm test"など
bin
実行可能ファイルを指定する
"bin": { "rimraf": "bin.js" }でnode_module の .bin にシンボリックリンクが作られる
パスが通るのでrimrafコマンドが使えるようになる
引数も取れる
npm run rimrafで使う
npx rimrafで使う
Yarn
yarn.lock と並列処理
当時 npm には package-lock.json がなかった
11章 Node.jsとJavaScript標準
Node.js
イベントループによる並行処理
ブラウザと共通言語
ブラウザ環境と同じ仕様を満たすことで学習コストを下げる
ECMAScript標準
Web標準から一部導入
WebAssembly
ESモジュール
常にstrictモード
'use strict'は必要ない
export と import
キャッシュされる
import文で指定したURL全体がキーとなる
import './esm-math.mjs?foo=1'は別のキーとなる
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モジュールとESモジュールの相互依存
import('./path/module.mjs').then(esm => {...でCommonJSにESモジュールを動的インポートできる
exports.a = 'a'などのシンプルな値はCommonJSから名前付きインポートもできる
WebAssembly
WebAssemblyテキスト形式
バイナリと双方向変換できる
Wasmモジュール
通常のESモジュールと同様にインポートできる
JavaScriptとコンパイル
Babel
1. パース、 2. ASTに変換してから対象部分を変更する、 3. ASTからソースコードを作る
↧