🚀

Vue3のルーティングについて調べてみた

に公開

Vue3のルーティング:Vue Router 4でSPAを構築しよう

Vue3でシングルページアプリケーション(SPA)を構築する際に欠かせないのがルーティング機能です。Vue Router 4は、Vue3に最適化された公式ルーティングライブラリで、モダンなWebアプリケーションの構築を強力にサポートします。

この記事では、Vue Router 4の基本的な使い方から実践的な応用まで、段階的に解説していきます。初心者の方でも理解できるよう、概念の説明から始まり、実際のコード例を豊富に交えながら進めていきます。

Vue Router 4とは?

Vue Router 4は、Vue3専用に設計された公式ルーティングライブラリです。Vue2で使用されていたVue Router 3から大幅にアップデートされ、Composition APIとの親和性が向上し、TypeScriptサポートも強化されています。

ルーティングとは?

ルーティングとは、URLの変更に応じて異なるコンテンツを表示する機能です。従来のWebサイトでは、ページを移動するたびにサーバーから新しいHTMLファイルを取得していましたが、SPAではJavaScriptを使って動的にコンテンツを切り替えます。

従来のWebサイト(MPA):

ユーザーがリンクをクリック → サーバーにリクエスト → 新しいHTMLページを返す → ページ全体が再読み込み

SPA(Single Page Application):

ユーザーがリンクをクリック → JavaScriptがURLを変更 → 対応するコンポーネントを表示 → ページは再読み込みされない

Vue Router 3からの主な変更点

Vue Router 4では、以下のような重要な変更が行われました:

1. Composition API対応

  • Vue Router 3: Options APIのみサポート
  • Vue Router 4: useRouter()とuseRoute()フックを追加し、Composition APIでルーティング機能を利用可能
// Vue Router 3 (Options API)
export default {
  methods: {
    goToAbout() {
      this.$router.push('/about')
    }
  },
  computed: {
    currentPath() {
      return this.$route.path
    }
  }
}

// Vue Router 4 (Composition API)
import { useRouter, useRoute } from 'vue-router'

export default {
  setup() {
    const router = useRouter()
    const route = useRoute()
    
    const goToAbout = () => {
      router.push('/about')
    }
    
    const currentPath = computed(() => route.path)
    
    return { goToAbout, currentPath }
  }
}

2. TypeScriptサポートの強化

  • より厳密な型定義により、開発時の型安全性が向上
  • ルートパラメータやクエリパラメータの型推論が改善

3. パフォーマンスの向上

  • より効率的なルートマッチングアルゴリズム
  • 不要な再レンダリングの削減

4. 新しいナビゲーションガード

  • より柔軟なガード機能
  • 非同期処理の改善

なぜVue Routerが必要なのか?

Vue Routerを使わずにSPAを構築することも可能ですが、以下の理由からVue Routerの使用を強く推奨します:

  1. URL管理: ブラウザの戻る/進むボタンが正常に動作
  2. ブックマーク対応: 特定のページをブックマークできる
  3. SEO対応: サーバーサイドレンダリング(SSR)との連携が容易
  4. 開発効率: ルーティング関連の機能が標準化されている
  5. 保守性: チーム開発での一貫したルーティング実装

基本的なセットアップ

Vue Router 4を使った基本的なセットアップから始めましょう。ここでは、プロジェクトの初期設定から実際にルーティングが動作するまでの手順を詳しく説明します。

インストール

まず、Vue Router 4をプロジェクトにインストールします。

# npmを使用する場合
npm install vue-router@4

# yarnを使用する場合
yarn add vue-router@4

# pnpmを使用する場合
pnpm add vue-router@4

注意: Vue Router 4はVue3専用です。Vue2プロジェクトではVue Router 3を使用してください。

プロジェクト構造の準備

ルーティング機能を整理して管理するために、以下のようなディレクトリ構造を推奨します:

src/
├── components/          # 再利用可能なコンポーネント
├── views/              # ページコンポーネント
│   ├── Home.vue
│   ├── About.vue
│   └── Contact.vue
├── router/             # ルーティング設定
│   └── index.js
├── App.vue
└── main.js

ルーターの設定

router/index.jsファイルでルーターを設定します:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

// ルート定義
const routes = [
  {
    path: '/',           // URLパス
    name: 'Home',        // ルート名(オプション)
    component: Home      // 表示するコンポーネント
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]

// ルーターインスタンスの作成
const router = createRouter({
  history: createWebHistory(),  // HTML5 History APIを使用
  routes                        // ルート定義
})

export default router

ルート設定の詳細説明

各ルートオブジェクトには以下のプロパティを設定できます:

  • path: URLパス(必須)
  • name: ルート名(オプション、プログラム的ナビゲーションで使用)
  • component: 表示するコンポーネント(必須)
  • components: 複数のコンポーネントを表示する場合
  • redirect: リダイレクト先
  • alias: エイリアス(別名)
  • meta: メタデータ
  • props: コンポーネントに渡すプロパティ

History Modeの選択

Vue Router 4では、以下のHistory Modeを選択できます:

// HTML5 History API(推奨)
history: createWebHistory()

// Hash Mode(#付きURL)
history: createWebHashHistory()

// Memory History(テスト用)
history: createMemoryHistory()

HTML5 History APIを使用する場合、サーバー側でSPAのフォールバック設定が必要です。

アプリケーションでの使用

main.jsでルーターをアプリケーションに登録します:

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)

