ESモジュール vs CommonJS 共存方法など

これも結構ややこしいので纏めました。

ESモジュールとCommonJS

ESモジュールは最新の標準であり、今後のJavaScript開発において主流となる方向性にあります。CommonJSは依然として多くのプロジェクトで使用されていますが、ESMへの移行が推奨されています。 しかし、古いライブラリなどはESモジュールに対応していない場合もある。

ESモジュール(ESM)の概要 —import

  • 正式名称: ECMAScript Modules(ESM)
  • 導入時期: ECMAScript 2015(ES6)で標準化、Node.js 14より使用可能。
  • 目的: JavaScriptにおける公式なモジュールシステムの提供。より構造化されたコード管理と再利用性の向上を目指しています。
  • node.jsの場合、package.jsonに”type”:”module”を設定することで、デフォルトで.jsファイルがESモジュールとして扱われます。 設定しない場合、.cjsはCommonJS、mjsはESモジュールとして認識されます。
  • ブラウザの場合、<script type="module">を指定してimportを使う。

ESモジュール(ESM)の特徴

  • 静的解析が可能: インポート/エクスポートがファイルの先頭で行われるため、ツールが依存関係を容易に解析・最適化できます。
  • ネイティブサポート: 現代のブラウザやNode.jsがネイティブでサポートしており、追加のトランスパイラなしで使用可能です。
  • ツールチェーンとの互換性: WebpackやRollupなどのモジュールバンドラもESMを前提に設計されています。

対比: CommonJS(CJS)—require

  • 歴史的背景: Node.jsがモジュールシステムを導入する際に採用された形式です。
  • 動的インポート: require()は動的にモジュールを読み込むため、静的解析が困難です。
  • 互換性: 多くの既存のNode.jsパッケージがCommonJS形式で提供されています。

require と import の違い、メリット・デメリット比較表

以下の表で、CommonJSのrequireとESMのimportの主な違い、およびそれぞれのメリット・デメリットを比較します。

項目CommonJS (require)ESモジュール (import)
導入時期Node.jsの初期から存在ECMAScript 2015(ES6)で標準化
シンタックスjavascript<br>const module = require('module');<br>module.exports = ...;<br>javascript<br>import module from 'module';<br>export default ...;<br>
モジュールの特性動的にモジュールを読み込む。実行時にrequireが呼び出される。静的な構文。コンパイル時にモジュールの依存関係が解析される。
非同期サポート同期的にモジュールを読み込む。ネイティブで非同期モジュールローディングをサポート(import() を使用)
ネイティブサポートNode.jsおよび一部のツールでサポートされているが、ブラウザでは基本的に使用不可。現代のブラウザや最新のNode.jsがネイティブでサポート。
ツールチェーン既存の多くのツールやライブラリがCommonJSに対応。モダンなビルドツールやバンドラがESMを前提に設計されている。
ホットリロード動的にモジュールを再読み込みするのが難しい。静的なインポートにより、より効率的なホットリロードが可能。
デフォルトエクスポートデフォルトエクスポートの概念がない。export default による明確なデフォルトエクスポートが可能。
互換性多くの既存プロジェクトやライブラリで広く使用されている。既存のCommonJSモジュールとの互換性に制限がある。
パフォーマンス同期的な読み込みのため、特にサーバーサイドではパフォーマンスが安定している。ネイティブサポートされた非同期読み込みにより、ブラウザ環境でのパフォーマンスが向上。
エラーハンドリングtry-catch を使用して動的インポート時のエラーを捕捉可能。import 文自体は静的であり、import() を使用することで非同期的なエラーハンドリングが可能。

メリット・デメリットのまとめ

形式メリットデメリット
CommonJS (require)– 広範な互換性と成熟度
– シンプルな同期的読み込み
– 既存の多くのパッケージが対応
– 静的解析が困難
– ブラウザでのネイティブサポートが限定的
– 名前付きエクスポートが不明確
ESモジュール (import)– 静的解析により最適化が容易
– ネイティブのブラウザサポート
– 名前付きエクスポートやデフォルトエクスポートが明確
– 一部の古いツールやライブラリとの互換性が低い
– CommonJSとの混在が複雑
– 非同期的な読み込みが必要な

requireとimport共存方法

基本はimportを使い、createRequireでrequireを作って、importに対応していないモジュールを取り込む。

ESモジュールベースの場合

requireを作ってESM対応していないライブラリを取り込みます。

// example.mjs
import { createRequire } from 'module';
const require = createRequire(import.meta.url);

// ESMで可能なモジュールのインポート
import fs from 'fs/promises';

// CommonJSモジュールのインポート
const express = require('express');

CommonJSベースの場合

動的importでCommonJS対応していないライブラリを取り込みます。

