Vanilla JavaScriptモダン開発：Vite npm で始める実践的プロジェクト構築


# Vanilla JavaScript モダン開発：Vite + npm で始める実践的プロジェクト構築

モダンなフロントエンド開発において、Vanilla JavaScript (プレーンなJavaScript) を活用することは、フレームワークの学習コストを抑えつつ、パフォーマンスと制御の自由度を高める上で有効な選択肢です。本記事では、Vite 6 と npm を組み合わせ、効率的かつスケーラブルな Vanilla JavaScript プロジェクトを構築するための実践的なアプローチを紹介します。単なるセットアップ手順の解説に留まらず、独自の視点と経験に基づいた、一歩進んだテクニックや問題解決方法を共有します。

## 1. プロジェクト設計：目的特化型ディレクトリ構成とモジュール境界の明確化

プロジェクトの成功は、最初の設計段階で決まると言っても過言ではありません。大規模なアプリケーションほど、ディレクトリ構成とモジュール境界の明確化が重要になります。

### 1.1. 目的特化型ディレクトリ構成

従来の「components」「utils」「services」といった汎用的なディレクトリ構成ではなく、**プロジェクトの目的（ユースケース）に特化したディレクトリ構成**を意識します。

例えば、ECサイトのフロントエンドであれば、以下のような構成が考えられます。

```
src/
├── features/           # 特定の機能やユースケースに対応するモジュール群
│   ├── product-listing/  # 商品一覧表示機能
│   │   ├── components/   # 商品カード、フィルタリングUIなどのコンポーネント
│   │   ├── api/          # 商品データ取得APIとの連携
│   │   └── utils/        # 商品データの変換やフォーマット
│   ├── cart/            # カート機能
│   │   └── ...
│   └── checkout/        # チェックアウト機能
│       └── ...
├── shared/             # 複数の機能で共有される汎用的なモジュール
│   ├── components/   # ボタン、アイコンなどのUIコンポーネント
│   ├── utils/        # 日付フォーマット、通貨フォーマットなどのユーティリティ
│   └── types/        # 共通の型定義
├── app.js              # アプリケーションのエントリーポイント
└── style.css           # グローバルスタイルシート
```

この構成のメリットは、

*   **コードの検索性向上:** 特定の機能に関するコードがどこにあるか一目瞭然。
*   **依存関係の明確化:** 各機能が依存するモジュールが限定され、変更の影響範囲を把握しやすい。
*   **再利用性の向上:** `shared/` ディレクトリに汎用的なモジュールを集約することで、異なる機能間でのコード再利用を促進。

### 1.2. モジュール境界の明確化：カスタムイベント駆動アーキテクチャ

Vanilla JavaScript で大規模なアプリケーションを構築する場合、コンポーネント間の通信は重要な課題となります。単純な関数呼び出しだけでなく、**カスタムイベント**を活用することで、疎結合なコンポーネント間の連携を実現できます。

例えば、商品一覧表示コンポーネントで商品がクリックされた際に、カートに追加するイベントを発火させる場合、以下のようなコードが考えられます。

```javascript
// product-listing/components/ProductCard.js
class ProductCard extends HTMLElement {
  constructor() {
    super();
    this.addEventListener('click', () => {
      const productId = this.dataset.productId;
      const event = new CustomEvent('product-added-to-cart', {
        detail: { productId }
      });
      document.dispatchEvent(event); // グローバルイベントを発火
    });
  }
}
customElements.define('product-card', ProductCard);

// cart/Cart.js
class Cart extends HTMLElement {
  constructor() {
    super();
    document.addEventListener('product-added-to-cart', (event) => {
      const productId = event.detail.productId;
      this.addProductToCart(productId);
    });
  }
}
customElements.define('cart-component', Cart);
```

この例では、`product-listing` コンポーネントと `cart` コンポーネントが直接的な依存関係を持たず、`document` オブジェクトを介してイベントを伝播することで、疎結合なアーキテクチャを実現しています。