// ルータープラグインを登録
app.use(router)

// アプリケーションをマウント
app.mount('#app')

ルーターリンクとルータービュー

App.vueでナビゲーションとコンテンツ表示を設定します:

<!-- App.vue -->
<template>
  <div id="app">
    <!-- ナビゲーションメニュー -->
    <nav class="navbar">
      <div class="nav-brand">
        <h1>My App</h1>
      </div>
      <div class="nav-links">
        <router-link to="/" class="nav-link">ホーム</router-link>
        <router-link to="/about" class="nav-link">について</router-link>
      </div>
    </nav>
    
    <!-- ルートコンポーネントの表示エリア -->
    <main class="main-content">
      <router-view />
    </main>
  </div>
</template>

<style scoped>
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  background-color: #2c3e50;
  color: white;
}

.nav-links {
  display: flex;
  gap: 1rem;
}

.nav-link {
  color: white;
  text-decoration: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  transition: background-color 0.3s;
}

.nav-link:hover {
  background-color: #34495e;
}

.nav-link.router-link-active {
  background-color: #3498db;
}

.main-content {
  padding: 2rem;
}
</style>

基本的なページコンポーネント

各ページのコンポーネントを作成します:

<!-- views/Home.vue -->
<template>
  <div class="home">
    <h1>ホームページ</h1>
    <p>Vue Router 4を使ったSPAのホームページです。</p>
    <div class="features">
      <h2>主な機能</h2>
      <ul>
        <li>クライアントサイドルーティング</li>
        <li>ブラウザの戻る/進むボタン対応</li>
        <li>ブックマーク対応</li>
        <li>SEO対応(SSR使用時)</li>
      </ul>
    </div>
  </div>
</template>

<script setup>
// Composition APIを使用した場合の例
import { onMounted } from 'vue'

onMounted(() => {
  console.log('ホームページがマウントされました')
})
</script>

<style scoped>
.home {
  max-width: 800px;
  margin: 0 auto;
}

.features {
  margin-top: 2rem;
  padding: 1rem;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.features ul {
  list-style-type: disc;
  margin-left: 2rem;
}
</style>
<!-- views/About.vue -->
<template>
  <div class="about">
    <h1>このアプリについて</h1>
    <p>Vue3とVue Router 4を使用して構築されたサンプルアプリケーションです。</p>
    
    <section class="tech-stack">
      <h2>使用技術</h2>
      <div class="tech-grid">
        <div class="tech-item">
          <h3>Vue 3</h3>
          <p>最新のVue.jsフレームワーク</p>
        </div>
        <div class="tech-item">
          <h3>Vue Router 4</h3>
          <p>公式ルーティングライブラリ</p>
        </div>
        <div class="tech-item">
          <h3>Composition API</h3>
          <p>Vue3の新しいAPI</p>
        </div>
      </div>
    </section>
  </div>
</template>

<style scoped>
.about {
  max-width: 800px;
  margin: 0 auto;
}

.tech-stack {
  margin-top: 2rem;
}

.tech-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}

.tech-item {
  padding: 1rem;
  border: 1px solid #ddd;
  border-radius: 8px;
  text-align: center;
}

.tech-item h3 {
  color: #2c3e50;
  margin-bottom: 0.5rem;
}
</style>

動作確認

セットアップが完了したら、以下の手順で動作を確認してください:

  1. 開発サーバーを起動
npm run dev
# または
yarn dev
  1. ブラウザでアプリケーションにアクセス
  2. ナビゲーションリンクをクリックしてページが切り替わることを確認
  3. ブラウザの戻る/進むボタンが正常に動作することを確認
  4. 直接URLを入力してページが表示されることを確認

これで基本的なVue Router 4のセットアップが完了しました。次のセクションでは、より高度なナビゲーション機能について説明します。

ナビゲーション

Vue Router 4では、ページ間の移動を実現するために2つの主要なナビゲーション方法があります:宣言的ナビゲーションとプログラム的ナビゲーションです。それぞれの特徴と使い分けについて詳しく説明します。

宣言的ナビゲーション

宣言的ナビゲーションは、<router-link>コンポーネントを使用してテンプレート内でリンクを定義する方法です。HTMLの<a>タグと似た使い方で、直感的で理解しやすいのが特徴です。

基本的な使い方

<template>
  <div class="navigation">
    <!-- 基本的なリンク -->
    <router-link to="/">ホーム</router-link>
    <router-link to="/about">について</router-link>
    <router-link to="/contact">お問い合わせ</router-link>
  </div>
