JavaScriptのモジュールシステム入門|importとexportでコードを整理する方法


はじめに

前回の記事ではJavaScriptのエラーハンドリングとしてtry・catch・finallyの使い方からカスタムエラーの作り方・Promiseのエラー処理・ユーザーへのエラー表示パターンまで解説しました。今回はJavaScriptのモジュールシステムを解説します。「JavaScriptのコードが1つのファイルに集まって長くなってきた」「どこに何が書いてあるかわからなくなってきた」という悩みをモジュールシステムで解決できます。import と export を使ってコードをファイルに分割することで、管理しやすく・再利用しやすい構造が作れます。


1. モジュールとは何か

1.1 モジュールの概念

モジュールとは:

JavaScriptのコードをファイル単位に分割して
必要なものだけを読み込む仕組み。

モジュールを使わない場合の問題点:

❌ すべてのコードが1ファイルに集まる
   → 数百行・数千行になって管理しにくい

❌ 変数・関数がグローバルスコープに漏れる
   → 別のファイルの変数と名前が衝突する

❌ 読み込み順序に依存する
   → HTMLでscriptタグの順番を間違えるとエラー

❌ どこで何が定義されているかわからない
   → 関数を変更するとどこに影響するか追えない

✅ モジュールを使うメリット:

→ ファイルごとに責務が明確になる
→ 変数・関数のスコープがファイル内に閉じる
→ 必要なものだけimportして使える
→ 再利用しやすいコードが書ける
→ 依存関係が明確になる

1.2 ES Modulesとは

ES Modules(ESM):

ECMAScript 2015(ES6)で標準化された
JavaScriptの公式モジュールシステム。

→ import / export を使う
→ モダンブラウザはすべて対応済み
→ Node.js(v12以降)でも使える
→ Vite・webpack などのバンドラーとも相性がよい

以前は CommonJS(CJS)が主流だった:
→ require() / module.exports を使う
→ Node.jsで長く使われてきた書き方
→ 現在もWordPress・古いNode.jsプロジェクトで使われる

この記事ではES Modulesを解説する。

2. exportの使い方

2.1 named export(名前付きエクスポート)

// utils/math.js

// 関数をexportする
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 RangeError( '0で割ることはできません' );
    return a / b;
}

// 変数をexportする
export const PI      = 3.14159265358979;
export const E       = 2.71828182845904;
export const VERSION = '1.0.0';

// 定義してからまとめてexportすることもできる
function clamp( value, min, max ) {
    return Math.min( Math.max( value, min ), max );
}

function lerp( start, end, t ) {
    return start + ( end - start ) * t;
}

export { clamp, lerp };

// exportしながら名前を変えることもできる(as)
export { clamp as clampValue, lerp as linearInterpolate };

2.2 default export(デフォルトエクスポート)

// utils/formatter.js

// default export:ファイルにつき1つだけ
export default class Formatter {
    constructor( locale = 'ja-JP' ) {
        this.locale = locale;
    }

    currency( value, currency = 'JPY' ) {
        return new Intl.NumberFormat( this.locale, {
            style:    'currency',
            currency,
        } ).format( value );
    }

    date( date, options = {} ) {
        return new Intl.DateTimeFormat( this.locale, options )
            .format( new Date( date ) );
    }

    number( value ) {
        return new Intl.NumberFormat( this.locale ).format( value );
    }
}

// ============================================

// 関数をdefault exportする
// components/Button.js

export default function createButton( { label, onClick, variant = 'primary' } ) {
    const btn = document.createElement( 'button' );
    btn.className   = `btn btn--${ variant }`;
    btn.textContent = label;
    btn.addEventListener( 'click', onClick );
    return btn;
}

// ============================================

// オブジェクトをdefault exportする
// config/api.js

export default {
    baseUrl:  'https://api.example.com',
    version:  'v1',
    timeout:  10000,
    headers: {
        'Content-Type': 'application/json',
    },
};

2.3 named exportとdefault exportの使い分け

named export を使う場面:
→ 複数の関数・変数をエクスポートするとき
→ ユーティリティ関数集
→ 定数の集まり
→ 複数のクラスをまとめたファイル

