T-CREATOR

JavaScript モジュール徹底解説:CommonJS・AMD・ESM の違いと使い分け

JavaScript モジュール徹底解説:CommonJS・AMD・ESM の違いと使い分け

現代のJavaScript開発において、モジュールシステムは欠かせない存在となりました。しかし、CommonJS、AMD、ES Modulesという3つの主要なモジュール形式が混在し、どれを選べば良いのか迷われる方も多いでしょう。

本記事では、これらのモジュールシステムがなぜ生まれ、どのような問題を解決してきたのかを歴史的な観点から詳しく解説いたします。各システムの特徴や使い分け方法を理解することで、適切な技術選択ができるようになります。

背景

JavaScript におけるモジュール化の必要性

JavaScriptは当初、Webページに少しのインタラクションを追加するための簡単なスクリプト言語として設計されました。しかし、Web技術の発展とともに、JavaScriptで構築されるアプリケーションは急速に複雑化していったのです。

以下の図は、JavaScript開発の複雑さがどのように進化してきたかを示しています。

mermaidflowchart TD
  simple[簡単なスクリプト<br/>数十行] --> medium[中規模アプリ<br/>数百行]
  medium --> complex[大規模SPA<br/>数万行]
  complex --> enterprise[エンタープライズ<br/>数十万行]
  
  simple -.-> issues1[名前空間汚染]
  medium -.-> issues2[依存関係の混乱]
  complex -.-> issues3[コード分割の困難]
  enterprise -.-> issues4[チーム開発の複雑化]

複雑になったアプリケーション開発では、コードの再利用性、保守性、テスト可能性が重要な要素となります。これらの課題を解決するために、モジュール化という概念が注目されました。

モジュール化には以下のような利点があります。

利点説明
1コードの整理 - 機能ごとに分割し、理解しやすい構造を作れます
2再利用性 - 一度作成したモジュールを他のプロジェクトでも活用できます
3保守性 - 変更の影響範囲を限定でき、バグの修正が容易になります
4テスト容易性 - 個別のモジュールを独立してテストできます
5チーム開発 - 複数人での並行開発が効率化されます

モジュールシステム誕生の歴史的背景

JavaScriptのモジュールシステムの歴史は、Web技術とサーバーサイドJavaScriptの発展と密接に関わっています。

2000年代後半になると、AjaxやJavaScriptライブラリの普及により、クライアントサイドのコード量が爆発的に増加しました。同時期に、Node.jsプロジェクトが開始され、JavaScriptをサーバーサイドでも本格的に使う動きが活発化したのです。

以下の時系列図で、主要なモジュールシステムの誕生と普及の流れを確認してみましょう。

mermaidtimeline
    title JavaScript モジュールシステムの歴史
    
    2009 : CommonJS仕様策定
         : Node.js登場
    
    2010 : RequireJS登場
         : AMD仕様策定
    
    2015 : ES2015(ES6)策定
         : ES Modules標準化
    
    2017 : Node.js ES Modules実験的サポート
    
    2020 : Node.js ES Modules正式サポート
         : 主要ブラウザ完全対応

このような背景から、異なる目的と環境に対応するため、複数のモジュールシステムが並行して発展してきました。

課題

初期 JavaScript の名前空間汚染問題

初期のJavaScript開発では、全ての変数と関数がグローバルスコープに配置されるため、名前の衝突が頻繁に発生しました。これを「名前空間汚染」と呼びます。

以下のコードで問題を具体的に見てみましょう。

javascript// ファイル1: utils.js
var userName = 'admin';
function getUserName() {
  return userName;
}
javascript// ファイル2: app.js  
var userName = 'guest'; // 同じ名前で上書きしてしまう
function showUser() {
  console.log(getUserName()); // 期待と異なる結果
}
javascript// HTMLファイル
<script src="utils.js"></script>
<script src="app.js"></script>
<script>
  showUser(); // 'guest'が表示される(期待: 'admin')