</template>

名前付きルートの使用

ルートにnameプロパティを設定している場合、パスではなく名前でリンクを指定できます:

<template>
  <div class="navigation">
    <!-- 名前付きルートを使用 -->
    <router-link :to="{ name: 'Home' }">ホーム</router-link>
    <router-link :to="{ name: 'About' }">について</router-link>
    <router-link :to="{ name: 'Contact' }">お問い合わせ</router-link>
  </div>
</template>

名前付きルートの利点:

  • URLパスが変更されても、名前が変わらない限りリンクが壊れない
  • より読みやすく、保守しやすいコード
  • リファクタリング時の影響を最小限に抑制

パラメータとクエリの使用

<template>
  <div class="navigation">
    <!-- パラメータ付きルート -->
    <router-link :to="{ name: 'User', params: { id: 123 } }">
      ユーザー詳細
    </router-link>
    
    <!-- クエリパラメータ付き -->
    <router-link :to="{ name: 'Search', query: { q: 'vue', page: 1 } }">
      検索結果
    </router-link>
    
    <!-- パラメータとクエリの組み合わせ -->
    <router-link :to="{ 
      name: 'Product', 
      params: { id: 456 }, 
      query: { color: 'red', size: 'L' } 
    }">
      商品詳細
    </router-link>
  </div>
</template>

アクティブクラスのカスタマイズ

現在のルートにマッチするリンクには自動的にrouter-link-activeクラスが適用されます。これをカスタマイズできます:

<template>
  <nav class="navbar">
    <router-link 
      to="/" 
      class="nav-link"
      active-class="nav-link-active"
      exact-active-class="nav-link-exact-active"
    >
      ホーム
    </router-link>
    
    <router-link 
      to="/about" 
      class="nav-link"
      active-class="nav-link-active"
      exact-active-class="nav-link-exact-active"
    >
      について
    </router-link>
  </nav>
</template>

<style scoped>
.nav-link {
  padding: 0.5rem 1rem;
  text-decoration: none;
  color: #666;
  border-radius: 4px;
  transition: all 0.3s;
}

.nav-link-active {
  background-color: #e3f2fd;
  color: #1976d2;
}

.nav-link-exact-active {
  background-color: #1976d2;
  color: white;
  font-weight: bold;
}
</style>

active-class vs exact-active-class:

  • active-class: ルートが部分的にマッチした場合に適用
  • exact-active-class: ルートが完全にマッチした場合に適用

カスタムタグの使用

<router-link>はデフォルトで<a>タグとしてレンダリングされますが、tagプロパティで変更できます:

<template>
  <div class="button-navigation">
    <!-- ボタンとしてレンダリング -->
    <router-link to="/" tag="button" class="btn">
      ホーム
    </router-link>
    
    <!-- divとしてレンダリング -->
    <router-link to="/about" tag="div" class="nav-item">
      について
    </router-link>
  </div>
</template>

プログラム的ナビゲーション

プログラム的ナビゲーションは、JavaScriptコード内でルーターインスタンスを使用してページを移動する方法です。ユーザーの操作や条件に応じて動的にナビゲーションを制御したい場合に使用します。

Composition APIでの基本的な使い方

<template>
  <div class="navigation-controls">
    <button @click="goToHome">ホームへ</button>
    <button @click="goToAbout">についてへ</button>
    <button @click="goToUser">ユーザーページへ</button>
    <button @click="goBack">戻る</button>
    <button @click="goForward">進む</button>
  </div>
</template>

<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

// 基本的なナビゲーション
const goToHome = () => {
  router.push('/')
}

const goToAbout = () => {
  router.push('/about')
}

// 名前付きルートを使用
const goToUser = () => {
  router.push({ name: 'User', params: { id: 123 } })
}

// 履歴操作
const goBack = () => {
  router.go(-1)  // 1つ前のページに戻る
}

const goForward = () => {
  router.go(1)   // 1つ先のページに進む
}
</script>

ナビゲーションメソッドの詳細

Vue Router 4では以下のナビゲーションメソッドが利用できます:

// router.push() - 新しいエントリを履歴スタックに追加
router.push('/about')
router.push({ name: 'About' })
router.push({ name: 'User', params: { id: 123 } })

// router.replace() - 現在のエントリを置き換える(履歴に追加しない)
router.replace('/about')
router.replace({ name: 'About' })

// router.go() - 履歴を指定した数だけ進む/戻る
router.go(1)    // 1つ進む
router.go(-1)   // 1つ戻る
router.go(0)    // 現在のページをリロード

// router.back() - 1つ戻る(router.go(-1)と同じ)
router.back()

// router.forward() - 1つ進む(router.go(1)と同じ)
router.forward()

条件付きナビゲーション

ユーザーの状態や条件に応じてナビゲーションを制御する例:

