• #vue
  • #nuxt
  • #routing
  • #ux
未分類

コーディング規約ビューア実装アプローチの比較

現状の実装

構成

  • 一覧ページ: /coding-standards/index.vue
  • ビューアページ: /coding-standards/viewer.vue
  • データソース: /public/data/coding-standards.json

現在のナビゲーション方式(ハッシュベース)

一覧ページからビューアページへのリンク:

<NuxtLink :to="`/coding-standards/viewer#${getRuleId(rule)}`">

URL例: http://localhost:3000/coding-standards/viewer#rule-4-3

実装の流れ (viewer.vue:325-353)

  1. ページロード時、window.location.hash をチェック
  2. ハッシュがあれば該当要素にスクロール
  3. 500ms後にハッシュをURLから削除 (history.replaceState)
    setTimeout(() => {
      history.replaceState(null, '', window.location.pathname);
    }, 500);
    

現在の課題

  1. URLの一貫性の問題
    • ハッシュを削除すると /coding-standards/viewer に戻る
    • 削除後は特定ルールへのディープリンクが失われる
  2. ブラウザ履歴の問題
    • ハッシュ削除により、戻るボタンの動作が直感的でない可能性
  3. 共有の問題
    • スクロール後のURLを共有しても、特定のルールを指せない

アプローチ比較

アプローチ1: ハッシュベース(現在の実装を改善)

変更案: ハッシュを削除しない

// viewer.vue の line 345-347 を削除または条件付きに変更
// ハッシュを保持することで、URLの一貫性を維持

メリット

  • ✅ 現在のスクロールベースのUX体験を維持
  • ✅ すべてのルールが1ページに存在(完全なコンテキスト)
  • ✅ 連続的な学習体験
  • ✅ コード変更が最小限

デメリット

  • ❌ ハッシュとスクロール位置の同期管理が必要である
  • ❌ ページ遷移なしのため、ブラウザ履歴が増えない
  • ❌ 各ルールが独立したページでない

実装の改善点

// ユーザーが手動スクロールしたときのみハッシュを更新
watch(activeIndex, (newIndex) => {
  const ruleId = getRuleId(steps.value[newIndex]);
  const newHash = `#${ruleId}`;

  // 現在のハッシュと異なる場合のみ更新
  if (window.location.hash !== newHash) {
    history.replaceState(null, '', `${window.location.pathname}${newHash}`);
  }
});

アプローチ2: 動的ルーティング

構成案

/coding-standards/viewer/           # すべてのルール一覧(オプション)
/coding-standards/viewer/rule-1-1   # 特定ルール
/coding-standards/viewer/rule-4-3   # 特定ルール

ファイル構成

pages/
  coding-standards/
    index.vue                    # 一覧ページ
    viewer/
      index.vue                  # すべてのルール(オプション)
      [ruleId].vue               # 個別ルールページ

実装イメージ (viewer/[ruleId].vue)

<script setup>
const route = useRoute();
const ruleId = route.params.ruleId;

const { data: allRules } = await useFetch('/data/coding-standards.json');
const currentIndex = computed(() =>
  allRules.value.findIndex(r => getRuleId(r) === ruleId)
);
const currentRule = computed(() => allRules.value[currentIndex.value]);

const prevRule = computed(() => {
  const prevIndex = currentIndex.value - 1;
  return prevIndex >= 0 ? allRules.value[prevIndex] : null;
});

const nextRule = computed(() => {
  const nextIndex = currentIndex.value + 1;
  return nextIndex < allRules.value.length ? allRules.value[nextIndex] : null;
});

function goToNextStep() {
  if (nextRule.value) {
    navigateTo(`/coding-standards/viewer/${getRuleId(nextRule.value)}`);
  }
}

function goToPrevStep() {
  if (prevRule.value) {
    navigateTo(`/coding-standards/viewer/${getRuleId(prevRule.value)}`);
  }
}
</script>