例:
export function formatDate() { ... }
export function formatCurrency() { ... }
export const DATE_FORMAT = 'YYYY/MM/DD';

→ importするときに {} が必要
→ importする側が名前を指定する

default export を使う場面:
→ ファイルの主役が1つのとき
→ コンポーネント・クラス・設定オブジェクト

例:
export default class Modal { ... }
export default function App() { ... }
export default { baseUrl, apiKey };

→ importするときに {} が不要
→ importする側が名前を自由につけられる

3. importの使い方

3.1 named importの基本

// named export したものをimportする
// { } の中にエクスポート名を書く

import { add, subtract, PI } from './utils/math.js';

console.log( add( 3, 4 ) );  // 7
console.log( PI );            // 3.14159265358979

// ============================================

// 名前を変えてimportする(as)
import { add as addition, multiply as times } from './utils/math.js';

console.log( addition( 2, 3 ) ); // 5
console.log( times( 4, 5 ) );    // 20

// ============================================

// すべてをまとめてimportする(* as)
import * as MathUtils from './utils/math.js';

console.log( MathUtils.add( 1, 2 ) );  // 3
console.log( MathUtils.PI );           // 3.14159265358979
console.log( MathUtils.clamp( 15, 0, 10 ) ); // 10

3.2 default importの基本

// default export したものをimportする
// { } は不要・名前は自由につけられる

import Formatter from './utils/formatter.js';

const formatter = new Formatter( 'ja-JP' );
console.log( formatter.currency( 1980 ) );  // ¥1,980
console.log( formatter.number( 123456 ) );  // 123,456

// ============================================

// 名前は自由につけられる(同じファイルでもOK)
import MyFormatter from './utils/formatter.js'; // どんな名前でもOK
import createButton from './components/Button.js';
import apiConfig from './config/api.js';

// ============================================

// named と default を同時にimportする
import Carousel, { DIRECTION, DEFAULT_OPTIONS } from './components/Carousel.js';

const carousel = new Carousel( '#hero', { ...DEFAULT_OPTIONS, autoPlay: true } );

3.3 副作用のみのimport

// 何もexportしていないファイルをimportする
// → ファイルを実行するだけ(副作用のみ)

// スタイルシートの読み込み(バンドラー使用時)
import './styles/reset.css';
import './styles/main.scss';

// ポリフィルの適用
import './polyfills/intersection-observer.js';

// グローバル設定の実行
import './config/setup.js';

4. モジュールのファイル構成

4.1 実践的なファイル構成

src/
  ├── main.js              # エントリーポイント
  │
  ├── config/
  │   ├── api.js           # API設定
  │   └── constants.js     # 定数
  │
  ├── utils/
  │   ├── index.js         # utils のまとめexport
  │   ├── math.js          # 数値計算ユーティリティ
  │   ├── date.js          # 日付ユーティリティ
  │   ├── string.js        # 文字列ユーティリティ
  │   ├── dom.js           # DOM操作ユーティリティ
  │   └── validator.js     # バリデーションユーティリティ
  │
  ├── api/
  │   ├── index.js         # APIクライアントのまとめ
  │   ├── client.js        # fetchのラッパー
  │   ├── posts.js         # 投稿APIの処理
  │   └── users.js         # ユーザーAPIの処理
  │
  ├── components/
  │   ├── Modal.js         # モーダル
  │   ├── Toast.js         # トースト通知
  │   ├── Carousel.js      # カルーセル
  │   └── FormValidator.js # フォームバリデーター
  │
  └── pages/
      ├── home.js          # トップページの処理
      └── contact.js       # お問い合わせページの処理

4.2 index.jsでまとめてexportする

// utils/index.js
// → ここからまとめてimportできるようにする

export { add, subtract, multiply, divide, clamp, lerp } from './math.js';
export { formatDate, formatRelative }                    from './date.js';
export { capitalize, truncate, slugify }                 from './string.js';
export { createElement, removeElement, toggleClass }    from './dom.js';
export { isEmail, isPhone, isRequired }                 from './validator.js';

// ============================================