<template>
  <div class="user-actions">
    <button @click="handleLogin">ログイン</button>
    <button @click="handleLogout">ログアウト</button>
    <button @click="viewProfile">プロフィール表示</button>
  </div>
</template>

<script setup>
import { useRouter } from 'vue-router'
import { ref } from 'vue'

const router = useRouter()
const isLoggedIn = ref(false)

const handleLogin = () => {
  // ログイン処理
  isLoggedIn.value = true
  // ログイン後にダッシュボードにリダイレクト
  router.push('/dashboard')
}

const handleLogout = () => {
  // ログアウト処理
  isLoggedIn.value = false
  // ログアウト後にホームページにリダイレクト
  router.replace('/')
}

const viewProfile = () => {
  if (isLoggedIn.value) {
    router.push('/profile')
  } else {
    // 未ログインの場合はログインページにリダイレクト
    router.push('/login')
  }
}
</script>

非同期ナビゲーション

データの取得や処理を待ってからナビゲーションする例:

<script setup>
import { useRouter } from 'vue-router'
import { ref } from 'vue'

const router = useRouter()
const isLoading = ref(false)

const saveAndNavigate = async () => {
  isLoading.value = true
  
  try {
    // データの保存処理
    await saveUserData()
    
    // 保存成功後にナビゲーション
    router.push('/success')
  } catch (error) {
    console.error('保存に失敗しました:', error)
    // エラーページにナビゲーション
    router.push('/error')
  } finally {
    isLoading.value = false
  }
}

const saveUserData = () => {
  return new Promise((resolve, reject) => {
    // 実際のAPI呼び出しをシミュレート
    setTimeout(() => {
      Math.random() > 0.5 ? resolve() : reject(new Error('保存失敗'))
    }, 1000)
  })
}
</script>

ナビゲーションのキャンセル

ナビゲーションをキャンセルしたい場合の例:

<script setup>
import { useRouter } from 'vue-router'
import { ref } from 'vue'

const router = useRouter()
const hasUnsavedChanges = ref(false)

const navigateWithConfirmation = (to) => {
  if (hasUnsavedChanges.value) {
    const confirmed = confirm('保存されていない変更があります。本当に移動しますか?')
    if (confirmed) {
      router.push(to)
    }
  } else {
    router.push(to)
  }
}

const saveChanges = () => {
  // 保存処理
  hasUnsavedChanges.value = false
  console.log('変更が保存されました')
}
</script>

ナビゲーションの使い分け

宣言的ナビゲーション(<router-link>)を使う場合:

  • 静的なナビゲーションメニュー
  • シンプルなページ間の移動
  • SEOを重視する場合(検索エンジンがリンクを認識しやすい)

プログラム的ナビゲーション(router.push()など)を使う場合:

  • ユーザーの操作に応じた動的なナビゲーション
  • 条件付きのページ移動
  • データ処理後のリダイレクト
  • フォーム送信後のページ移動

パフォーマンスの考慮事項

大量のリンクがある場合の最適化例:

<template>
  <nav class="mega-menu">
    <div v-for="category in categories" :key="category.id" class="category">
      <h3>{{ category.name }}</h3>
      <ul>
        <li v-for="item in category.items" :key="item.id">
          <router-link 
            :to="{ name: 'Product', params: { id: item.id } }"
            class="product-link"
          >
            {{ item.name }}
          </router-link>
        </li>
      </ul>
    </div>
  </nav>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const categories = ref([])

onMounted(async () => {
  // カテゴリデータを非同期で取得
  categories.value = await fetchCategories()
})

const fetchCategories = async () => {
  // 実際のAPI呼び出し
  return [
    { id: 1, name: 'カテゴリ1', items: [...] },
    { id: 2, name: 'カテゴリ2', items: [...] }
  ]
}
</script>

これで、Vue Router 4のナビゲーション機能について包括的に理解できました。次のセクションでは、動的ルーティングについて詳しく説明します。

動的ルーティング

動的ルーティングは、URLパスに変数を含めることで、同じコンポーネントで異なるデータを表示する機能です。例えば、ユーザーIDや商品IDなどの動的な値をURLに含めて、それに応じてコンテンツを切り替えることができます。

パラメータ付きルート

基本的なパラメータ

URLパスにコロン(:)を付けることで、動的パラメータを定義できます:

// router/index.js
const routes = [
  {
    path: '/user/:id',        // :id が動的パラメータ
    name: 'User',
    component: User
  },
  {
    path: '/product/:category/:id',  // 複数のパラメータ
    name: 'Product',
    component: Product
  },
  {
    path: '/user/:id/post/:postId',  // ネストしたパラメータ
    name: 'UserPost',
    component: UserPost
  }
]

URL例:

  • /user/123 → { id: '123' }
  • /product/electronics/456 → { category: 'electronics', id: '456' }
  • /user/123/post/789 → { id: '123', postId: '789' }

パラメータの取得

Composition APIを使用してパラメータを取得する方法:

<!-- User.vue -->
<template>
  <div class="user-profile">
    <h1>ユーザー詳細</h1>
    <div v-if="loading" class="loading">読み込み中...</div>
    <div v-else-if="user" class="user-info">
      <img :src="user.avatar" :alt="user.name" class="avatar">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
      <p>ユーザーID: {{ userId }}</p>
    </div>
    <div v-else class="error">ユーザーが見つかりません</div>
  </div>
</template>

<script setup>
import { useRoute } from 'vue-router'
import { ref, watch, onMounted } from 'vue'

const route = useRoute()
const userId = ref(route.params.id)
const user = ref(null)
const loading = ref(false)

// パラメータの変更を監視
watch(() => route.params.id, async (newId) => {
  userId.value = newId
  await loadUser(newId)
})

onMounted(() => {
  loadUser(userId.value)
})

const loadUser = async (id) => {
  loading.value = true
  try {
    const response = await fetch(`/api/users/${id}`)
    if (response.ok) {
      user.value = await response.json()
    } else {
      user.value = null
    }
  } catch (error) {
    console.error('ユーザー情報の取得に失敗:', error)
    user.value = null
  } finally {
    loading.value = false
  }
}
</script>

<style scoped>
.user-profile {
  max-width: 600px;
  margin: 0 auto;
  padding: 2rem;
}

.loading {
  text-align: center;
  padding: 2rem;
}

.user-info {
  text-align: center;
}

.avatar {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  margin-bottom: 1rem;
}

.error {
  color: #e74c3c;
  text-align: center;
  padding: 2rem;
}
</style>

パラメータの型変換

URLパラメータは常に文字列として取得されます。数値として使用したい場合は変換が必要です:

<script setup>
import { useRoute } from 'vue-router'
import { computed } from 'vue'

const route = useRoute()

// 文字列として取得
const userIdString = route.params.id

// 数値に変換
const userId = computed(() => parseInt(route.params.id, 10))

// バリデーション付きの変換
const validUserId = computed(() => {
  const id = parseInt(route.params.id, 10)
  return isNaN(id) ? null : id
})

// 使用例
const loadUserData = async () => {
  if (validUserId.value) {
    const userData = await fetchUser(validUserId.value)
    return userData
  } else {
    throw new Error('無効なユーザーIDです')
  }
}
</script>

オプショナルパラメータとワイルドカード

オプショナルパラメータ

パラメータの後に?を付けることで、オプショナル(任意)にできます:

const routes = [
  {
    path: '/user/:id?',  // idはオプショナル
    name: 'User',
    component: User
  },
  {
    path: '/blog/:category?/:post?',  // 両方ともオプショナル
    name: 'Blog',
    component: Blog
  }
]

URL例:

  • /user → { id: undefined }
  • /user/123 → { id: '123' }
  • /blog → { category: undefined, post: undefined }
  • /blog/tech → { category: 'tech', post: undefined }
  • /blog/tech/vue3 → { category: 'tech', post: 'vue3' }

ワイルドカード

*を使用してワイルドカードパターンを定義できます:

const routes = [
  {
    path: '/user-*',  // /user-で始まる任意のパス
    name: 'UserWildcard',
    component: UserWildcard
  },
  {
    path: '/files/*',  // /files/で始まる任意のパス
    name: 'FileBrowser',
    component: FileBrowser
  }
]

404ページの実装

すべてのパスにマッチするルートを定義して、404ページを実装できます:

const routes = [
  // 通常のルート
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  // 404ページ(最後に配置)
  {
    path: '/:pathMatch(.*)*',  // すべてのパスにマッチ
    name: 'NotFound',
    component: NotFound
  }
]
<!-- NotFound.vue -->
<template>
  <div class="not-found">
    <div class="error-container">
      <h1>404</h1>
      <h2>ページが見つかりません</h2>
      <p>お探しのページは存在しないか、移動された可能性があります。</p>
      <div class="actions">
        <router-link to="/" class="btn btn-primary">ホームに戻る</router-link>
        <button @click="goBack" class="btn btn-secondary">前のページに戻る</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

const goBack = () => {
  router.go(-1)
}
</script>

<style scoped>
.not-found {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 60vh;
}

.error-container {
  text-align: center;
  max-width: 500px;
  padding: 2rem;
}

.error-container h1 {
  font-size: 6rem;
  color: #e74c3c;
  margin: 0;
}

.error-container h2 {
  color: #2c3e50;
  margin: 1rem 0;
}

.actions {
  margin-top: 2rem;
  display: flex;
  gap: 1rem;
  justify-content: center;
}

.btn {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 4px;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.3s;
}

.btn-primary {
  background-color: #3498db;
  color: white;
}

.btn-primary:hover {
  background-color: #2980b9;
}

.btn-secondary {
  background-color: #95a5a6;
  color: white;
}

.btn-secondary:hover {
  background-color: #7f8c8d;
}
</style>