## 2. Vite 6 + npm 初期設定：tsconfig.json を制する者が開発を制す

Vite は、その高速なビルド速度と優れた開発体験で、モダンなフロントエンド開発に欠かせないツールとなりました。Vite 6では、Rollup 4のサポートによる高速化、Node.js 18.0以上のサポート、そしてより強化されたTypeScriptサポートが導入されています。Vite の潜在能力を最大限に引き出すためには、`tsconfig.json` の設定を適切に行う必要があります。

### 2.1. 型安全性を追求する tsconfig.json

単なる JavaScript プロジェクトであっても、TypeScript の型チェック機能を利用することで、開発効率とコード品質を大幅に向上させることができます。

以下は、Vanilla JavaScript プロジェクトにおける `tsconfig.json` の推奨設定例です。

```json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    "types": ["node"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
```

*   **`strict: true`:** 型チェックを厳格に行い、潜在的なエラーを早期に発見します。
*   **`esModuleInterop: true`:** CommonJS モジュールと ES モジュール間の相互運用性を高めます。
*   **`baseUrl` と `paths`:** 絶対パスによるモジュールインポートを可能にし、可読性とメンテナンス性を向上させます。
*   **`types: ["node"]`:** Node.js 環境の型定義を含め、Node.js API の利用を支援します。

**型定義ファイル(.d.ts)の活用:**

Vanilla JavaScript プロジェクトであっても、外部ライブラリの型定義ファイル(.d.ts)を積極的に活用することで、型チェックの恩恵を受けることができます。例えば、`fetch` API の型定義ファイルを利用することで、API レスポンスの型を明示的に定義し、型安全なコードを書くことができます。

### 2.2. ESLint/Prettier 連携：型チェックとコードフォーマットの自動化

ESLint と Prettier を連携させることで、型チェックとコードフォーマットを自動化し、一貫性のあるコードスタイルを維持することができます。

Vite プロジェクトにおける ESLint と Prettier の設定は、以下の手順で行います。

1.  **必要なパッケージをインストール:**

    ```bash
    npm install -D eslint prettier eslint-plugin-prettier eslint-config-prettier
    ```

2.  **.eslintrc.js を作成:**

    ```javascript
    module.exports = {
      extends: [
        'eslint:recommended',
        'plugin:prettier/recommended'
      ],
      env: {
        browser: true,
        es2021: true,
        node: true
      },
      parserOptions: {
        ecmaVersion: 12,
        sourceType: 'module'
      },
      rules: {
        // 必要に応じてルールをカスタマイズ
      }
    };
    ```

3.  **.prettierrc.js を作成:**

    ```javascript
    module.exports = {
      semi: false,
      singleQuote: true,
      trailingComma: 'es5'
    };
    ```

4.  **package.json に lint と format のスクリプトを追加:**

    ```json
    "scripts": {
      "lint": "eslint src/**/*.js",
      "format": "prettier --write src/**/*.js"
    }
    ```

5.  **VS Code の設定:**

    VS Code の設定で、保存時に自動的にフォーマットされるように設定します。

    ```json
    {
      "editor.formatOnSave": true,
      "editor.defaultFormatter": "esbenp.prettier-vscode"
    }
    ```

## 3. 開発効率化：ホットリロードとモジュールバンドルの裏側

Vite のホットリロードは、開発効率を飛躍的に向上させる強力な機能です。しかし、その裏側を理解することで、より効果的に活用することができます。

### 3.1. ホットリロードの仕組み：ESM の可能性を最大限に引き出す

Vite のホットリロードは、ESM (ECMAScript Modules) の特性を最大限に活用しています。従来のバンドラーとは異なり、Vite は開発時にすべてのモジュールをバンドルしません。代わりに、ブラウザが直接 ESM としてモジュールをリクエストし、変更されたモジュールのみを更新します。

この仕組みにより、高速なホットリロードを実現しています。しかし、大規模なアプリケーションでは、モジュールの依存関係が複雑になり、ホットリロードの速度が低下する可能性があります。