// 使う側ではutils/index.jsからまとめてimportできる
import { add, formatDate, capitalize, isEmail } from './utils/index.js';

// または utils/ だけでもindex.jsを探してくれる(バンドラー使用時)
import { add, formatDate } from './utils';

5. 実践的なモジュール設計パターン

5.1 APIクライアントの設計

// api/client.js

const BASE_URL = 'https://api.example.com/v1';
const DEFAULT_TIMEOUT = 10000;

class ApiError extends Error {
    constructor( status, message, data = null ) {
        super( message );
        this.name   = 'ApiError';
        this.status = status;
        this.data   = data;
    }
}

async function request( endpoint, options = {} ) {
    const {
        method  = 'GET',
        body    = null,
        headers = {},
        timeout = DEFAULT_TIMEOUT,
    } = options;

    const controller = new AbortController();
    const timerId    = setTimeout( () => controller.abort(), timeout );

    try {
        const response = await fetch( `${ BASE_URL }${ endpoint }`, {
            method,
            headers: {
                'Content-Type': 'application/json',
                ...headers,
            },
            body:   body ? JSON.stringify( body ) : null,
            signal: controller.signal,
        } );

        clearTimeout( timerId );

        if ( !response.ok ) {
            const errorData = await response.json().catch( () => null );
            throw new ApiError(
                response.status,
                `HTTP ${ response.status }`,
                errorData
            );
        }

        return response.json();

    } catch ( error ) {
        clearTimeout( timerId );

        if ( error.name === 'AbortError' ) {
            throw new ApiError( 408, 'リクエストがタイムアウトしました' );
        }

        throw error;
    }
}

export { ApiError };
export const get    = ( endpoint, options ) => request( endpoint, { ...options, method: 'GET' } );
export const post   = ( endpoint, body, options ) => request( endpoint, { ...options, method: 'POST', body } );
export const put    = ( endpoint, body, options ) => request( endpoint, { ...options, method: 'PUT', body } );
export const del    = ( endpoint, options ) => request( endpoint, { ...options, method: 'DELETE' } );

export default { get, post, put, del };
// api/posts.js
import { get, post, put, del } from './client.js';

export async function getPosts( { page = 1, limit = 10, category = '' } = {} ) {
    const params = new URLSearchParams( { page, limit, category } );
    return get( `/posts?${ params }` );
}

export async function getPost( id ) {
    return get( `/posts/${ id }` );
}

export async function createPost( data ) {
    return post( '/posts', data );
}

export async function updatePost( id, data ) {
    return put( `/posts/${ id }`, data );
}

export async function deletePost( id ) {
    return del( `/posts/${ id }` );
}

5.2 ユーティリティのモジュール化

// utils/dom.js

/**
 * CSSセレクターで要素を取得する(型安全版)
 */
export function $( selector, parent = document ) {
    return parent.querySelector( selector );
}

export function $$( selector, parent = document ) {
    return [ ...parent.querySelectorAll( selector ) ];
}

/**
 * 要素を作成する
 */
export function createElement( tag, { className, text, html, attrs, children } = {} ) {
    const el = document.createElement( tag );

    if ( className ) el.className = className;
    if ( text )      el.textContent = text;
    if ( html )      el.innerHTML = html;

    if ( attrs ) {
        Object.entries( attrs ).forEach( ( [ key, val ] ) => {
            el.setAttribute( key, val );
        } );
    }

    if ( children ) {
        children.forEach( ( child ) => el.appendChild( child ) );
    }

    return el;
}

/**
 * イベントリスナーをまとめて設定する
 */
export function on( el, events, handler, options ) {
    events.split( ' ' ).forEach( ( event ) => {
        el.addEventListener( event.trim(), handler, options );
    } );
    // クリーンアップ関数を返す
    return () => {
        events.split( ' ' ).forEach( ( event ) => {
            el.removeEventListener( event.trim(), handler );
        } );
    };
}

/**
 * 要素が画面内に入ったら処理する
 */
