404 Not Found

404 Not Found


nginx

Node.js のモジュールシステム

チャーリーのプロジェクトは、3つのファイルから30ファイルへと膨れ上がっていた。すべての関数、設定、ユーティリティクラスが1つの巨大なapp.jsに詰め込まれており、たった1つの関数を変更するだけでも、2,000行ものコードを30分かけて探し回らなければならなかった。彼はコードを個別のモジュールに分割することにしたが、その結果、requireimportの外観が異なっていたり、module.exportsexportsが常に混同されたり、循環依存関係によってプログラムが大量のundefinedを吐き出したりしていることに気づいた。このレッスンでは、チャーリーがNode.jsのモジュールシステムを解き明かし、絡み合ったコードを整理された構成要素のセットへと変貌させていく様子を追っていきます。

学習内容:


1. CommonJS モジュール

(1) module.exports を使用したエクスポート

Node.js はデフォルトで CommonJS モジュール仕様を採用しています。各ファイルはモジュールであり、module.exports を使用して値をエクスポートし、他のファイルは require() を使用してそれらをロードします。

JAVASCRIPT
// 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 の値を返します。

JAVASCRIPT
// app.js
const math = require('./math');

console.log(math.add(10, 3));      // 13
console.log(math.subtract(10, 3)); // 7

(3) exports ショートカット

exportsmodule.exports を参照しており、プロパティを1つずつ追加するのに適しています。

JAVASCRIPT
// logger.js
exports.info = function (msg) {
  console.log(`[INFO] ${msg}`);
};

exports.error = function (msg) {
  console.log(`[ERROR] ${msg}`);
};

▶ 例:単一の関数のエクスポートとオブジェクトのエクスポート

JAVASCRIPT
// 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,
};
JAVASCRIPT
// 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モジュール標準です。

JAVASCRIPT
// utils.mjs
export function square(n) {
  return n * n;
}

export const VERSION = '2.0.0';

export default function greet(name) {
  return `Hello, ${name}!`;
}
JAVASCRIPT
// 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)に使用されるコマンドライン引数
JSON
// package.json
{
  "type": "module"
}

(3) 指定エクスポートとデフォルトエクスポート

JAVASCRIPT
// 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}`;
  }
}

▶ 例:統一された輸出および再輸出

JAVASCRIPT
// 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) 値のコピーとバインディング

JAVASCRIPT
// counter.cjs — CommonJS
let count = 0;
function increment() {
  count++;
}
module.exports = { count, increment };
JAVASCRIPT
// counter.mjs — ESM
export let count = 0;
export function increment() {
  count++;
}
JAVASCRIPT
// 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)
JAVASCRIPT
// ESM: count is bound, real-time updates
import { count, increment } from './counter.mjs';
increment();
console.log(count); // 1 (updated)

▶ 例:ESMでCJSモジュールをインポートする

JAVASCRIPT
// legacy.cjs
module.exports = { legacyMethod() { return 'old school'; } };
JAVASCRIPT
// 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 は以下の順序で検索を行います:

100%
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パッケージ

▶ 例:モジュールの解決パスの表示

JAVASCRIPT
// show-paths.js
console.log(module.paths);
TEXT
[
  '/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 同じモジュールが呼び出された際には、コードを再実行することなく、キャッシュされた結果が直接返されます。

JAVASCRIPT
// counter.js
console.log('counter.js executed!');
let count = 0;
module.exports = {
  increment() { return ++count; },
  getCount() { return count; },
};
JAVASCRIPT
// 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 オブジェクトにキャッシュされます。

JAVASCRIPT
// 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

▶ 例:キャッシュをクリアしてホットリローディングを実現する

JAVASCRIPT
// 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 パス処理 joinresolveparseextnamebasename
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) 組み込みモジュールはインストールが不要です

JAVASCRIPT
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 を使ったクイックスタート

JAVASCRIPT
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 での処理方法

JAVASCRIPT
// a.js
exports.loaded = false;
const b = require('./b');
exports.loaded = true;
console.log('a.js - b.loaded =', b.loaded);
JAVASCRIPT
// 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);
BASH
node a.js
TEXT
b.js - a.loaded = false
a.js - b.loaded = true

