JavaScriptのモジュールシステムとVite入門|import・exportの使い方からバンドラーの基本まで


はじめに

前回の記事では localStoragesessionStorage を使ったデータ永続化のパターンを解説しました。コードの中でラッパー関数や各種クラスを作るようになってくると「ファイルを分割して管理したい」という場面が増えてきます。今回はJavaScriptのモジュールシステムimportexport)の使い方と、モジュールをまとめてブラウザで動かすためのバンドラーViteの基本的な使い方を解説します。


1. モジュールシステムとは何か

1.1 ファイルを分割して管理するメリット

これまでの連載ではJavaScriptを1つのファイルに書いていましたが、コードが増えるにつれて以下の問題が生じます。

1ファイルに書き続けた場合の問題:

❌ ファイルが長くなりすぎて読みにくい
❌ 同じ関数名を別の場所で使ってしまう(グローバル変数の汚染)
❌ どこで定義した変数かわからなくなる
❌ 再利用したいコードを別のファイルにコピーしてしまう

モジュールシステムを使うと:

✅ ファイルを機能ごとに分割して管理できる
✅ 各モジュールのスコープが独立する(変数名の衝突を防げる)
✅ 必要な機能だけを明示的にimportして使える
✅ 同じコードを複数のファイルで再利用できる

1.2 JavaScriptのモジュール形式の種類

形式環境書き方
ES Modules(ESM)ブラウザ・Node.js(現在の主流)importexport
CommonJS(CJS)Node.js(古い書き方)require()module.exports
AMDブラウザ(古い書き方・Requirejs)define()require()

💡 現在のフロントエンド開発では importexport を使うESMが標準です: Node.jsでも13以降からESMが正式対応しています。新しくコードを書く場合はESMを使いましょう。


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 const PI = 3.14159265;

// まとめてexportする書き方(上記と同じ意味)
function add( a, b )      { return a + b; }
function subtract( a, b ) { return a - b; }
const PI = 3.14159265;

export { add, subtract, PI };

// 別名(エイリアス)を付けてexportする
export { add as sum, subtract as diff };

2.2 デフォルトエクスポート(Default Export)

// utils/formatter.js

// デフォルトエクスポート(1ファイルに1つだけ)
export default function formatDate( date, locale = 'ja-JP' ) {
    return new Intl.DateTimeFormat( locale, {
        year:  'numeric',
        month: 'long',
        day:   'numeric',
    } ).format( date );
}

// アロー関数をデフォルトエクスポートする
const formatCurrency = ( amount, currency = 'JPY' ) =>
    new Intl.NumberFormat( 'ja-JP', { style: 'currency', currency } )
            .format( amount );

export default formatCurrency;

// クラスをデフォルトエクスポートする
export default class ApiClient {
    constructor( baseUrl ) {
        this.baseUrl = baseUrl;
    }
    // ...
}

2.3 名前付きとデフォルトの使い分け

使い分け向いているケース
名前付きエクスポートユーティリティ関数・定数など複数をまとめてエクスポートするとき
デフォルトエクスポートクラス・コンポーネントなどファイルの「主役」が1つのとき

3. importの書き方

3.1 名前付きインポート

// main.js

// 名前付きエクスポートをインポートする
import { add, subtract, PI } from './utils/math.js';

console.log( add( 1, 2 ) ); // 3
console.log( PI );           // 3.14159265

// 別名(エイリアス)を付けてインポートする
import { add as sum } from './utils/math.js';
console.log( sum( 3, 4 ) ); // 7

// モジュール全体を1つのオブジェクトとしてインポートする
import * as MathUtils from './utils/math.js';
console.log( MathUtils.add( 5, 6 ) );   // 11
console.log( MathUtils.PI );             // 3.14159265

3.2 デフォルトインポート

// デフォルトエクスポートをインポートする(名前は自由に付けられる)
import formatDate     from './utils/formatter.js';
import DateFormatter  from './utils/formatter.js'; // 同じ意味・名前は自由