</script>

この例では、userName変数が意図せず上書きされ、予期しない動作を引き起こしています。大規模なアプリケーションでは、このような問題を発見するのは非常に困難です。

スクリプト読み込み順序の複雑化

複数のJavaScriptファイルを使用する際、依存関係を正しい順序で読み込む必要がありました。しかし、プロジェクトが大きくなるにつれて、この順序管理は極めて困難になったのです。

html<!-- 依存関係を手動で管理する必要がある -->
<script src="jquery.js"></script>          <!-- 基盤ライブラリ -->
<script src="underscore.js"></script>      <!-- ユーティリティ -->
<script src="backbone.js"></script>        <!-- jQueryに依存 -->
<script src="models/user.js"></script>     <!-- Backboneに依存 -->
<script src="views/userView.js"></script>  <!-- User model に依存 -->
<script src="app.js"></script>             <!-- 全てに依存 -->

この方法では以下の問題が生じます。

問題点影響
1順序管理の複雑さ - ファイル数が増えると依存関係の把握が困難
2読み込みエラー - 順序を間違えると実行時エラーが発生
3パフォーマンス低下 - 不要なスクリプトも読み込んでしまう
4保守性の悪化 - ファイル追加や削除時の影響範囲が不明確

依存関係管理の困難さ

従来のJavaScriptでは、どのファイルがどのライブラリや関数に依存しているかを明示的に宣言する仕組みがありませんでした。これにより、以下のような問題が発生していました。

javascript// math.js - 依存関係が不明確
function calculate(a, b) {
  // lodashを使っているが、どこにも明記されていない
  return _.sum([a, b, getMultiplier()]);
}

function getMultiplier() {
  // utilsモジュールの関数だが依存関係が不明
  return utils.getConfigValue('multiplier');
}

このコードの問題点を図で整理してみましょう。

mermaidflowchart TB
  mathjs[math.js] -.-> lodash[lodash ライブラリ]
  mathjs -.-> utils[utils.js]
  utils -.-> config[config.js]
  
  classDef implicit stroke-dasharray: 5 5
  classDef explicit stroke-width:3px
  
  class lodash,utils,config implicit
  
  note1[暗黙的な依存関係<br/>コードを読まないと分からない]
  note2[実行時まで依存関係の<br/>エラーが発見できない]

図の要点:

  • 点線は暗黙的な依存関係を示しています
  • 依存関係がコード内に隠れており、外部からは把握できません
  • 実行時にならないとエラーが判明しないリスクがあります

これらの課題を解決するために、明示的な依存関係管理を可能にするモジュールシステムが必要となったのです。

解決策

CommonJS の登場と特徴

CommonJSは2009年に策定されたモジュール仕様で、主にサーバーサイドJavaScript(特にNode.js)での使用を前提として設計されました。同期的なモジュール読み込みを特徴とし、シンプルで理解しやすい構文を提供します。

CommonJS の基本構文

CommonJSでは require() 関数でモジュールを読み込み、module.exports でモジュールを公開します。

javascript// math.js - モジュールの定義
const PI = 3.14159;

function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

// 公開する関数を指定
module.exports = {
  add,
  multiply,
  PI
};
javascript// app.js - モジュールの使用
const math = require('./math');

console.log(math.add(5, 3));      // 8
console.log(math.multiply(4, 2)); // 8
console.log(math.PI);             // 3.14159

分割代入を使用したより柔軟な読み込み方法も可能です。

javascript// 必要な関数のみを取り出して使用
const { add, PI } = require('./math');

console.log(add(10, 20)); // 30
console.log(PI);          // 3.14159

require と module.exports の仕組み

CommonJSの動作原理を理解するため、内部的な仕組みを見てみましょう。