### 3.2. 環境変数の型安全な利用：`import.meta.env` の活用

Vite は、`import.meta.env` を通じて環境変数にアクセスすることができます。しかし、デフォルトでは、環境変数の型は `string` 型として扱われます。

環境変数の型を明示的に定義することで、型安全性を向上させることができます。

1.  **`.env` ファイルを作成:**

    ```
    VITE_API_URL=https://example.com/api
    VITE_ENABLE_FEATURE_X=true
    ```

2.  **`src/env.d.ts` ファイルを作成:**

    ```typescript
    interface ImportMetaEnv {
      readonly VITE_API_URL: string
      readonly VITE_ENABLE_FEATURE_X: boolean
    }

    interface ImportMeta {
      readonly env: ImportMetaEnv
    }
    ```

3.  **`tsconfig.json` に `src/env.d.ts` を含める:**

    ```json
    {
      "include": ["src/**/*", "src/env.d.ts"]
    }
    ```

このように設定することで、`import.meta.env.VITE_API_URL` は `string` 型、`import.meta.env.VITE_ENABLE_FEATURE_X` は `boolean` 型として扱われるようになります。

## 4. npm パッケージ管理：セマンティックバージョニングと npm scripts の魔術

npm は、JavaScript プロジェクトにおける依存関係管理のデファクトスタンダードです。しかし、その機能を最大限に活用するためには、セマンティックバージョニング (SemVer) と npm scripts を深く理解する必要があります。

### 4.1. セマンティックバージョニング：依存関係地獄からの脱却

セマンティックバージョニングは、パッケージのバージョン番号の意味を明確に定義することで、依存関係の衝突を回避するためのルールです。

バージョン番号は、`MAJOR.MINOR.PATCH` の形式で表されます。

*   **MAJOR:** 互換性のない API の変更
*   **MINOR:** 後方互換性のある機能追加
*   **PATCH:** 後方互換性のあるバグ修正

依存関係を管理する際には、バージョン範囲指定 (e.g., `^1.2.3`, `~1.2.3`) を適切に利用することで、互換性を維持しつつ、最新の機能や修正を取り込むことができます。

**バージョン範囲指定の注意点:**

*   `^`: メジャーバージョンが固定され、マイナーバージョンとパッチバージョンが自動的に更新されます。
*   `~`: メジャーバージョンとマイナーバージョンが固定され、パッチバージョンが自動的に更新されます。
*   `*`: すべてのバージョンが許可されます (非推奨)。

### 4.2. npm scripts の魔術：タスク自動化の達人

npm scripts は、`package.json` に定義されたタスクを実行するための仕組みです。ビルド、テスト、デプロイなど、様々なタスクを自動化することができます。

以下は、npm scripts の活用例です。

```json
"scripts": {
  "dev": "vite",
  "build": "vite build",
  "lint": "eslint src/**/*.js",
  "format": "prettier --write src/**/*.js",
  "test": "vitest",
  "deploy": "npm run build && gh-pages -d dist"
}
```

**npm scripts の連鎖:**

npm scripts は、`&&` 演算子を使って連鎖させることができます。例えば、`npm run deploy` は、`npm run build` を実行し、その後に `gh-pages -d dist` を実行します。

**環境変数の利用:**

npm scripts から環境変数にアクセスすることができます。例えば、`npm run build --mode production` は、`NODE_ENV=production` 環境変数を設定して `vite build` を実行します。

## 5. 実践的コード例：DOM操作、非同期処理、コンポーネント分割の極意

Vanilla JavaScript でモダンなアプリケーションを構築するためには、DOM 操作、非同期処理、コンポーネント分割のスキルが不可欠です。

### 5.1. DOM 操作：Virtual DOM のエッセンスを取り入れる

Vanilla JavaScript で大規模なアプリケーションを構築する場合、DOM 操作のパフォーマンスがボトルネックになることがあります。