const { app, BrowserWindow,WebContentsView , ipcMain } = require('electron');
const path = require('path');

async function listRunningApplications() {
  try {
    // 動的インポートで ps-list を読み込む
    const psListModule = await import('ps-list');
        // ps-list のデフォルトエクスポートを取得
    const psList = psListModule.default || psListModule;
    const processes = await psList();
    
    // Extract unique process names
    const uniqueProcessNames = [
      ...new Set(processes.map((proc) => proc.name)),
    ];
    
    console.log('Currently Running Applications:');
    uniqueProcessNames.forEach((name, index) => {
      console.log(`${index + 1}. ${name}`);
    });
    
    // Optionally, send this data to the renderer process
    if (mainWindow) {
      mainWindow.webContents.send('process-list', uniqueProcessNames);
    }
  } catch (error) {
    console.error('Error fetching process list:', error);
  }
}

helperライブラリなどの記述方法

ES Moduleの場合

// helper.js
export function extractAddress(externalObj) {
    const externalStr = externalObj.toString(); // 例: "[External: 574400f46430]"
    const match = externalStr.match(/\\[External:\s+([0-9a-fA-F]+)\\]/);
    if (match && match[1]) {
        return `0x${match[1]}`;
    }
    throw new Error('アドレスの抽出に失敗しました。');
}

CommonJSの場合

// helper.js
function extractAddress(externalObj) {
    const externalStr = externalObj.toString(); // 例: "[External: 574400f46430]"
    const match = externalStr.match(/\\[External:\s+([0-9a-fA-F]+)\\]/);
    if (match && match[1]) {
        return `0x${match[1]}`;
    }
    throw new Error('アドレスの抽出に失敗しました。');
}

// 関数をエクスポートする(CommonJS形式)
module.exports = { extractAddress };

main.jsには (ES Module)

import { extractAddress } from './helper.js';

main.jsには (CommonJS)

const { extractAddress } = require('./helper');

その他 electronの場合

以下の様に配置されている時、main.jsをESMで使うには、同じディレクトリ下にあるsrc/package.jsonに『”type”:”module”』を書込む。

preloader.jsはElectron20からsandbox化されており、node.jsの全ての機能が使えません。 preloader.jsはipc通信する為だけに使うようになったようです。 なのでimportは使えずrequireしか使えません。

またelectronの場合、renderer.jsではnode.jsのパッケージを使用できないので、ipcMain通信を使ってmain.jsやreploader.jsで処理をしてもらって、その返事だけを受け取る方法を使います。 下の例ではpreloader.jsで『api』と言う名前でsendとreceiveメソッドを持ったオブジェクトを作って、renderer.jsが使えるように公開(expose)しています。 この例では、

  1. renderer.jsのwindow.api.sendで=>preloader.js
  2. preloader.jsの sendで受けて、”toMain”チャンネルで=>main.js
  3. main.jsのipcMain.onで受けて、event.sender.sendで=>preloader.js
  4. preloader.jsのreceiveで受けて、ipcRenderer.onで=>renderer.js
// preload.js
const { contextBridge, ipcRenderer } = require('electron');

// 安全なAPIを定義してレンダラーに公開
contextBridge.exposeInMainWorld('api', {
  send: (channel, data) => {
    // 許可されたチャンネルのみ通信を許可
    const validChannels = ['toMain'];
    if (validChannels.includes(channel)) {
      ipcRenderer.send(channel, data);
    }
  },
  receive: (channel, func) => {
    const validChannels = ['fromMain'];
    if (validChannels.includes(channel)) {
      // リスナー登録
      ipcRenderer.on(channel, (event, ...args) => func(...args));
    }
  }
});

main.js

app.whenReady().then(createMainWindow);

// IPCイベントのハンドリング例
ipcMain.on('toMain', (event, args) => {
  console.log('Received from renderer:', args);
  // 必要な処理を実行
  event.sender.send('fromMain', 'データを受け取りました');
});

renderer.js

// renderer.js

// 何らかのイベント(例: ボタンクリック)でメインプロセスにデータを送信
document.getElementById('sendButton').addEventListener('click', () => {
  window.api.send('toMain', 'こんにちは、メインプロセス!');
});

// メインプロセスからのメッセージを受信
window.api.receive('fromMain', (data) => {
  console.log('Received from main:', data);
});

index.html

// renderer.js

// 何らかのイベント(例: ボタンクリック)でメインプロセスにデータを送信
document.getElementById('sendButton').addEventListener('click', () => {
  window.api.send('toMain', 'こんにちは、メインプロセス!');
});

// メインプロセスからのメッセージを受信
window.api.receive('fromMain', (data) => {
  console.log('Received from main:', data);
});

コメント