javascript// Node.jsが内部的に行っている処理(疑似コード)
function require(modulePath) {
  // 1. モジュールファイルの読み込み
  const moduleContent = fs.readFileSync(modulePath, 'utf8');
  
  // 2. モジュールを関数でラップ
  const moduleFunction = new Function(
    'require', 'module', 'exports', '__filename', '__dirname',
    moduleContent
  );
  
  // 3. module オブジェクトの準備
  const module = { exports: {} };
  
  // 4. モジュール関数を実行
  moduleFunction(require, module, module.exports, __filename, __dirname);
  
  // 5. exports された内容を返す
  return module.exports;
}

この仕組みにより、各モジュールは独立したスコープを持ち、明示的に公開されたもののみが他のモジュールから利用可能になります。

Node.js での採用背景

Node.jsがCommonJSを採用した理由は、以下の技術的特徴が適していたからです。

特徴Node.jsでの利点
1同期読み込み - サーバーサイドではファイルI/Oが高速なため問題なし
2シンプルな構文 - 開発者が学習しやすく、導入コストが低い
3ファイルシステム前提 - サーバー環境のファイルアクセスに最適化
4循環参照対応 - 複雑なアプリケーション構造に対応可能

以下の図で、Node.js環境でのCommonJSの動作フローを確認しましょう。

mermaidsequenceDiagram
    participant App as アプリケーション
    participant Node as Node.js
    participant FS as ファイルシステム
    participant Module as モジュール
    
    App->>Node: require('./math')
    Node->>FS: math.js を読み込み
    FS-->>Node: ファイル内容を返却
    Node->>Module: モジュール関数を実行
    Module-->>Node: module.exports を返却
    Node-->>App: モジュールオブジェクトを返却
    
    Note over Node: 同期的な処理のため<br/>読み込み完了まで待機

AMD(Asynchronous Module Definition)の登場

AMDは2010年頃に登場したモジュール仕様で、ブラウザ環境での非同期モジュール読み込みを実現するために設計されました。RequireJSライブラリによって普及し、クライアントサイドJavaScript開発の標準的な手法となりました。

AMD の基本構文と RequireJS

AMDでは define() 関数でモジュールを定義し、require() 関数で非同期に読み込みます。

javascript// math.js - AMDモジュールの定義
define(function() {
  const PI = 3.14159;
  
  function add(a, b) {
    return a + b;
  }
  
  function multiply(a, b) {
    return a * b;
  }
  
  // 公開するオブジェクトを返す
  return {
    add: add,
    multiply: multiply,
    PI: PI
  };
});

依存関係がある場合は、配列で依存モジュールを指定します。

javascript// calculator.js - 依存関係を持つモジュール
define(['./math', 'jquery'], function(math, $) {
  
  function createCalculator() {
    return {
      compute: function(operation, a, b) {
        switch(operation) {
          case 'add':
            return math.add(a, b);
          case 'multiply':
            return math.multiply(a, b);
          default:
            return 0;
        }
      },
      
      displayResult: function(result) {
        $('#result').text(result);
      }
    };
  }
  
  return createCalculator;
});

モジュールの使用は以下のように行います。

javascript// app.js - AMDモジュールの使用
require(['./calculator'], function(Calculator) {
  const calc = Calculator();
  
  const result = calc.compute('add', 10, 5);
  calc.displayResult(result); // 15
});

非同期読み込みの利点

AMDの最大の特徴は非同期読み込みです。これにより、ブラウザ環境で以下のような利点を得られます。

javascript// 複数のモジュールを並行して読み込み
require(['moduleA', 'moduleB', 'moduleC'], function(A, B, C) {
  // 全てのモジュールの読み込み完了後に実行される
  console.log('全てのモジュールが準備完了');
  
  // モジュールを使用した処理
  const resultA = A.process();
  const resultB = B.calculate(resultA);
  const resultC = C.format(resultB);
});

非同期読み込みのメリットを図で整理してみましょう。