パラメータのバリデーション

ルートレベルでパラメータのバリデーションを行うことができます:

const routes = [
  {
    path: '/user/:id',
    name: 'User',
    component: User,
    beforeEnter: (to, from, next) => {
      const id = parseInt(to.params.id, 10)
      if (isNaN(id) || id <= 0) {
        next('/404')  // 無効なIDの場合は404ページへ
      } else {
        next()
      }
    }
  },
  {
    path: '/product/:category/:id',
    name: 'Product',
    component: Product,
    beforeEnter: (to, from, next) => {
      const validCategories = ['electronics', 'clothing', 'books']
      const category = to.params.category
      const id = parseInt(to.params.id, 10)
      
      if (!validCategories.includes(category) || isNaN(id)) {
        next('/404')
      } else {
        next()
      }
    }
  }
]

動的ルートの実践例

ブログシステムの例

// router/index.js
const routes = [
  {
    path: '/blog',
    name: 'BlogList',
    component: BlogList
  },
  {
    path: '/blog/:category',
    name: 'BlogCategory',
    component: BlogCategory,
    props: true  // パラメータをpropsとして渡す
  },
  {
    path: '/blog/:category/:slug',
    name: 'BlogPost',
    component: BlogPost,
    props: true
  }
]
<!-- BlogPost.vue -->
<template>
  <article class="blog-post">
    <header class="post-header">
      <nav class="breadcrumb">
        <router-link to="/blog">ブログ</router-link>
        <span> / </span>
        <router-link :to="{ name: 'BlogCategory', params: { category } }">
          {{ categoryName }}
        </router-link>
        <span> / </span>
        <span>{{ post.title }}</span>
      </nav>
      <h1>{{ post.title }}</h1>
      <div class="post-meta">
        <span class="author">{{ post.author }}</span>
        <span class="date">{{ formatDate(post.publishedAt) }}</span>
      </div>
    </header>
    <div class="post-content" v-html="post.content"></div>
  </article>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'

// propsとしてパラメータを受け取る
const props = defineProps({
  category: String,
  slug: String
})

const route = useRoute()
const post = ref(null)
const loading = ref(false)

// カテゴリ名のマッピング
const categoryNames = {
  'tech': 'テクノロジー',
  'lifestyle': 'ライフスタイル',
  'travel': '旅行'
}

const categoryName = computed(() => 
  categoryNames[props.category] || props.category
)

onMounted(async () => {
  await loadPost()
})

const loadPost = async () => {
  loading.value = true
  try {
    const response = await fetch(`/api/blog/${props.category}/${props.slug}`)
    if (response.ok) {
      post.value = await response.json()
    } else {
      // 404ページにリダイレクト
      route.router.push('/404')
    }
  } catch (error) {
    console.error('記事の読み込みに失敗:', error)
  } finally {
    loading.value = false
  }
}

const formatDate = (dateString) => {
  return new Date(dateString).toLocaleDateString('ja-JP')
}
</script>

パフォーマンスの最適化

大量の動的ルートがある場合の最適化例:

// 遅延読み込みを使用
const routes = [
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('../views/User.vue'),
    // メタデータでキャッシュ戦略を指定
    meta: { 
      cache: true,
      cacheTime: 300000  // 5分間キャッシュ
    }
  }
]
<!-- User.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const user = ref(null)
const cache = new Map()

onMounted(async () => {
  const userId = route.params.id
  
  // キャッシュをチェック
  if (cache.has(userId)) {
    user.value = cache.get(userId)
    return
  }
  
  // APIからデータを取得
  const userData = await fetchUser(userId)
  user.value = userData
  
  // キャッシュに保存
  cache.set(userId, userData)
})

onUnmounted(() => {
  // 必要に応じてキャッシュをクリア
  const cacheTime = route.meta.cacheTime || 300000
  setTimeout(() => {
    cache.delete(route.params.id)
  }, cacheTime)
})
</script>

これで、動的ルーティングの包括的な理解ができました。次のセクションでは、ネストしたルートについて詳しく説明します。

ネストしたルート

子ルートの定義

// router/index.js
const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        path: '',
        component: UserProfile
      },
      {
        path: 'posts',
        component: UserPosts
      },
      {
        path: 'settings',
        component: UserSettings
      }
    ]
  }
]

ネストしたルートの表示

<!-- User.vue -->
<template>
  <div class="user">
    <h1>ユーザーページ</h1>
    <nav>
      <router-link to="/user/123">プロフィール</router-link>
      <router-link to="/user/123/posts">投稿</router-link>
      <router-link to="/user/123/settings">設定</router-link>
    </nav>
    <router-view />
  </div>
</template>

ナビゲーションガード

グローバルガード

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes
})

