Node.js のモジュールシステム
チャーリーのプロジェクトは、3つのファイルから30ファイルへと膨れ上がっていた。すべての関数、設定、ユーティリティクラスが1つの巨大なapp.jsに詰め込まれており、たった1つの関数を変更するだけでも、2,000行ものコードを30分かけて探し回らなければならなかった。彼はコードを個別のモジュールに分割することにしたが、その結果、requireとimportの外観が異なっていたり、module.exportsとexportsが常に混同されたり、循環依存関係によってプログラムが大量のundefinedを吐き出したりしていることに気づいた。このレッスンでは、チャーリーがNode.jsのモジュールシステムを解き明かし、絡み合ったコードを整理された構成要素のセットへと変貌させていく様子を追っていきます。
学習内容:
- CommonJS を使用してコードを整理する
require/module.exports/exports - ESモジュールを使用した
import、export、および"type":"module"の構成 requireのモジュール検索の仕組み(組み込み → node_modules → パス)を理解する- モジュールのキャッシュの仕組みと、
require.cacheの役割を理解する - 循環依存の問題を特定し、Node.js がそれらをどのように処理するかを理解する
1. CommonJS モジュール
(1) module.exports を使用したエクスポート
Node.js はデフォルトで CommonJS モジュール仕様を採用しています。各ファイルはモジュールであり、module.exports を使用して値をエクスポートし、他のファイルは require() を使用してそれらをロードします。
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
(2) require を使用したモジュールの読み込み
require() モジュール識別子を受け取り、そのモジュールに対応する module.exports の値を返します。
// app.js
const math = require('./math');
console.log(math.add(10, 3)); // 13
console.log(math.subtract(10, 3)); // 7
(3) exports ショートカット
exports は module.exports を参照しており、プロパティを1つずつ追加するのに適しています。
// logger.js
exports.info = function (msg) {
console.log(`[INFO] ${msg}`);
};
exports.error = function (msg) {
console.log(`[ERROR] ${msg}`);
};
▶ 例:単一の関数のエクスポートとオブジェクトのエクスポート
// greet.js — Export a Single Function
module.exports = function (name) {
return `Hello, ${name}!`;
};
// config.js — Export Objects
module.exports = {
port: 3000,
host: 'localhost',
debug: true,
};
// app.js
const greet = require('./greet');
const config = require('./config');
console.log(greet('Charlie')); // Hello, Charlie!
console.log(`Server: ${config.host}:${config.port}`); // Server: localhost:3000
(4) module.exports と exports の違い
| 機能 | module.exports | exports |
|---|---|---|
| エッセンス | モジュールから実際にエクスポートされたオブジェクト | module.exports への参照 |
| 課題のエクスポート | ✅ module.exports = fn |
❌ exports = fn 参照を削除 |
| Add one by one | ✅ module.exports.foo = fn |
✅ exports.foo = fn |
| 単一の値をエクスポート | ✅ 推奨 | ❌ 利用不可 |
| セキュリティ | 常に有効 | 再割り当て後に失効 |
基本原則:単一の関数、クラス、またはまったく新しいオブジェクトをエクスポートする必要がある場合は、
module.exportsを使用しなければなりません。exportsはプロパティを追加する場合にのみ使用できます。
2. ESモジュール
(1) 基本的な構文
ESモジュール(ESM)は、exportおよびimportの構文を採用した、公式のJavaScriptモジュール標準です。
// utils.mjs
export function square(n) {
return n * n;
}
export const VERSION = '2.0.0';
export default function greet(name) {
return `Hello, ${name}!`;
}
// app.mjs
import greet, { square, VERSION } from './utils.mjs';
console.log(greet('Charlie')); // Hello, Charlie!
console.log(square(5)); // 25
console.log(VERSION); // 2.0.0
(2) ESMを有効にする3つの方法
| 方法 | 説明 |
|---|---|
ファイル拡張子 .mjs |
Node.js はこれを自動的に ESM として処理します |
package.json 内の "type": "module" |
プロジェクト内の .js という名前のファイルは、デフォルトで ESM 形式となります |
--input-type=module |
標準入力(stdin)に使用されるコマンドライン引数 |
// package.json
{
"type": "module"
}
(3) 指定エクスポートとデフォルトエクスポート
// shapes.mjs
export const PI = 3.14159;
export function circleArea(radius) {
return PI * radius * radius;
}
export default class Shape {
constructor(name) {
this.name = name;
}
describe() {
return `This is a ${this.name}`;
}
}
▶ 例:統一された輸出および再輸出
// api.mjs — Batch Export
export { addUser, removeUser } from './users.mjs';
export { logError } from './logger.mjs';
// You can also rename it
export { add as addUser } from './math.mjs';
3. CommonJS と ESM の比較
(1) 主な相違点
| ディメンション | CommonJS | ESモジュール |
|---|---|---|
| 構文 | require() / module.exports |
import / export |
| 読み込み方法 | 同期、実行時読み込み | 非同期、コンパイル時の静的解析 |
| 値の型 | 値のコピー(プリミティブ型) | 値のバインディング(ライブ参照) |
| トップレベル | module.exports |
undefined |
| 循環依存関係 | 未解決のエクスポートを返す | 参照バインディングだが、TDZ内にある可能性がある |
| ユースケース | Node.js プロジェクト(デフォルト) | 新規プロジェクト、ブラウザ上でのコード共有 |
| ファイル拡張子 | .js / .cjs |
.mjs / .js(タイプ:モジュール) |
(2) 値のコピーとバインディング
// counter.cjs — CommonJS
let count = 0;
function increment() {
count++;
}
module.exports = { count, increment };
// counter.mjs — ESM
export let count = 0;
export function increment() {
count++;
}
// CJS: count is a copy, it won't change
const c = require('./counter.cjs');
c.increment();
console.log(c.count); // 0 (still the initial value)
// ESM: count is bound, real-time updates
import { count, increment } from './counter.mjs';
increment();
console.log(count); // 1 (updated)
▶ 例:ESMでCJSモジュールをインポートする
// legacy.cjs
module.exports = { legacyMethod() { return 'old school'; } };
// app.mjs
import cjs from './legacy.cjs';
console.log(cjs.legacyMethod()); // old school
ESMでCJSモジュールを
importする場合、module.exportsの値がデフォルトのエクスポートとして使用されます。
4. require モジュールの検索メカニズム
(1) 検索プロセス
require('express') と入力すると、Node.js は以下の順序で検索を行います:
flowchart TD
A["require('express')"] --> B{Does it have a built-in module??}
B -- Yes --> C[Return built-in module]
B -- No --> D{Path starting with ./ or / ?}
D -- Yes --> E[Find file by path]
E --> E1[Try .js / .json / .node]
E1 --> E2[Try index.js]
D -- No --> F[Search node_modules]
F --> F1[Current Directory/node_modules/express]
F1 --> F2[Parent directory/node_modules/express]
F2 --> F3[Move up one level at a time until root directory]
F3 --> F4{Found?}
F4 -- No --> G[Throw MODULE_NOT_FOUND]
F4 -- Yes --> H[Load and cache module]
E2 --> H
C --> H
(2) パス解決ルール
| 必須パラメータ | 解析方法 | 例 |
|---|---|---|
./math |
現在のファイルからの相対パス | ./math → /project/src/math.js |
../utils |
親ディレクトリを基準として | ../utils → /project/utils.js |
/abs/path |
絶対パス | /lib/helper.js |
express |
組み込みモジュール → node_modules | レベル別検索 |
| Scopeパッケージ |
▶ 例:モジュールの解決パスの表示
// show-paths.js
console.log(module.paths);
[
'/project/src/node_modules',
'/project/node_modules',
'/node_modules',
'C:\\Users\\Charlie\\.node_modules',
'C:\\Users\\Charlie\\.node_libraries',
'C:\\Program Files\\nodejs\\lib\\node'
]
5. モジュールのキャッシュ機構
(1) キャッシュの仕組み
require モジュールが初めて読み込まれると、そのコードが実行され、その結果がキャッシュされます。その後、require 同じモジュールが呼び出された際には、コードを再実行することなく、キャッシュされた結果が直接返されます。
// counter.js
console.log('counter.js executed!');
let count = 0;
module.exports = {
increment() { return ++count; },
getCount() { return count; },
};
// app.js
const c1 = require('./counter'); // counter.js executed!
const c2 = require('./counter'); // (no output, using cache)
console.log(c1 === c2); // true
console.log(c1.increment()); // 1
console.log(c2.getCount()); // 1 (shared state)
(2) require.cache
読み込まれたすべてのモジュールは、モジュールの絶対パスをキーとして、require.cache オブジェクトにキャッシュされます。
// inspect-cache.js
const path = require('path');
const math = require('./math');
const cacheKey = path.resolve(__dirname, 'math.js');
console.log(require.cache[cacheKey] !== undefined); // true
console.log(require.cache[cacheKey].exports === math); // true
▶ 例:キャッシュをクリアしてホットリローディングを実現する
// hot-reload.js
function loadConfig() {
const path = require('path');
const cacheKey = path.resolve(__dirname, 'config.js');
delete require.cache[cacheKey];
return require('./config');
}
const cfg1 = loadConfig();
// ... config.js Modified ...
const cfg2 = loadConfig(); // Re-execute, loading the latest content
require.cacheのエントリを削除してからrequireを再度実行すると、Node.js はそのモジュールを再実行します。これは開発環境でのホットリロードには便利ですが、本番環境では注意して使用してください。
6. 組み込みモジュールの概要
(1) 一般的な組み込みモジュールのクイックリファレンス
Node.js には、インストールせずにすぐに使える多数の組み込みモジュールが用意されています。
| モジュール | 目的 | 一般的なメソッド/プロパティ |
|---|---|---|
fs |
ファイルシステム操作 | readFile, writeFile, readdir, stat |
path パス処理 join、resolve、parse、extname、basename |
||
http |
HTTP サーバー/クライアント | createServer, get, request |
https |
HTTPS サーバー/クライアント | createServer, get, request |
url URLの解析と生成 URL, fileURLToPath, pathToFileURL |
||
os |
オペレーティングシステム情報 | cpus, freemem, hostname, platform |
events |
イベントエミッタ | EventEmitter, on, emit, off |
stream |
ストリーム処理 | Readable, Writable, Transform, pipe |
crypto |
暗号化とハッシュ | createHash, createHmac, randomBytes |
util |
ユーティリティ | promisify, callbackify, format, inspect |
child_process |
子プロセスの管理 | exec, spawn, fork |
buffer |
バイナリデータ処理 | Buffer.alloc, Buffer.from, concat |
(2) 組み込みモジュールはインストールが不要です
const fs = require('fs');
const path = require('path');
const os = require('os');
console.log(os.platform()); // win32 / darwin / linux
console.log(path.join('/project', 'src', 'app.js')); // /project/src/app.js
▶ 例:path および os を使ったクイックスタート
const path = require('path');
const os = require('os');
const filePath = '/project/src/utils/helper.js';
console.log(path.extname(filePath)); // .js
console.log(path.dirname(filePath)); // /project/src/utils
console.log(path.basename(filePath)); // helper.js
console.log(`CPU cores: ${os.cpus().length}`);
console.log(`Free memory: ${(os.freemem() / 1024 / 1024).toFixed(0)} MB`);
7. 循環依存関係
(1) 循環依存とは何か?
モジュール A はモジュール B を必要とし、モジュール B はモジュール A を必要とするため、循環参照が生じます。Node.js は無限ループには入りません。その代わりに、すでに実行された部分から exports を返します。
(2) Node.js での処理方法
// a.js
exports.loaded = false;
const b = require('./b');
exports.loaded = true;
console.log('a.js - b.loaded =', b.loaded);
// b.js
exports.loaded = false;
const a = require('./a'); // Received a Unfinished exports { loaded: false }
exports.loaded = true;
console.log('b.js - a.loaded =', a.loaded);
node a.js
b.js - a.loaded = false
a.js - b.loaded = true
b.js が require('./a') を実行するとき、a.js の実行はまだ完了していないため、Node.js はその時点で値が割り当てられている a.js の部分({ loaded: false })を返します。
(3) 循環依存を回避するための戦略
| 戦略 | 説明 |
|---|---|
| 共通ロジックを抽出 | 共通部分を第3のモジュールに移動 |
| 遅延読み込み | require を関数内に移動し、呼び出されたときにのみ読み込まれるようにする |
| イベントの分離 | 直接呼び出しの代わりに EventEmitter を使用する |
| 依存性注入 | 依存性をハードコーディングするのではなく、パラメータを介して渡す |
▶ 例:循環依存関係を解消するために require を遅延させる
// user.js
exports.getName = function () {
return 'Charlie';
};
exports.getProfile = function () {
const format = require('./format'); // Delay until the time of the call require
return format.upper(exports.getName());
};
// format.js
exports.upper = function (str) {
return str.toUpperCase();
};
exports.getUserDisplay = function () {
const user = require('./user'); // Deferred require
return `User: ${user.getName()}`;
};
requireを遅延させることで、モジュールはその依存関係を、メソッドが初めて呼び出された時点で初めて読み込むようになります。その時点では両方のモジュールが完全に初期化されているため、不完全なexportsが取得されるのを防ぐことができます。
8. 総合的な例:モジュール型プロジェクト
以下では、ユーティリティモジュール、ロギングモジュール、およびメインのエントリポイントを含むモジュール型プロジェクトを作成します。
// math.js — Tools Module
const PI = 3.14159;
function circleArea(radius) {
return PI * radius * radius;
}
function rectangleArea(width, height) {
return width * height;
}
function round(value, decimals = 2) {
const factor = Math.pow(10, decimals);
return Math.round(value * factor) / factor;
}
module.exports = { circleArea, rectangleArea, round };
// logger.js — Log Module
const LEVELS = { INFO: 'INFO', WARN: 'WARN', ERROR: 'ERROR' };
function formatMessage(level, msg) {
const timestamp = new Date().toISOString();
return `[${timestamp}] [${level}] ${msg}`;
}
function info(msg) {
console.log(formatMessage(LEVELS.INFO, msg));
}
function warn(msg) {
console.warn(formatMessage(LEVELS.WARN, msg));
}
function error(msg) {
console.error(formatMessage(LEVELS.ERROR, msg));
}
module.exports = { info, warn, error, LEVELS };
// app.js — Main Entrance
const { circleArea, rectangleArea, round } = require('./math');
const { info, error } = require('./logger');
const radius = 5;
const area = round(circleArea(radius));
info(`Circle area (r=${radius}): ${area}`);
const roomArea = rectangleArea(4.5, 6.2);
info(`Room area: ${round(roomArea)} sqm`);
if (radius < 0) {
error('Radius cannot be negative');
} else {
info('Calculation complete');
}
node app.js
[2026-07-03T10:30:00.000Z] [INFO] Circle area (r=5): 78.54
[2026-07-03T10:30:00.001Z] [INFO] Room area: 27.9 sqm
[2026-07-03T10:30:00.001Z] [INFO] Calculation complete
❓ よくある質問
module.exportsを使用)、CJSではrequireを使用してESMモジュールをロードすることはできません。その場合は、代わりに動的なimport()関数を使用する必要があります。プロジェクトでは、単一のモジュール仕様に統一することを推奨します。require は同期型ですか、それとも非同期型ですか?require は、モジュールの読み込みが完了するまでコードの実行をブロックします。そのため、Node.js では、require をファイルの先頭に配置し、実行時にホットパスにある新しいモジュールに対して require を頻繁に呼び出さないようにすることが推奨されています。module.exports and exports?exports is a shorthand reference to module.exports. You can add properties using exports.xxx = ..., but exports = xxx breaks the reference, causing the export to fail. When you need to export a single function or a brand-new object, you must use module.exports = xxx.require.cache オブジェクトを使用して確認できます。キーはモジュールの絶対パス、値はモジュールオブジェクトです。キーを削除すると (delete require.cache[key])、次に require を使用する際に、そのモジュールが再実行されます。exports(不完全なオブジェクトである可能性があります)を返すため、未定義のプロパティが生じる可能性があります。解決策としては、共通モジュールの抽出、require呼び出しの延期、イベントの分離などが挙げられます。importステートメントを最上位に記述しなければならないのですか?import()関数を使用できます。require を使って JSON ファイルを読み込むとどうなりますか?JSON.parse() を使用して自動的に解析し、解析済みの JavaScript オブジェクトを返します。これは、設定ファイルの読み込みによく使われます。📖 まとめ
- CommonJS は Node.js のデフォルトのモジュール仕様です。読み込みには
requireを、エクスポートにはmodule.exportsを使用します。 - ESモジュールはJavaScriptの公式標準です。
import/exportを使用し、.mjsまたは"type":"module"という接尾辞を付けて有効にしてください。 require検索順序:組み込みモジュール → 相対パス/絶対パス → node_modules、レベルごとに上方向へ順に検索- モジュールが最初に読み込まれた後、そのモジュールは
require.cacheにキャッシュされます。その後のrequireの呼び出しでは、キャッシュされたバージョンが直接返されます。 exportsはmodule.exportsを参照しています。これを再代入すると、その参照が切断されます- 循環依存が発生した場合、Node.js は未解決の
exportsを返します。これは、requireの呼び出しを遅らせるなどの対策をとることで回避できます。 - 組み込みモジュール(fs、path、http、os など)はインストールする必要がなく、
requireを使用するだけで利用できます。
📝 練習問題
calculator.jsモジュールを作成し、4つの関数add、subtract、multiply、およびdivideをエクスポートした後、main.js内でそれらをインポートして使用する- 前の質問をESM版に変換する:
exportという構文と.mjsという接尾辞を使用し、importを使って読み込む。 require.cacheが存在することを確認するコードを記述してください。モジュールをインポートした後、require.cacheからそのモジュールに関する情報を出力してください。- 意図的に循環依存関係(a.js が b.js を要求し、b.js が a.js を要求する)を作成し、出力を確認した後、遅延要求を使用してこれを修正する。
pathおよびosモジュールを使用して、現在のオペレーティングシステムのプラットフォーム、CPU コア数、および現在のディレクトリの絶対パスを表示してください。



