ルーティングシステム
前の章で、タスク管理ロジックはPiniaを通じて永続化を実現しました。次に、アプリケーションに魂を注入する必要があります——ルーティングシステム。
エンタープライズ級開発において、ルーティングはページを切り替えるだけでなく、以下の重任も担っています:セキュリティ(権限制御)とユーザー体験(動的タイトル)という重任です。
インストールと基本設定
まずVue Routerをインストールします:
npm install vue-router
にsrcディレクトリの下にrouterディレクトリを作成し、 ファイルを作成します。router/index.jsファイル。

2つのページを定義します:ワークベンチ(Dashboard)とログインページ(Login)。
インスタンス
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: 'ログイン - TaskHub', requiresAuth: false }
},
{
path: '/',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
// ルートメタ情報 meta:カスタムデータを格納するために使用
meta: {
title: 'マイワークベンチ',
requiresAuth: true,
breadcrumb: 'ホーム'
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: 'ログイン - TaskHub', requiresAuth: false }
},
{
path: '/',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
// ルートメタ情報 meta:カスタムデータを格納するために使用
meta: {
title: 'マイワークベンチ',
requiresAuth: true,
breadcrumb: 'ホーム'
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
にsrcの下に作成viewsフォルダを作成し、ページを割り当てます。
src/App.vue:ルートコンテナ。 とグローバルアニメーションのみを配置します。<router-view />とグローバルアニメーション。src/views/Dashboard.vue:元のTaskHubコア機能(タスクリスト、入力ボックスなど)。src/views/Login.vue:新しく追加されたログインページ。

改造App.vue(クリーンなコンテナにする)
インスタンス
<template>
<div class="min-h-screen bg-slate-50">
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</template>
<style>
/* ページ切り替えのフェードイン・フェードアウト効果 */
.page-enter-active, .page-leave-active {
transition: opacity 0.2s ease;
}
.page-enter-from, .page-leave-to {
opacity: 0;
}
</style>
<div class="min-h-screen bg-slate-50">
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</template>
<style>
/* ページ切り替えのフェードイン・フェードアウト効果 */
.page-enter-active, .page-leave-active {
transition: opacity 0.2s ease;
}
.page-enter-from, .page-leave-to {
opacity: 0;
}
</style>
元のロジックを移行するDashboard.vue
以前に に書いたApp.vueのコード(Storeの導入、コンポーネントの導入、テンプレートレイアウト)をすべて切り取って に移します。src/views/Dashboard.vueに。
Dashboard.vue ファイルのコード
<script setup>
import { storeToRefs } from 'pinia'
import { useTaskStore } from '@/stores/taskStore'
import { useRouter } from 'vue-router' // ルーターを導入
import TaskHeader from '@/components/TaskHeader.vue'
import TaskInput from '@/components/TaskInput.vue'
import TaskFilter from '@/components/TaskFilter.vue'
import TaskItem from '@/components/TaskItem.vue'
const taskStore = useTaskStore()
const router = useRouter()
const { filter, filteredTasks } = storeToRefs(taskStore)
const { addTask, removeTask, toggleTask } = taskStore
// ログアウト処理
const handleLogout = () => {
localStorage.removeItem('isLoggedIn')
router.push('/login')
}
</script>
<template>
<div class="py-12 px-4">
<div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl border border-slate-100 overflow-hidden">
<TaskHeader />
<main class="p-6">
<TaskInput @add-task="addTask" />
<TaskFilter v-model="filter" />
<ul class="space-y-3">
<TransitionGroup name="list">
<TaskItem
v-for="task in filteredTasks"
:key="task.id"
:task="task"
@toggle="toggleTask"
@remove="removeTask"
/>
</TransitionGroup>
</ul>
<button
@click="handleLogout"
class="mt-8 w-full py-2 text-xs text-slate-400 hover:text-red-500 transition-colors"
>
現在のアカウントからログアウト
</button>
</main>
</div>
</div>
</template>
import { storeToRefs } from 'pinia'
import { useTaskStore } from '@/stores/taskStore'
import { useRouter } from 'vue-router' // ルーターを導入
import TaskHeader from '@/components/TaskHeader.vue'
import TaskInput from '@/components/TaskInput.vue'
import TaskFilter from '@/components/TaskFilter.vue'
import TaskItem from '@/components/TaskItem.vue'
const taskStore = useTaskStore()
const router = useRouter()
const { filter, filteredTasks } = storeToRefs(taskStore)
const { addTask, removeTask, toggleTask } = taskStore
// ログアウト処理
const handleLogout = () => {
localStorage.removeItem('isLoggedIn')
router.push('/login')
}
</script>
<template>
<div class="py-12 px-4">
<div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl border border-slate-100 overflow-hidden">
<TaskHeader />
<main class="p-6">
<TaskInput @add-task="addTask" />
<TaskFilter v-model="filter" />
<ul class="space-y-3">
<TransitionGroup name="list">
<TaskItem
v-for="task in filteredTasks"
:key="task.id"
:task="task"
@toggle="toggleTask"
@remove="removeTask"
/>
</TransitionGroup>
</ul>
<button
@click="handleLogout"
class="mt-8 w-full py-2 text-xs text-slate-400 hover:text-red-500 transition-colors"
>
現在のアカウントからログアウト
</button>
</main>
</div>
</div>
</template>
ログインロジックの実装 (Login.vue)
Tailwind v4を利用してミニマルなログインページを素早く構築し、ログイン動作をシミュレートします。
インスタンス
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const isLoading = ref(false)
const handleLogin = () => {
isLoading.value = true
// 非同期リクエストをシミュレート
setTimeout(() => {
localStorage.setItem('isLoggedIn', 'true')
router.push('/') // ログイン成功でホームへ遷移
isLoading.value = false
}, 1000)
}
</script>
<template>
<div class="min-h-screen flex items-center justify-center bg-slate-50">
<div class="p-8 bg-white rounded-3xl shadow-xl w-full max-w-sm border border-slate-100">
<h2 class="text-2xl font-black mb-6 text-slate-800">おかえりなさい</h2>
<button
@click="handleLogin"
:disabled="isLoading"
class="w-full bg-linear-to-r from-blue-600 to-indigo-600 text-white py-3 rounded-xl font-bold hover:opacity-90 active:scale-95 transition-all disabled:opacity-50"
>
{{ isLoading ? 'ログイン中...' : 'ワンクリックでシステムへ' }}
</button>
<p class="mt-4 text-center text-xs text-slate-400">テスト環境:クリックでログインできます</p>
</div>
</div>
</template>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const isLoading = ref(false)
const handleLogin = () => {
isLoading.value = true
// 非同期リクエストをシミュレート
setTimeout(() => {
localStorage.setItem('isLoggedIn', 'true')
router.push('/') // ログイン成功でホームへ遷移
isLoading.value = false
}, 1000)
}
</script>
<template>
<div class="min-h-screen flex items-center justify-center bg-slate-50">
<div class="p-8 bg-white rounded-3xl shadow-xl w-full max-w-sm border border-slate-100">
<h2 class="text-2xl font-black mb-6 text-slate-800">おかえりなさい</h2>
<button
@click="handleLogin"
:disabled="isLoading"
class="w-full bg-linear-to-r from-blue-600 to-indigo-600 text-white py-3 rounded-xl font-bold hover:opacity-90 active:scale-95 transition-all disabled:opacity-50"
>
{{ isLoading ? 'ログイン中...' : 'ワンクリックでシステムへ' }}
</button>
<p class="mt-4 text-center text-xs text-slate-400">テスト環境:クリックでログインできます</p>
</div>
</div>
</template>
仕上げrouter/index.jsのガードロジック
ルート設定でガードモードが有効になっていることを確認してください:
インスタンス
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: 'ログイン - TaskHub', requiresAuth: false }
},
{
path: '/',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
// ルートメタ情報 meta:カスタムデータを格納するために使用
meta: {
title: 'マイワークベンチ',
requiresAuth: true,
breadcrumb: 'ホーム'
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// src/router/index.js (核心部分)
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('isLoggedIn') === 'true'
// ホームへ行くが未ログインの場合 -> ログインページへ
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
}
// ログイン済みなのにログインページへ行こうとする -> ホームへ戻す
else if (to.path === '/login' && isAuthenticated) {
next('/')
}
else {
next()
}
})
export default router
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: 'ログイン - TaskHub', requiresAuth: false }
},
{
path: '/',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
// ルートメタ情報 meta:カスタムデータを格納するために使用
meta: {
title: 'マイワークベンチ',
requiresAuth: true,
breadcrumb: 'ホーム'
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// src/router/index.js (核心部分)
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('isLoggedIn') === 'true'
// ホームへ行くが未ログインの場合 -> ログインページへ
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
}
// ログイン済みなのにログインページへ行こうとする -> ホームへ戻す
else if (to.path === '/login' && isAuthenticated) {
next('/')
}
else {
next()
}
})
export default router
にmain.jsで最後のピースを完成させる
あなたの がmain.jsルートを導入してマウントしていることを確認してください:
インスタンス
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router' // ルーターを導入
import App from './App.vue'
import './style.css'
const app = createApp(App)
app.use(createPinia())
app.use(router) // 2. ルーターを登録
app.mount('#app')
import { createPinia } from 'pinia'
import router from './router' // ルーターを導入
import App from './App.vue'
import './style.css'
const app = createApp(App)
app.use(createPinia())
app.use(router) // 2. ルーターを登録
app.mount('#app')
最終的な効果
- / にアクセス
もしログインしていない場合、ページが一瞬表示されてからリダイレクトされます。/login。 - /login にアクセス
ボタンをクリックして、ローカルストレージに保存します。isLoggedInになる。true になり、その後瞬時にワークベンチへ切り替わります。 - ページを更新:PiniaはLocalStorageからタスクを読み取り、ルーターガードがログイン状態をチェックします。すべてのデータは失われません。

ログイン後、「ログアウト」をクリックできます:

知識ポイントの解説
- なぜ
App.vue空になったのですか?
シングルページアプリケーション(SPA)では、App.vueはアプリケーション全体のシェルです。URLの変化に応じて、その中に異なるページ(DashboardまたはLogin)を動的に挿入できるように、空にしています。 useRouterの実践:
にDashboard.vueでは、 を通じてrouter.push('/login')ログアウト機能を実装しました。注意:pushはブラウザの履歴に新しいレコードを追加するため、ブラウザの戻るボタンをクリックすると戻ることができます。- Tailwind v4のレイアウト継承:
私たちは のApp.vueのグローバルコンテナに を追加したため、bg-slate-50とmin-h-screenすべての子ページ(Dashboard、Login)はデフォルトでこの薄い灰色の背景を継承し、視覚的な統一性が保証されます。