はじめに#
Node.jsでモダンなJavaScriptプロジェクトを構築する際、**ES Modules(ESM)**の理解は欠かせません。ES Modulesは、ECMAScript標準として策定されたモジュールシステムであり、ブラウザとサーバーサイドの両方でネイティブにサポートされています。
本記事では、ES Modulesの各種機能を深掘りし、実践的なNode.jsプロジェクトを構築するための知識を提供します。
- 名前付きエクスポートとデフォルトエクスポートの使い分け
import.metaオブジェクトの全プロパティ(url、dirname、filename、resolve)
- 動的
import()の活用パターン
- Top-level awaitによるモジュール初期化
- ESMプロジェクトのベストプラクティス
この記事を読み終えると、ES Modulesを使用したモダンなNode.jsプロジェクトを自信を持って構築できるようになります。
実行環境#
本記事のコードは以下の環境で動作確認しています。
| 項目 |
バージョン |
| Node.js |
20.x LTS以上 |
| npm |
10.x以上 |
| OS |
Windows/macOS/Linux |
前提条件#
- JavaScriptの基礎知識(変数、関数、クラス、async/await)
- Node.jsプロジェクトの初期化経験(
npm init)
- CommonJSとES Modulesの違いの基本的な理解
ES ModulesとCommonJSの違いについては、Node.jsモジュール入門 - CommonJSとES Modulesの違いを理解するを参照してください。
export(エクスポート)の詳細#
ES Modulesでは、exportキーワードを使ってモジュールから値を公開します。エクスポートには「名前付きエクスポート」と「デフォルトエクスポート」の2種類があり、それぞれ適切な場面で使い分けることが重要です。
名前付きエクスポート(Named Export)#
名前付きエクスポートは、複数の値を個別の名前で公開する方法です。ユーティリティ関数や定数を複数公開する場合に適しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// utils/math.js
// 方法1: 宣言時にexportを付ける
export const PI = 3.14159265359;
export const E = 2.71828182845;
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
export function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
|
ファイル末尾でまとめてエクスポートすることも可能です。
1
2
3
4
5
6
7
8
9
|
// utils/string.js
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const lowercase = (str) => str.toLowerCase();
const uppercase = (str) => str.toUpperCase();
const trim = (str) => str.trim();
// 方法2: まとめてエクスポート
export { capitalize, lowercase, uppercase, trim };
|
エクスポート時のリネーム#
asキーワードを使用して、エクスポート時に別名を付けることができます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// utils/validators.js
function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
function validatePhone(phone) {
const regex = /^\d{10,11}$/;
return regex.test(phone);
}
// 別名でエクスポート
export {
validateEmail as isValidEmail,
validatePhone as isValidPhone,
};
|
デフォルトエクスポート(Default Export)#
デフォルトエクスポートは、モジュールのメイン機能を1つだけ公開する方法です。1つのモジュールにつき、デフォルトエクスポートは1つだけ定義できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
// services/UserService.js
class UserService {
constructor(apiClient) {
this.apiClient = apiClient;
}
async findById(id) {
return await this.apiClient.get(`/users/${id}`);
}
async findAll() {
return await this.apiClient.get('/users');
}
async create(userData) {
return await this.apiClient.post('/users', userData);
}
async update(id, userData) {
return await this.apiClient.put(`/users/${id}`, userData);
}
async delete(id) {
return await this.apiClient.delete(`/users/${id}`);
}
}
// デフォルトエクスポート
export default UserService;
|
関数やオブジェクトも直接デフォルトエクスポートできます。
1
2
3
4
5
6
7
8
9
10
|
// config/database.js
export default {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
database: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || '',
ssl: process.env.NODE_ENV === 'production',
};
|
名前付きエクスポートとデフォルトエクスポートの併用#
1つのモジュールで両方のエクスポート方式を併用することも可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
// http/client.js
// デフォルトエクスポート: メインのクライアントクラス
export default class HttpClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async get(path) {
const response = await fetch(`${this.baseURL}${path}`);
return response.json();
}
async post(path, data) {
const response = await fetch(`${this.baseURL}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return response.json();
}
}
// 名前付きエクスポート: ヘルパー関数
export function createHeaders(token) {
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
};
}
export function handleError(error) {
console.error('HTTP Error:', error.message);
throw error;
}
// 名前付きエクスポート: 定数
export const HTTP_STATUS = {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
};
|
再エクスポート(Re-export)#
複数のモジュールから値を集約して、単一のエントリーポイントから公開することができます。
1
2
3
4
5
6
7
8
9
|
// utils/index.js
// 他のモジュールから再エクスポート
export { add, subtract, multiply, divide, PI, E } from './math.js';
export { capitalize, lowercase, uppercase, trim } from './string.js';
export { isValidEmail, isValidPhone } from './validators.js';
// デフォルトエクスポートを名前付きで再エクスポート
export { default as DateFormatter } from './DateFormatter.js';
|
import(インポート)の詳細#
import文を使用して、他のモジュールからエクスポートされた値を読み込みます。
名前付きインポート#
名前付きエクスポートされた値をインポートする場合は、波括弧{}を使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// main.js
// 個別にインポート
import { add, subtract, PI } from './utils/math.js';
console.log(add(10, 5)); // 15
console.log(subtract(10, 5)); // 5
console.log(PI); // 3.14159265359
// インポート時にリネーム
import { isValidEmail as checkEmail } from './utils/validators.js';
console.log(checkEmail('test@example.com')); // true
|
デフォルトインポート#
デフォルトエクスポートされた値は、任意の名前でインポートできます。
1
2
3
4
5
6
7
|
// app.js
import UserService from './services/UserService.js';
import dbConfig from './config/database.js';
const userService = new UserService(apiClient);
console.log(dbConfig.host); // localhost
|
名前空間インポート#
モジュール全体を1つのオブジェクトとしてインポートすることができます。
1
2
3
4
5
6
7
|
// analytics.js
import * as MathUtils from './utils/math.js';
console.log(MathUtils.add(1, 2)); // 3
console.log(MathUtils.PI); // 3.14159265359
console.log(MathUtils.multiply(3, 4)); // 12
|
混合インポート#
デフォルトインポートと名前付きインポートを同時に行うことができます。
1
2
3
4
5
6
7
8
|
// api-client.js
import HttpClient, { createHeaders, HTTP_STATUS } from './http/client.js';
const client = new HttpClient('https://api.example.com');
const headers = createHeaders('my-token');
console.log(HTTP_STATUS.OK); // 200
|
Node.js組み込みモジュールのインポート#
Node.jsの組み込みモジュールは、node:プレフィックスを付けてインポートすることが推奨されています。
1
2
3
4
5
6
7
8
9
10
|
// file-utils.js
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { EventEmitter } from 'node:events';
// fsモジュールの使用例
const content = await fs.readFile('data.txt', 'utf-8');
console.log(content);
|
node:プレフィックスを使用することで、組み込みモジュールとサードパーティパッケージを明確に区別できます。
import.metaは、ES Modulesでのみ使用可能なメタプロパティオブジェクトです。現在のモジュールに関する情報を提供します。
import.meta.urlは、現在のモジュールファイルの絶対URLを文字列で返します。
1
2
3
4
5
6
7
8
9
10
11
12
|
// src/config/loader.js
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
console.log(import.meta.url);
// 出力例: file:///C:/projects/myapp/src/config/loader.js
// URLオブジェクトを使用した相対パス解決
const configPath = new URL('./settings.json', import.meta.url);
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
console.log(config);
|
import.meta.dirnameは、現在のモジュールが存在するディレクトリの絶対パスを返します。CommonJSの__dirnameに相当します。
1
2
3
4
5
6
7
8
9
10
11
12
|
// src/utils/paths.js
console.log(import.meta.dirname);
// 出力例: C:\projects\myapp\src\utils
// または: /home/user/projects/myapp/src/utils
// ディレクトリパスを使用した操作
import path from 'node:path';
const dataDir = path.join(import.meta.dirname, '..', 'data');
console.log(dataDir);
// 出力例: C:\projects\myapp\src\data
|
import.meta.filenameは、現在のモジュールファイルの絶対パスを返します。CommonJSの__filenameに相当します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// src/logger/index.js
console.log(import.meta.filename);
// 出力例: C:\projects\myapp\src\logger\index.js
// または: /home/user/projects/myapp/src/logger/index.js
import path from 'node:path';
// ファイル名のみを取得
const fileName = path.basename(import.meta.filename);
console.log(fileName); // index.js
// 拡張子を除いたファイル名
const baseName = path.basename(import.meta.filename, '.js');
console.log(baseName); // index
|
Node.js 20.11.0より前のバージョンでの代替手法#
import.meta.dirnameとimport.meta.filenameがサポートされていない古いバージョンでは、import.meta.urlから導出します。
1
2
3
4
5
6
7
8
9
|
// 互換性のある実装
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
console.log(__filename); // /path/to/current/file.js
console.log(__dirname); // /path/to/current
|
import.meta.resolve()は、モジュール指定子を絶対URLに解決します。require.resolve()のES Module版です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// src/plugins/loader.js
// 相対パスの解決
const utilsPath = import.meta.resolve('./utils.js');
console.log(utilsPath);
// 出力例: file:///C:/projects/myapp/src/plugins/utils.js
// パッケージの解決
const lodashPath = import.meta.resolve('lodash');
console.log(lodashPath);
// 出力例: file:///C:/projects/myapp/node_modules/lodash/lodash.js
// サブパスの解決
const lodashGetPath = import.meta.resolve('lodash/get.js');
console.log(lodashGetPath);
// 出力例: file:///C:/projects/myapp/node_modules/lodash/get.js
|
import.meta.resolve()は同期的に動作し、実際のファイル読み込みは行いません。モジュールパスの事前検証やアセットパスの解決に便利です。
1
2
3
4
5
6
7
8
9
|
// アセットパスの解決例
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// パッケージ内のアセットファイルのパスを取得
const cssPath = import.meta.resolve('bootstrap/dist/css/bootstrap.min.css');
const cssFilePath = fileURLToPath(cssPath);
console.log('Bootstrap CSS path:', cssFilePath);
|
動的import()#
動的import()は、実行時にモジュールを非同期で読み込む機能です。静的なimport文とは異なり、条件分岐やループ内でも使用できます。
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
|
// main.js
async function loadModule() {
// 動的にモジュールをインポート
const { add, multiply } = await import('./utils/math.js');
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
}
loadModule();
|
条件付きインポート#
実行環境や条件に応じて、異なるモジュールを読み込むことができます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// logger.js
async function createLogger() {
const env = process.env.NODE_ENV;
if (env === 'production') {
// 本番環境用の高機能ロガー
const { default: ProductionLogger } = await import('./loggers/production.js');
return new ProductionLogger();
} else {
// 開発環境用のシンプルなロガー
const { default: DevelopmentLogger } = await import('./loggers/development.js');
return new DevelopmentLogger();
}
}
const logger = await createLogger();
logger.info('Application started');
|
プラグインシステムの実装#
動的import()を活用して、プラグインシステムを実装できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
// plugins/loader.js
import fs from 'node:fs/promises';
import path from 'node:path';
class PluginLoader {
constructor(pluginDir) {
this.pluginDir = pluginDir;
this.plugins = new Map();
}
async loadAll() {
const files = await fs.readdir(this.pluginDir);
const jsFiles = files.filter(f => f.endsWith('.js'));
for (const file of jsFiles) {
const pluginPath = path.join(this.pluginDir, file);
await this.load(pluginPath);
}
return this.plugins;
}
async load(pluginPath) {
try {
// 動的インポートでプラグインを読み込み
const module = await import(`file://${pluginPath}`);
const plugin = module.default || module;
if (plugin.name && typeof plugin.init === 'function') {
this.plugins.set(plugin.name, plugin);
await plugin.init();
console.log(`Plugin loaded: ${plugin.name}`);
}
} catch (error) {
console.error(`Failed to load plugin: ${pluginPath}`, error);
}
}
get(name) {
return this.plugins.get(name);
}
}
export default PluginLoader;
|
遅延読み込み(Lazy Loading)#
必要になるまでモジュールの読み込みを遅延させることで、初期起動時間を短縮できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// services/analytics.js
let analyticsModule = null;
export async function trackEvent(eventName, eventData) {
// 初回呼び出し時のみモジュールを読み込む
if (!analyticsModule) {
analyticsModule = await import('./heavy-analytics-sdk.js');
await analyticsModule.initialize();
}
analyticsModule.track(eventName, eventData);
}
export async function trackPageView(pagePath) {
if (!analyticsModule) {
analyticsModule = await import('./heavy-analytics-sdk.js');
await analyticsModule.initialize();
}
analyticsModule.pageView(pagePath);
}
|
JSONのインポート#
ES Modulesでは、JSONファイルをインポートする際にimport attributes(with構文)が必要です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// config/loader.js
// 静的インポート(import attributes使用)
import packageJson from '../package.json' with { type: 'json' };
console.log(packageJson.name);
console.log(packageJson.version);
// 動的インポートでのJSON読み込み
async function loadConfig(configName) {
const config = await import(`./configs/${configName}.json`, {
with: { type: 'json' }
});
return config.default;
}
const dbConfig = await loadConfig('database');
console.log(dbConfig);
|
Top-level await#
Top-level awaitは、ES Modulesのトップレベル(関数の外)でawaitを使用できる機能です。モジュールの初期化時に非同期処理を実行する場合に便利です。
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// config/database.js
import { createConnection } from './db-client.js';
// Top-level awaitでデータベース接続を初期化
const connection = await createConnection({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
});
console.log('Database connected');
export default connection;
|
1
2
3
4
5
6
7
8
|
// main.js
// database.jsがインポートされた時点で、接続が確立されている
import db from './config/database.js';
// すぐにデータベース操作が可能
const users = await db.query('SELECT * FROM users');
console.log(users);
|
設定ファイルの非同期読み込み#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// config/index.js
import fs from 'node:fs/promises';
import path from 'node:path';
const configPath = path.join(import.meta.dirname, 'app-config.json');
const configContent = await fs.readFile(configPath, 'utf-8');
const config = JSON.parse(configContent);
// 環境変数でオーバーライド
config.port = parseInt(process.env.PORT) || config.port;
config.logLevel = process.env.LOG_LEVEL || config.logLevel;
export default Object.freeze(config);
|
APIからの初期設定取得#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// config/remote.js
async function fetchRemoteConfig(endpoint) {
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`Failed to fetch config: ${response.status}`);
}
return response.json();
}
// サーバー起動時にリモート設定を取得
const remoteConfig = await fetchRemoteConfig(
process.env.CONFIG_ENDPOINT || 'https://config.example.com/api/settings'
);
export const featureFlags = remoteConfig.features;
export const apiEndpoints = remoteConfig.endpoints;
export default remoteConfig;
|
Top-level awaitの注意点#
Top-level awaitを使用する際は、以下の点に注意が必要です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 注意点1: 解決されないPromiseはプロセスを停止させる
// 悪い例: 永遠に解決されないPromise
// await new Promise(() => {}); // プロセスがexit code 13で終了
// 良い例: タイムアウトを設定する
const timeout = (ms) => new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
const config = await Promise.race([
fetchConfig(),
timeout(5000)
]).catch(error => {
console.error('Config fetch failed, using defaults:', error.message);
return defaultConfig;
});
export default config;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 注意点2: エラーハンドリング
// 悪い例: エラーが握り潰される可能性
const data = await riskyOperation();
// 良い例: 明示的なエラーハンドリング
let data;
try {
data = await riskyOperation();
} catch (error) {
console.error('Failed to initialize:', error);
process.exit(1);
}
export default data;
|
1
2
3
4
5
6
7
8
9
10
11
|
// 注意点3: インポートチェーンへの影響
// Top-level awaitを持つモジュールをインポートすると、
// そのawaitが完了するまでインポート元も待機する
// database.js - 接続に3秒かかる場合
export const db = await connect(); // 3秒待機
// app.js - database.jsのインポートで3秒待機
import { db } from './database.js'; // この時点で3秒待機
console.log('App started'); // database.jsの初期化完了後に実行
|
ESMプロジェクトのベストプラクティス#
package.jsonの設定#
ESMプロジェクトでは、package.jsonに"type": "module"を設定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
{
"name": "my-esm-project",
"version": "1.0.0",
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"exports": {
".": {
"import": "./src/index.js",
"types": "./src/index.d.ts"
},
"./utils": {
"import": "./src/utils/index.js",
"types": "./src/utils/index.d.ts"
}
},
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js",
"test": "node --test"
}
}
|
ディレクトリ構造#
推奨されるESMプロジェクトのディレクトリ構造を示します。
graph TD
A[my-esm-project/] --> B[src/]
A --> C[tests/]
A --> D[package.json]
B --> E[index.js]
B --> F[config/]
B --> G[services/]
B --> H[utils/]
B --> I[models/]
F --> J[index.js]
F --> K[database.js]
G --> L[index.js]
G --> M[UserService.js]
H --> N[index.js]
H --> O[math.js]
H --> P[string.js]
C --> Q[unit/]
C --> R[integration/]インデックスファイルによるバレルパターン#
関連するモジュールをindex.jsで集約し、インポートを簡潔にします。
1
2
3
4
5
6
7
8
9
10
11
|
// src/utils/index.js
// 各ユーティリティから再エクスポート
export * from './math.js';
export * from './string.js';
export * from './date.js';
export * from './validators.js';
// デフォルトエクスポートは名前付きで再エクスポート
export { default as Logger } from './Logger.js';
export { default as Cache } from './Cache.js';
|
1
2
3
4
5
6
7
|
// src/app.js
// 集約されたインポート
import { add, capitalize, formatDate, Logger } from './utils/index.js';
const logger = new Logger();
logger.info(`Result: ${add(1, 2)}`);
|
ファイル拡張子の明示#
ES Modulesでは、相対パスでインポートする際にファイル拡張子を必ず指定します。
1
2
3
4
5
6
7
|
// 良い例: 拡張子を明示
import { add } from './utils/math.js';
import config from './config/index.js';
// 悪い例: 拡張子を省略(エラーになる)
// import { add } from './utils/math';
// import config from './config';
|
環境変数の管理#
ESMプロジェクトでの環境変数管理パターンを示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// src/config/env.js
import { config } from 'dotenv';
// .envファイルを読み込み
config();
// 必須環境変数のバリデーション
const requiredEnvVars = ['DATABASE_URL', 'API_KEY', 'JWT_SECRET'];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
}
// 型安全な環境変数オブジェクト
export const env = Object.freeze({
nodeEnv: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT) || 3000,
databaseUrl: process.env.DATABASE_URL,
apiKey: process.env.API_KEY,
jwtSecret: process.env.JWT_SECRET,
logLevel: process.env.LOG_LEVEL || 'info',
isDevelopment: process.env.NODE_ENV === 'development',
isProduction: process.env.NODE_ENV === 'production',
});
|
エラーハンドリングのパターン#
ESMプロジェクトでの統一的なエラーハンドリングパターンを示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// src/errors/AppError.js
export class AppError extends Error {
constructor(message, statusCode = 500, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
export class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
}
}
export class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// src/middleware/errorHandler.js
import { AppError } from '../errors/AppError.js';
export function errorHandler(error, req, res, next) {
if (error instanceof AppError) {
return res.status(error.statusCode).json({
status: 'error',
message: error.message,
});
}
// 予期しないエラー
console.error('Unexpected error:', error);
return res.status(500).json({
status: 'error',
message: 'Internal server error',
});
}
|
モジュールの依存関係図#
大規模なESMプロジェクトでは、モジュール間の依存関係を明確に設計することが重要です。
graph TB
subgraph "Entry Points"
A[src/index.js]
end
subgraph "Application Layer"
B[src/app.js]
C[src/routes/]
end
subgraph "Service Layer"
D[src/services/UserService.js]
E[src/services/OrderService.js]
end
subgraph "Data Access Layer"
F[src/repositories/UserRepository.js]
G[src/repositories/OrderRepository.js]
end
subgraph "Infrastructure"
H[src/config/database.js]
I[src/utils/]
end
A --> B
B --> C
C --> D
C --> E
D --> F
E --> G
F --> H
G --> H
D --> I
E --> IESMとCommonJSの相互運用#
ESMプロジェクトでCommonJSモジュールを使用する場合の注意点を説明します。
CommonJSモジュールのインポート#
1
2
3
4
5
6
7
8
9
|
// ESMからCommonJSモジュールをインポート
// デフォルトインポート(推奨)
import lodash from 'lodash';
console.log(lodash.chunk([1, 2, 3, 4], 2));
// 名前付きインポート(一部のパッケージでサポート)
import { chunk, groupBy } from 'lodash';
console.log(chunk([1, 2, 3, 4], 2));
|
createRequireを使用したrequireの再現#
どうしてもrequireが必要な場合は、module.createRequire()を使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// src/legacy-support.js
import { createRequire } from 'node:module';
// ESM内でrequire関数を作成
const require = createRequire(import.meta.url);
// CommonJSモジュールをrequireでインポート
const legacyModule = require('./legacy-cjs-module.cjs');
// JSON読み込み(import attributesの代替)
const packageJson = require('../package.json');
console.log(packageJson.version);
|
まとめ#
本記事では、Node.jsのES Modulesについて詳しく解説しました。
ES Modulesの主要な機能を振り返ります。
| 機能 |
説明 |
ユースケース |
| 名前付きエクスポート |
複数の値を個別の名前で公開 |
ユーティリティ関数、定数 |
| デフォルトエクスポート |
モジュールのメイン機能を1つ公開 |
クラス、メイン関数 |
import.meta.url |
モジュールの絶対URL |
相対パス解決 |
import.meta.dirname |
ディレクトリパス |
ファイル操作 |
import.meta.filename |
ファイルパス |
ログ、デバッグ |
import.meta.resolve() |
モジュールパス解決 |
パッケージ内アセット取得 |
動的import() |
実行時のモジュール読み込み |
条件付きインポート、プラグイン |
| Top-level await |
トップレベルでの非同期処理 |
設定の初期化、DB接続 |
ES Modulesを使用することで、よりモダンで保守性の高いNode.jsプロジェクトを構築できます。静的解析ツールやバンドラーとの相性も良く、長期的なプロジェクトの品質向上に貢献します。
参考リンク#