Vue.js ルーター戦略比較:ネスト/動的セグメント/ガードの設計コスト
Vue.js でシングルページアプリケーション(SPA)を構築する際、Vue Router の設計戦略によって開発効率や保守性が大きく変わります。本記事では、ルーター設計における「ネストルート」「動的セグメント」「ナビゲーションガード」の 3 つの戦略に焦点を当て、それぞれの設計コストと実装パターンを比較します。
プロジェクトの規模や要件に応じて最適な戦略を選択できるよう、具体的なコード例と共に解説していきますので、ぜひ最後までご覧ください。
背景
Vue Router とは
Vue Router は、Vue.js の公式ルーティングライブラリです。SPA におけるページ遷移を制御し、URL と Vue コンポーネントをマッピングする役割を担います。
一般的な MPA(マルチページアプリケーション)では、サーバー側で URL ごとに HTML を生成しますが、SPA では JavaScript がクライアント側で URL を解釈し、対応するコンポーネントを動的に表示します。
ルーター設計の重要性
ルーター設計は、アプリケーション全体のアーキテクチャに影響を与える重要な要素です。適切な設計により、以下のようなメリットが得られます。
設計による主なメリット
| # | 項目 | 内容 |
|---|---|---|
| 1 | コードの見通し | ルート構造が明確になり、開発者が迷わない |
| 2 | 保守性の向上 | 変更箇所が限定され、影響範囲を把握しやすい |
| 3 | スケーラビリティ | 機能追加時の拡張が容易になる |
| 4 | セキュリティ | アクセス制御を一元管理できる |
一方で、不適切な設計は技術的負債を生み出します。コンポーネントの再利用が困難になったり、認証フローが複雑化したり、URL 構造が直感的でなくなったりするリスクがあるのです。
ルーター設計の 3 つの戦略
Vue Router における主要な設計戦略として、以下の 3 つが挙げられます。
以下の図で、これら 3 つの戦略がどのように組み合わさるかを示します。
mermaidflowchart TB
root["ルーター設計"]
nested["ネストルート<br/>階層的な URL 構造"]
dynamic["動的セグメント<br/>パラメータによる動的ルート"]
guard["ナビゲーションガード<br/>遷移の制御・認証"]
root --> nested
root --> dynamic
root --> guard
nested --> impl1["親子コンポーネント連携"]
dynamic --> impl2["柔軟なパス設計"]
guard --> impl3["セキュリティ制御"]
それぞれの戦略には固有の設計コストと学習コストが存在し、プロジェクトの要件に応じた選択が求められます。次のセクションでは、各戦略における具体的な課題を見ていきましょう。
課題
ネストルートの設計課題
ネストルートは、親子関係のある階層的な URL 構造を実現する戦略ですが、いくつかの設計課題があります。
親子コンポーネントの依存関係
ネストルートでは、親コンポーネントに <router-view> を配置し、子ルートのコンポーネントを表示します。この構造により、親コンポーネントの状態変更が子コンポーネントに影響を与える可能性があり、コンポーネント間の依存関係が複雑化しやすくなります。
レイアウトの制約
親コンポーネントがレイアウトを決定するため、異なるレイアウトを持つページを同じネスト階層に配置することが困難です。例えば、管理画面とユーザー画面で全く異なるレイアウトが必要な場合、ネスト構造の設計が複雑になります。
データの受け渡し
親から子へのデータ受け渡しには、Props や Provide/Inject を使用しますが、階層が深くなるほど管理が煩雑になります。また、子コンポーネントが親のデータに依存することで、再利用性が低下するリスクもあります。
動的セグメントの設計課題
動的セグメントは、URL にパラメータを含めることで柔軟なルーティングを実現しますが、以下のような課題があります。
パラメータの型安全性
TypeScript を使用していても、URL パラメータは基本的に文字列として扱われます。数値や特定の形式を期待する場合、バリデーションやパース処理が必要になり、実装コストが増加します。
mermaidflowchart LR
url["URL: /user/123"] --> param["パラメータ取得<br/>$route.params.id"]
param --> validate["バリデーション<br/>数値チェック"]
validate --> error["エラー処理<br/>不正な値の場合"]
validate --> success["処理実行<br/>正常な値の場合"]
パラメータの欠落
オプショナルなパラメータを扱う際、パラメータが存在しない場合の処理を適切に実装する必要があります。存在チェックを怠ると、ランタイムエラーが発生するリスクが高まります。
複数パラメータの管理
複数の動的セグメントを含むルートでは、パラメータの順序や依存関係を明確にする必要があります。例えば、/user/:userId/post/:postId のような構造では、両方のパラメータが必須であることを開発者が理解していなければなりません。
ナビゲーションガードの設計課題
ナビゲーションガードは、ルート遷移時の制御を行う強力な機能ですが、以下の課題があります。
ガードの実行順序
Vue Router には、グローバルガード、ルート単位のガード、コンポーネント内ガードの 3 種類があり、それぞれ実行順序が異なります。この順序を理解していないと、意図しない動作を引き起こす可能性があります。
ガードの実行順序
| # | ガードの種類 | 実行タイミング | 用途例 |
|---|---|---|---|
| 1 | グローバル beforeEach | すべての遷移前 | 認証チェック |
| 2 | ルート beforeEnter | 特定ルート遷移前 | 権限チェック |
| 3 | コンポーネント beforeRouteEnter | コンポーネント生成前 | データ取得 |
非同期処理の複雑化
認証チェックや API 呼び出しなど、ガード内で非同期処理を行う場合、エラーハンドリングやローディング状態の管理が複雑になります。また、複数のガードで非同期処理が連鎖すると、パフォーマンスにも影響を与えます。
無限ループのリスク
ガード内でルート遷移を行う際、条件設定を誤ると無限ループに陥る危険性があります。例えば、認証失敗時にログインページへリダイレクトする処理で、ログインページ自体が認証を要求するような設定になっていると、無限リダイレクトが発生します。
これらの課題を踏まえ、次のセクションでは各戦略の具体的な解決策を見ていきましょう。
解決策
ネストルート設計の最適化
ネストルートの設計課題に対する解決策として、以下のパターンを推奨します。
レイアウトコンポーネントの分離
親コンポーネントをレイアウト専用にすることで、ビジネスロジックとレイアウトを分離します。
typescript// router/index.ts - ルート定義
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
children: [
{
path: 'dashboard',
name: 'AdminDashboard',
component: () =>
import('@/views/admin/Dashboard.vue'),
},
],
},
],
});
AdminLayout.vue では、レイアウト構造のみを定義します。
vue<!-- layouts/AdminLayout.vue - レイアウトコンポーネント -->
<template>
<div class="admin-layout">
<!-- サイドバー -->
<aside class="sidebar">
<nav>
<router-link to="/admin/dashboard"
>ダッシュボード</router-link
>
</nav>
</aside>
<!-- メインコンテンツエリア -->
<main class="content">
<router-view />
</main>
</div>
</template>
このパターンにより、レイアウトの変更が子コンポーネントに影響を与えにくくなります。
Provide/Inject による状態共有
親から子への状態共有には、Provide/Inject パターンを活用します。
vue<!-- layouts/AdminLayout.vue - 親コンポーネントでの Provide -->
<script setup lang="ts">
import { ref, provide } from 'vue';
// 共有する状態を定義
const sidebarCollapsed = ref(false);
// 子コンポーネントに提供
provide('sidebarCollapsed', sidebarCollapsed);
const toggleSidebar = () => {
sidebarCollapsed.value = !sidebarCollapsed.value;
};
provide('toggleSidebar', toggleSidebar);
</script>
子コンポーネント側では、inject で受け取ります。
vue<!-- views/admin/Dashboard.vue - 子コンポーネントでの Inject -->
<script setup lang="ts">
import { inject } from 'vue';
// 親から提供された状態を取得
const sidebarCollapsed = inject('sidebarCollapsed');
const toggleSidebar = inject('toggleSidebar');
</script>
<template>
<div>
<button @click="toggleSidebar">
サイドバー切り替え
</button>
<p>
サイドバー状態:
{{ sidebarCollapsed ? '折りたたみ' : '展開' }}
</p>
</div>
</template>
この方法により、Props のバケツリレーを避けつつ、状態を共有できます。
名前付きビューによる柔軟なレイアウト
複数のコンテンツエリアを持つレイアウトには、名前付きビューを使用します。
typescript// router/index.ts - 名前付きビューの定義
{
path: '/settings',
components: {
default: () => import('@/layouts/SettingsLayout.vue'),
sidebar: () => import('@/components/SettingsSidebar.vue')
},
children: [
{
path: 'profile',
components: {
default: () => import('@/views/settings/Profile.vue'),
sidebar: () => import('@/components/ProfileSidebar.vue')
}
}
]
}
レイアウトコンポーネントでは、複数の <router-view> を配置します。
vue<!-- layouts/SettingsLayout.vue - 複数のビューエリア -->
<template>
<div class="settings-layout">
<aside>
<router-view name="sidebar" />
</aside>
<main>
<router-view />
</main>
</div>
</template>
これにより、ページごとに異なるサイドバーを表示するなど、柔軟なレイアウト設計が可能になります。
動的セグメント設計の最適化
動的セグメントの課題に対しては、型安全性とバリデーションを重視した設計が有効です。
Zod による型安全なパラメータ検証
Zod を使用して、URL パラメータの型検証を行います。
typescript// utils/routeParams.ts - パラメータスキーマの定義
import { z } from 'zod';
// ユーザー ID のスキーマ
export const userIdSchema = z
.string()
.regex(/^\d+$/)
.transform(Number);
// 記事 ID のスキーマ
export const postIdSchema = z.string().uuid();
コンポーネント内でスキーマを適用します。
vue<!-- views/UserDetail.vue - パラメータの検証 -->
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { userIdSchema } from '@/utils/routeParams';
import { ref, onMounted } from 'vue';
const route = useRoute();
const userId = ref<number | null>(null);
const error = ref<string | null>(null);
onMounted(() => {
try {
// パラメータを検証・変換
userId.value = userIdSchema.parse(route.params.id);
} catch (e) {
error.value = 'ユーザー ID が不正です';
}
});
</script>
この実装により、型安全性を担保しつつ、不正なパラメータを早期に検出できます。
Composable による再利用可能なパラメータ処理
パラメータ処理を Composable として抽象化します。
typescript// composables/useRouteParam.ts - 汎用的なパラメータ処理
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { z } from 'zod';
export function useRouteParam<T>(
paramName: string,
schema: z.ZodSchema<T>
) {
const route = useRoute();
// パラメータの取得と検証
const value = computed(() => {
const rawValue = route.params[paramName];
const result = schema.safeParse(rawValue);
if (!result.success) {
return null;
}
return result.data;
});
// エラー情報
const error = computed(() => {
const rawValue = route.params[paramName];
const result = schema.safeParse(rawValue);
return result.success ? null : result.error;
});
return { value, error };
}
コンポーネント内での使用例です。
vue<!-- views/UserDetail.vue - Composable の使用 -->
<script setup lang="ts">
import { useRouteParam } from '@/composables/useRouteParam';
import { userIdSchema } from '@/utils/routeParams';
// パラメータの取得
const { value: userId, error } = useRouteParam(
'id',
userIdSchema
);
</script>
<template>
<div>
<div v-if="error">エラー: {{ error }}</div>
<div v-else-if="userId">ユーザー ID: {{ userId }}</div>
</div>
</template>
この Composable により、パラメータ処理の重複コードを削減できます。
オプショナルパラメータのパターン
オプショナルなパラメータには、正規表現を活用した柔軟なルート定義を行います。
typescript// router/index.ts - オプショナルパラメータの定義
{
path: '/search/:keyword?',
name: 'Search',
component: () => import('@/views/Search.vue')
}
コンポーネント内では、パラメータの存在チェックを行います。
vue<!-- views/Search.vue - オプショナルパラメータの処理 -->
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
// パラメータの取得(デフォルト値を設定)
const keyword = computed(() => {
const param = route.params.keyword;
return typeof param === 'string' ? param : '';
});
// 検索実行判定
const shouldSearch = computed(
() => keyword.value.length > 0
);
</script>
デフォルト値を適切に設定することで、パラメータ欠落時のエラーを防げます。
ナビゲーションガード設計の最適化
ナビゲーションガードの複雑さを解消するには、責務の分離と明確な実行フローが重要です。
グローバルガードによる認証制御
認証チェックは、グローバル beforeEach ガードで一元管理します。
typescript// router/guards/auth.ts - 認証ガードの定義
import type {
NavigationGuardNext,
RouteLocationNormalized,
} from 'vue-router';
import { useAuthStore } from '@/stores/auth';
export async function authGuard(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) {
const authStore = useAuthStore();
// 認証不要なルートはスキップ
if (to.meta.requiresAuth === false) {
return next();
}
// 認証状態を確認
if (!authStore.isAuthenticated) {
// ログインページへリダイレクト
return next({
name: 'Login',
query: { redirect: to.fullPath },
});
}
next();
}
ルーター設定でガードを登録します。
typescript// router/index.ts - ガードの登録
import { createRouter, createWebHistory } from 'vue-router';
import { authGuard } from './guards/auth';
const router = createRouter({
history: createWebHistory(),
routes: [
/* routes */
],
});
// グローバルガードの登録
router.beforeEach(authGuard);
export default router;
この構成により、認証ロジックを一箇所に集約できます。
ルート単位のガードによる権限制御
特定のルートに対する権限チェックは、beforeEnter ガードで実装します。
typescript// router/guards/role.ts - 権限チェックガード
import type {
RouteLocationNormalized,
NavigationGuardNext,
} from 'vue-router';
import { useAuthStore } from '@/stores/auth';
export function createRoleGuard(allowedRoles: string[]) {
return (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) => {
const authStore = useAuthStore();
const userRole = authStore.user?.role;
// 権限チェック
if (!userRole || !allowedRoles.includes(userRole)) {
return next({ name: 'Forbidden' });
}
next();
};
}
ルート定義で使用します。
typescript// router/index.ts - 権限ガードの適用
import { createRoleGuard } from './guards/role'
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
beforeEnter: createRoleGuard(['admin', 'superadmin']),
children: [/* child routes */]
}
この実装により、ルートごとに異なる権限設定が可能になります。
コンポーネントガードによるデータ取得
コンポーネント表示前のデータ取得には、beforeRouteEnter を使用します。
vue<!-- views/UserDetail.vue - データ取得ガード -->
<script setup lang="ts">
import { ref } from 'vue';
import type {
RouteLocationNormalized,
NavigationGuardNext,
} from 'vue-router';
// データの状態
const user = ref(null);
const loading = ref(true);
// ルートガードの定義
async function beforeRouteEnter(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) {
try {
const userId = to.params.id;
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
// コンポーネントインスタンスにデータを渡す
next((vm: any) => {
vm.user = userData;
vm.loading = false;
});
} catch (error) {
// エラー時は別ページへ
next({ name: 'NotFound' });
}
}
</script>
このガードにより、データ取得が完了してからコンポーネントが表示されます。
無限ループ防止パターン
無限リダイレクトを防ぐには、遷移先の確認を行います。
typescript// router/guards/auth.ts - 無限ループ防止
export async function authGuard(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) {
const authStore = useAuthStore();
// ログインページへの遷移は無条件で許可
if (to.name === 'Login') {
return next();
}
// 認証不要なルートはスキップ
if (to.meta.requiresAuth === false) {
return next();
}
// 認証チェック
if (!authStore.isAuthenticated) {
// from と to が同じ場合は無限ループを防止
if (from.name === 'Login') {
return next(false);
}
return next({
name: 'Login',
query: { redirect: to.fullPath },
});
}
next();
}
遷移元と遷移先をチェックすることで、無限ループを回避できます。
以下の図で、ガードの実行フローとエラーハンドリングを示します。
mermaidflowchart TB
start["ルート遷移開始"] --> global["グローバルガード<br/>beforeEach"]
global --> authCheck["認証チェック"]
authCheck -->|未認証| login["ログインページへ"]
authCheck -->|認証済み| route["ルート単位ガード<br/>beforeEnter"]
route --> roleCheck["権限チェック"]
roleCheck -->|権限なし| forbidden["403 Forbidden"]
roleCheck -->|権限あり| component["コンポーネントガード<br/>beforeRouteEnter"]
component --> dataFetch["データ取得"]
dataFetch -->|成功| render["コンポーネント表示"]
dataFetch -->|失敗| notfound["404 Not Found"]
これらの解決策により、各戦略の設計コストを大幅に削減できます。
具体例
実践例:EC サイトのルーター設計
実際の EC サイトを例に、3 つの戦略を組み合わせたルーター設計を見ていきましょう。
プロジェクト構成
以下のような機能要件を持つ EC サイトを想定します。
機能要件一覧
| # | 機能 | 要件 | 使用する戦略 |
|---|---|---|---|
| 1 | 商品一覧・詳細 | 動的な商品 ID に対応 | 動的セグメント |
| 2 | マイページ | 管理画面レイアウトを共有 | ネストルート |
| 3 | 購入フロー | 未ログインユーザーは制限 | ナビゲーションガード |
この要件を満たすルーター設計を実装していきます。
ルート定義の全体像
まず、ルート定義の全体構造を示します。
typescript// router/index.ts - ルート定義の全体構造
import { createRouter, createWebHistory } from 'vue-router';
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: { requiresAuth: false },
},
{
path: '/products',
name: 'ProductList',
component: () => import('@/views/ProductList.vue'),
meta: { requiresAuth: false },
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
次に、各機能の詳細実装を見ていきます。
商品詳細ページ(動的セグメント)
商品詳細ページでは、動的セグメントで商品 ID を受け取ります。
typescript// router/index.ts - 商品詳細ルート
{
path: '/products/:productId',
name: 'ProductDetail',
component: () => import('@/views/ProductDetail.vue'),
meta: { requiresAuth: false }
}
パラメータスキーマを定義します。
typescript// utils/schemas/product.ts - 商品 ID スキーマ
import { z } from 'zod';
export const productIdSchema = z
.string()
.regex(/^[A-Z0-9]{8}$/, '商品コードは8桁の英数字です');
コンポーネントでパラメータを処理します。
vue<!-- views/ProductDetail.vue - 商品詳細コンポーネント -->
<script setup lang="ts">
import { useRouteParam } from '@/composables/useRouteParam';
import { productIdSchema } from '@/utils/schemas/product';
import { ref, watch } from 'vue';
// パラメータの取得
const { value: productId, error: paramError } =
useRouteParam('productId', productIdSchema);
// 商品データ
const product = ref(null);
const loading = ref(false);
// 商品データの取得
async function fetchProduct(id: string) {
loading.value = true;
try {
const response = await fetch(`/api/products/${id}`);
product.value = await response.json();
} catch (error) {
console.error('商品取得エラー:', error);
} finally {
loading.value = false;
}
}
// パラメータ変更時にデータ再取得
watch(
productId,
(newId) => {
if (newId) {
fetchProduct(newId);
}
},
{ immediate: true }
);
</script>
テンプレート部分です。
vue<!-- views/ProductDetail.vue - テンプレート -->
<template>
<div class="product-detail">
<div v-if="paramError" class="error">
無効な商品コードです
</div>
<div v-else-if="loading" class="loading">
読み込み中...
</div>
<div v-else-if="product" class="content">
<h1>{{ product.name }}</h1>
<p>価格: ¥{{ product.price.toLocaleString() }}</p>
<p>{{ product.description }}</p>
</div>
</div>
</template>
このように、動的セグメントと型検証を組み合わせることで、安全な商品詳細ページが実装できます。
マイページ(ネストルート)
マイページでは、共通レイアウトを持つネストルートを構成します。
typescript// router/index.ts - マイページのネストルート
{
path: '/mypage',
component: () => import('@/layouts/MyPageLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'MyPageDashboard',
component: () => import('@/views/mypage/Dashboard.vue')
},
{
path: 'orders',
name: 'MyPageOrders',
component: () => import('@/views/mypage/Orders.vue')
},
{
path: 'orders/:orderId',
name: 'MyPageOrderDetail',
component: () => import('@/views/mypage/OrderDetail.vue')
}
]
}
マイページのレイアウトコンポーネントです。
vue<!-- layouts/MyPageLayout.vue - マイページレイアウト -->
<template>
<div class="mypage-layout">
<header class="header">
<h1>マイページ</h1>
</header>
<div class="container">
<!-- ナビゲーション -->
<nav class="sidebar">
<ul>
<li>
<router-link :to="{ name: 'MyPageDashboard' }">
ダッシュボード
</router-link>
</li>
<li>
<router-link :to="{ name: 'MyPageOrders' }">
注文履歴
</router-link>
</li>
</ul>
</nav>
<!-- コンテンツエリア -->
<main class="content">
<router-view />
</main>
</div>
</div>
</template>
スタイル定義です。
vue<!-- layouts/MyPageLayout.vue - スタイル -->
<style scoped>
.mypage-layout {
min-height: 100vh;
}
.container {
display: flex;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.sidebar {
width: 200px;
margin-right: 20px;
}
.content {
flex: 1;
}
</style>
子ルートのコンポーネント例です。
vue<!-- views/mypage/Orders.vue - 注文履歴ページ -->
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const orders = ref([]);
const loading = ref(true);
// 注文履歴の取得
onMounted(async () => {
try {
const response = await fetch('/api/orders');
orders.value = await response.json();
} finally {
loading.value = false;
}
});
// 注文詳細へ遷移
function viewOrderDetail(orderId: string) {
router.push({
name: 'MyPageOrderDetail',
params: { orderId },
});
}
</script>
<template>
<div class="orders">
<h2>注文履歴</h2>
<div v-if="loading">読み込み中...</div>
<ul v-else class="order-list">
<li v-for="order in orders" :key="order.id">
<div>注文番号: {{ order.orderNumber }}</div>
<div>
合計金額: ¥{{ order.total.toLocaleString() }}
</div>
<button @click="viewOrderDetail(order.id)">
詳細を見る
</button>
</li>
</ul>
</div>
</template>
ネストルートにより、共通のナビゲーションを維持しながら、ページ遷移できます。
購入フロー(ナビゲーションガード)
購入フローでは、認証状態に応じてアクセスを制御します。
まず、認証ストアを作成します。
typescript// stores/auth.ts - 認証ストア
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useAuthStore = defineStore('auth', () => {
// ユーザー情報
const user = ref<{ id: string; email: string } | null>(
null
);
// 認証状態
const isAuthenticated = computed(
() => user.value !== null
);
// ログイン処理
async function login(email: string, password: string) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
user.value = await response.json();
return true;
}
return false;
}
// ログアウト処理
function logout() {
user.value = null;
}
return {
user,
isAuthenticated,
login,
logout,
};
});
認証ガードを実装します。
typescript// router/guards/auth.ts - 認証ガード実装
import type {
NavigationGuardNext,
RouteLocationNormalized,
} from 'vue-router';
import { useAuthStore } from '@/stores/auth';
export function authGuard(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) {
// ログインページは常に許可
if (to.name === 'Login') {
return next();
}
// 認証不要なルートはスキップ
if (to.meta.requiresAuth === false) {
return next();
}
// 認証チェック
const authStore = useAuthStore();
if (!authStore.isAuthenticated) {
// 未認証の場合、ログインページへリダイレクト
return next({
name: 'Login',
query: {
redirect: to.fullPath,
},
});
}
// 認証済みの場合、そのまま遷移
next();
}
購入フローのルート定義です。
typescript// router/index.ts - 購入フローのルート
{
path: '/checkout',
component: () => import('@/layouts/CheckoutLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: 'cart',
name: 'Cart',
component: () => import('@/views/checkout/Cart.vue')
},
{
path: 'shipping',
name: 'Shipping',
component: () => import('@/views/checkout/Shipping.vue')
},
{
path: 'payment',
name: 'Payment',
component: () => import('@/views/checkout/Payment.vue')
},
{
path: 'confirm',
name: 'Confirm',
component: () => import('@/views/checkout/Confirm.vue')
}
]
}
ガードをルーターに登録します。
typescript// router/index.ts - ガードの登録
import { createRouter, createWebHistory } from 'vue-router';
import { authGuard } from './guards/auth';
const router = createRouter({
history: createWebHistory(),
routes: [
/* routes */
],
});
// グローバルガードを登録
router.beforeEach(authGuard);
export default router;
以下の図で、購入フローにおける認証ガードの動作を示します。
mermaidsequenceDiagram
participant User as ユーザー
participant Router as Vue Router
participant Guard as 認証ガード
participant Store as 認証ストア
participant Comp as コンポーネント
User->>Router: /checkout/cart へ遷移
Router->>Guard: beforeEach 実行
Guard->>Store: 認証状態を確認
alt 未認証の場合
Store-->>Guard: 未認証
Guard->>Router: /login へリダイレクト
Router->>User: ログインページ表示
else 認証済みの場合
Store-->>Guard: 認証済み
Guard->>Router: 遷移を許可
Router->>Comp: コンポーネント表示
Comp->>User: カートページ表示
end
この実装により、購入フローへのアクセスは認証済みユーザーに限定されます。
設計コストの比較
3 つの戦略における設計コストを比較します。
戦略別の設計コスト比較
| # | 戦略 | 初期コスト | 学習コスト | 保守コスト | 拡張性 |
|---|---|---|---|---|---|
| 1 | ネストルート | 中 | 低 | 低 | 高 |
| 2 | 動的セグメント | 低 | 低 | 中 | 高 |
| 3 | ナビゲーションガード | 高 | 高 | 中 | 中 |
初期コストは実装時の工数、学習コストはチームメンバーが習得するまでの時間、保守コストは変更時の影響範囲、拡張性は新機能追加のしやすさを示します。
ネストルートは、レイアウト設計が明確になるため保守性が高い一方、初期の構造設計に時間がかかります。動的セグメントは実装が容易ですが、型安全性を確保するために追加の仕組みが必要です。ナビゲーションガードは強力な機能ですが、実行順序や非同期処理の理解が必要なため学習コストが高めです。
プロジェクトの規模や要件に応じて、これらの戦略を適切に組み合わせることが重要です。
まとめ
本記事では、Vue Router における 3 つの主要な設計戦略「ネストルート」「動的セグメント」「ナビゲーションガード」について、設計コストと実装パターンを比較してきました。
各戦略の特徴と推奨ケース
| # | 戦略 | 主な用途 | 推奨するケース |
|---|---|---|---|
| 1 | ネストルート | 階層的なレイアウト | 管理画面、マイページなど共通 UI を持つ画面群 |
| 2 | 動的セグメント | 動的なコンテンツ表示 | 商品詳細、ユーザープロフィールなど ID 指定が必要なページ |
| 3 | ナビゲーションガード | アクセス制御 | 認証が必要なページ、権限別の画面制御 |
ネストルートは、Provide/Inject や名前付きビューを活用することで、柔軟で保守性の高い設計が可能になります。動的セグメントでは、Zod などのバリデーションライブラリと Composable を組み合わせることで、型安全性を確保しつつ再利用可能な実装ができます。ナビゲーションガードは、責務を分離し、実行順序を明確にすることで、複雑な認証フローも管理しやすくなります。
これらの戦略は単独で使うのではなく、組み合わせることで真価を発揮します。EC サイトの例で示したように、用途に応じて適切な戦略を選択し、型安全性とエラーハンドリングを考慮した実装を心がけることが大切です。
Vue Router の設計は、アプリケーション全体のアーキテクチャに影響を与える重要な要素です。初期設計に時間をかけることで、後の保守性や拡張性が大きく向上しますので、本記事で紹介したパターンをぜひプロジェクトに活用してみてください。
関連リンク
articleVue.js ルーター戦略比較:ネスト/動的セグメント/ガードの設計コスト
articleVue.js でメモリリーク?watch/effect/イベント登録の落とし穴と検知法
articleVue.js リアクティビティ内部解剖:Proxy/ref/computed を図で読み解く
articleVue.js 本番運用チェックリスト:CSP/SRI/Cache-Control/エラーログの要点
articleVue.js コンポーネント API 設計:props/emit/slot を最小 API でまとめる
articleVue.js `<script setup>` マクロ辞典:defineProps/defineEmits/defineExpose/withDefaults
articleYarn vs npm vs pnpm 徹底比較:速度・メモリ・ディスク・再現性を実測
articleWeb Components と Declarative Shadow DOM の現在地:HTML だけで描くサーバー発 UI
articleVue.js ルーター戦略比較:ネスト/動的セグメント/ガードの設計コスト
articleCursor × Monorepo 構築:Yarn Workspaces/Turborepo/tsconfig path のベストプラクティス
articleTailwind CSS のコンテナクエリ vs 伝統的ブレイクポイント:適応精度を実測
articleCline × Monorepo(Yarn Workspaces)導入:パス解決とルート権限の最適解
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来