第8章: Vue Router で画面遷移

第8章: Vue Router で画面遷移

概要

本章では、Vue Routerを使用して、単一ページアプリケーション(SPA)の画面遷移を実装する方法について学びます。ルートの定義、ナビゲーションの実装、パラメータの受け渡し、ルートガードなどの重要な概念について理解を深めます。

1. Vue Routerの概要

Vue Routerは、Vue.jsの公式ルーティングライブラリです。単一ページアプリケーション(SPA)を構築するための重要な要素であり、URLに基づいてコンポーネントをレンダリングします。Vue Routerを使用すると、ページ全体をリロードすることなく、アプリケーション内を移動できます。

Vue Routerの主な機能

2. Vue Routerのインストールと設定

まず、Vue Routerをインストールし、Vueアプリケーションに統合する方法を学びましょう。

インストール

npm(またはyarn)を使用して、Vue Routerをインストールします:

npm install vue-router@4

Vue Router 4は、Vue.js 3と互換性があります。Vue.js 2を使用している場合は、Vue Router 3をインストールする必要があります。

基本的な設定

Vue Routerを設定するには、ルート定義を作成し、ルーターインスタンスを作成して、Vueアプリケーションに登録します。

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

// ルート定義
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    // 404ページ
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
  }
]

// ルーターインスタンスの作成
const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

次に、作成したルーターをVueアプリケーションに登録します:

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

createApp(App)
  .use(router)
  .mount('#app')

ルーターの使用

ルーターをアプリケーションに統合するには、<router-view>コンポーネントを使用します。これは、現在のルートに一致するコンポーネントをレンダリングするプレースホルダーです。

<!-- App.vue -->
<template>
  <div id="app">
    <nav>
      <router-link to="/">ホーム</router-link> |
      <router-link to="/about">概要</router-link>
    </nav>
    <router-view />
  </div>
</template>

3. ルートの定義と構成

ルート定義は、パス、名前、コンポーネント、その他のオプションで構成されます。

基本的なルート

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

動的セグメント

動的セグメントは、コロン(:)を使用して定義します。これにより、URLの一部をパラメータとして捕捉できます。

{
  path: '/user/:id',
  name: 'User',
  component: User
}

ネストされたルート

childrenプロパティを使用して、ネストされたルートを定義できます。

{
  path: '/user/:id',
  name: 'User',
  component: User,
  children: [
    {
      path: 'profile',  // /user/:id/profile に一致
      name: 'UserProfile',
      component: UserProfile
    },
    {
      path: 'posts',    // /user/:id/posts に一致
      name: 'UserPosts',
      component: UserPosts
    }
  ]
}

ネストされたルートをレンダリングするには、親コンポーネント内に<router-view>を配置します。

<!-- User.vue -->
<template>
  <div>
    <h2>ユーザー {{ $route.params.id }}</h2>
    <nav>
      <router-link :to="{ name: 'UserProfile', params: { id: $route.params.id } }">プロフィール</router-link> |
      <router-link :to="{ name: 'UserPosts', params: { id: $route.params.id } }">投稿</router-link>
    </nav>
    <router-view />
  </div>
</template>

名前付きビュー

複数の<router-view>を同時にレンダリングする場合は、名前付きビューを使用できます。

{
  path: '/dashboard',
  name: 'Dashboard',
  components: {
    default: DashboardMain,
    sidebar: DashboardSidebar,
    header: DashboardHeader
  }
}
<!-- App.vue -->
<template>
  <div id="app">
    <router-view name="header" />
    <div class="content">
      <router-view name="sidebar" />
      <router-view /> <!-- default -->
    </div>
  </div>
</template>

リダイレクト

特定のURLから別のURLにリダイレクトするルートを定義できます。

{
  path: '/home',
  redirect: '/'
}

// または名前付きルートへのリダイレクト
{
  path: '/home',
  redirect: { name: 'Home' }
}

// または動的リダイレクト
{
  path: '/search/:query',
  redirect: to => {
    // 関数はルートオブジェクトを受け取り、リダイレクト先を返す
    return { path: '/results', query: { q: to.params.query } }
  }
}