mermaidgantt
    title モジュール読み込みの比較
    dateFormat X
    axisFormat %s
    
    section 同期読み込み(従来)
    ModuleA     :0, 100
    ModuleB     :100, 200  
    ModuleC     :200, 300
    実行開始     :300, 350
    
    section 非同期読み込み(AMD)
    ModuleA     :0, 100
    ModuleB     :0, 150
    ModuleC     :0, 120
    実行開始     :150, 200

図で理解できる要点:

  • 同期読み込みでは各モジュールを順次読み込むため時間がかかります
  • 非同期読み込みでは複数のモジュールを並行読み込みし、総時間を短縮できます

ブラウザでの使用場面

AMDが特に有効な場面は以下の通りです。

場面AMDの効果
1SPA(Single Page Application) - 必要なモジュールのみを動的読み込み
2大規模Webアプリ - 初期読み込み時間の短縮
3モバイルWebアプリ - ネットワーク帯域の効率的活用
4CDN活用 - 外部ライブラリの非同期読み込み

実際のWebアプリケーションでの設定例:

javascript// requirejs-config.js - RequireJS設定
requirejs.config({
  baseUrl: 'js',
  paths: {
    'jquery': 'lib/jquery-3.6.0',
    'lodash': 'lib/lodash-4.17.21',
    'bootstrap': 'lib/bootstrap-5.1.3'
  },
  shim: {
    'bootstrap': {
      deps: ['jquery']
    }
  }
});

// アプリケーション開始
require(['jquery', 'lodash', 'app/main'], function($, _, main) {
  $(document).ready(function() {
    main.initialize();
  });
});

ES Modules(ESM)の標準化

ES Modules(ESM)は2015年のECMAScript 2015(ES6)で標準化されたJavaScriptの公式モジュールシステムです。静的解析が可能な設計により、バンドラーによる最適化やTree Shakingを実現し、現代のJavaScript開発の基盤となっています。

import/export の基本構文

ESMでは export でモジュールを公開し、import で読み込みます。

javascript// math.js - ESMモジュールの定義
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// デフォルトエクスポート
export default function subtract(a, b) {
  return a - b;
}
javascript// app.js - ESMモジュールの使用
import subtract, { add, multiply, PI } from './math.js';

console.log(add(5, 3));      // 8
console.log(multiply(4, 2)); // 8
console.log(subtract(10, 4)); // 6
console.log(PI);             // 3.14159

様々なimport/exportパターンも利用できます。

javascript// 全てをオブジェクトとして読み込み
import * as math from './math.js';
console.log(math.add(1, 2)); // 3

// 動的インポート(非同期)
const mathModule = await import('./math.js');
console.log(mathModule.PI); // 3.14159

静的解析の利点

ESMの重要な特徴は、コンパイル時に依存関係を解析できることです。これにより、以下のような最適化が可能になります。

javascript// utils.js - 複数の関数をエクスポート
export function formatDate(date) {
  return date.toLocaleDateString();
}

export function formatCurrency(amount) {
  return new Intl.NumberFormat('ja-JP', {
    style: 'currency',
    currency: 'JPY'
  }).format(amount);
}

export function formatPercentage(value) {
  return `${(value * 100).toFixed(2)}%`;
}

export function debugLog(message) {
  console.log(`[DEBUG] ${message}`);
}
javascript// app.js - 必要な関数のみをインポート
import { formatDate, formatCurrency } from './utils.js';

const today = new Date();
const price = 1500;

console.log(formatDate(today));        // 2024/1/15
console.log(formatCurrency(price));    // ¥1,500

// debugLog と formatPercentage は使用していない
// → Tree Shaking で最終バンドルから除外される

静的解析による最適化のフローを図で確認してみましょう。

mermaidflowchart LR
  subgraph "ソースコード"
    A[utils.js<br/>4つの関数] --> B[app.js<br/>2つの関数を import]
  end
  
  subgraph "ビルドプロセス"
    B --> C[静的解析]
    C --> D[Tree Shaking]
    D --> E[デッドコード除去]
  end
  
  subgraph "最終バンドル"
    E --> F[最適化されたコード<br/>使用される2つの関数のみ]
  end