Virtual DOM のエッセンスを取り入れることで、DOM 操作のパフォーマンスを向上させることができます。

Virtual DOM とは、実際の DOM を抽象化した JavaScript オブジェクトです。変更が発生した際には、Virtual DOM を比較し、差分のみを実際の DOM に適用することで、DOM 操作の回数を減らすことができます。

**Virtual DOM の簡単な実装例:**

```javascript
function render(vnode, container) {
  // vnode: Virtual DOM ノード
  // container: DOM 要素

  // DOM 要素を作成
  const el = document.createElement(vnode.tag);

  // プロパティを設定
  for (const key in vnode.props) {
    el.setAttribute(key, vnode.props[key]);
  }

  // 子要素をレンダリング
  if (Array.isArray(vnode.children)) {
    vnode.children.forEach(child => render(child, el));
  } else {
    el.textContent = vnode.children;
  }

  // DOM 要素をコンテナに追加
  container.appendChild(el);
}

// Virtual DOM ノードの作成例
const vnode = {
  tag: 'div',
  props: {
    id: 'app'
  },
  children: [
    { tag: 'h1', props: {}, children: 'Hello, World!' },
    { tag: 'p', props: {}, children: 'This is a paragraph.' }
  ]
};

// DOM 要素にレンダリング
const container = document.getElementById('root');
render(vnode, container);
```

この例は非常に簡略化されたものですが、Virtual DOM の基本的な概念を理解することができます。

### 5.2. 非同期処理：Async/Await を使いこなす

Async/Await は、非同期処理をより簡潔に記述するための構文です。

Fetch API を使って API リクエストを行う場合、Async/Await を使うことで、Promise チェーンをネストすることなく、同期的なコードのように記述することができます。

```javascript
async function fetchData() {
  try {
    const response = await fetch('https://example.com/api/data');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error fetching data:', error);
    return null;
  }
}

async function main() {
  const data = await fetchData();
  if (data) {
    console.log('Data:', data);
  }
}

main();
```

**エラーハンドリング:**

Async/Await を使う場合、`try...catch` ブロックを使ってエラーハンドリングを行う必要があります。

**並行処理:**

複数の非同期処理を並行して実行する場合は、`Promise.all()` を使うことができます。

```javascript
async function fetchMultipleData() {
  const [data1, data2] = await Promise.all([
    fetchData1(),
    fetchData2()
  ]);
  console.log('Data 1:', data1);
  console.log('Data 2:', data2);
}
```

### 5.3. コンポーネント分割：Web Components を活用する

Web Components は、再利用可能なカスタム HTML 要素を作成するための標準規格です。Vanilla JavaScript でコンポーネント分割を行う場合、Web Components を活用することで、コードの再利用性と保守性を向上させることができます。

**Web Components の作成例:**

```javascript
class MyComponent extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: 'open' });
    this.shadow.innerHTML = `
      <style>
        p {
          color: blue;
        }
      </style>
      <p>Hello, World!</p>
    `;
  }
}

customElements.define('my-component', MyComponent);
```

**Shadow DOM:**

Web Components は、Shadow DOM を使うことで、コンポーネントのスタイルを外部のスタイルシートから分離することができます。これにより、コンポーネントの再利用性が向上します。

**Custom Elements:**

Web Components は、Custom Elements を使うことで、独自の HTML 要素を定義することができます。

## 6. テスト導入：Vitest で始めるユニットテストの旅

テストは、コードの品質を保証し、リファクタリングを安全に行うための重要なプロセスです。Vanilla JavaScript プロジェクトにおいても、ユニットテストを導入することで、バグの早期発見とコードの信頼性向上に貢献します。

### 6.1. Vitest の導入：設定不要の快適テスト環境

Vitest は、Vite をベースにした高速なテストランナーです。Vite と同じ設定ファイル (`vite.config.js`) を使用するため、設定が不要で、すぐにテストを始めることができます。