エイリアス

エイリアスを使用すると、異なるURLで同じコンポーネントにアクセスできます。

{
  path: '/posts',
  name: 'Posts',
  component: PostList,
  alias: ['/articles', '/blog']
}

遅延ローディング(コード分割)

大規模なアプリケーションでは、必要なときにのみコンポーネントをロードする遅延ローディングが便利です。

const routes = [
  {
    path: '/about',
    name: 'About',
    // 動的インポートを使用した遅延ローディング
    component: () => import('@/views/About.vue')
  }
]

4. ルーターリンクとナビゲーション

Vue Routerは、ページ間のナビゲーションを実装するためのいくつかの方法を提供します。

router-linkコンポーネント

<router-link>コンポーネントは、ナビゲーションを宣言的に実装するために使用されます。

<!-- 基本的な使用法 -->
<router-link to="/">ホーム</router-link>

<!-- 完全なURL -->
<router-link to="/about">概要</router-link>

<!-- 名前付きルート -->
<router-link :to="{ name: 'User', params: { id: 123 } }">ユーザー</router-link>

<!-- クエリパラメータ -->
<router-link :to="{ path: '/search', query: { q: 'vue' } }">検索</router-link>

<!-- アクティブクラス -->
<router-link to="/about" active-class="active">概要</router-link>

<!-- 完全一致 -->
<router-link to="/user" exact>ユーザー</router-link>

プログラムによるナビゲーション

JavaScriptコード内でナビゲーションを実行する場合は、ルーターインスタンスのメソッドを使用できます。

// Composition API
import { useRouter } from 'vue-router'

export default {
  setup() {
    const router = useRouter()
    
    function goToHome() {
      // パスによるナビゲーション
      router.push('/')
    }
    
    function goToUser(userId) {
      // オブジェクトによるナビゲーション
      router.push({
        name: 'User',
        params: { id: userId }
      })
    }
    
    function goToSearch(query) {
      // クエリパラメータ付きナビゲーション
      router.push({
        path: '/search',
        query: { q: query }
      })
    }
    
    function goBack() {
      // 履歴を戻る
      router.back()
    }
    
    function goForward() {
      // 履歴を進む
      router.forward()
    }
    
    function replaceCurrentPage() {
      // 現在のページを置き換え(履歴に追加しない)
      router.replace('/login')
    }
    
    return {
      goToHome,
      goToUser,
      goToSearch,
      goBack,
      goForward,
      replaceCurrentPage
    }
  }
}

5. ルートパラメータとクエリ

ルートパラメータとクエリを使用して、ルート間でデータを渡すことができます。

ルートパラメータへのアクセス

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

const route = useRoute()

// URLが /user/123 の場合、route.params.id は "123"
console.log(route.params.id)
</script>

<template>
  <div>
    <h1>ユーザーID: {{ $route.params.id }}</h1>
  </div>
</template>

クエリパラメータへのアクセス

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

const route = useRoute()

// URLが /search?q=vue&sort=desc の場合
console.log(route.query.q)     // "vue"
console.log(route.query.sort)  // "desc"
</script>

<template>
  <div>
    <h1>検索結果: {{ $route.query.q }}</h1>
    <p>並び順: {{ $route.query.sort || 'デフォルト' }}</p>
  </div>
</template>

パラメータの変更を監視

同じコンポーネントを再利用しながらパラメータが変更された場合、コンポーネントは再マウントされません。パラメータの変更を監視するには、watchを使用します。

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

const route = useRoute()

// コンポーネントのマウント時とパラメータが変更されたときに実行
function fetchUserData() {
  const userId = route.params.id
  console.log(`ユーザーID ${userId} のデータを取得中...`)
  // ユーザーデータをフェッチするAPI呼び出し
}

// 初期ロード
fetchUserData()

// パラメータの変更を監視
watch(
  () => route.params.id,
  (newId, oldId) => {
    fetchUserData()
  }
)
</script>

6. ルートガード

ルートガードは、特定のルートへのナビゲーションを制御するための仕組みです。ユーザー認証や権限チェックなどの用途に使用できます。

グローバルガード

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