export function onVisible( el, callback, options = { threshold: 0.1 } ) {
    const observer = new IntersectionObserver( ( entries ) => {
        entries.forEach( ( entry ) => {
            if ( entry.isIntersecting ) {
                callback( entry.target );
                observer.unobserve( entry.target );
            }
        } );
    }, options );

    observer.observe( el );
    return () => observer.disconnect();
}
// utils/validator.js

export const isRequired = ( value ) =>
    value !== null && value !== undefined && String( value ).trim() !== '';

export const isEmail = ( value ) =>
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( value );

export const isPhone = ( value ) =>
    /^0\d{9,10}$/.test( value.replace( /-/g, '' ) );

export const isUrl = ( value ) => {
    try {
        new URL( value );
        return true;
    } catch {
        return false;
    }
};

export const minLength = ( min ) => ( value ) => String( value ).length >= min;
export const maxLength = ( max ) => ( value ) => String( value ).length <= max;
export const min       = ( minimum ) => ( value ) => Number( value ) >= minimum;
export const max       = ( maximum ) => ( value ) => Number( value ) <= maximum;

/**
 * バリデーションルールをまとめて実行する
 */
export function validate( value, rules ) {
    for ( const [ ruleFn, message ] of rules ) {
        if ( !ruleFn( value ) ) return { valid: false, message };
    }
    return { valid: true, message: null };
}

// 使用例
// const result = validate( email, [
//     [ isRequired, '必須項目です' ],
//     [ isEmail,    '正しいメールアドレスを入力してください' ],
// ] );

5.3 メインファイルでまとめる

// main.js(エントリーポイント)

// スタイルの読み込み
import './styles/main.scss';

// ユーティリティ
import { $$, onVisible } from './utils/dom.js';
import { isEmail }       from './utils/validator.js';

// コンポーネント
import Modal         from './components/Modal.js';
import Toast         from './components/Toast.js';
import Carousel      from './components/Carousel.js';
import FormValidator from './components/FormValidator.js';

// APIクライアント
import { getPosts } from './api/posts.js';

// ページごとの処理を初期化する
async function init() {
    // トースト通知
    const toast = new Toast();
    window.toast = toast; // グローバルに公開(任意)

    // カルーセルの初期化
    $$( '.carousel' ).forEach( ( el ) => {
        new Carousel( `#${ el.id }`, { autoPlay: true } );
    } );

    // モーダルの初期化
    $$( '[data-modal]' ).forEach( ( btn ) => {
        const modal = new Modal( `#${ btn.dataset.modal }` );
        btn.addEventListener( 'click', () => modal.show() );
    } );

    // フェードインアニメーション
    $$( '.fade-in' ).forEach( ( el ) => {
        onVisible( el, ( target ) => {
            target.classList.add( 'is-visible' );
        } );
    } );

    // 投稿一覧の取得
    const postList = document.querySelector( '#post-list' );
    if ( postList ) {
        try {
            const posts = await getPosts( { limit: 6 } );
            postList.innerHTML = posts
                .map( ( post ) => `
                    <article class="card" data-id="${ post.id }">
                        <h2>${ post.title }</h2>
                        <p>${ post.body }</p>
                    </article>
                ` )
                .join( '' );
        } catch ( error ) {
            console.error( '投稿の取得に失敗:', error );
            toast.error( '記事の読み込みに失敗しました' );
        }
    }
}

// DOMが読み込まれたら実行する
document.addEventListener( 'DOMContentLoaded', init );

6. 動的インポート

6.1 import() で遅延読み込みする

/*
  動的インポート:
  import() 関数を使って
  必要なタイミングでモジュールを読み込む。

  メリット:
  → 初期読み込みを軽くできる(コード分割)
  → 条件によって読み込むものを変えられる
  → Promise を返すので async/await で使える
*/

// ボタンをクリックしたときだけモジュールを読み込む
const btn = document.querySelector( '#load-chart' );

btn.addEventListener( 'click', async () => {
    try {
        // クリックされたときに初めてChart.jsを読み込む
        const { default: Chart } = await import( './components/Chart.js' );

        const chart = new Chart( '#chart-canvas', {
            type: 'line',
            data: { /* ... */ },
        } );
    } catch ( error ) {
        console.error( 'チャートの読み込みに失敗:', error );
    }
} );

