1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

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

// 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 の推奨設定例です。

{
  "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 モジュール間の相互運用性を高めます。
  • baseUrlpaths: 絶対パスによるモジュールインポートを可能にし、可読性とメンテナンス性を向上させます。
  • 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. 必要なパッケージをインストール:

    npm install -D eslint prettier eslint-plugin-prettier eslint-config-prettier
    
  2. .eslintrc.js を作成:

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

    module.exports = {
      semi: false,
      singleQuote: true,
      trailingComma: 'es5'
    };
    
  4. package.json に lint と format のスクリプトを追加:

    "scripts": {
      "lint": "eslint src/**/*.js",
      "format": "prettier --write src/**/*.js"
    }
    
  5. VS Code の設定:

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

    {
      "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 ファイルを作成:

    interface ImportMetaEnv {
      readonly VITE_API_URL: string
      readonly VITE_ENABLE_FEATURE_X: boolean
    }
    
    interface ImportMeta {
      readonly env: ImportMetaEnv
    }
    
  3. tsconfig.jsonsrc/env.d.ts を含める:

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

このように設定することで、import.meta.env.VITE_API_URLstring 型、import.meta.env.VITE_ENABLE_FEATURE_Xboolean 型として扱われるようになります。

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 の活用例です。

"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 の簡単な実装例:

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 チェーンをネストすることなく、同期的なコードのように記述することができます。

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() を使うことができます。

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 の作成例:

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 をインストール:

    npm install -D vitest
    
  2. vite.config.js にテストの設定を追加:

    import { defineConfig } from 'vite'
    
    export default defineConfig({
      // ...
      test: {
        environment: 'jsdom', // ブラウザ環境をエミュレート
      }
    })
    
  3. package.json にテストスクリプトを追加:

    "scripts": {
      "test": "vitest"
    }
    
  4. テストファイルを作成:

    // 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. テストを実行:

    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生成の最適化: デバッグ時のパフォーマンス向上

圧縮設定の例:

// 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 にデプロイスクリプトを追加:

    "scripts": {
      "deploy": "npm run build && gh-pages -d dist"
    }
    
  2. gh-pages をインストール:

    npm install -D gh-pages
    
  3. リポジトリの設定で GitHub Pages を有効化:

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

    npm run deploy
    

8.2. Netlify へのデプロイ

  1. Netlify にアカウントを作成:

  2. Netlify CLI をインストール:

    npm install -g netlify-cli
    
  3. Netlify にログイン:

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

    netlify deploy --prod --dir=dist
    

8.3. Vercel へのデプロイ

  1. Vercel にアカウントを作成:

  2. Vercel CLI をインストール:

    npm install -g vercel
    
  3. Vercel にログイン:

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

    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 コードを実行します。
document.addEventListener('DOMContentLoaded', () => {
  // DOM 要素がロードされてから実行されるコード
});

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

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

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

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

まとめ

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

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?