はじめに#
Node.jsにはCommonJSと**ES Modules(ESM)**という2つのモジュールシステムが存在します。歴史的にNode.jsはCommonJSを採用してきましたが、ECMAScript標準としてES Modulesが策定されて以降、Node.jsでもESMのサポートが進んでいます。
本記事では、以下の内容を解説します。
- CommonJSとES Modulesの基本構文
package.jsonのtypeフィールドによるモジュール形式の指定
- 拡張子
.mjsと.cjsの使い分け
- 2つのモジュールシステム間の相互運用性
- プロジェクトに適したモジュール形式の選択基準
この記事を読み終えると、Node.jsプロジェクトで適切なモジュール形式を選択し、実装できるようになります。
実行環境#
本記事のコードは以下の環境で動作確認しています。
| 項目 |
バージョン |
| Node.js |
20.x LTS以上 |
| npm |
10.x以上 |
| OS |
Windows/macOS/Linux |
CommonJSの基本#
CommonJSはNode.jsで最初から採用されているモジュールシステムです。require()関数でモジュールを読み込み、module.exportsまたはexportsでエクスポートします。
CommonJSの構文#
CommonJSでは、モジュールを読み込む際にrequire()関数を使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// mathUtils.js(CommonJS形式)
// 関数を定義
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// module.exportsでエクスポート
module.exports = {
add,
subtract,
};
|
このモジュールを使用する側は、require()で読み込みます。
1
2
3
4
5
6
7
8
9
10
11
12
|
// main.js(CommonJS形式)
// モジュールの読み込み
const mathUtils = require('./mathUtils');
// 関数の使用
console.log(mathUtils.add(2, 3)); // 5
console.log(mathUtils.subtract(5, 2)); // 3
// 分割代入で個別に取得することも可能
const { add, subtract } = require('./mathUtils');
console.log(add(10, 5)); // 15
|
exportsショートカット#
module.exportsへの参照としてexportsオブジェクトも使用できます。ただし、exportsを直接再代入すると正しく動作しません。
1
2
3
4
5
6
7
8
9
10
|
// 正しい使い方
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
// 誤った使い方(動作しない)
exports = {
add: (a, b) => a + b,
};
// この場合、module.exportsとの参照が切れるため、
// require()しても空オブジェクトが返される
|
CommonJSの特徴#
CommonJSには以下の特徴があります。
| 特徴 |
説明 |
| 同期的な読み込み |
require()はファイルを同期的に読み込む |
| 動的な読み込み |
条件分岐やループ内でもrequire()を呼び出せる |
| キャッシング |
一度読み込んだモジュールはキャッシュされる |
__filenameと__dirname |
現在のファイルパスとディレクトリパスが利用可能 |
1
2
3
4
5
6
|
// 動的な読み込みの例
const moduleName = process.env.NODE_ENV === 'production'
? './prodConfig'
: './devConfig';
const config = require(moduleName);
|
ES Modulesの基本#
ES Modules(ESM)はECMAScript 2015(ES6)で標準化されたモジュールシステムです。importとexportキーワードを使用します。
ES Modulesの構文#
ES Modulesでは、exportキーワードでエクスポートし、importキーワードでインポートします。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// mathUtils.mjs(ES Modules形式)
// 名前付きエクスポート
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// 定数のエクスポート
export const PI = 3.14159;
|
インポート側では、import文を使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// main.mjs(ES Modules形式)
// 名前付きインポート
import { add, subtract, PI } from './mathUtils.mjs';
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
console.log(PI); // 3.14159
// 別名でインポート
import { add as sum } from './mathUtils.mjs';
console.log(sum(10, 20)); // 30
// すべてをまとめてインポート
import * as math from './mathUtils.mjs';
console.log(math.add(1, 2)); // 3
|
デフォルトエクスポート#
モジュールのメイン機能を1つだけエクスポートする場合は、デフォルトエクスポートを使用できます。
1
2
3
4
5
6
7
8
9
10
11
|
// Calculator.mjs
export default class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
}
|
1
2
3
4
5
6
7
|
// main.mjs
// デフォルトインポート(任意の名前を付けられる)
import Calculator from './Calculator.mjs';
const calc = new Calculator();
console.log(calc.add(5, 3)); // 8
|
ES Modulesの特徴#
ES Modulesには以下の特徴があります。
| 特徴 |
説明 |
| 静的な構造 |
インポート/エクスポートはファイルのトップレベルに記述 |
| 非同期読み込み |
動的import()はPromiseを返す |
| Strict mode |
自動的にStrict modeで実行される |
import.meta |
モジュールのメタ情報(URL等)にアクセス可能 |
1
2
3
|
// 動的インポート(ES Modules)
const moduleName = './dynamicModule.mjs';
const module = await import(moduleName);
|
package.jsonのtypeフィールド#
Node.jsはpackage.jsonのtypeフィールドを参照して、.jsファイルをどのモジュールシステムとして解釈するかを決定します。
typeフィールドの設定#
1
2
3
4
5
|
{
"name": "my-esm-project",
"version": "1.0.0",
"type": "module"
}
|
typeフィールドには以下の値を設定できます。
| 値 |
説明 |
"module" |
.jsファイルをES Modulesとして解釈 |
"commonjs" |
.jsファイルをCommonJSとして解釈(デフォルト) |
| 未指定 |
CommonJSとして解釈(後方互換性のため) |
モジュール形式の判定フロー#
Node.jsは以下の順序でモジュール形式を判定します。
flowchart TD
A[ファイルを読み込む] --> B{拡張子は?}
B -->|.mjs| C[ES Modulesとして実行]
B -->|.cjs| D[CommonJSとして実行]
B -->|.js| E{package.jsonのtypeは?}
E -->|"module"| C
E -->|"commonjs"または未指定| D推奨設定#
新規プロジェクトでは、以下のように明示的にtypeフィールドを設定することを推奨します。
1
2
3
4
5
6
7
8
|
{
"name": "modern-nodejs-project",
"version": "1.0.0",
"type": "module",
"engines": {
"node": ">=20.0.0"
}
}
|
拡張子.mjsと.cjsの使い分け#
package.jsonのtypeフィールドに関わらず、拡張子によってモジュール形式を明示できます。
拡張子ごとの挙動#
| 拡張子 |
モジュール形式 |
用途 |
.mjs |
ES Modules |
CommonJSプロジェクト内でESMを使いたい場合 |
.cjs |
CommonJS |
ESMプロジェクト内でCommonJSを使いたい場合 |
.js |
typeフィールドに依存 |
プロジェクトのデフォルト形式に従う |
混在させる場合の例#
ESMプロジェクト内でCommonJS形式の設定ファイルを使用する例を示します。
1
2
3
4
|
{
"name": "esm-project",
"type": "module"
}
|
1
2
3
4
5
|
// config.cjs(CommonJS形式で記述)
module.exports = {
port: 3000,
host: 'localhost',
};
|
1
2
3
4
5
6
7
8
|
// server.js(ES Modules形式)
import { createRequire } from 'node:module';
// ES Modules内でrequireを使用するためのヘルパー
const require = createRequire(import.meta.url);
const config = require('./config.cjs');
console.log(config.port); // 3000
|
CommonJSとES Modulesの相互運用#
2つのモジュールシステム間での相互運用には、いくつかの制約と注意点があります。
ES ModulesからCommonJSを読み込む#
ES ModulesからCommonJSモジュールをimportすることは可能です。
1
2
3
4
5
|
// legacy.cjs(CommonJS)
module.exports = {
greet: (name) => `Hello, ${name}!`,
version: '1.0.0',
};
|
1
2
3
4
5
6
7
8
9
10
|
// main.mjs(ES Modules)
// デフォルトインポートでmodule.exportsを取得
import legacy from './legacy.cjs';
console.log(legacy.greet('World')); // Hello, World!
console.log(legacy.version); // 1.0.0
// 名前付きインポートも可能(静的解析による推測)
import { greet } from './legacy.cjs';
console.log(greet('Node.js')); // Hello, Node.js!
|
CommonJSからES Modulesを読み込む#
Node.js 20以降では、CommonJSから同期的なES Modulesをrequire()で読み込むことが可能になりました(トップレベルawaitを含まない場合)。
1
2
3
4
5
6
7
8
9
10
|
// modern.mjs(ES Modules、トップレベルawaitなし)
export function calculate(x) {
return x * 2;
}
export default class Calculator {
multiply(a, b) {
return a * b;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// main.cjs(CommonJS)
// ES Modulesをrequireで読み込む(Node.js 20+)
const modern = require('./modern.mjs');
console.log(modern);
// [Module: null prototype] {
// calculate: [Function: calculate],
// default: [class Calculator]
// }
console.log(modern.calculate(5)); // 10
const Calculator = modern.default;
const calc = new Calculator();
console.log(calc.multiply(3, 4)); // 12
|
ただし、トップレベルawaitを含むESMはrequire()で読み込めません。その場合は動的import()を使用します。
1
2
3
|
// asyncModule.mjs(トップレベルawaitを含む)
const data = await fetch('https://api.example.com/data');
export const result = await data.json();
|
1
2
3
4
5
6
7
8
|
// main.cjs(CommonJS)
// 非同期ESMは動的import()で読み込む
async function main() {
const { result } = await import('./asyncModule.mjs');
console.log(result);
}
main();
|
相互運用時の注意点#
相互運用時には以下の点に注意が必要です。
| 項目 |
注意点 |
__dirname/__filename |
ES Modulesでは使用不可。import.meta.dirname/import.meta.filenameを使用 |
require.resolve |
ES Modulesではimport.meta.resolve()を使用 |
| JSONインポート |
ES Modulesではwith { type: 'json' }が必要 |
| 拡張子の省略 |
ES Modulesでは拡張子の指定が必須 |
1
2
3
4
5
6
7
|
// ES ModulesでのJSONインポート
import packageJson from './package.json' with { type: 'json' };
console.log(packageJson.name);
// ES Modulesでの__dirnameの代替
console.log(import.meta.dirname); // /path/to/directory
console.log(import.meta.filename); // /path/to/file.mjs
|
どちらのモジュール形式を選ぶべきか#
プロジェクトの特性に応じて、適切なモジュール形式を選択しましょう。
ES Modulesを選ぶべきケース#
- 新規プロジェクトを開始する場合
- モダンなJavaScript構文を積極的に使用したい場合
- ブラウザとNode.jsで同じコードを共有したい場合
- Tree shakingなどのビルド最適化を活用したい場合
CommonJSを維持すべきケース#
- 既存のCommonJSプロジェクトを保守する場合
- CommonJS専用の依存パッケージが多い場合
- レガシー環境との互換性が必要な場合
判断フローチャート#
flowchart TD
A[プロジェクトを開始] --> B{新規プロジェクト?}
B -->|はい| C{ESMに対応していない<br>依存パッケージがある?}
C -->|はい| D[CommonJSを選択]
C -->|いいえ| E[ES Modulesを選択]
B -->|いいえ| F{大規模な改修が可能?}
F -->|はい| G{ESM移行のメリットが大きい?}
G -->|はい| E
G -->|いいえ| D
F -->|いいえ| D実践的な設定例#
実際のプロジェクトで使用する設定例を示します。
ES Modulesプロジェクトの推奨設定#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
{
"name": "esm-project",
"version": "1.0.0",
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"exports": {
".": {
"import": "./src/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"start": "node src/index.js",
"build": "tsc"
}
}
|
デュアルパッケージ(ESMとCommonJS両対応)#
ライブラリを公開する場合、両方の形式に対応することが求められる場合があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
{
"name": "dual-package-library",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}
|
まとめ#
本記事では、Node.jsの2つのモジュールシステムについて解説しました。
- CommonJSは
require()/module.exportsを使用する従来のモジュールシステム
- ES Modulesは
import/exportを使用するECMAScript標準のモジュールシステム
package.jsonのtypeフィールドで.jsファイルの解釈を制御
- 拡張子
.mjs/.cjsでファイル単位でモジュール形式を明示可能
- 新規プロジェクトではES Modulesの採用を推奨
- 相互運用時は各システムの制約に注意
モジュールシステムを正しく理解することで、Node.jsプロジェクトの構成を適切に設計できるようになります。
参考リンク#