console.log( formatDate( new Date() ) ); // 2025年1月1日

// デフォルトと名前付きを同時にインポートする
import ApiClient, { API_VERSION, API_TIMEOUT } from './api/client.js';

3.3 副作用のためのインポート(Side Effect Import)

// 実行するだけでよいファイル(エクスポートを受け取る必要がない)
import './styles/reset.css';        // CSSの読み込み(バンドラー使用時)
import './polyfills/intersection.js'; // ポリフィルの実行
import './analytics/setup.js';       // 初期化処理の実行

4. 実践的なファイル構成

4.1 機能別にファイルを分割する

前回までの連載で作ったコードをモジュールとして整理した場合の構成例です。

src/
├── main.js                  ← エントリーポイント
├── api/
│   ├── client.js            ← APIクライアント
│   └── endpoints.js         ← エンドポイント定義
├── storage/
│   ├── storage.js           ← storageラッパー
│   ├── timedStorage.js      ← 有効期限付きストレージ
│   └── formAutoSave.js      ← フォーム自動保存
├── ui/
│   ├── theme.js             ← テーマ管理
│   ├── asyncState.js        ← ローディング状態管理
│   └── formValidation.js    ← バリデーション
└── utils/
    ├── escapeHtml.js        ← XSS対策
    ├── debounce.js          ← デバウンス
    └── formatters.js        ← 日付・金額フォーマット

4.2 インデックスファイルで再エクスポートする

// utils/index.js(バレルファイル)
export { escapeHtml }         from './escapeHtml.js';
export { debounce }           from './debounce.js';
export { formatDate, formatCurrency } from './formatters.js';

// 使う側のコード
import { escapeHtml, debounce, formatDate } from './utils/index.js';
// ↓ utils/index.js経由でまとめてインポートできる

💡 index.js をバレルファイルとして使うと import が簡潔になります: 複数のファイルからインポートする代わりに ./utils だけを書けばよくなります。ただしファイルが増えるとバンドルサイズが大きくなる場合もあるため使いすぎには注意しましょう。

4.3 実際のモジュール例

// storage/storage.js
const storage = {
    set( key, value ) {
        try {
            localStorage.setItem( key, JSON.stringify( value ) );
            return true;
        } catch ( error ) {
            console.error( `storage.set('${key}') failed:`, error );
            return false;
        }
    },
    get( key, defaultValue = null ) {
        try {
            const item = localStorage.getItem( key );
            return item === null ? defaultValue : JSON.parse( item );
        } catch ( error ) {
            return defaultValue;
        }
    },
    remove: ( key ) => localStorage.removeItem( key ),
    has:    ( key ) => localStorage.getItem( key ) !== null,
};

export default storage;
// ---

// storage/timedStorage.js
import storage from './storage.js'; // 別モジュールをインポート

const timedStorage = {
    set( key, value, ttlMs ) {
        return storage.set( key, {
            value,
            expiresAt: ttlMs ? Date.now() + ttlMs : null,
        } );
    },
    get( key, defaultValue = null ) {
        const item = storage.get( key );
        if ( !item ) return defaultValue;
        if ( item.expiresAt && Date.now() > item.expiresAt ) {
            storage.remove( key );
            return defaultValue;
        }
        return item.value;
    },
};

export default timedStorage;

5. ブラウザのネイティブESMを使う

5.1 type=”module”でスクリプトを読み込む

バンドラーなしでもモダンブラウザなら type="module" 属性でESMを使えます。

<!-- index.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>My App</title>
</head>
<body>
    <!-- type="module"を付けるとimport/exportが使える -->
    <script type="module" src="./src/main.js"></script>
</body>
</html>
// src/main.js
import { add } from './utils/math.js';
import storage from './storage/storage.js';

console.log( add( 1, 2 ) );
storage.set( 'test', 'hello' );