// グローバルビフォーガード
router.beforeEach((to, from, next) => {
  console.log('ナビゲーション前:', to.path)
  
  // 認証チェック
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

// グローバルアフターガード
router.afterEach((to, from) => {
  console.log('ナビゲーション後:', to.path)
})

ルート固有のガード

const routes = [
  {
    path: '/admin',
    component: Admin,
    beforeEnter: (to, from, next) => {
      if (isAdmin()) {
        next()
      } else {
        next('/')
      }
    }
  }
]

コンポーネント内ガード

<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

// ルートを離れる前
onBeforeRouteLeave((to, from, next) => {
  if (hasUnsavedChanges.value) {
    if (confirm('保存されていない変更があります。本当に離れますか?')) {
      next()
    } else {
      next(false)
    }
  } else {
    next()
  }
})

// 同じコンポーネント内でルートが更新される前
onBeforeRouteUpdate((to, from, next) => {
  // パラメータが変更された時の処理
  loadUserData(to.params.id)
  next()
})
</script>

ルートメタフィールド

メタデータの定義

const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      requiresAdmin: true,
      title: '管理画面'
    }
  },
  {
    path: '/profile',
    component: Profile,
    meta: {
      requiresAuth: true,
      title: 'プロフィール'
    }
  }
]

メタデータの使用

<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()

// ページタイトルの設定
import { watch } from 'vue'
watch(() => route.meta.title, (newTitle) => {
  document.title = newTitle || 'デフォルトタイトル'
}, { immediate: true })
</script>

実践的な使用例

認証付きアプリケーション

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
    meta: { guest: true }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: { requiresAuth: true }
  },
  {
    path: '/admin',
    name: 'Admin',
    component: Admin,
    meta: { requiresAuth: true, requiresAdmin: true }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// 認証チェック
router.beforeEach((to, from, next) => {
  const isAuthenticated = checkAuth()
  const isAdmin = checkAdmin()
  
  if (to.meta.requiresAuth && !isAuthenticated) {
    next('/login')
  } else if (to.meta.requiresAdmin && !isAdmin) {
    next('/dashboard')
  } else if (to.meta.guest && isAuthenticated) {
    next('/dashboard')
  } else {
    next()
  }
})

export default router

遅延読み込み(Lazy Loading)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue')
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('../views/Admin.vue'),
    meta: { requiresAuth: true }
  }
]

エラーハンドリング

<!-- ErrorBoundary.vue -->
<template>
  <div v-if="error" class="error">
    <h1>エラーが発生しました</h1>
    <p>{{ error.message }}</p>
    <button @click="retry">再試行</button>
  </div>
  <router-view v-else />
</template>

<script setup>
import { ref, onErrorCaptured } from 'vue'

const error = ref(null)

onErrorCaptured((err) => {
  error.value = err
  return false
})

const retry = () => {
  error.value = null
}
</script>

トラブルシューティング

Vue Router 4を使用する際によく遭遇する問題とその解決方法について説明します。

よくある問題と解決方法

1. ページが表示されない(404エラー)

問題: ルートを設定したのにページが表示されない

原因と解決方法:

// ❌ 間違った例
const routes = [
  {
    path: '/user/:id',
    component: User  // componentが未定義
  }
]

// ✅ 正しい例
import User from '../views/User.vue'

const routes = [
  {
    path: '/user/:id',
    component: User
  }
]

チェックポイント:

  • コンポーネントが正しくインポートされているか
  • ルートのパスが正しく設定されているか
  • <router-view />が配置されているか

2. ナビゲーションが動作しない

問題: <router-link>をクリックしてもページが切り替わらない

解決方法:

<!-- ❌ 間違った例 -->
<template>
  <a href="/about">について</a>  <!-- 通常のaタグ -->
</template>

<!-- ✅ 正しい例 -->
<template>
  <router-link to="/about">について</router-link>
</template>

3. パラメータが取得できない

問題: 動的ルートでパラメータが取得できない

解決方法:

<script setup>
import { useRoute } from 'vue-router'
import { watch } from 'vue'

const route = useRoute()

// ❌ 間違った例(リアクティブでない)
const userId = route.params.id

// ✅ 正しい例(リアクティブ)
const userId = computed(() => route.params.id)

// パラメータの変更を監視
watch(() => route.params.id, (newId) => {
  console.log('新しいID:', newId)
})
</script>

4. 履歴が正しく動作しない

問題: ブラウザの戻る/進むボタンが期待通りに動作しない

解決方法:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  // ✅ HTML5 History APIを使用
  history: createWebHistory(),
  routes
})

// ❌ Hash Modeは通常推奨されない
// history: createWebHashHistory()

サーバー設定が必要:

# Nginx設定例
location / {
  try_files $uri $uri/ /index.html;
}

5. ナビゲーションガードが無限ループする

問題: 認証チェックで無限ループが発生する

解決方法:

// ❌ 間違った例(無限ループ)
router.beforeEach((to, from, next) => {
  if (!isAuthenticated()) {
    router.push('/login')  // これが無限ループを引き起こす
  } else {
    next()
  }
})