1.  **Vitest をインストール:**

    ```bash
    npm install -D vitest
    ```

2.  **`vite.config.js` にテストの設定を追加:**

    ```javascript
    import { defineConfig } from 'vite'

    export default defineConfig({
      // ...
      test: {
        environment: 'jsdom', // ブラウザ環境をエミュレート
      }
    })
    ```

3.  **`package.json` にテストスクリプトを追加:**

    ```json
    "scripts": {
      "test": "vitest"
    }
    ```

4.  **テストファイルを作成:**

    ```javascript
    // src/utils/math.js
    export function add(a, b) {
      return a + b;
    }

    // test/utils/math.test.js
    import { add } from '../../src/utils/math';
    import { describe, expect, it } from 'vitest';

    describe('add', () => {
      it('should return the sum of two numbers', () => {
        expect(add(1, 2)).toBe(3);
        expect(add(-1, 1)).toBe(0);
      });
    });
    ```

5.  **テストを実行:**

    ```bash
    npm run test
    ```

### 6.2. テスト駆動開発 (TDD) の簡単な紹介：Red-Green-Refactor サイクル

テスト駆動開発 (TDD) は、テストを先に書き、そのテストを満たすようにコードを実装する開発手法です。

TDD は、以下の Red-Green-Refactor サイクルで進めます。

1.  **Red:** 実装する機能のテストを書きます。テストは失敗します (Red)。
2.  **Green:** テストをパスするように、必要最小限のコードを実装します。テストは成功します (Green)。
3.  **Refactor:** コードをリファクタリングし、品質を高めます。テストは成功したままです。

TDD は、設計段階で要件を明確にし、テスト可能なコードを書くことを促進します。

## 7. ビルドと最適化：本番環境への備え

Vite は、開発環境だけでなく、本番環境向けのビルドも高速かつ効率的に行うことができます。

### 7.1. コード分割とアセットの最適化

Vite 6は、Rollup 4を採用することで、より効率的なコード分割とアセットの最適化を実現しています。デフォルトでChunk Splittingが有効化され、初期ロード時間を短縮します。また、新しいアセット最適化戦略により、より効率的なバンドルサイズの削減が可能になりました。

**最適化のヒント:**

*   **動的インポート:** 不要なコードを遅延ロードします。Vite 6では、動的インポートのパフォーマンスが向上しています。
*   **共通モジュールの抽出:** 複数のページで共有されるモジュールを共通チャンクとして抽出します。
*   **ベンダーチャンクの分離:** 依存ライブラリをベンダーチャンクとして分離します。
*   **アセットの最適化:** 新しいアセット処理パイプラインにより、画像やその他のアセットの最適化が改善されています。
*   **Tree Shaking:** より効率的なTree Shakingにより、未使用コードの削除が強化されています。

### 7.2. ビルドの最適化と新機能

Vite 6では、ビルドプロセスが大幅に改善され、より効率的な最適化が可能になりました。

**主な改善点と新機能:**

* **Rollup 4の採用:** より高速なビルドと効率的なコード生成を実現
* **Node.js 18.0以上のサポート:** 最新のNode.js機能を活用した高速化
* **アセット処理の改善:** 画像やその他のアセットの最適化が強化
* **依存関係の事前バンドル:** より効率的な依存関係の処理
* **Source Map生成の最適化:** デバッグ時のパフォーマンス向上

**圧縮設定の例:**

```javascript
// vite.config.js
import { defineConfig } from 'vite'
import compression from 'vite-plugin-compression'

export default defineConfig({
  build: {
    // Rollup 4の新機能を活用
    rollupOptions: {
      output: {
        manualChunks: {
          // ベンダーチャンクの最適化
          vendor: ['lodash', 'axios'],
        },
      },
    },
    // アセット最適化の設定
    assetsInlineLimit: 4096,
    // Source Map生成の設定
    sourcemap: true,
  },
  plugins: [
    // gzipとbrotli圧縮の設定
    compression({
      algorithm: 'gzip',
      ext: '.gz',
    }),
    compression({
      algorithm: 'brotliCompress',
      ext: '.br',
    }),
  ],
})
```