⚠️ ネイティブESMはローカルの file:// プロトコルでは動作しません: CORSエラーが発生するため、VSCodeの「Live Server」拡張機能やNode.jsのローカルサーバーが必要です。これが次に解説するViteが便利な理由の一つです。


6. Viteとは何か

6.1 バンドラーが必要な理由

ネイティブESMは便利ですが、実際の開発では以下の問題があります。

ネイティブESMの課題:

❌ ファイルが多いとリクエスト数が増えて遅い
❌ CSSや画像をimportできない(バンドラーがないと)
❌ 古いブラウザではESMが動かない
❌ TypeScriptやSassをそのままは使えない
❌ コードを圧縮・最適化する仕組みがない

バンドラーはこれらの問題を解決して、複数のモジュールファイルを本番環境に最適化された少数のファイルにまとめる(バンドルする)ツールです。

6.2 Viteの特徴

Vitevitejs.dev)はVue.jsの作者Evan You氏が開発した次世代バンドラーです。

特徴内容
開発サーバーの起動が爆速ファイルをバンドルせずネイティブESMを使うため即座に起動する
HMR(ホットモジュールリプレースメント)ファイルを保存するとブラウザが自動でリロードされる
本番ビルドはRollup最適化された圧縮バンドルを生成する
設定がほぼ不要デフォルトでTypeScript・CSS・画像のimportに対応している
プラグインが豊富React・Vue・Svelte等のフレームワークもプラグインで対応

7. Viteのセットアップ手順

7.1 新規プロジェクトを作成する

# Node.jsがインストールされている必要がある
# Node.jsのバージョン確認
node -v  # v18以上を推奨

# Viteでプロジェクトを作成する
npm create vite@latest my-project

# テンプレートを選択するプロンプトが表示される
# → フレームワークを選択:Vanilla(フレームワークなしの純粋なJavaScript)
# → バリアント:JavaScript(またはTypeScript)

# 作成したディレクトリに移動する
cd my-project

# 依存パッケージをインストールする
npm install

# 開発サーバーを起動する
npm run dev
起動後のターミナル出力例:

  VITE v5.x.x  ready in 150ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: http://192.168.x.x:5173/

7.2 Viteプロジェクトのフォルダ構成

my-project/
├── index.html          ← エントリーポイントのHTML
├── package.json        ← プロジェクト設定・スクリプト
├── vite.config.js      ← Viteの設定ファイル
├── public/             ← 静的ファイル(そのままコピーされる)
│   └── favicon.ico
└── src/                ← ソースコード
    ├── main.js         ← JavaScriptエントリーポイント
    ├── style.css       ← グローバルCSS
    └── counter.js      ← サンプルモジュール

7.3 package.jsonのスクリプト

{
  "scripts": {
    "dev":     "vite",           // 開発サーバーを起動する
    "build":   "vite build",     // 本番用にビルドする
    "preview": "vite preview"    // ビルド結果をローカルで確認する
  }
}

8. Viteでの実践的な開発

8.1 CSSをJavaScriptからimportする

// Viteではjsファイルからcssをimportできる
import './style.css';
import './components/button.css';

// CSSモジュール(クラス名の衝突を防ぐ)
import styles from './button.module.css';
document.querySelector( 'button' ).className = styles.primary;

8.2 画像をimportする

// 画像をimportするとURLが返る
import logoUrl from './assets/logo.png';

const img    = document.createElement( 'img' );
img.src      = logoUrl;
img.alt      = 'ロゴ';
document.body.appendChild( img );

8.3 環境変数を使う

# .env(プロジェクトルートに作成する)
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My Portfolio

# .env.development(開発環境用)
VITE_API_URL=http://localhost:3000

# .env.production(本番環境用)
VITE_API_URL=https://api.example.com
// Viteの環境変数はVITE_プレフィックスが必要
const API_URL   = import.meta.env.VITE_API_URL;
const APP_TITLE = import.meta.env.VITE_APP_TITLE;