b.jsrequire('./a') を実行するとき、a.js の実行はまだ完了していないため、Node.js はその時点で値が割り当てられている a.js の部分({ loaded: false })を返します。

(3) 循環依存を回避するための戦略

戦略 説明
共通ロジックを抽出 共通部分を第3のモジュールに移動
遅延読み込み require を関数内に移動し、呼び出されたときにのみ読み込まれるようにする
イベントの分離 直接呼び出しの代わりに EventEmitter を使用する
依存性注入 依存性をハードコーディングするのではなく、パラメータを介して渡す

▶ 例:循環依存関係を解消するために require を遅延させる

JAVASCRIPT
// 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());
};
JAVASCRIPT
// 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. 総合的な例:モジュール型プロジェクト

以下では、ユーティリティモジュール、ロギングモジュール、およびメインのエントリポイントを含むモジュール型プロジェクトを作成します。

JAVASCRIPT
// 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 };
JAVASCRIPT
// 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 };
JAVASCRIPT
// 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');
}
BASH
node app.js
TEXT
[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

❓ よくある質問

Q CommonJSとESMは併用できますか?
A 制限があります。ESMでは、CJSモジュールをインポートすることは可能ですが(デフォルトのエクスポートとしてmodule.exportsを使用)、CJSではrequireを使用してESMモジュールをロードすることはできません。その場合は、代わりに動的なimport()関数を使用する必要があります。プロジェクトでは、単一のモジュール仕様に統一することを推奨します。
Q require は同期型ですか、それとも非同期型ですか?
A 同期型です。require は、モジュールの読み込みが完了するまでコードの実行をブロックします。そのため、Node.js では、require をファイルの先頭に配置し、実行時にホットパスにある新しいモジュールに対して require を頻繁に呼び出さないようにすることが推奨されています。
Q What is the difference between module.exports and exports?
A 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.
Q モジュールのキャッシュを確認するにはどうすればよいですか?
A require.cache オブジェクトを使用して確認できます。キーはモジュールの絶対パス、値はモジュールオブジェクトです。キーを削除すると (delete require.cache[key])、次に require を使用する際に、そのモジュールが再実行されます。
Q 循環依存関係とは何ですか?Node.js ではどのように処理されますか?
A 循環依存関係とは、2つ以上のモジュールが互いに依存し合っている状態のことです。Node.jsは無限ループに陥ることはありません。その代わりに、ループが始まった時点でまだ完全に実行されていないexports(不完全なオブジェクトである可能性があります)を返すため、未定義のプロパティが生じる可能性があります。解決策としては、共通モジュールの抽出、require呼び出しの延期、イベントの分離などが挙げられます。
Q なぜESMではimportステートメントを最上位に記述しなければならないのですか?
A ESMは静的解析が行われるため、依存関係はコンパイル段階で決定され、これによりツリーシェーキングや最適化が容易になります。動的読み込みを行う場合は、import()関数を使用できます。
Q require を使って JSON ファイルを読み込むとどうなりますか?
A Node.js は JSON ファイルを読み込み、JSON.parse() を使用して自動的に解析し、解析済みの JavaScript オブジェクトを返します。これは、設定ファイルの読み込みによく使われます。

📖 まとめ


📝 練習問題

  1. calculator.js モジュールを作成し、4つの関数 addsubtractmultiply、および divide をエクスポートした後、main.js 内でそれらをインポートして使用する
  2. 前の質問をESM版に変換する:exportという構文と.mjsという接尾辞を使用し、importを使って読み込む。
  3. require.cache が存在することを確認するコードを記述してください。モジュールをインポートした後、require.cache からそのモジュールに関する情報を出力してください。
  4. 意図的に循環依存関係(a.js が b.js を要求し、b.js が a.js を要求する)を作成し、出力を確認した後、遅延要求を使用してこれを修正する。
  5. path および os モジュールを使用して、現在のオペレーティングシステムのプラットフォーム、CPU コア数、および現在のディレクトリの絶対パスを表示してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%