## 8. デプロイ戦略：GitHub Pages, Netlify, Vercel への道

Vite で構築した Vanilla JavaScript プロジェクトは、GitHub Pages, Netlify, Vercel など、様々なプラットフォームに簡単にデプロイすることができます。

### 8.1. GitHub Pages へのデプロイ

1.  **`package.json` にデプロイスクリプトを追加:**

    ```json
    "scripts": {
      "deploy": "npm run build && gh-pages -d dist"
    }
    ```

2.  **`gh-pages` をインストール:**

    ```bash
    npm install -D gh-pages
    ```

3.  **リポジトリの設定で GitHub Pages を有効化:**

4.  **デプロイスクリプトを実行:**

    ```bash
    npm run deploy
    ```

### 8.2. Netlify へのデプロイ

1.  **Netlify にアカウントを作成:**
2.  **Netlify CLI をインストール:**

    ```bash
    npm install -g netlify-cli
    ```

3.  **Netlify にログイン:**

    ```bash
    netlify login
    ```

4.  **プロジェクトを Netlify にデプロイ:**

    ```bash
    netlify deploy --prod --dir=dist
    ```

### 8.3. Vercel へのデプロイ

1.  **Vercel にアカウントを作成:**
2.  **Vercel CLI をインストール:**

    ```bash
    npm install -g vercel
    ```

3.  **Vercel にログイン:**

    ```bash
    vercel login
    ```

4.  **プロジェクトを Vercel にデプロイ:**

    ```bash
    vercel --prod
    ```

## 9. トラブルシューティング：知っておくと役立つ裏技集

Vanilla JavaScript プロジェクトの開発中に遭遇する可能性のあるトラブルシューティングについて、独自の視点から解説します。

### 9.1. `TypeError: Cannot read properties of undefined (reading 'addEventListener')`

このエラーは、DOM 要素がロードされる前に JavaScript コードが実行された場合に発生します。

**解決策:**

*   **`defer` 属性:** `<script>` タグに `defer` 属性を追加することで、HTML の解析が完了してからスクリプトが実行されるようにします。
*   **`DOMContentLoaded` イベント:** `DOMContentLoaded` イベントリスナーを使って、DOM 要素がロードされてから JavaScript コードを実行します。

```javascript
document.addEventListener('DOMContentLoaded', () => {
  // DOM 要素がロードされてから実行されるコード
});
```

### 9.2. パフォーマンス改善のヒント：リフローとリペイントの削減

ブラウザは、DOM 要素のレイアウトやスタイルが変更されるたびに、リフロー (レイアウトの再計算) とリペイント (画面の再描画) を行います。リフローとリペイントは、パフォーマンスに大きな影響を与えるため、できる限り削減する必要があります。

**リフローとリペイントを削減するためのヒント:**

*   **DOM 操作のバッチ処理:** 複数の DOM 操作をまとめて実行します。
*   **`requestAnimationFrame()` の利用:** アニメーションをスムーズに実行するために、`requestAnimationFrame()` を利用します。
*   **`will-change` プロパティの利用:** 変更される可能性のある要素に対して、`will-change` プロパティを設定します。

## まとめ

本記事では、Vite と npm を活用した Vanilla JavaScript モダン開発の実践的なアプローチを紹介しました。プロジェクト設計、初期設定、開発効率化、パッケージ管理、コード例、テスト導入、ビルドと最適化、デプロイ戦略、トラブルシューティングなど、幅広いトピックを網羅しました。

Vanilla JavaScript は、フレームワークに依存しないため、学習コストが低く、パフォーマンスと制御の自由度が高いというメリットがあります。本記事で紹介したテクニックを活用することで、効率的かつスケーラブルな Vanilla JavaScript プロジェクトを構築し、モダンなフロントエンド開発の可能性を広げることができます。