<template>
  <div class="rule-viewer">
    <!-- 現在のルールのみ表示 -->
    <div class="rule-content">
      <h2>{{ currentRule.title }}</h2>
      <p v-html="formatDescription(currentRule.description)"></p>

      <div class="code-examples">
        <div class="bad-example">
          <div class="code-label bad">❌ Bad</div>
          <pre><code class="language-javascript">{{ currentRule.badCode }}</code></pre>
        </div>

        <div class="good-example">
          <div class="code-label good">✅ Good</div>
          <pre><code class="language-javascript">{{ currentRule.goodCode }}</code></pre>
        </div>
      </div>
    </div>

    <!-- ナビゲーション -->
    <div class="nav-buttons">
      <button @click="goToPrevStep" :disabled="!prevRule">← 前へ</button>
      <span>{{ currentIndex + 1 }} / {{ allRules.length }}</span>
      <button @click="goToNextStep" :disabled="!nextRule">次へ →</button>
    </div>
  </div>
</template>

メリット

  • 明確なURL: 各ルールが独自のURLを持つ
  • ブラウザ履歴: 戻る/進むが正しく機能
  • 共有しやすい: ディープリンクが常に有効
  • SEOフレンドリー: 各ページが独立してインデックス可能
  • 状態管理がシンプル: URLがstate of truth

デメリット

  • ❌ ページ遷移のオーバーヘッド
  • ❌ 現在のスクロールベースの連続的なUXが失われる
  • ❌ 実装の変更範囲が大きい
  • ❌ すべてのルールを一度に見渡せない

推奨アプローチ

短期的な解決策: アプローチ1の改善版

現在の実装で以下の変更を行う:

  1. ハッシュを削除せず、常にURLに反映
    // viewer.vue:345-347 を削除
    
  2. スクロールに応じてハッシュを更新
    watch(activeIndex, (newIndex) => {
      const ruleId = getRuleId(steps.value[newIndex]);
      if (window.location.hash !== `#${ruleId}`) {
        history.replaceState(null, '', `${window.location.pathname}#${ruleId}`);
      }
    });
    

理由:

  • 現在のスクロールベースの優れたUX体験を維持
  • コード変更が最小限
  • URLの一貫性と共有可能性が向上

長期的な選択肢: ハイブリッドアプローチ

両方の利点を活かす:

/coding-standards/viewer           # スクロール形式(全体を学習)
/coding-standards/viewer/[ruleId]  # 個別ルールページ(リファレンス用)
  • 学習モード: viewer ですべてのルールをスクロールで閲覧
  • リファレンスモード: viewer/[ruleId] で特定ルールに直接アクセス

実装の優先順位

  1. Phase 1(即時): ハッシュ削除ロジックの修正
  2. Phase 2(検討): activeIndexの変更をハッシュに反映
  3. Phase 3(将来): 必要に応じて動的ルーティングを追加

コード差分(Phase 1の修正)

viewer.vue の修正

  // Handle hash navigation
  if (window.location.hash) {
    const hash = window.location.hash.slice(1); // Remove '#'
    const targetElement = document.getElementById(hash);
    if (targetElement) {
      // Find and set active index immediately
      const targetIndex = stepElements.indexOf(targetElement);
      if (targetIndex !== -1) {
        setActive(targetIndex);
      }

      setTimeout(() => {
        const elementTop = targetElement.getBoundingClientRect().top + window.scrollY;
        // Position the step at 0.5vh (middle of the detection range 0.2-0.8)
        const offset = window.innerHeight * 0.5;
        window.scrollTo({
          top: elementTop - offset,
          behavior: 'smooth'
        });
-
-       // Clear hash after scrolling to allow normal scrolling
-       setTimeout(() => {
-         history.replaceState(null, '', window.location.pathname);
-       }, 500);
      }, 100); // Small delay to ensure layout is complete
    }
  } else {
    // Only call updateProgress on initial load if there's no hash
    updateProgress();
  }

Phase 2の追加(スクロールでハッシュ更新)

// activeIndexが変更されたらURLのハッシュも更新
watch(activeIndex, (newIndex) => {
  const step = steps.value[newIndex];
  if (step) {
    const ruleId = getRuleId(step);
    const newHash = `#${ruleId}`;

    // 現在のハッシュと異なる場合のみ更新
    if (window.location.hash !== newHash) {
      history.replaceState(null, '', `${window.location.pathname}${newHash}`);
    }
  }
});

結論

推奨: アプローチ1(ハッシュベースの改善)から開始し、必要に応じて動的ルーティングを追加

現在の実装は優れたスクロールベースのストーリーテリングUXを提供しているため、これを維持しつつURLの一貫性を改善するのが最善である。