ブラウザとNode.js での対応状況

ESMの対応状況は時代とともに改善され、現在では主要な環境で利用可能です。

環境対応状況備考
1モダンブラウザ - Chrome 61+, Firefox 60+, Safari 11+<script type="module"> で利用可能
2Node.js - バージョン 14+ で正式サポートpackage.json で "type": "module" 設定が必要
3Webpack - 完全サポートTree Shaking と最適化が利用可能
4Vite - ネイティブサポート開発時はESMをそのまま使用

ブラウザでのESM使用例:

html<!DOCTYPE html>
<html>
<head>
  <title>ESM Example</title>
</head>
<body>
  <!-- モジュールスクリプトの読み込み -->
  <script type="module">
    import { add, multiply } from './math.js';
    
    console.log(add(3, 4));      // 7
    console.log(multiply(3, 4)); // 12
  </script>
  
  <!-- レガシーブラウザ向けのフォールバック -->
  <script nomodule src="./legacy-bundle.js"></script>
</body>
</html>

Node.jsでのESM使用設定:

json// package.json
{
  "name": "esm-project",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  }
}

具体例

CommonJS を使用した実装例

CommonJSを使用したNode.jsアプリケーションの実装例をご紹介します。ユーザー管理システムを例に、実際の開発現場での使用方法を確認しましょう。

javascript// models/user.js - ユーザーモデル
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
    this.createdAt = new Date();
  }
  
  validate() {
    return this.name && this.email && this.email.includes('@');
  }
  
  toJSON() {
    return {
      name: this.name,
      email: this.email,
      createdAt: this.createdAt
    };
  }
}

module.exports = User;
javascript// utils/validator.js - バリデーションユーティリティ
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

function validateEmail(email) {
  return emailRegex.test(email);
}

function validateName(name) {
  return name && name.trim().length >= 2;
}

module.exports = {
  validateEmail,
  validateName
};
javascript// services/userService.js - ユーザーサービス
const User = require('../models/user');
const { validateEmail, validateName } = require('../utils/validator');

class UserService {
  constructor() {
    this.users = [];
  }
  
  createUser(userData) {
    const { name, email } = userData;
    
    // バリデーション
    if (!validateName(name)) {
      throw new Error('Invalid name');
    }
    
    if (!validateEmail(email)) {
      throw new Error('Invalid email');
    }
    
    // ユーザー作成
    const user = new User(name, email);
    this.users.push(user);
    
    return user;
  }
  
  getAllUsers() {
    return this.users.map(user => user.toJSON());
  }
}

module.exports = UserService;
javascript// app.js - メインアプリケーション
const UserService = require('./services/userService');

const userService = new UserService();

try {
  // ユーザー作成のテスト
  const user1 = userService.createUser({
    name: '田中太郎',
    email: 'tanaka@example.com'
  });
  
  const user2 = userService.createUser({
    name: '佐藤花子',
    email: 'sato@example.com'
  });
  
  console.log('作成されたユーザー:');
  console.log(userService.getAllUsers());
  
} catch (error) {
  console.error('エラー:', error.message);
}

AMD を使用した実装例

AMDを使用したWebアプリケーションの実装例です。画像ギャラリーアプリケーションを通じて、ブラウザ環境でのAMD活用方法を確認してください。