console.log( import.meta.env.MODE );        // 'development' または 'production'
console.log( import.meta.env.DEV );         // true(開発環境のとき)
console.log( import.meta.env.PROD );        // true(本番環境のとき)

⚠️ 環境変数には VITE_ プレフィックスが必要です: プレフィックスなしの変数はクライアント側のコードに含まれません(セキュリティのため)。APIキーなどの機密情報はサーバーサイドで管理して、クライアントコードには含めないようにしましょう。

8.4 本番用ビルドを実行する

npm run build
dist/                     ← ビルド結果
├── index.html            ← 最適化されたHTML
├── assets/
│   ├── index-Abc123.js   ← バンドル・圧縮されたJS(ハッシュ付き)
│   └── index-Xyz789.css  ← バンドル・圧縮されたCSS(ハッシュ付き)
└── favicon.ico

💡 ファイル名にハッシュが付くのはキャッシュバスティングのためです: コードが変わるとファイル名のハッシュ部分も変わるため、ブラウザが古いキャッシュを使い続ける問題を自動的に防いでくれます。


9. Viteの設定ファイル

9.1 vite.config.jsの基本設定

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig( {

    // 開発サーバーの設定
    server: {
        port:  3000,      // ポートを変更する(デフォルト:5173)
        open:  true,      // 起動時にブラウザを自動で開く
        https: false,     // HTTPSを使う場合はtrue
    },

    // ビルドの設定
    build: {
        outDir:        'dist',    // 出力ディレクトリ(デフォルト:dist)
        minify:        'esbuild', // 圧縮方法
        sourcemap:     false,     // ソースマップを生成するかどうか
        target:        'es2015',  // ターゲットブラウザのES仕様
        rollupOptions: {
            output: {
                // チャンクファイルの命名規則
                chunkFileNames: 'assets/[name]-[hash].js',
                assetFileNames: 'assets/[name]-[hash][extname]',
            },
        },
    },

    // パスのエイリアス(@でsrcディレクトリを指定できる)
    resolve: {
        alias: {
            '@': '/src',
        },
    },
} );
// エイリアスを使うとimportのパスが簡潔になる
// 変更前
import storage from '../../storage/storage.js';

// 変更後
import storage from '@/storage/storage.js';

10. 動作確認チェックリスト

Node.jsがインストールされていて node -v でバージョンが確認できるか

npm create vite@latest でプロジェクトが作成できたか

npm install で依存パッケージがインストールできたか

npm run dev で開発サーバーが起動してブラウザで確認できるか

ファイルを保存するとHMRでブラウザが自動更新されるか

importexport を使って機能をファイル分割できたか

CSSをJavaScriptファイルからimportして適用できるか

.env ファイルで環境変数を設定して import.meta.env で取得できるか

npm run build でdistフォルダにビルドが出力されるか

npm run preview でビルド結果をローカルで確認できるか


まとめ

今回はJavaScriptのモジュールシステムとバンドラーViteの基本的な使い方を解説しました。ポイントをまとめると:

  • ESM(importexport)を使うとファイルを機能ごとに分割してグローバルスコープの汚染を防げる
  • export は名前付きエクスポートとデフォルトエクスポートの2種類がある
  • import * as でモジュール全体をオブジェクトとしてまとめてインポートできる
  • index.js のバレルファイルを使うと複数モジュールをまとめてインポートできる
  • Viteは開発サーバーの起動が爆速でHMRによる自動リロードが快適
  • npm create vite@latest でプロジェクトを作成して npm run dev で開発開始できる
  • 環境変数は .env ファイルに VITE_ プレフィックスを付けて定義する
  • npm run build でRollupを使った最適化バンドルが dist フォルダに出力される

次の記事では、TypeScriptの基本的な型注釈とインターフェースの使い方を解説して、JavaScriptのコードに型安全性を持たせる方法を紹介します。お楽しみに!

コメント

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