// グローバル前処理ガード
router.beforeEach((to, from, next) => {
  // ナビゲーションを実行するには next() を呼び出す
  // ナビゲーションを中止するには next(false) を呼び出す
  // 別のルートにリダイレクトするには next('/login') または next({ name: 'Login' }) を呼び出す
  
  // 例: ログイン必須ルートの保護
  const isAuthenticated = localStorage.getItem('token')
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
  
  if (requiresAuth && !isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

// グローバル解決ガード
router.beforeResolve((to, from, next) => {
  // ルートコンポーネントがロードされた後、
  // ナビゲーションが確定する直前に呼び出される
  console.log('beforeResolve')
  next()
})

// グローバル後処理フック
router.afterEach((to, from) => {
  // ナビゲーションが完了した後に呼び出される
  // next 関数はない
  console.log(`${from.path} から ${to.path} へナビゲーション完了`)
})

ルート単位のガード

const routes = [
  {
    path: '/admin',
    component: Admin,
    beforeEnter: (to, from, next) => {
      // ルート単位の前処理ガード
      const isAdmin = checkAdminStatus()
      
      if (isAdmin) {
        next()
      } else {
        next('/not-authorized')
      }
    }
  }
]

コンポーネント内ガード

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

// コンポーネントが現在のルートから離れる前に呼び出される
onBeforeRouteLeave((to, from, next) => {
  const answer = window.confirm('変更内容が保存されていません。このページを離れますか?')
  if (answer) {
    next()
  } else {
    next(false)
  }
})

// パラメータが変更されたときなど、同じコンポーネントを再利用する場合に呼び出される
onBeforeRouteUpdate((to, from, next) => {
  // 例: データを再フェッチする
  console.log('ルートが更新されました')
  next()
})
</script>

メタフィールド

ルート定義にメタフィールドを追加して、ルートに関連する追加情報を格納できます。

const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      role: 'admin'
    }
  }
]

メタフィールドは、ルートガードなどでアクセスできます。

router.beforeEach((to, from, next) => {
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
  const requiredRole = to.matched.find(record => record.meta.role)?.meta.role
  
  if (requiresAuth) {
    const isAuthenticated = checkAuth()
    const userRole = getUserRole()
    
    if (!isAuthenticated) {
      next('/login')
    } else if (requiredRole && userRole !== requiredRole) {
      next('/not-authorized')
    } else {
      next()
    }
  } else {
    next()
  }
})

7. ルートのトランジション

Vue.jsのトランジション機能を使用して、ルート間の切り替え効果を追加できます。

<!-- App.vue -->
<template>
  <div id="app">
    <transition name="fade" mode="out-in">
      <router-view />
    </transition>
  </div>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

ルート別のトランジション

特定のルートに対して異なるトランジションを適用することもできます。

<template>
  <div id="app">
    <router-view v-slot="{ Component, route }">
      <transition :name="route.meta.transition || 'fade'">
        <component :is="Component" />
      </transition>
    </router-view>
  </div>
</template>
const routes = [
  {
    path: '/about',
    component: About,
    meta: {
      transition: 'slide'
    }
  }
]
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

.slide-enter-active,
.slide-leave-active {
  transition: transform 0.3s;
}

.slide-enter-from {
  transform: translateX(100%);
}

.slide-leave-to {
  transform: translateX(-100%);
}

8. Vue Routerの高度な機能

スクロール位置の制御

ルーターインスタンスを作成するときに、scrollBehaviorオプションを指定することで、ナビゲーション後のスクロール位置を制御できます。

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 戻る/進むボタンでナビゲーションした場合
    if (savedPosition) {
      return savedPosition
    }
    
    // ハッシュがあるルートの場合はその要素までスクロール
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth'
      }
    }
    
    // デフォルトではページの先頭にスクロール
    return { top: 0 }
  }
})

ナビゲーションフェイルの処理

import { useRouter } from 'vue-router'

const router = useRouter()

// Promise チェーンを使用
router.push('/admin')
  .then(() => {
    // ナビゲーション成功
    console.log('ナビゲーション成功')
  })
  .catch(error => {
    // ナビゲーション失敗
    console.error('ナビゲーション失敗:', error)
  })

