Node.jsのモジュールシステムの基本:CommonJS と ESM の違い

Node.js でのアプリケーション開発において、「モジュール」という概念は避けて通れない重要な要素です。コードを機能別に分割し、再利用可能な形で管理することは、保守性の高いアプリケーションを構築するための基盤となります。
しかし、Node.js には現在 2 つの主要なモジュールシステムが存在し、多くの開発者が「どちらを使うべきか」「どのような違いがあるのか」と迷われているのではないでしょうか。それが CommonJS と ESM(ES Modules) です。
この記事では、それぞれのモジュールシステムの特徴から実際の使い分け方法まで、現代の Node.js 開発で必要となる知識を体系的に解説いたします。適切なモジュールシステムを選択することで、より効率的で未来指向の開発が可能になります。
モジュールシステムとは何か
モジュールシステムは、JavaScript における「コードの組織化」と「再利用性の向上」を実現するための仕組みです。大規模なアプリケーション開発において欠かせない概念となっています。
JavaScript におけるモジュールの概念
従来の JavaScript では、すべてのコードが同一のグローバルスコープに配置されていました。これにより、以下のような問題が発生していたのです:
グローバル名前空間の汚染
javascript// 従来のスクリプトベースの問題例
// file1.js
var userName = '田中';
function getUserInfo() {
return userName;
}
// file2.js
var userName = '佐藤'; // 同名変数により競合が発生
function getUserInfo() {
// 同名関数により上書き
return '管理者: ' + userName;
}
このような問題を解決するために、モジュールシステムが導入されました。モジュールシステムでは、以下の重要な機能を提供します:
# | 機能 | 説明 | メリット |
---|---|---|---|
1 | スコープ分離 | モジュール内の変数は外部から直接参照不可 | 名前空間の競合を回避 |
2 | 明示的なエクスポート | 外部に公開する要素を明確に定義 | API の意図を明確化 |
3 | 依存関係管理 | 必要なモジュールを明示的にインポート | コードの依存関係を可視化 |
4 | 遅延読み込み | 必要な時点でモジュールを読み込み | パフォーマンスの最適化 |
モジュール化による改善例
javascript// userModule.js - モジュールとして分離
const userName = '田中';
function getUserInfo() {
return userName;
}
function getFormattedUserInfo(prefix = '') {
return `${prefix}${getUserInfo()}`;
}
// 外部に公開する関数のみエクスポート
module.exports = {
getUserInfo,
getFormattedUserInfo,
};
// main.js - 必要な機能のみインポート
const { getUserInfo } = require('./userModule');
console.log(getUserInfo()); // "田中"
コードの分割・再利用の必要性
モダンな Web 開発では、アプリケーションの複雑さが飛躍的に増大しています。この複雑さに対処するために、コードの分割と再利用が不可欠になっています。
分割の利点
-
保守性の向上
- 機能別にファイルを分けることで、バグの特定と修正が容易
- 新機能追加時の影響範囲を限定可能
-
開発効率の向上
- 複数人での並行開発が可能
- 機能単位でのテストが簡単
-
コードの再利用
- 一度作成した機能を別のプロジェクトでも活用
- 共通ライブラリとしての展開が容易
実際のプロジェクト構造例
bashproject/
├── src/
│ ├── utils/
│ │ ├── dateFormatter.js # 日付処理ユーティリティ
│ │ ├── validation.js # 入力値検証
│ │ └── logger.js # ログ出力
│ ├── services/
│ │ ├── userService.js # ユーザー関連ビジネスロジック
│ │ ├── authService.js # 認証処理
│ │ └── dataService.js # データアクセス層
│ ├── controllers/
│ │ ├── userController.js # ユーザーAPI
│ │ └── adminController.js # 管理者API
│ └── app.js # アプリケーションエントリーポイント
└── package.json
この構造により、各モジュールは明確な責任を持ち、必要に応じて他のモジュールと連携します。
CommonJS の基本概念
CommonJS は、Node.js で長年使用されてきた伝統的なモジュールシステムです。サーバーサイド JavaScript の標準として確立され、現在でも多くのプロジェクトで使用されています。
require() と module.exports の仕組み
CommonJS の中心となるのは、require()
関数と module.exports
オブジェクトです。
基本的なエクスポート方法
javascript// mathUtils.js - 数学系ユーティリティモジュール
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
function calculate(operation, a, b) {
switch (operation) {
case 'add':
return add(a, b);
case 'multiply':
return multiply(a, b);
default:
throw new Error(`未対応の演算: ${operation}`);
}
}
// 方法1: オブジェクトとしてエクスポート
module.exports = {
add,
multiply,
calculate,
};
// 方法2: 個別にエクスポート
// module.exports.add = add;
// module.exports.multiply = multiply;
// 方法3: 関数そのものをエクスポート(単一エクスポート)
// module.exports = calculate;
基本的なインポート方法
javascript// calculator.js - mathUtilsを使用するモジュール
// 方法1: 全体をインポート
const mathUtils = require('./mathUtils');
console.log(mathUtils.add(5, 3)); // 8
// 方法2: 分割代入でインポート
const { add, multiply } = require('./mathUtils');
console.log(add(5, 3)); // 8
console.log(multiply(4, 6)); // 24
// 方法3: 個別インポート(エイリアス使用)
const { calculate: calc } = require('./mathUtils');
console.log(calc('add', 10, 20)); // 30
動的なインポート CommonJS の大きな特徴の一つは、実行時の動的なモジュール読み込みです:
javascript// 条件に応じたモジュールの動的読み込み
function loadProcessor(type) {
let processor;
if (type === 'image') {
processor = require('./imageProcessor');
} else if (type === 'text') {
processor = require('./textProcessor');
} else {
processor = require('./defaultProcessor');
}
return processor;
}
// 設定に基づいた動的読み込み
const config = require('./config.json');
const database = require(`./db/${config.database.type}`);
Node.js での標準的な使用方法
Node.js では、CommonJS が標準のモジュールシステムとして長年使用されてきました。
ファイルシステム操作の例
javascript// fileManager.js - ファイル操作ユーティリティ
const fs = require('fs');
const path = require('path');
class FileManager {
constructor(baseDir = './data') {
this.baseDir = baseDir;
this.ensureDirectoryExists();
}
ensureDirectoryExists() {
if (!fs.existsSync(this.baseDir)) {
fs.mkdirSync(this.baseDir, { recursive: true });
}
}
async readFile(filename) {
try {
const filePath = path.join(this.baseDir, filename);
const content = await fs.promises.readFile(
filePath,
'utf8'
);
return content;
} catch (error) {
console.error(
`ファイル読み込みエラー: ${filename}`,
error
);
throw error;
}
}
async writeFile(filename, content) {
try {
const filePath = path.join(this.baseDir, filename);
await fs.promises.writeFile(
filePath,
content,
'utf8'
);
console.log(`ファイル保存完了: ${filename}`);
} catch (error) {
console.error(
`ファイル保存エラー: ${filename}`,
error
);
throw error;
}
}
listFiles() {
try {
return fs.readdirSync(this.baseDir);
} catch (error) {
console.error('ディレクトリ読み込みエラー', error);
return [];
}
}
}
module.exports = FileManager;
Express.js での活用例
javascript// routes/users.js - ユーザー関連ルート
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const { validateUser } = require('../utils/validation');
const router = express.Router();
// ユーザー登録
router.post('/register', async (req, res) => {
try {
const { error } = validateUser(req.body);
if (error) {
return res
.status(400)
.json({ error: error.details[0].message });
}
const { email, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const user = new User({
email,
password: hashedPassword,
});
await user.save();
res
.status(201)
.json({ message: 'ユーザー登録が完了しました' });
} catch (error) {
res
.status(500)
.json({ error: 'サーバーエラーが発生しました' });
}
});
// ユーザーログイン
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (
!user ||
!(await bcrypt.compare(password, user.password))
) {
return res
.status(401)
.json({ error: '認証に失敗しました' });
}
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET
);
res.json({
token,
user: { id: user._id, email: user.email },
});
} catch (error) {
res
.status(500)
.json({ error: 'サーバーエラーが発生しました' });
}
});
module.exports = router;
ESM(ES Modules)の登場背景
ESM(ES Modules)は、ECMAScript 2015(ES6)で標準化された、JavaScript 言語仕様レベルでのモジュールシステムです。ブラウザとサーバーサイドの両方で統一的に使用できる点が大きな特徴です。
ECMAScript 標準としての位置づけ
ESM が登場する以前、JavaScript には言語仕様レベルでのモジュールシステムが存在しませんでした。そのため、様々な独自実装が生まれていました:
# | モジュールシステム | 開発元/標準化団体 | 主な使用場面 | 特徴 |
---|---|---|---|---|
1 | CommonJS | Node.js | サーバーサイド | 同期的読み込み |
2 | AMD | RequireJS | ブラウザ | 非同期読み込み |
3 | UMD | コミュニティ | ユニバーサル | 複数環境対応 |
4 | ESM | ECMAScript | 標準仕様 | 言語レベルでの標準化 |
ESM 標準化の意義
ESM の標準化により、以下のメリットが実現されました:
-
統一された仕様
- ブラウザとサーバーサイドで同一の構文
- 学習コストの削減と知識の移転可能性
-
静的解析の向上
- バンドラーによる最適化が容易
- Tree shaking(不要コードの除去)が効率的
-
将来への投資
- 言語仕様として長期的にサポート
- 新機能の追加も標準化プロセスに沿って実施
Node.js での ESM サポート状況
javascript// package.json での設定
{
"name": "esm-example",
"version": "1.0.0",
"type": "module", // ESM を標準として使用
"scripts": {
"start": "node src/app.js"
}
}
ブラウザとの互換性向上
ESM の最大の利点の一つは、ブラウザとの互換性です。現代のブラウザは ESM をネイティブサポートしており、同じコードをサーバーサイドとクライアントサイドで共有できます。
ブラウザでの ESM 使用例
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>ESM ブラウザテスト</title>
</head>
<body>
<h1>ES Modules テスト</h1>
<div id="result"></div>
<!-- ESM を直接ブラウザで読み込み -->
<script type="module">
import {
formatDate,
addDays,
} from './utils/dateUtils.js';
import UserManager from './modules/UserManager.js';
const userManager = new UserManager();
const today = new Date();
const nextWeek = addDays(today, 7);
document.getElementById('result').innerHTML = `
<p>今日: ${formatDate(today)}</p>
<p>来週: ${formatDate(nextWeek)}</p>
<p>ユーザー数: ${userManager.getUserCount()}</p>
`;
</script>
</body>
</html>
サーバーサイドとの共有モジュール例
javascript// utils/dateUtils.js - サーバーとブラウザで共通使用
export function formatDate(date) {
return new Intl.DateTimeFormat('ja-JP', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(date);
}
export function addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
export function isWeekend(date) {
const day = date.getDay();
return day === 0 || day === 6; // 日曜日または土曜日
}
// Node.js でも同じコードが動作
// import { formatDate, addDays } from './utils/dateUtils.js';
モダンな Web 開発での活用
javascript// frontend/src/api/client.js - APIクライアント
export class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async get(endpoint) {
const response = await fetch(
`${this.baseURL}${endpoint}`
);
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}`
);
}
return response.json();
}
async post(endpoint, data) {
const response = await fetch(
`${this.baseURL}${endpoint}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}
);
return response.json();
}
}
// backend/src/utils/apiClient.js - 同じクラスをサーバーでも使用
// (fetch の代わりに node-fetch や undici を使用)
export { ApiClient } from '../../frontend/src/api/client.js';
CommonJS と ESM の技術的差異
CommonJS と ESM には、単なる構文の違いを超えた根本的な技術的差異があります。これらの違いを理解することで、適切な選択ができるようになります。
構文と API の違い
最も分かりやすい違いは、モジュールの読み込みと出力の構文です。
エクスポート構文の比較
javascript// CommonJS のエクスポート
// utils.js
function greet(name) {
return `こんにちは、${name}さん!`;
}
function farewell(name) {
return `さようなら、${name}さん!`;
}
const DEFAULT_NAME = 'ゲスト';
// 方法1: オブジェクトとしてまとめてエクスポート
module.exports = {
greet,
farewell,
DEFAULT_NAME,
};
// 方法2: 個別エクスポート
// exports.greet = greet;
// exports.farewell = farewell;
// exports.DEFAULT_NAME = DEFAULT_NAME;
javascript// ESM のエクスポート
// utils.js
export function greet(name) {
return `こんにちは、${name}さん!`;
}
export function farewell(name) {
return `さようなら、${name}さん!`;
}
export const DEFAULT_NAME = 'ゲスト';
// デフォルトエクスポート
export default function welcomeMessage(
name = DEFAULT_NAME
) {
return `ようこそ、${name}さん!`;
}
インポート構文の比較
javascript// CommonJS のインポート
const utils = require('./utils'); // 全体インポート
const { greet, farewell } = require('./utils'); // 分割代入
const customName = require('./utils').DEFAULT_NAME; // 個別取得
// 動的インポート
if (condition) {
const dynamicUtils = require('./dynamicUtils');
}
javascript// ESM のインポート
import utils from './utils.js'; // デフォルトインポート
import { greet, farewell } from './utils.js'; // 名前付きインポート
import { greet as sayHello } from './utils.js'; // エイリアス
import * as allUtils from './utils.js'; // 全体インポート
// 動的インポート(Promiseベース)
if (condition) {
const { dynamicFunction } = await import(
'./dynamicUtils.js'
);
}
高度なインポート・エクスポートパターン
javascript// ESM の高度なパターン例
// re-export(再エクスポート)
export { greet, farewell } from './utils.js';
export { default as welcome } from './welcome.js';
// 条件付きエクスポート
export const config =
process.env.NODE_ENV === 'production'
? await import('./config.prod.js')
: await import('./config.dev.js');
// 複数ファイルからの統合エクスポート
export * from './userUtils.js';
export * from './dateUtils.js';
export * from './validationUtils.js';
読み込みタイミングと実行方式の差
CommonJS と ESM では、モジュールの読み込みと実行のタイミングが大きく異なります。
CommonJS の同期的読み込み
javascriptconsole.log('メインスクリプト開始');
// require は同期的に実行される
console.log('モジュール読み込み前');
const utils = require('./utils');
console.log('モジュール読み込み後');
// utils.js の内容がここで完全に利用可能
console.log(utils.greet('田中'));
console.log('メインスクリプト終了');
javascript// utils.js の実行順序
console.log('utils.js の実行開始');
function greet(name) {
console.log(`greet関数が ${name} で呼ばれました`);
return `こんにちは、${name}さん!`;
}
console.log('utils.js の実行完了');
module.exports = { greet };
ESM の非同期的読み込み
javascriptconsole.log('メインスクリプト開始');
// import は静的にホイスティングされる
import { greet } from './utils.js';
console.log('インポート文の後');
console.log(greet('田中'));
console.log('メインスクリプト終了');
実行タイミングの詳細比較
# | 項目 | CommonJS | ESM |
---|---|---|---|
1 | 読み込みタイミング | 実行時(同期) | パース時(静的解析) |
2 | ホイスティング | なし | インポート文が先頭に移動 |
3 | 条件付き読み込み | 簡単(if 文内で require 可能) | 動的 import が必要 |
4 | キャッシュ | モジュールオブジェクト | バインディング |
5 | 循環参照 | 部分的に可能 | より安全に処理 |
循環参照の処理
javascript// CommonJS での循環参照(問題が発生しやすい)
// a.js
console.log('a.js 開始');
const b = require('./b');
console.log('a.js での b:', b);
function funcA() {
return 'Function A';
}
module.exports = { funcA };
console.log('a.js 終了');
// b.js
console.log('b.js 開始');
const a = require('./a'); // この時点で a は空のオブジェクト
console.log('b.js での a:', a); // {}
function funcB() {
return 'Function B';
}
module.exports = { funcB };
console.log('b.js 終了');
javascript// ESM での循環参照(より安全)
// a.js
console.log('a.js 開始');
import { funcB } from './b.js';
console.log('a.js での funcB:', funcB);
export function funcA() {
return 'Function A';
}
console.log('a.js 終了');
// b.js
console.log('b.js 開始');
import { funcA } from './a.js'; // 安全に参照可能
console.log('b.js での funcA:', funcA);
export function funcB() {
return 'Function B';
}
console.log('b.js 終了');
実際のコード例と使い分け
理論的な違いを理解した上で、実際のプロジェクトでどのように使い分けるかを見てみましょう。
CommonJS での実装パターン
Express.js ベースの REST API
javascript// server.js - メインサーバーファイル
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const userRoutes = require('./routes/users');
const productRoutes = require('./routes/products');
const authMiddleware = require('./middleware/auth');
const errorHandler = require('./middleware/errorHandler');
const logger = require('./utils/logger');
const app = express();
const PORT = process.env.PORT || 3000;
// ミドルウェアの設定
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// レート制限
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 100, // 最大100リクエスト
});
app.use(limiter);
// ルーティング
app.use('/api/users', authMiddleware, userRoutes);
app.use('/api/products', productRoutes);
// エラーハンドリング
app.use(errorHandler);
app.listen(PORT, () => {
logger.info(`サーバーがポート ${PORT} で起動しました`);
});
module.exports = app;
javascript// utils/database.js - データベース接続管理
const mongoose = require('mongoose');
const logger = require('./logger');
class DatabaseManager {
constructor() {
this.connection = null;
}
async connect() {
try {
const connectionString =
process.env.MONGODB_URI ||
'mongodb://localhost:27017/myapp';
this.connection = await mongoose.connect(
connectionString,
{
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
}
);
logger.info('データベース接続が確立されました');
return this.connection;
} catch (error) {
logger.error('データベース接続エラー:', error);
throw error;
}
}
async disconnect() {
if (this.connection) {
await mongoose.disconnect();
logger.info('データベース接続を切断しました');
}
}
getConnection() {
return this.connection;
}
}
// シングルトンパターンで実装
const dbManager = new DatabaseManager();
module.exports = dbManager;
設定管理システム
javascript// config/index.js - 環境別設定管理
const path = require('path');
// 環境変数の読み込み
require('dotenv').config({
path: path.join(
__dirname,
`../.env.${process.env.NODE_ENV || 'development'}`
),
});
const config = {
app: {
name: process.env.APP_NAME || 'MyApp',
port: parseInt(process.env.PORT) || 3000,
env: process.env.NODE_ENV || 'development',
},
database: {
uri:
process.env.MONGODB_URI ||
'mongodb://localhost:27017/myapp',
options: {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10,
},
},
auth: {
jwtSecret: process.env.JWT_SECRET || 'fallback-secret',
jwtExpiry: process.env.JWT_EXPIRY || '24h',
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS) || 10,
},
email: {
service: process.env.EMAIL_SERVICE || 'gmail',
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
};
// 環境別の設定をマージ
if (config.app.env === 'production') {
const productionConfig = require('./production');
Object.assign(config, productionConfig);
} else if (config.app.env === 'test') {
const testConfig = require('./test');
Object.assign(config, testConfig);
}
module.exports = config;
ESM での実装パターン
モダンな Web アプリケーション
javascript// src/app.js - ESM ベースのメインアプリケーション
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import userRoutes from './routes/users.js';
import productRoutes from './routes/products.js';
import authMiddleware from './middleware/auth.js';
import errorHandler from './middleware/errorHandler.js';
import logger from './utils/logger.js';
import { DatabaseManager } from './utils/database.js';
const app = express();
const PORT = process.env.PORT || 3000;
// データベース接続
const dbManager = new DatabaseManager();
await dbManager.connect();
// ミドルウェアの設定
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// レート制限
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
});
app.use(limiter);
// ルーティング
app.use('/api/users', authMiddleware, userRoutes);
app.use('/api/products', productRoutes);
// エラーハンドリング
app.use(errorHandler);
app.listen(PORT, () => {
logger.info(`サーバーがポート ${PORT} で起動しました`);
});
export default app;
モジュラーサービス設計
javascript// services/UserService.js - ユーザー関連ビジネスロジック
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { User } from '../models/User.js';
import {
ValidationError,
AuthenticationError,
} from '../utils/errors.js';
import logger from '../utils/logger.js';
export class UserService {
constructor(config) {
this.jwtSecret = config.auth.jwtSecret;
this.jwtExpiry = config.auth.jwtExpiry;
this.bcryptRounds = config.auth.bcryptRounds;
}
async createUser(userData) {
try {
const { email, password, name } = userData;
// 既存ユーザーチェック
const existingUser = await User.findOne({ email });
if (existingUser) {
throw new ValidationError(
'このメールアドレスは既に使用されています'
);
}
// パスワードハッシュ化
const hashedPassword = await bcrypt.hash(
password,
this.bcryptRounds
);
// ユーザー作成
const user = new User({
email,
password: hashedPassword,
name,
createdAt: new Date(),
});
await user.save();
logger.info(`新規ユーザー作成: ${email}`);
// パスワードを除いて返却
const { password: _, ...userResponse } =
user.toObject();
return userResponse;
} catch (error) {
logger.error('ユーザー作成エラー:', error);
throw error;
}
}
async authenticateUser(email, password) {
try {
const user = await User.findOne({ email });
if (!user) {
throw new AuthenticationError('認証に失敗しました');
}
const isValidPassword = await bcrypt.compare(
password,
user.password
);
if (!isValidPassword) {
throw new AuthenticationError('認証に失敗しました');
}
// JWT トークン生成
const token = jwt.sign(
{ userId: user._id, email: user.email },
this.jwtSecret,
{ expiresIn: this.jwtExpiry }
);
logger.info(`ユーザーログイン: ${email}`);
return {
token,
user: {
id: user._id,
email: user.email,
name: user.name,
},
};
} catch (error) {
logger.error('認証エラー:', error);
throw error;
}
}
async getUserProfile(userId) {
try {
const user = await User.findById(userId).select(
'-password'
);
if (!user) {
throw new ValidationError(
'ユーザーが見つかりません'
);
}
return user;
} catch (error) {
logger.error(
'ユーザープロフィール取得エラー:',
error
);
throw error;
}
}
}
// デフォルトエクスポート
export default UserService;
設定とファクトリーパターン
javascript// config/ConfigManager.js - ESM での設定管理
import { readFile } from 'fs/promises';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export class ConfigManager {
constructor() {
this.config = null;
}
async loadConfig(environment = 'development') {
try {
// 基本設定の読み込み
const baseConfigPath = join(__dirname, 'base.json');
const baseConfig = JSON.parse(
await readFile(baseConfigPath, 'utf8')
);
// 環境別設定の読み込み
const envConfigPath = join(
__dirname,
`${environment}.json`
);
let envConfig = {};
try {
envConfig = JSON.parse(
await readFile(envConfigPath, 'utf8')
);
} catch (error) {
console.warn(
`環境設定ファイルが見つかりません: ${envConfigPath}`
);
}
// 設定のマージ
this.config = {
...baseConfig,
...envConfig,
app: {
...baseConfig.app,
...envConfig.app,
environment,
startTime: new Date().toISOString(),
},
};
return this.config;
} catch (error) {
console.error('設定読み込みエラー:', error);
throw error;
}
}
getConfig() {
if (!this.config) {
throw new Error(
'設定が読み込まれていません。loadConfig()を先に実行してください。'
);
}
return this.config;
}
get(path) {
const keys = path.split('.');
let value = this.getConfig();
for (const key of keys) {
value = value?.[key];
if (value === undefined) break;
}
return value;
}
}
// シングルトンインスタンス
export const configManager = new ConfigManager();
// 便利な関数もエクスポート
export async function initializeConfig(environment) {
return await configManager.loadConfig(environment);
}
export function getConfig(path) {
return configManager.get(path);
}
相互運用の方法
現実のプロジェクトでは、CommonJS と ESM の混在環境が発生することがよくあります。
ESM から CommonJS モジュールを使用
javascript// ESM プロジェクトで CommonJS パッケージを使用
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
// CommonJS パッケージの読み込み
const oldLibrary = require('old-commonjs-library');
// 動的インポートでの CommonJS 読み込み
const { default: express } = await import('express');
const app = express();
// 条件付きの動的インポート
let processor;
if (process.env.USE_NEW_PROCESSOR === 'true') {
const { NewProcessor } = await import(
'./processors/new.js'
);
processor = new NewProcessor();
} else {
const OldProcessor = require('./processors/old');
processor = new OldProcessor();
}
CommonJS プロジェクトで ESM を使用
javascript// CommonJS プロジェクトで ESM モジュールを使用
async function loadESMModule() {
try {
// 動的インポートを使用
const { someFunction } = await import(
'./esm-module.js'
);
return someFunction;
} catch (error) {
console.error('ESM モジュール読み込みエラー:', error);
throw error;
}
}
// 使用例
async function main() {
const esmFunction = await loadESMModule();
const result = esmFunction('test data');
console.log(result);
}
main().catch(console.error);
package.json での設定管理
json{
"name": "hybrid-project",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": "./src/index.js",
"require": "./dist/index.cjs"
},
"./utils": {
"import": "./src/utils/index.js",
"require": "./dist/utils.cjs"
}
},
"scripts": {
"build": "yarn build:esm && yarn build:cjs",
"build:esm": "tsc",
"build:cjs": "tsc --module commonjs --outDir dist-cjs"
}
}
まとめ
Node.js におけるモジュールシステムの選択は、プロジェクトの将来性と開発効率に大きく影響する重要な決定です。CommonJS と ESM それぞれに明確な特徴と適用場面があることを理解していただけたでしょうか。
この記事で解説した内容をまとめると、以下のポイントが重要です:
モジュールシステムの本質的価値
- コードの分割と再利用による保守性の向上
- 明示的な依存関係管理による品質向上
- スコープ分離による安全性の確保
CommonJS の特徴と適用場面
- 動的な読み込みが可能で柔軟性が高い
- 既存の Node.js エコシステムとの互換性が優秀
- サーバーサイド特化のプロジェクトに最適
ESM の特徴と将来性
- 言語標準として長期的にサポート
- ブラウザとの互換性によるコード共有が可能
- 静的解析による最適化とツールサポートが優秀
実践的な選択指針
- 新規プロジェクト:ESM を推奨
- 既存 CommonJS プロジェクト:段階的な移行を検討
- ライブラリ開発:両方式に対応することを推奨
- チーム開発:統一されたモジュールシステムの採用
現在の Web 開発トレンドを考慮すると、ESM が将来の主流となることは間違いありません。しかし、CommonJS も当面は重要な役割を果たし続けるでしょう。重要なのは、それぞれの特徴を理解し、プロジェクトの要件に応じて適切な選択をすることです。
モジュールシステムの理解を深めることで、より構造化された、保守しやすい Node.js アプリケーションの開発が可能になります。この知識を活用して、効率的で未来指向の開発を進めていきましょう。
関連リンク
- review
チーム開発が劇的に変わった!『リーダブルコード』Dustin Boswell & Trevor Foucher
- review
アジャイル初心者でも大丈夫!『アジャイルサムライ − 達人開発者への道』Jonathan Rasmusson
- review
人生が作品になる!『自分の中に毒を持て』岡本太郎
- review
体調不良の 99%が解決!『眠れなくなるほど面白い 図解 自律神経の話』小林弘幸著で学ぶ、現代人必須の自律神経コントロール術と人生を変える健康革命
- review
衝撃の事実!『睡眠こそ最強の解決策である』マシュー・ウォーカー著が明かす、99%の人が知らない睡眠の驚くべき真実と人生を変える科学的メカニズム
- review
人生が激変!『嫌われる勇気』岸見一郎・古賀史健著から学ぶ、アドラー心理学で手に入れる真の幸福と自己実現