NodeJS 速習チュートリアル

Node.js ES Modules

1. ES Modules(ESM)の概要

ES Modules (ESM) は、JavaScript コードを再利用可能なコンポーネントとしてパッケージ化するための、公式な標準フォーマットです。
ES6(ES2015)で導入され、現在は Node.js でもネイティブにサポートされています。

ES Modules が普及する前、Node.js では CommonJSrequire/exports)のみが使用されてきました。現在は、プロジェクトの要件に応じて CommonJS と ES Modules を自由に選択できます。
ES Modules は CommonJS と比較して、静的に解析可能な構造を提供します。これにより、不要なコードを削除する ツリーシェイキング(Tree-shaking) が可能になり、ビルドサイズの軽量化に貢献します。

2. CommonJS と ES Modules の比較

CommonJS と ES Modules の主な違いは以下の通りです:

機能CommonJSES Modules
ファイル拡張子.js (デフォルト).mjs (または設定済みの .js)
インポート構文require()import
エクスポート構文module.exports / exportsexport / 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)); // 出力: 8

2.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)); // 出力: 8

3. 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.jsonexports フィールドでエントリーポイントを分けます:

{
  "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 の設定など、適切な構成が重要である