概要

先日、ひさしぶりに読書をしたぜ。

その勢いでもうひとつ読書をしよう。今度は Node.js っつうプログラミング言語の技術書を読んでみるぜ。まったく知らねえけれども、 JavaScript は知っているからなんとか読めるだろ!

今回の本は "ハンズオン" という名のとおり、実践しながら読み進めることを推奨しているけれども、ゴメン……ぼくは読み物として楽しみたいだけだからそれはいいや。

 

Node.js って何? に答えるシリーズ

  • とりあえずこれを言えれば格好はつくぜリスト --> この記事に収録
  • これを言えれば Node.js のパッケージ管理システムについては格好がつくぜリスト --> この記事に収録
  • これを言えれば Node.js のモジュールシステムについては格好がつくぜリスト --> この記事に収録
  • これを言えれば Node.js の非同期処理については格好がつくぜリスト --> この記事に収録
  • これを言えれば Node.js のユニットテストについては格好がつくぜリスト --> 次の記事に収録
  • これを言えれば Node.js のコンパイルについては格好がつくぜリスト --> 次の記事に収録
  • Node.js の構文でオモロかったところリスト --> 次の記事に収録

 

とりあえずこれを言えれば格好はつくぜリスト

  • Node.js は JavaScript の実行環境のひとつだ。もともとブラウザ環境でだけ動く言語だったけれど、 Node.js のおかげでどこでも動くようになった。
  • JavaScript は Stack Overflow Developer Survey において「もっともよく使われる言語」8年連勝している。
  • JavaScript の仕様は "ECMAScript 標準" で決まっている。
    • これは ECMA International という団体の TC39 という技術委員会が作っている。
  • ES1, ES11, ES2020 というふうにバージョン数がある。
    • いや2020ってなんだよ。
    • 1997年に ES1 が出て、2015年に ES6 が出たわけなんだが、 ES6 で「西暦のほうがよくね?」と気づいた奴がおり、 ES6=ES2015 以降は西暦でバージョン名がつくようになっている。
  • ECMAScript 標準で決まっているからといって、ブラウザの JavaScript で有効とは限らない。だってブラウザ側が対応しているかは別の問題だから。
  • というわけでブラウザの JavaScript にはブラウザ環境独自の標準がある。たとえば DOM の API はブラウザ独自のものだから ES には関係ないからね。
    • ブラウザ環境での仕様は W3C とか WHATWG という組織が作っている。
  • JavaScript は変数、関数をグローバルスコープに配置するため、何が何に依存するのかわかりづらいヒッドい状態だった。 Node.js はモジュールシステムによってそれを解決している。
  • このモジュールシステムを "CommonJS module" という。こいつはスゴイぜ。節を設けてサマるぞ。
  • 他の言語が並行処理をするときはマルチスレッドを使っていたのだが、 Node.js はシングルスレッドで実現している。こいつもスゴイぜ。節を設けてサマるぞ。

 

