2
6

【技術書まとめ】ハンズオンNode.js

Last updated at Posted at 2021-07-08

冒頭
「理論を伴わない経験は盲目だが、経験を伴わない理論は知的遊戯に過ぎない」
カント

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に由来しているから
      • イベントループにはフェーズがある
        • スクリーンショット 2021-05-07 8.24.50.png
      • コールバックヘル
        • 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.readdirsetTimeout
    • ジェネレータ
      • function*で始まる
        • yieldがある
          • generator.next()yieldごとに実行される
            • イテレータプロトコル
              • valuedoneを返す
                • イテラブル
                  • [1, 2, 3][Symbol.iterator]()
            • next()が呼ばれたタイミングでyieldで値を返す
              • 値の生成を遅らせることができる
                • 反復回数が膨大なとき
                • 無限超のイテラブルを処理するとき
          • yieldをつけることで非同期処理を同期処理と同じように扱える
      • ジェネレータ関数内の処理を一時停止、再開できる
      • next()の引数を取得できる
      • throw()の引数を投げる
    • async/await
      • ジェネレータと同様のことが標準構文として提供された
        • functionの後ろの「*」がfunction前のasyncに変わった
        • yieldがawaitになった
      • await をつけると関数内の処理が一時停止する
        • スレッド処理はブロックしない
      • async関数は必ず Promiseインスタンスを返す

3章 EventEmitterとストリーム

  • 1回の要求で処理が複数の非同期処理はどうするか
    • Webサーバの処理
      • スクリーンショット 2021-05-15 17.08.40.png
      • 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でラップする
                • Uint8ArrayInt32Array
              • スレッド間の読み書きが競合する
                • スレッドセーフに更新したい
                  • 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-*の情報
            • 404500でも戻り値のPromiseはrejectではなくfulfilledになる
              • response.okなどを見てハンドリングする
  • Node.jsプロジェクト
    • npmパッケージとして開発する
      • npm init -yでパッケージ初期化
        • パッケージのメタデータのpakage.jsonができる
      • ビルドやテスト開発時のみ使うならnpm install -D
      • scriptsnpm 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-Typetext/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)
    • スクリーンショット 2021-06-04 8.37.54.png
    • 簡単に拡張できるが、拡張するときのコード修正は少ない
      • require(./${process.env.npm_lifecycle_event})
        • npm run file-stystemfile-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章 ユニットテストとデバッグ

スクリーンショット 2021-06-15 7.54.15.png

  • テストダブル
    • スパイ
      • 引数や実行回数などを記録する
    • スタブ
      • 代用品として最低限の機能
        • ハードコードされた値を返す関数など
    • モック
      • 代用品への事前の期待が記述されていて検証機能を持つ

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で使う
                スクリーンショット 2021-07-06 8.03.33.png
  • 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, __dirnamefileURLToPath(import.meta.url)で参照する
    • CommonJSモジュールとESモジュールの識別
      • .cjsはCommonJS
      • .mjsはESモジュール
      • .jsはトップレベルのpackage.jsonのtype値による 
        • 同一または親階層にpackage.jsonがなければCommonJS
        • 指定なしはCommonJS
        • module指定はESモジュール
    • CommonJSモジュールとESモジュールの違い
      • CommonJSモジュール
        • 動的
        • 同期的
        • デフォルトで非strictモード
        • thismodule.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からソースコードを作る
2
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
6