JSエコシステムぶらり探訪(2): Node.jsとCommonJS modules

JSエコシステムの進化を語るにはNode.jsを避けて通ることはできません。Node.jsと、それ自身の持つモジュール機能について歴史的な背景を踏まえつつ説明します。

[←前](https://qiita.com/qnighy/items/fc19f328cf36fc28658c) [目次](https://qiita.com/qnighy/items/16fdd8e58309a1f706a0) [次→](https://qiita.com/qnighy/items/677bf64063c666b21034)

## Node.js

Node.jsは非同期I/Oを備えたサーバーサイドJavaScriptのための実行環境として2009年に登場しました。[^server-side-js-before-node] 現在はサーバーサイドJavaScriptだけではなく、JavaScriptのビルド環境として無くてはならないものになっています。

[^server-side-js-before-node]: [Wikipediaの記述](https://en.wikipedia.org/wiki/Node.js#History)によると、それ以前にもサーバーサイドJavaScriptの技術自体は存在していたようです。

要するにNode.jsは、PerlスクリプトやRubyスクリプトと同じようにJavaScriptのコードを実行するための環境です。

```javascript:main.js
console.log("Hello, world!");
```

```console
$ node main.js
Hello, world!
```

## CommonJS modules (CJS)

[CommonJS modulesはNode.jsで主に使われているモジュール形式](https://nodejs.org/docs/latest/api/modules.html)です。[CommonJS](https://en.wikipedia.org/wiki/CommonJS)という規格群の一部として2009年に登場し、生まれて間もないNode.jsのモジュールシステムもすぐに[CommonJSに適合](https://github.com/nodejs/node/blob/v14.9.0/doc/changelogs/CHANGELOG_ARCHIVE.md#20091103-version-0116)しました。 [^commonjs-modules-origin]

[^commonjs-modules-origin]: 根拠を探す余裕がなかったのでこのように書きましたが、実際のところNode.jsの初期のモジュールシステムをベースにしてCommonJSが生まれた可能性が高いと思います。

後述するESM (ES Modules) で置き換えられつつあり、CJSのほうが伝統的な形式といえます。

```javascript:util.js
var private_value = 42;
// exports.square = ... でもよい
module.exports.square = function(x) {
  return x * x;
};
```

```javascript:main.js
var util = require('./util');
console.log(util.square(2)); // => 4
console.log(util.private_value); // => undefined
```

また、 `module.exports` に別の値を代入することもできます。この場合は `exports` にだけ代入しても有効ではないので注意する必要があります。

```javascript:square.js
// module.exportsへの代入は必須。exportsにも代入しておくと後々便利
module.exports = exports = function(x) {
  return x * x;
};
// JavaScriptの関数はそれ自体オブジェクトなので、プロパティーに代入できる
exports.version = "0.1.0";
```

上の例では、 `require` は関数を返します。

```javascript:main.js
var square = require('./square');
console.log(square(2)); // => 4
console.log(square.version); // => "0.1.0"
```

Node.jsにおいては最初に呼ばれるJavaScriptファイルもモジュールです。

```javascript:main.js
var x = 42;
// global is Node.js equivalent of windows
// (There's also globalThis usable in both environments)
console.log(global.x); // => undefined
```

## Node.jsのCJSの仕組み

CJSのrequireはNode.js (など、それぞれの処理系) が提供するプリミティブ関数ですが、その動作は比較的シンプルに理解できます。つまり、ファイルを発見して読み取り、関数に包んで `eval` する処理と考えることができます。[^cjs-require-other-considerations]

[^cjs-require-other-considerations]: 他に、ファイルの読み取りが同期的に行われる点、複数回requireしたときにキャッシュする仕組み、巡回参照の処理などを考える必要がある

実際にevalされるときは[以下のような関数の本体](https://nodejs.org/docs/latest/api/modules.html#modules_the_module_wrapper)として扱われます。(つまり、 `exports`, `require`, `module`, `__filename`, `__dirname` という5つのローカル変数があらかじめ存在するものとして扱われます。)

```javascript
(function(exports, require, module, __filename, __dirname) {
  // ファイルの中身
});
```

`module` と `exports` はあらかじめ以下のように初期化されたものと考えることができます。

```javascript
var module = {}, exports = {};
module.exports = exports;
// その他、moduleの様々なプロパティーを設定
```

`require` は `module` オブジェクトをキャッシュしておき、eval終了後に `module.exports` の値を戻り値として返すと考えることができます。これにより `module.exports` と `exports` の関係についても説明がつきます。

## モジュールの副作用とオブジェクトの同一性

以下のようなモジュールを考えます。

```javascript:module1.js
console.log("Hello, world!");

var counter = 1;
exports.fresh = function() {
  return counter++;
};
```

このモジュールには以下の特徴があります。

- モジュールのトップレベル処理に副作用がある。
- モジュールが状態を持っている。

このような場合、モジュールの同一性を気にする必要が出てきます。全く同じ内容の `module2.js` をコピーとして作成し、以下のように `main.js` から呼び出してみます。

```javascript:main.js
var module1 = require('./module1'); // => Hello, world!
var module2 = require('./module2'); // => Hello, world!

console.log(module1.fresh()); // => 1
console.log(module1.fresh()); // => 2
console.log(module2.fresh()); // => 1
console.log(module2.fresh()); // => 2
console.log(module1.fresh === module2.fresh); // => false
```

両方の `console.log` が実行され、 `fresh` は別々にカウントされ、 `fresh` のオブジェクトとしての同一性も `false` になりました。

Node.jsでは、パスが同じものは同一モジュールになります[^symlink]。先ほどの `main.js` を書き換えて `./module1` を2回インポートするようにしてみます。

[^symlink]: シンボリックリンクについては次回言及予定

```javascript:main.js
var module1a = require('./module1'); // => Hello, world!
var module1b = require('./module1'); // (no output)

console.log(module1a.fresh()); // => 1
console.log(module1a.fresh()); // => 2
console.log(module1b.fresh()); // => 3
console.log(module1b.fresh()); // => 4
console.log(module1a.fresh === module1b.fresh); // => true
```

`console.log` は1回しか実行されず、 `fresh` は同じカウンタを使うようになり、2つの `fresh` 関数はオブジェクトとしても同一になりました。

## 冠頭形モジュール

CJSのインポートは単なる `require` という関数であり、どこでも呼び出すことができます。これは一見すると便利で妥当な設計に見えますが、実際はNode.js以外の環境にモジュールシステムを移植するにあたってこの「どこでも呼び出せる」という性質が邪魔になってきます。

そこで、以降で解説するモジュールシステムの理解を助けるために、本稿独自の用語として「(CJSの)冠頭形モジュール」という概念を導入します[^request-for-proper-names]。

[^request-for-proper-names]: これについて、より広く使われている名称があれば教えていただけるとありがたいです。

**定義**. あるCommonJSモジュールが**冠頭形である (is a prenex-form module)** とは、以下を満たすことである。

- そのモジュールファイルはヘッダ部と本体に分けられる。 (ヘッダ部に続いて本体が来るものとする)
- ヘッダ部の各文は以下のいずれかの形式である。
  - `var <変数名> = require(<文字列リテラル>);`
  - `let <変数名> = require(<文字列リテラル>);`
  - `const <変数名> = require(<文字列リテラル>);`
  - `require(<文字列リテラル>);`
- 本体では `require` 関数は使われていない。


冠頭形であれば、モジュールファイルの中身を実際にevalしなくても、あらかじめ依存先モジュールを決定することができます。

冠頭形ではないものの例としては以下のようなものがあります[^webpack-is-awesome]。

[^webpack-is-awesome]: バンドラーによっては、ここに挙げたような例をうまく処理できてしまう場合もありますが、それでも一般的な場合を全てカバーするのは困難です。

- 条件つきインポート
- `require` の引数が動的に決まるようなインポート
- 当該モジュール読み込み時ではなく、あとで必要になってから行うインポート

## まとめ

特に重要なのが以下の点です。

- Node.jsによって「ブラウザー以外のJavaScript実行環境」が大きな地位を獲得した。
- Node.jsによって、JavaScriptに優れたモジュールシステムがもたらされた。

このことがJavaScriptに2つの大きな課題をもたらしました:

- Webブラウザーもモジュールシステムの恩恵を受けられるようにすること。
- Node.jsとWebブラウザーの間のコードの相互運用性を高めること。

これらの課題がJavaScriptバンドラーの誕生、そして各種の新しいモジュールシステムの提案へとつながっていくと考えられます。が、次回はその前に、Node.jsのパッケージシステムについて扱います。

[←前](https://qiita.com/qnighy/items/fc19f328cf36fc28658c) [目次](https://qiita.com/qnighy/items/16fdd8e58309a1f706a0) [次→](https://qiita.com/qnighy/items/677bf64063c666b21034)