// または await を使用
async function navigate() {
  try {
    await router.push('/admin')
    console.log('ナビゲーション成功')
  } catch (error) {
    console.error('ナビゲーション失敗:', error)
  }
}

ナビゲーションの変更を監視

// 監視の登録
const unwatch = router.beforeEach(() => {
  console.log('ナビゲーション開始')
})

// 監視の解除
unwatch()

動的ルーティング

アプリケーションの実行中にルートを追加または削除できます。

// ルートの追加
router.addRoute({
  path: '/dynamic',
  name: 'Dynamic',
  component: () => import('@/views/Dynamic.vue')
})

// 名前付きルートの追加
router.addRoute('User', {
  path: 'settings',
  name: 'UserSettings',
  component: () => import('@/views/UserSettings.vue')
})

// ルートの削除(同じ名前を持つルートを追加するか、以下のメソッドを使用)
const removeRoute = router.addRoute(newRoute)
removeRoute() // 追加したルートを削除

// 名前を使用してルートを削除
router.removeRoute('Dynamic')

// すべてのルートをリセット
router.hasRoute('Dynamic') // ルートが存在するかチェック
router.getRoutes() // 現在のルート一覧を取得

9. HTML5 History モードとハッシュモード

Vue Routerには、2つの動作モードがあります。

HTML5 History モード

デフォルトモードで、URLにハッシュを含まない綺麗なURLを使用します。

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

このモードでは、サーバー側の設定が必要です。すべてのURLリクエストをindex.htmlにフォールバックさせる必要があります。

// server.js (Node.js + Express)
const express = require('express')
const app = express()
const path = require('path')

// 静的ファイルを配信
app.use(express.static(path.join(__dirname, 'dist')))

// すべてのリクエストをindex.htmlにリダイレクト
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist', 'index.html'))
})

app.listen(3000)

ハッシュモード

ハッシュモードは、URLのハッシュ部分を使用してルーティングを行います。サーバー側の設定が不要ですが、SEOには適していません。

// 例: /#/about
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

10. 実践的な例:認証システム

Vue Routerを使用して、認証システムを実装する方法を見てみましょう。

ルート設定

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Login from '@/views/Login.vue'
import Dashboard from '@/views/Dashboard.vue'
import Profile from '@/views/Profile.vue'
import NotFound from '@/views/NotFound.vue'

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: '/profile',
    name: 'Profile',
    component: Profile,
    meta: {
      requiresAuth: true // 認証が必要
    }
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
  }
]

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

// グローバルガードで認証状態をチェック
router.beforeEach((to, from, next) => {
  // 認証が必要なルートかチェック
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
  // ゲスト専用ルートかチェック
  const isGuestRoute = to.matched.some(record => record.meta.guest)
  
  // 認証状態を取得(実際のアプリでは適切な認証管理を使用)
  const isAuthenticated = localStorage.getItem('token') !== null
  
  if (requiresAuth && !isAuthenticated) {
    // 認証が必要なのに認証されていない場合はログインページへ
    next({
      name: 'Login',
      query: { redirect: to.fullPath } // リダイレクト用のクエリパラメータを設定
    })
  } else if (isGuestRoute && isAuthenticated) {
    // ゲスト専用ルートに認証済みユーザーがアクセスした場合はダッシュボードへ
    next({ name: 'Dashboard' })
  } else {
    // それ以外は通常通り次のルートへ
    next()
  }
})

export default router

ログインコンポーネント

<!-- src/views/Login.vue -->
<script setup>
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'

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

const username = ref('')
const password = ref('')
const error = ref('')
const loading = ref(false)