// ✅ 正しい例
router.beforeEach((to, from, next) => {
  if (!isAuthenticated() && to.path !== '/login') {
    next('/login')  // next()を使用
  } else {
    next()
  }
})

6. コンポーネントが再マウントされない

問題: 同じコンポーネント内でパラメータが変更されても再レンダリングされない

解決方法:

<script setup>
import { useRoute } from 'vue-router'
import { watch, onMounted } from 'vue'

const route = useRoute()

// パラメータの変更を監視
watch(() => route.params.id, async (newId) => {
  await loadData(newId)
})

// または onBeforeRouteUpdate を使用
import { onBeforeRouteUpdate } from 'vue-router'

onBeforeRouteUpdate(async (to, from, next) => {
  await loadData(to.params.id)
  next()
})
</script>

7. メタデータが正しく取得できない

問題: ルートのメタデータが取得できない

解決方法:

// router/index.js
const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      title: '管理画面'
    }
  }
]
<script setup>
import { useRoute } from 'vue-router'
import { computed } from 'vue'

const route = useRoute()

// ✅ 正しい取得方法
const pageTitle = computed(() => route.meta.title)
const requiresAuth = computed(() => route.meta.requiresAuth)
</script>

デバッグのコツ

1. ルーターの状態を確認

<script setup>
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

// デバッグ用
console.log('現在のルート:', route)
console.log('ルーターインスタンス:', router)
</script>

2. ナビゲーションガードでのデバッグ

router.beforeEach((to, from, next) => {
  console.log('ナビゲーション前:', {
    to: to.path,
    from: from.path,
    params: to.params,
    query: to.query
  })
  next()
})

3. ルートマッチングの確認

// ルートが正しくマッチするかテスト
const matchedRoutes = router.resolve('/user/123')
console.log('マッチしたルート:', matchedRoutes)

パフォーマンスの問題

1. 大量のルートがある場合

// 遅延読み込みを使用
const routes = [
  {
    path: '/user/:id',
    component: () => import('../views/User.vue')
  }
]

2. 不要な再レンダリングを防ぐ

<script setup>
import { useRoute } from 'vue-router'
import { computed } from 'vue'

const route = useRoute()

// 必要な部分のみをリアクティブにする
const userId = computed(() => route.params.id)
const queryParams = computed(() => route.query)
</script>

エラーハンドリング

1. ルートエラーの処理

// router/index.js
const router = createRouter({
  history: createWebHistory(),
  routes
})

// エラーハンドリング
router.onError((error) => {
  console.error('ルーターエラー:', error)
  // エラーページにリダイレクト
  router.push('/error')
})

2. 非同期コンポーネントのエラー

const routes = [
  {
    path: '/user/:id',
    component: () => import('../views/User.vue').catch(() => {
      // フォールバックコンポーネント
      return import('../views/Error.vue')
    })
  }
]

ベストプラクティス

1. ルートの整理

// routes/index.js - ルートを分割
export const publicRoutes = [
  { path: '/', component: Home },
  { path: '/about', component: About }
]

export const authRoutes = [
  { path: '/login', component: Login },
  { path: '/register', component: Register }
]

export const protectedRoutes = [
  { path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } }
]

2. 型安全性の向上(TypeScript使用時)

// types/router.ts
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    title?: string
    roles?: string[]
  }
}

3. ルートのテスト

// tests/router.test.js
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from '../router'

describe('Router', () => {
  const router = createRouter({
    history: createWebHistory(),
    routes
  })

  test('ルートが正しく設定されている', () => {
    expect(router.getRoutes()).toHaveLength(routes.length)
  })

  test('ユーザールートが正しくマッチする', () => {
    const resolved = router.resolve('/user/123')
    expect(resolved.name).toBe('User')
    expect(resolved.params.id).toBe('123')
  })
})

まとめ

Vue Router 4は、Vue3アプリケーションのルーティングを強力にサポートするライブラリです。この記事で紹介した内容を活用することで:

  • 基本的なSPAの構築: シンプルなルーティングから始められます
  • 動的ルーティング: パラメータを使った柔軟なルート設計
  • 認証システム: ナビゲーションガードを使った安全なアプリケーション
  • パフォーマンス最適化: 遅延読み込みによる効率的なバンドル管理
  • トラブルシューティング: よくある問題の解決方法

Vue Router 4の機能を理解し、適切に活用することで、ユーザーフレンドリーで保守性の高いWebアプリケーションを構築できます。

学習の次のステップ

  1. 実際のプロジェクトで実践: 小さなプロジェクトから始めて、段階的に機能を追加
  2. SSRとの連携: Nuxt.jsなどでのサーバーサイドレンダリング
  3. 高度な機能: ルートの遅延読み込み、プリフェッチング
  4. テスト: ルーティング機能のユニットテストとE2Eテスト

参考リンク


この記事がVue3のルーティング学習の参考になれば幸いです。質問やフィードバックがあれば、お気軽にコメントしてください!

Discussion