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 モジュール間の相互運用性を高めます。 -
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 の設定は、以下の手順で行います。
-
必要なパッケージをインストール:
npm install -D eslint prettier eslint-plugin-prettier eslint-config-prettier
-
.eslintrc.js を作成:
module.exports = { extends: [ 'eslint:recommended', 'plugin:prettier/recommended' ], env: { browser: true, es2021: true, node: true }, parserOptions: { ecmaVersion: 12, sourceType: 'module' }, rules: { // 必要に応じてルールをカスタマイズ } };
-
.prettierrc.js を作成:
module.exports = { semi: false, singleQuote: true, trailingComma: 'es5' };
-
package.json に lint と format のスクリプトを追加:
"scripts": { "lint": "eslint src/**/*.js", "format": "prettier --write src/**/*.js" }
-
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
型として扱われます。
環境変数の型を明示的に定義することで、型安全性を向上させることができます。
-
.env
ファイルを作成:VITE_API_URL=https://example.com/api VITE_ENABLE_FEATURE_X=true
-
src/env.d.ts
ファイルを作成:interface ImportMetaEnv { readonly VITE_API_URL: string readonly VITE_ENABLE_FEATURE_X: boolean } interface ImportMeta { readonly env: ImportMetaEnv }
-
tsconfig.json
にsrc/env.d.ts
を含める:{ "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 の活用例です。
"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
) を使用するため、設定が不要で、すぐにテストを始めることができます。
-
Vitest をインストール:
npm install -D vitest
-
vite.config.js
にテストの設定を追加:import { defineConfig } from 'vite' export default defineConfig({ // ... test: { environment: 'jsdom', // ブラウザ環境をエミュレート } })
-
package.json
にテストスクリプトを追加:"scripts": { "test": "vitest" }
-
テストファイルを作成:
// 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); }); });
-
テストを実行:
npm run test
6.2. テスト駆動開発 (TDD) の簡単な紹介:Red-Green-Refactor サイクル
テスト駆動開発 (TDD) は、テストを先に書き、そのテストを満たすようにコードを実装する開発手法です。
TDD は、以下の Red-Green-Refactor サイクルで進めます。
- Red: 実装する機能のテストを書きます。テストは失敗します (Red)。
- Green: テストをパスするように、必要最小限のコードを実装します。テストは成功します (Green)。
- 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 へのデプロイ
-
package.json
にデプロイスクリプトを追加:"scripts": { "deploy": "npm run build && gh-pages -d dist" }
-
gh-pages
をインストール:npm install -D gh-pages
-
リポジトリの設定で GitHub Pages を有効化:
-
デプロイスクリプトを実行:
npm run deploy
8.2. Netlify へのデプロイ
-
Netlify にアカウントを作成:
-
Netlify CLI をインストール:
npm install -g netlify-cli
-
Netlify にログイン:
netlify login
-
プロジェクトを Netlify にデプロイ:
netlify deploy --prod --dir=dist
8.3. Vercel へのデプロイ
-
Vercel にアカウントを作成:
-
Vercel CLI をインストール:
npm install -g vercel
-
Vercel にログイン:
vercel login
-
プロジェクトを 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 プロジェクトを構築し、モダンなフロントエンド開発の可能性を広げることができます。