// ログイン処理
async function login() {
  if (!username.value || !password.value) {
    error.value = 'ユーザー名とパスワードを入力してください'
    return
  }
  
  error.value = ''
  loading.value = true
  
  try {
    // APIログイン処理(ダミー)
    // 実際のアプリでは、実際のAPIを呼び出す
    await new Promise(resolve => setTimeout(resolve, 1000))
    
    // ダミーバリデーション
    if (username.value === 'admin' && password.value === 'password') {
      // ログイン成功
      const token = 'dummy-token-' + Date.now()
      localStorage.setItem('token', token)
      
      // リダイレクト
      const redirectPath = route.query.redirect || '/dashboard'
      router.push(redirectPath)
    } else {
      // ログイン失敗
      error.value = 'ユーザー名またはパスワードが正しくありません'
    }
  } catch (err) {
    console.error(err)
    error.value = 'ログイン中にエラーが発生しました'
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <div class="login-container">
    <h1>ログイン</h1>
    
    <form @submit.prevent="login" class="login-form">
      <div v-if="error" class="error-message">{{ error }}</div>
      
      <div class="form-group">
        <label for="username">ユーザー名</label>
        <input
          id="username"
          v-model="username"
          type="text"
          :disabled="loading"
          placeholder="ユーザー名を入力"
          required
        >
      </div>
      
      <div class="form-group">
        <label for="password">パスワード</label>
        <input
          id="password"
          v-model="password"
          type="password"
          :disabled="loading"
          placeholder="パスワードを入力"
          required
        >
      </div>
      
      <button
        type="submit"
        :disabled="loading"
        class="login-button"
      >
        {{ loading ? 'ログイン中...' : 'ログイン' }}
      </button>
      
      <div class="hint">
        <small>ヒント: username = "admin", password = "password"</small>
      </div>
    </form>
  </div>
</template>

<style scoped>
.login-container {
  max-width: 400px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.login-form {
  display: flex;
  flex-direction: column;
}

.form-group {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

input {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

.login-button {
  padding: 10px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  margin-top: 10px;
}

.login-button:disabled {
  background-color: #95d5b7;
  cursor: not-allowed;
}

.error-message {
  color: #ff4c4c;
  margin-bottom: 15px;
  padding: 10px;
  background-color: #fff2f2;
  border: 1px solid #ffdddd;
  border-radius: 4px;
}

.hint {
  margin-top: 15px;
  text-align: center;
  color: #666;
}
</style>

ナビゲーションバー

<!-- src/components/Navbar.vue -->
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()

// 認証状態のチェック
const isAuthenticated = computed(() => {
  return localStorage.getItem('token') !== null
})

// ログアウト処理
function logout() {
  localStorage.removeItem('token')
  router.push('/login')
}
</script>

<template>
  <nav class="navbar">
    <div class="navbar-brand">
      <router-link to="/">Vue Router デモ</router-link>
    </div>
    
    <div class="navbar-menu">
      <router-link to="/" exact-active-class="active">ホーム</router-link>
      
      <template v-if="isAuthenticated">
        <router-link to="/dashboard" active-class="active">ダッシュボード</router-link>
        <router-link to="/profile" active-class="active">プロフィール</router-link>
        <a href="#" @click.prevent="logout" class="logout-link">ログアウト</a>
      </template>
      
      <template v-else>
        <router-link to="/login" active-class="active">ログイン</router-link>
      </template>
    </div>
  </nav>
</template>

<style scoped>
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 20px;
  background-color: #f8f9fa;
  border-bottom: 1px solid #ddd;
}

.navbar-brand a {
  font-size: 20px;
  font-weight: bold;
  color: #42b983;
  text-decoration: none;
}

.navbar-menu {
  display: flex;
  gap: 15px;
}

.navbar-menu a {
  color: #333;
  text-decoration: none;
  padding: 5px 10px;
  border-radius: 4px;
}

.navbar-menu a:hover {
  background-color: #f0f0f0;
}

.navbar-menu a.active {
  background-color: #42b983;
  color: white;
}

.logout-link {
  cursor: pointer;
}
</style>

ダッシュボードコンポーネント

<!-- src/views/Dashboard.vue -->
<template>
  <div class="dashboard">
    <h1>ダッシュボード</h1>
    <p>ようこそ!認証されたユーザー専用ページです。</p>
    
    <div class="dashboard-content">
      <div class="dashboard-card">
        <h3>統計</h3>
        <p>アクティブユーザー: 1,234</p>
        <p>総訪問数: 5,678</p>
      </div>
      
      <div class="dashboard-card">
        <h3>最近のアクティビティ</h3>
        <ul>
          <li>プロフィールを更新しました</li>
          <li>新しいプロジェクトを作成しました</li>
          <li>チームメンバーを招待しました</li>
        </ul>
      </div>
    </div>
  </div>
</template>

<style scoped>
.dashboard {
  padding: 20px;
}

.dashboard-content {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  margin-top: 20px;
}

.dashboard-card {
  flex: 1;
  min-width: 300px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

h3 {
  color: #42b983;
  margin-top: 0;
}

ul {
  padding-left: 20px;
}

li {
  margin-bottom: 5px;
}
</style>

アプリケーションのルートコンポーネント

<!-- src/App.vue -->
<script setup>
import Navbar from '@/components/Navbar.vue'
</script>

<template>
  <div id="app">
    <Navbar />
    
    <div class="content">
      <transition name="fade" mode="out-in">
        <router-view />
      </transition>
    </div>
  </div>
</template>

<style>
/* グローバルスタイル */
body {
  margin: 0;
  font-family: Arial, sans-serif;
  line-height: 1.6;
  color: #333;
}

.content {
  padding: 20px;
}

/* トランジション効果 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

11. Vue Routerのベストプラクティス

Vue Routerを効果的に使用するためのベストプラクティスを紹介します。

モジュール化されたルート設定の例

// src/router/modules/auth.js
import Login from '@/views/auth/Login.vue'
import Register from '@/views/auth/Register.vue'
import ForgotPassword from '@/views/auth/ForgotPassword.vue'

export default [
  {
    path: '/login',
    name: 'Login',
    component: Login,
    meta: { guest: true }
  },
  {
    path: '/register',
    name: 'Register',
    component: Register,
    meta: { guest: true }
  },
  {
    path: '/forgot-password',
    name: 'ForgotPassword',
    component: ForgotPassword,
    meta: { guest: true }
  }
]

// src/router/modules/admin.js
export default [
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/admin/AdminDashboard.vue'),
    meta: { requiresAuth: true, role: 'admin' },
    children: [
      {
        path: 'users',
        name: 'AdminUsers',
        component: () => import('@/views/admin/Users.vue')
      },
      {
        path: 'settings',
        name: 'AdminSettings',
        component: () => import('@/views/admin/Settings.vue')
      }
    ]
  }
]

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

// モジュールのインポート
import authRoutes from './modules/auth'
import adminRoutes from './modules/admin'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  // スプレッド構文でモジュールを展開
  ...authRoutes,
  ...adminRoutes,
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
  }
]

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

export default router

ナビゲーションロジックのコンポーザブル関数例

// src/composables/useAuth.js
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'

export function useAuth() {
  const router = useRouter()
  const token = ref(localStorage.getItem('token'))
  
  const isAuthenticated = computed(() => token.value !== null)
  
  function login(newToken) {
    token.value = newToken
    localStorage.setItem('token', newToken)
  }
  
  function logout() {
    token.value = null
    localStorage.removeItem('token')
    router.push('/login')
  }
  
  function requireAuth(to) {
    if (!isAuthenticated.value) {
      router.push({
        name: 'Login',
        query: { redirect: to }
      })
      return false
    }
    return true
  }
  
  return {
    token,
    isAuthenticated,
    login,
    logout,
    requireAuth
  }
}

12. まとめ

本章では、Vue Routerを使用して単一ページアプリケーション(SPA)の画面遷移を実装する方法について学びました。ルートの定義、ナビゲーションの実装、パラメータの受け渡し、ルートガードなどの重要な概念について理解を深めました。

また、HTML5 Historyモードとハッシュモードの違い、ルートのトランジション効果、高度なルーティング機能についても学びました。認証システムの実装例を通じて、実際のアプリケーションでVue Routerをどのように使用するかも理解しました。

Vue Routerは、Vue.jsアプリケーションでページ間のナビゲーションを実装するための強力なツールです。適切に使用することで、ユーザーフレンドリーで高速なSPAを構築できます。

次の章では、Piniaを使用した状態管理について学びます。

目次に戻る