javascript// models/image.js - 画像モデル
define(function() {
  
  function Image(src, title, description) {
    this.src = src;
    this.title = title || '';
    this.description = description || '';
    this.loaded = false;
  }
  
  Image.prototype.load = function() {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => {
        this.loaded = true;
        resolve(this);
      };
      img.onerror = reject;
      img.src = this.src;
    });
  };
  
  return Image;
});
javascript// utils/gallery.js - ギャラリーユーティリティ
define(['jquery'], function($) {
  
  function createThumbnail(image) {
    return $(`
      <div class="thumbnail" data-src="${image.src}">
        <img src="${image.src}" alt="${image.title}" />
        <h4>${image.title}</h4>
        <p>${image.description}</p>
      </div>
    `);
  }
  
  function createModal(image) {
    return $(`
      <div class="modal">
        <div class="modal-content">
          <img src="${image.src}" alt="${image.title}" />
          <h3>${image.title}</h3>
          <p>${image.description}</p>
          <button class="close-modal">閉じる</button>
        </div>
      </div>
    `);
  }
  
  return {
    createThumbnail: createThumbnail,
    createModal: createModal
  };
});
javascript// views/galleryView.js - ギャラリービュー
define(['jquery', '../models/image', '../utils/gallery'], 
function($, Image, galleryUtils) {
  
  function GalleryView(container) {
    this.container = $(container);
    this.images = [];
  }
  
  GalleryView.prototype.addImages = function(imageData) {
    const promises = imageData.map(data => {
      const image = new Image(data.src, data.title, data.description);
      return image.load().then(() => {
        this.images.push(image);
        return image;
      });
    });
    
    return Promise.all(promises).then(() => {
      this.render();
    });
  };
  
  GalleryView.prototype.render = function() {
    this.container.empty();
    
    this.images.forEach(image => {
      const thumbnail = galleryUtils.createThumbnail(image);
      
      thumbnail.on('click', () => {
        this.showModal(image);
      });
      
      this.container.append(thumbnail);
    });
  };
  
  GalleryView.prototype.showModal = function(image) {
    const modal = galleryUtils.createModal(image);
    $('body').append(modal);
    
    modal.find('.close-modal').on('click', () => {
      modal.remove();
    });
  };
  
  return GalleryView;
});
javascript// app.js - アプリケーションの起動
require(['views/galleryView'], function(GalleryView) {
  
  const gallery = new GalleryView('#gallery-container');
  
  const sampleImages = [
    {
      src: 'images/photo1.jpg',
      title: '美しい風景',
      description: '山間の湖畔で撮影した写真です。'
    },
    {
      src: 'images/photo2.jpg', 
      title: '都市の夜景',
      description: '高層ビルから見下ろした夜の街並み。'
    },
    {
      src: 'images/photo3.jpg',
      title: '花のクローズアップ',
      description: '庭に咲いていた薔薇の花びら。'
    }
  ];
  
  gallery.addImages(sampleImages)
    .then(() => {
      console.log('ギャラリーの初期化が完了しました');
    })
    .catch(error => {
      console.error('画像の読み込みに失敗しました:', error);
    });
});

ESM を使用した実装例

ES Modulesを使用したモダンなWebアプリケーションの実装例です。タスク管理アプリケーションを通じて、ESMの特徴的な機能を確認しましょう。

javascript// types/task.js - タスクの型定義とクラス
export class Task {
  constructor(title, description = '', priority = 'medium') {
    this.id = Date.now().toString();
    this.title = title;
    this.description = description;
    this.priority = priority;
    this.completed = false;
    this.createdAt = new Date();
  }
  
  toggle() {
    this.completed = !this.completed;
  }
  
  updatePriority(priority) {
    this.priority = priority;
  }
}

export const PRIORITY_LEVELS = {
  high: '高',
  medium: '中',
  low: '低'
};
javascript// utils/storage.js - ローカルストレージユーティリティ
const STORAGE_KEY = 'tasks';

export function saveTasks(tasks) {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
    return true;
  } catch (error) {
    console.error('タスクの保存に失敗しました:', error);
    return false;
  }
}

export function loadTasks() {
  try {
    const stored = localStorage.getItem(STORAGE_KEY);
    return stored ? JSON.parse(stored) : [];
  } catch (error) {
    console.error('タスクの読み込みに失敗しました:', error);
    return [];
  }
}

export function clearTasks() {
  try {
    localStorage.removeItem(STORAGE_KEY);
    return true;
  } catch (error) {
    console.error('タスクのクリアに失敗しました:', error);
    return false;
  }
}
javascript// services/taskService.js - タスクサービス
import { Task } from '../types/task.js';
import { saveTasks, loadTasks } from '../utils/storage.js';