これを言えれば Node.js のパッケージ管理システムについては格好がつくぜリスト

  • Node.js のパッケージ管理ツールは Node Package Manager (npm) だ。Isaac Z. Schlueter が作った。公開されたデータベース (レジストリという) にパッケージが保存されている。
  • 自分の npm プロジェクト (パッケージというみたい) のメタデータは package.json にある。
    • name: 1〜214文字。スコープ付きパッケージなら @SCOPE_NAME/ という名前。
    • version: セマンティックバージョニング v2.0.0 の仕様を満たさないとダメ。 [メジャーバージョン].[マイナーバージョン].[バッチバージョン] 互換性のない変更.互換性を保って機能追加.互換性を保って不具合修正
    • description, keywords: 検索に使われる。
    • main: パッケージのエントリポイント。
    • author, contributors: 作者。
    • license, private: もし非公開にするなら "UNLICENSED", true とする。
    • dependencies: 依存先パッケージが書かれる。 "^ などを使って範囲をもたせてバージョンを指定するほうが多くの場合好ましいです"
    • devDependencies: テストやビルドのためのパッケージを書く。 npm install --save-dev PACKAGE_NAME
    • peerDependencies: 依存してないが欲しいもの。どーゆーときかっつーと、「何かのライブラリのプラグインを作ってる」場合。
    • optionalDependencies: dependencies と同じようにインストールされるけど、たといインストールに失敗してもエラーにならない。いや、そんなパッケージあるかよ。
    • bundledDependencies: 公開されたパッケージを一部改変して利用していたりする場合に使おう!
    • scripts: 任意のスクリプトを記述。 npm run SCRIPT_NAME で実行可能。
    • bin: ここに書いとくと、コマンドが使えるようになる。ああー、 npm install したり pip install したりするとコマンドとして使えるやつは、こういうふうに実装されてたんだ。
    • repository: このパッケージのコードを管理するリポジトリを書く。
    • engines: そのパッケージの動作に必要な Node.js と npm のバージョン。 npm config set engine-strict true しておくと、不一致の環境で npm install するとエラーになる。
    • acceptDependencies: なんか……これが利用可能ならこれも利用可能だよみたいなやつを書く。 (よくわかってナイ)
    • type: この項がなければ js ファイルを CommonJS module として扱う。 module であれば js ファイルを ES module として扱う。ただ js ではなく cjs だったら CommonJS module だし、 mjs であれば ES module として扱うことになる。拡張子が優先だ。
  • peerDependencies に関連して、 UNMET PEER DEPENDENCY の警告が出ることがある。これはだな、複数のパッケージが、違うバージョンの peerDependencies に依存していることを示す。この警告を出すために peerDependencies に定義しているのだ。 npm v6 だと "警告" なんだけど npm v7 だと "エラー" になる。バージョンが違うのに読み込んでしまうことを防ぐためのものだ。
  • npm のコマンド: npm install
    • node_modules を削除せず、未インストールのものだけインストールする。 package-lock.json がなければ作る。
  • npm のコマンド: npm ci
    • node_modules を削除し、 package-lock.json の基づいてパッケージをインストールする。 npm ci は npm install の倍の速度で完了することもある。イチオシ。
  • npm のコマンド: npm ls --all (all は npm v7 から有効)
    • インストールしたパッケージ一覧。
    • この一覧に deduped という表示があるときは、 "複数パッケージの依存パッケージだが、このバージョンだけのインストールで済みました" の意味。
    • deduped のときは node_modules 直下に置かれるけれど、 deduped できなくて複数バージョンがインストールされたときはネストして配置される。
  • パッケージ管理には npm じゃなくて yarn を使ってもいい。 2016年に Facebook が作ったもので、インストールが速い。
    • npm との違いは……実はあんまりない。しかし yarn が出た当時は package-lock.json がなかったので、 yarn.lock を持つ yarn はみんなから歓迎された。
  • yarn はバージョンに注意。 yarn 1 と 2 はかなり違くて、別の名前のツールとしてリリースしてはどうか、と議論があったほど。

 

これを言えれば Node.js のモジュールシステムについては格好がつくぜリスト

  • モジュールシステムといえば "CommonJS module" と "ES module" がある。
    • "ES module" は実験的な機能なので忘れていい。 "CommonJS module" を極めよう。
  • CommonJS module では各 js ファイルが CommonJS moduleとなる。 Python と同じだね。
  • 書いただけでモジュールになるわけじゃなくて、 module.exports を通して外部に公開し、 require() でロードする。
// cjs-math.js でこうやって公開し……
module.exports.add = (a, b) => a + b;

// こうロードする。拡張子は省略可能。 Python とおんなじだね。
const math = require('./cjs-math');
// こう使える。 Python の import とほとんど同じで使いやすいね。
math.add(1, 2);

// CommonJS moduleは関数を export するだけでなく、 json をそのままロードすることもできる。
// key-value.json
{ "key": "value" }

// ロードするときはこう。
require('./key-value');
  • なんとなく require らへんはビルトイン関数なのかなと思ってるけど、これは "モジュールスコープ変数" と呼ばれているっぽい。
