Node.js ES Modules
1. ES Modules(ESM)の概要
ES Modules (ESM) は、JavaScript コードを再利用可能なコンポーネントとしてパッケージ化するための、公式な標準フォーマットです。
ES6(ES2015)で導入され、現在は Node.js でもネイティブにサポートされています。
ES Modules が普及する前、Node.js では CommonJS(require/exports)のみが使用されてきました。現在は、プロジェクトの要件に応じて CommonJS と ES Modules を自由に選択できます。
ES Modules は CommonJS と比較して、静的に解析可能な構造を提供します。これにより、不要なコードを削除する ツリーシェイキング(Tree-shaking) が可能になり、ビルドサイズの軽量化に貢献します。
2. CommonJS と ES Modules の比較
CommonJS と ES Modules の主な違いは以下の通りです:
| 機能 | CommonJS | ES Modules |
|---|---|---|
| ファイル拡張子 | .js (デフォルト) | .mjs (または設定済みの .js) |
| インポート構文 | require() | import |
| エクスポート構文 | module.exports / exports | export / export default |
| 読み込みタイミング | 動的(実行時) | 静的(実行前に解析) |
| Top-level Await | 非サポート | サポート |
| URL形式のパス | 不要 | ローカルファイルでも必須 |
2.1 CommonJS モジュールの例
// math.js (CommonJS)
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = {
add,
subtract
};
// app.js (CommonJS)
const math = require('./math');
console.log(math.add(5, 3)); // 出力: 82.2 ES Modules の例
// math.mjs (ES Module)
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// app.mjs (ES Module)
import { add, subtract } from './math.mjs';
console.log(add(5, 3)); // 出力: 83. ES Modules を有効にする方法
Node.js で ES Modules を使用するには、主に 3 つの方法があります:
3.1 .mjs 拡張子を使用する
最もシンプルな方法は、ファイルの拡張子を .mjs にすることです。Node.js はこれらのファイルを自動的に ES Modules として扱います。
3.2 package.json で "type": "module" を設定する
プロジェクト内のすべての .js ファイルを ES Modules として扱いたい場合は、package.json に以下の設定を追加します:
{
"name": "my-package",
"version": "1.0.0",
"type": "module"
}3.3 --input-type=module フラグを使用する
スクリプトを直接実行する際、コマンドライン引数でモジュールシステムを指定することも可能です:node --input-type=module script.js
注意: 既存のコードベースが主に CommonJS で構成されており、特定のファイルだけ ES Modules を使いたい場合は、.mjs 拡張子を使うのが最も明示的でエラーの少ないアプローチです。4. インポートとエクスポートの構文
ES Modules は CommonJS よりも柔軟なエクスポート・インポート手法を提供します。
4.1 エクスポート構文 (Export Syntax)
名前付きエクスポート (Named Exports)
// 複数の名前付きエクスポート
export function sayHello() {
console.log('こんにちは');
}
export function sayGoodbye() {
console.log('さようなら');
}
// 別解: ファイルの最後でまとめてエクスポート
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
export { add, subtract };デフォルトエクスポート (Default Export)
// モジュールごとに 1 つだけ設定可能
export default function() {
console.log('これはデフォルトエクスポートです');
}
// 関数、クラス、オブジェクトをデフォルトとして指定する場合
function mainFunction() {
return 'メインの機能';
}
export default mainFunction;4.2 インポート構文 (Import Syntax)
名前付きエクスポートのインポート
// 特定の名前付きエクスポートをインポート
import { sayHello, sayGoodbye } from './greetings.mjs';
sayHello(); // こんにちは
// 衝突を避けるために名前を変更(エイリアス)してインポート
import { add as sum, subtract as minus } from './math.mjs';
console.log(sum(5, 3)); // 8
// すべての名前付きエクスポートをオブジェクトとしてインポート
import * as math from './math.mjs';
console.log(math.add(7, 4)); // 11デフォルトエクスポートのインポート
// デフォルトエクスポートのインポート(任意の名前を付けられます)
import mainFunction from './main.mjs';
mainFunction();
import anyNameYouWant from './main.mjs';
anyNameYouWant();5. 動的インポート (Dynamic Imports)
ES Modules は動的インポートをサポートしており、条件に応じて、あるいはオンデマンドでモジュールをロードできます。
// app.mjs
async function loadModule(moduleName) {
try {
// 動的インポートは Promise を返します
const module = await import(`./${moduleName}.mjs`);
return module;
} catch (error) {
console.error(`${moduleName} の読み込みに失敗しました:`, error);
}
}
// 条件に基づいてモジュールをロード
const moduleName = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';
loadModule(moduleName).then(module => {
module.default(); // デフォルトエクスポートを実行
});
// よりシンプルな await 構文
(async () => {
const mathModule = await import('./math.mjs');
console.log(mathModule.add(10, 5)); // 15
})();ユースケース: 動的インポートは、コード分割(Code-splitting)、モジュールの遅延読み込み(Lazy-loading)、あるいは実行時の条件に基づいたモジュールロードに最適です。
6. Top-level Await
CommonJS とは異なり、ES Modules では Top-level Await がサポートされています。これにより、async 関数の外側(モジュールのトップレベル)で await を使用できます。
// data-loader.mjs
console.log('データを読み込み中...');
// Top-level await - ここでモジュールの実行が一時停止します
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
console.log('データの読み込みが完了しました!');
export { data };
// 他のモジュールがこれをインポートすると、
// Top-level await の処理がすべて完了した後にエクスポートを受け取りますTop-level await は、ファイルやリモートソースからの設定の読み込み、機能のエクスポート前のデータベース接続などに非常に有用です。
7. ベストプラクティス
7.1 ファイル拡張子を明示する
ローカルファイルをインポートする際は、常に拡張子を含めてください:
- Good:
import { someFunction } from './utils.mjs'; - Bad:
import { someFunction } from './utils';(設定によっては動作しません)
7.2 ディレクトリインデックスの活用
ディレクトリ単位でインポートする場合、index.mjs ファイルを作成します:
// utils/index.mjs
export * from './string-utils.mjs';
export * from './number-utils.mjs';
// app.mjs
import { formatString, add } from './utils/index.mjs';7.3 エクスポートスタイルの選択
複数の関数や値がある場合は名前付きエクスポートを、主要な機能にはデフォルトエクスポートを使用します。
- ユーティリティライブラリ:名前付きエクスポート
- コンポーネントやクラス:デフォルトエクスポート
7.4 CommonJS からの移行と互換性
CommonJS と ES Modules が混在する場合:
- ES Modules から CommonJS をインポート:
importで可能(module.exportsがデフォルトインポートになります) - CommonJS から ES Modules をインポート:動的
import()を使用する必要があります - Node.js の module パッケージにある互換ヘルパーを活用してください
7.5 Dual Package Hazard への対策
npm パッケージで両方のシステムをサポートする場合、package.json の exports フィールドでエントリーポイントを分けます:
{
"name": "my-package",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.cjs"
}
}
}8. まとめ
ES Modules は Node.js の開発において標準的な選択肢となりました。require から import への移行は、単なる構文の変化ではなく、静的解析やモダンな非同期処理(Top-level Await)の恩恵を受けるための重要なステップです。
本記事のポイント:
- Node.js は v12 以降、ES Modules をフルサポートしている
import/exportによる静的なモジュール管理が可能- 動的インポートや Top-level Await により柔軟なコーディングができる
- 拡張子の明示や
package.jsonの設定など、適切な構成が重要である