export class TaskService {
  constructor() {
    this.tasks = this.loadAllTasks();
  }
  
  loadAllTasks() {
    const taskData = loadTasks();
    return taskData.map(data => {
      const task = Object.assign(new Task(''), data);
      task.createdAt = new Date(data.createdAt);
      return task;
    });
  }
  
  addTask(title, description, priority) {
    const task = new Task(title, description, priority);
    this.tasks.push(task);
    this.saveAllTasks();
    return task;
  }
  
  toggleTask(taskId) {
    const task = this.tasks.find(t => t.id === taskId);
    if (task) {
      task.toggle();
      this.saveAllTasks();
      return task;
    }
    return null;
  }
  
  deleteTask(taskId) {
    const index = this.tasks.findIndex(t => t.id === taskId);
    if (index !== -1) {
      const task = this.tasks.splice(index, 1)[0];
      this.saveAllTasks();
      return task;
    }
    return null;
  }
  
  getTasksByPriority(priority) {
    return this.tasks.filter(task => task.priority === priority);
  }
  
  getCompletedTasks() {
    return this.tasks.filter(task => task.completed);
  }
  
  saveAllTasks() {
    saveTasks(this.tasks);
  }
}
javascript// components/taskList.js - タスクリストコンポーネント
import { PRIORITY_LEVELS } from '../types/task.js';

export class TaskList {
  constructor(container, taskService) {
    this.container = container;
    this.taskService = taskService;
    this.init();
  }
  
  init() {
    this.render();
    this.attachEvents();
  }
  
  render() {
    const tasks = this.taskService.tasks;
    
    this.container.innerHTML = `
      <div class="task-header">
        <h2>タスク一覧 (${tasks.length}件)</h2>
        <button id="add-task-btn" class="btn btn-primary">新規タスク追加</button>
      </div>
      <div class="task-filters">
        <button class="filter-btn" data-filter="all">全て</button>
        <button class="filter-btn" data-filter="active">未完了</button>
        <button class="filter-btn" data-filter="completed">完了済み</button>
      </div>
      <div class="task-list" id="task-list">
        ${this.renderTasks(tasks)}
      </div>
    `;
  }
  
  renderTasks(tasks) {
    return tasks.map(task => `
      <div class="task-item ${task.completed ? 'completed' : ''}" data-task-id="${task.id}">
        <div class="task-content">
          <h3>${task.title}</h3>
          <p>${task.description}</p>
          <div class="task-meta">
            <span class="priority ${task.priority}">${PRIORITY_LEVELS[task.priority]}</span>
            <span class="date">${task.createdAt.toLocaleDateString()}</span>
          </div>
        </div>
        <div class="task-actions">
          <button class="toggle-btn ${task.completed ? 'undo' : 'complete'}">
            ${task.completed ? '未完了に戻す' : '完了'}
          </button>
          <button class="delete-btn">削除</button>
        </div>
      </div>
    `).join('');
  }
  
  attachEvents() {
    this.container.addEventListener('click', (e) => {
      const taskId = e.target.closest('.task-item')?.dataset.taskId;
      
      if (e.target.classList.contains('toggle-btn') && taskId) {
        this.taskService.toggleTask(taskId);
        this.render();
      }
      
      if (e.target.classList.contains('delete-btn') && taskId) {
        if (confirm('このタスクを削除しますか?')) {
          this.taskService.deleteTask(taskId);
          this.render();
        }
      }
    });
  }
}
javascript// app.js - メインアプリケーション
import { TaskService } from './services/taskService.js';
import { TaskList } from './components/taskList.js';