// cjs-filename-dirname.js
module.exports = { __filename, __dirname };

// 見てみる。
require('./cjs-filename-dirname');
{
    __filename: '/path/to/dir/cjs-filename-dirname.js',
    __dirname: '/path/to/dir'
}
  • ここでスゲー大切なことだが、 CommonJS module ではつねにファイルのてっぺんに 'use strict' をつけること。これは緑さんのような超 A 型には必須ですわ。
  • 'use strict' は ES5 (西暦になるひとつ前) から導入された。
'use strict'
let myString = 'a';
myStrng = 'b';  // 変数名のタイプミス
// ここで ReferenceError: myStrng is not defined が出るようになる。
// てか出ないのが異常やろ。

 

これを言えれば Node.js の非同期処理については格好がつくぜリスト

  • Node.js は並行処理をシングルスレッドで実現している (イベントループというらしい)。
  • というかこれが Node.js 開発の動機だと Ryan Dahl サンは申している。
  • 並行処理の動機は、時間のかかる処理……ブロッキングという……に対応することだ。フツーはマルチスレッドで対応する。
  • これまで技術書は何冊か読んで、マルチスレッドの問題点は何度も読んだ。マルチスレッドの問題点は、どの本でも同じように語られている。すなわち……
    • メモリをたくさん消費するし、
    • Web アプリの場合マルチスレッド (= クライアント) が1万個になるとレスポンスが激重になる現象……C10K問題という……が発生するし、
    • スレッドセーフのプログラムを書かないといけなくて面倒。
  • で、 Node.js はこれらデメリットをイベントループで解決している。が、これはこれでデメリットが……
    • コールバック・ヘルのリスクがあったり、
    • 同期処理と非同期処理を混ぜないよう気を遣う手間があったり、
    • Promise がクソムズであったり。
  • ところでマルチプロセスってのもあるな。マルチプロセス間では値のやり取りをコピーで実現する。一方マルチスレッドではメモリを共有できるので、値を直接やり取りできるぞ。
  • 同期処理と非同期処理の例として、本書が用意しているレンチンのコードがわかりやすいのでノートしておく↓。
const 金額 = バーコードリーダー.読む(弁当);  // <-- 同期処理。
電子レンジ.チン(  // <-- 非同期処理。レンチンは時間のかかる処理だけどブロッキングしない(ノンブロッキングという)。
  弁当,
  温まった弁当 => 商品を渡す(温まった弁当)
);
レジ.会計する(金額);
  • 非同期処理の関数には規約がある。実例として JSON をパースする非同期処理をノートしておく。
    • また、 "同期処理と非同期処理を混ぜない" についても併記する。
//                             ↓規約ひとつめ。コールバックはパラメータの最後にする。
function parseJSONAsync(json, callback) {

  // もし結果がキャッシュに入ってる場合は、1秒かけずにすぐに結果を返したい。
  // が、同期的に return してはいけない。これが "同期処理と非同期処理を混ぜない"
  // そういうときは待ち時間0秒で setTimeout したり……
  setTimeout(() => callback(cached.err, cached.result), 0);
  // setTimeout がダサいと思うならこうしたり……
  process.nextTick(() => callback(cached.err, cached.result));
  // ブラウザ環境なら nextTick は使えないのでこうする……。
  queueMicrotask(() => callback(cached.err, cached.result));

  setTimeout(() => {
    try {
      //        ↓規約ふたつめ。コールバックのパラメータは (エラー, 処理結果) とする。
      callback(null, JSON.parse(json));
    } catch (err) {
      callback(err);
    }
  }, 1000);  // 処理に1秒かかる重い処理を再現しているだけ。
}
parseJSONAsync('不正なJSON', (err, result) => {
  console.info('parse結果', err, result);
});
  • コールバック・ヘルを防ぐための async await は ES2017 でようやく追加された。
async function asyncFunc(input) {
  try {
    const result1 = await asyncFunc1(input)
    const result2 = await asyncFunc2(result1)
  } catch (e) {
    // エラーハンドリング
  }
}