// ============================================

// 言語に応じてモジュールを切り替える
async function loadTranslations( lang ) {
    try {
        const { default: translations } = await import( `./locales/${ lang }.js` );
        return translations;
    } catch {
        // フォールバック:日本語を読み込む
        const { default: ja } = await import( './locales/ja.js' );
        return ja;
    }
}

const lang = navigator.language.startsWith( 'en' ) ? 'en' : 'ja';
const t    = await loadTranslations( lang );
console.log( t.greeting ); // 'こんにちは' or 'Hello'

7. CommonJS との比較

7.1 ES Modules vs CommonJS

// ============================================
// CommonJS(Node.js・古い書き方)
// ============================================

// エクスポート
// math.js
function add( a, b ) { return a + b; }
module.exports = { add };

// または
module.exports.add = function( a, b ) { return a + b; };

// デフォルトエクスポート
module.exports = class Formatter { ... };

// インポート
const { add }     = require( './math.js' );
const Formatter   = require( './utils/formatter.js' );
const path        = require( 'path' ); // Node.js組み込みモジュール

// ============================================
// ES Modules(モダンな書き方)
// ============================================

// エクスポート
export function add( a, b ) { return a + b; }
export default class Formatter { ... }

// インポート
import { add }    from './math.js';
import Formatter  from './utils/formatter.js';

// ============================================
// WordPressのJavaScriptでよく見る CommonJS
// ============================================
jQuery( function( $ ) {
    // jQueryのコード
    const utils = require( './utils.js' ); // webpack使用時
} );

8. ブラウザでのES Modules

8.1 HTMLでtype=”module”を使う

<!-- type="module" を指定するとES Modulesが使える -->
<script type="module" src="./src/main.js"></script>

<!--
  type="module" の特徴:
  → import / export が使える
  → 自動的にdeferされる(DOMContentLoadedを待つ)
  → 厳格モード('use strict')が自動で適用される
  → モジュールスコープになる(グローバルに漏れない)
  → 同じURLのモジュールは1回しか読み込まれない
-->

<!-- 古いブラウザ向けのフォールバック -->
<script nomodule src="./dist/bundle.js"></script>

9. 実践チェックリスト

✓ export と import を使ってコードをファイルに分割できているか

✓ named export と default export の使い分けができているか

✓ utils/index.js でまとめてエクスポートして import 元を一本化しているか

✓ APIクライアントを独立したモジュールとして設計できているか

✓ コンポーネント・ユーティリティ・API・設定などをフォルダで整理しているか

✓ 動的インポート import() で必要なタイミングだけモジュールを読み込む工夫をしているか

✓ <script type="module"> でブラウザでもES Modulesを使えているか

✓ Viteなどのバンドラーを使う場合に .js 拡張子の省略やエイリアスを活用しているか


まとめ

今回はJavaScriptのモジュールシステムとして import と export の使い方・ファイル分割の方法を解説しました。ポイントをまとめると:

  • モジュールを使うとコードをファイル単位に分割でき・変数のスコープがファイル内に閉じて名前の衝突を防げる
  • named export は export function / export const の形で複数エクスポートできて・import側は { } を使う
  • default export はファイルにつき1つで export default の形でエクスポートして・import側は { } なしで自由な名前をつけられる
  • utils/index.js でまとめてエクスポートすると複数のユーティリティを1行でimportできて便利
  • APIクライアント・コンポーネント・ユーティリティ・定数をフォルダで整理するとコードの見通しがよくなる
  • 動的インポート import() を使うと必要なタイミングだけモジュールを読み込めて初期ロードを軽くできる
  • ブラウザでは <script type="module"> を指定するだけでES Modulesが使える・自動でdeferされDOMContentLoadedを待つ
  • Viteなどのバンドラーを使う場合は拡張子の省略・パスエイリアス・ツリーシェイキングなども活用できる

次の記事では、Web制作の効率を大きく上げるVSCodeの便利な設定と拡張機能を紹介します。毎日使うエディタを整えることでコーディングのスピードと快適さが格段に上がる方法をお届けします。お楽しみに!

コメント

タイトルとURLをコピーしました