// アプリケーションの初期化
document.addEventListener('DOMContentLoaded', async () => {
  const taskService = new TaskService();
  const container = document.getElementById('app');
  
  // タスクリストコンポーネントの作成
  const taskList = new TaskList(container, taskService);
  
  // サンプルタスクの追加(初回起動時のみ)
  if (taskService.tasks.length === 0) {
    taskService.addTask(
      'プロジェクトの企画書作成',
      '来週のミーティングで使用する企画書を作成する',
      'high'
    );
    
    taskService.addTask(
      'コードレビューの実施',
      'チームメンバーのプルリクエストをレビューする',
      'medium'
    );
    
    taskService.addTask(
      '技術ブログの執筆',
      '学んだ技術について記事を書く',
      'low'
    );
    
    taskList.render();
  }
  
  console.log('タスク管理アプリケーションが起動しました');
});

相互運用性の実現方法

実際のプロジェクトでは、複数のモジュールシステムが混在することがよくあります。ここでは、異なるシステム間での相互運用を実現する方法をご紹介します。

javascript// universal-module.js - 複数の形式に対応したモジュール
(function(root, factory) {
  // AMD対応
  if (typeof define === 'function' && define.amd) {
    define(['dependency'], factory);
  }
  // CommonJS対応
  else if (typeof module === 'object' && module.exports) {
    module.exports = factory(require('dependency'));
  }
  // グローバル変数として公開(ブラウザ)
  else {
    root.MyModule = factory(root.Dependency);
  }
}(typeof self !== 'undefined' ? self : this, function(dependency) {
  
  // モジュールの実装
  function MyModule() {
    this.version = '1.0.0';
  }
  
  MyModule.prototype.process = function(data) {
    return dependency.transform(data);
  };
  
  return MyModule;
}));

Node.jsでESMとCommonJSを混在させる場合の設定:

json// package.json
{
  "name": "hybrid-project",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    }
  }
}
javascript// dist/esm/index.js - ESM版
export { default as MyClass } from './MyClass.js';
export { utils } from './utils.js';
javascript// dist/cjs/index.js - CommonJS版  
const { MyClass } = require('./MyClass');
const { utils } = require('./utils');

module.exports = {
  MyClass,
  utils
};

モジュールシステム間の変換を図で整理してみましょう。

mermaidflowchart TB
  subgraph "ソースコード"
    ESM[ES Modules<br/>import/export]
  end
  
  subgraph "ビルドツール"
    Babel[Babel]
    Webpack[Webpack]
    Rollup[Rollup]
  end
  
  subgraph "出力形式"
    CJS[CommonJS<br/>require/exports]
    AMD_OUT[AMD<br/>define/require]
    UMD[UMD<br/>Universal Module]
    IIFE[IIFE<br/>即時実行関数]
  end
  
  ESM --> Babel
  ESM --> Webpack
  ESM --> Rollup
  
  Babel --> CJS
  Webpack --> AMD_OUT
  Webpack --> UMD
  Rollup --> IIFE

このように、モダンな開発環境では、開発時はESMを使用し、ビルドツールによって目的に応じた形式に変換するのが一般的な手法となっています。

まとめ

本記事では、JavaScriptモジュールシステムの歴史的発展とそれぞれの特徴について詳しく解説いたしました。

CommonJS、AMD、ES Modulesは、それぞれ異なる課題を解決するために生まれ、現在でもそれぞれの強みを活かした場面で活用されています。CommonJSはNode.js環境での同期的なモジュール読み込みに優れ、AMDはブラウザでの非同期読み込みを実現し、ES Modulesは標準仕様として最も汎用性と将来性を持っています。

現代の開発では、ES Modulesを基本とし、必要に応じて他のシステムとの相互運用を図るのが最適なアプローチです。ビルドツールを活用することで、開発時の利便性と本番環境での互換性を両立できます。

適切なモジュールシステムの選択により、保守性が高く、チーム開発に適したJavaScriptアプリケーションを構築することができるでしょう。技術の進歩とともに、これらのシステムの理解を深め、プロジェクトに最適な選択を行っていただければと思います。

関連リンク