未分類
コーディング規約ビューア実装アプローチの比較
現状の実装
構成
- 一覧ページ:
/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)
- ページロード時、
window.location.hashをチェック - ハッシュがあれば該当要素にスクロール
- 500ms後にハッシュをURLから削除 (
history.replaceState)setTimeout(() => { history.replaceState(null, '', window.location.pathname); }, 500);
現在の課題
- URLの一貫性の問題
- ハッシュを削除すると
/coding-standards/viewerに戻る - 削除後は特定ルールへのディープリンクが失われる
- ハッシュを削除すると
- ブラウザ履歴の問題
- ハッシュ削除により、戻るボタンの動作が直感的でない可能性
- 共有の問題
- スクロール後の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の改善版
現在の実装で以下の変更を行う:
- ハッシュを削除せず、常にURLに反映
// viewer.vue:345-347 を削除 - スクロールに応じてハッシュを更新
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]で特定ルールに直接アクセス
実装の優先順位
- Phase 1(即時): ハッシュ削除ロジックの修正
- Phase 2(検討): activeIndexの変更をハッシュに反映
- 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の一貫性を改善するのが最善である。