開発tax-assistantメモ
Vue.js タブURL同期のリファクタリングとブラウザバック問題の修正
tax-assistantの各タブでURLクエリパラメータの管理が重複していたため、useTabQuerySync composableに共通化した。また、ブラウザの戻る操作(Alt+矢印キー)が効かない問題も修正した。
背景
問題点
- コードの重複: 各タブ(ReceiptTab, ResultTab, DuplicateView等)で同じようなURL更新処理が書かれていた
- Alt+矢印キーが効かない: ShiwakeRulesViewのキーボードハンドラがAlt修飾キーをチェックせず、ブラウザバックをブロックしていた
- 履歴管理の混乱:
router.pushとrouter.replaceの使い分けが不適切で、戻るボタンで意図したページに戻れない - タブ切り替え時の二重更新: タブ変更時にURL更新が複数回発火し、履歴が上書きされる
解決策
useTabQuerySync composable
タブのURLクエリパラメータ管理を共通化するcomposableを作成。
// composables/useTabQuerySync.ts
export function useTabQuerySync(options: {
tabName: string
buildQuery: () => Record<string, string | undefined>
restoreFromQuery: (query: LocationQuery) => void
}) {
const route = useRoute()
const router = useRouter()
const isInitialized = ref(false)
const isActive = inject<Ref<string>>('currentTab')
// URLクエリパラメータを更新
function updateQueryParams(push = false) {
if (!isActive?.value || isActive.value !== options.tabName) return
if (!isInitialized.value) return
const query = { tab: options.tabName, ...options.buildQuery() }
// undefined値を除外
const cleanQuery = Object.fromEntries(
Object.entries(query).filter(([_, v]) => v !== undefined)
)
if (push) {
router.push({ query: cleanQuery })
} else {
router.replace({ query: cleanQuery })
}
}
// タブがアクティブになった時にURLを更新
watch(
() => isActive?.value,
(newTab, oldTab) => {
if (newTab === options.tabName && oldTab !== options.tabName) {
nextTick(() => {
updateQueryParams(true) // タブ切り替えはpush
})
}
}
)
// 初期化完了をマーク
function markInitialized() {
isInitialized.value = true
}
return { updateQueryParams, markInitialized, isInitialized }
}
履歴ポリシーの使い分け
| 操作 | 方法 | 理由 |
|---|---|---|
| タブ切り替え | router.push | 戻るボタンで前タブに戻れるように |
| フィルタ変更(年月等) | router.replace | 細かい履歴を積まない |
| 帳票クリック遷移 | router.push | 元の画面に戻れるように |
Alt+矢印キーの修正
キーボードイベントハンドラでAlt修飾キーをチェックし、ブラウザナビゲーションをブロックしないように修正。
// Before
function handleKeydown(e) {
if (e.key === 'ArrowLeft') {
e.preventDefault() // ← ブラウザバックもブロックしていた
goToPrev()
}
}
// After
function handleKeydown(e) {
if (e.altKey) return // Alt+矢印はブラウザナビゲーション用
if (e.key === 'ArrowLeft') {
e.preventDefault()
goToPrev()
}
}
遷移時の履歴上書き防止
クレカ明細から帳票に遷移する際、複数のrouter.replaceが発火してrouter.pushの効果が消えていた問題を修正。
// index.vue - handleGoToReceipt
const isNavigatingToReceipt = ref(false)
function handleGoToReceipt(data: { batchId: string, fileName: string }) {
// フラグを立てて、watcherでのreplace更新を抑制
isNavigatingToReceipt.value = true
// まずpushで履歴を追加
router.push({
query: {
tab: 'receipt',
batch: data.batchId,
receiptFile: data.fileName,
// ... 他のパラメータ
}
})
// タブ切り替え
currentTab.value = 'receipt'
// 次のtickでフラグをクリア
nextTick(() => {
isNavigatingToReceipt.value = false
})
}
// ReceiptTab.vue - watcherでフラグを確認
const isNavigatingToReceipt = inject<Ref<boolean>>('isNavigatingToReceipt')
watch(currentItem, () => {
if (isNavigatingToReceipt?.value) return // 遷移中はスキップ
updateQueryParams(false)
})
修正したファイル
| ファイル | 変更内容 |
|---|---|
useTabQuerySync.ts | 新規作成 |
ShiwakeRulesView.vue | composable適用、Alt+矢印対応 |
CreditCardMatchingView.vue | composable適用 |
SquareMatchingView.vue | composable適用 |
ReceiptTab.vue | composable適用、遷移フラグ対応 |
ResultTab.vue | composable適用 |
DuplicateView.vue | composable適用 |
MillerColumnsView.vue | composable適用、Alt+矢印対応 |
MatrixView.vue | composable適用(新規追加) |
index.vue | isNavigatingToReceiptフラグ追加 |
テスト結果
- Alt+矢印キー: 全タブでブラウザバックが正常に動作
- タブ切り替え: 月次推移表 → 重複チェック → 戻るで正しく戻れる
- 帳票遷移: クレカ明細 → 帳票 → 戻るでクレカ明細に戻れる
- 連続クリック: 1ボタン → 戻る → 2ボタン → 戻るで毎回正しく戻れる
学び
- Vue Routerの
pushとreplaceの違いを意識する: 履歴に残すかどうかで使い分け - イベントハンドラでブラウザのデフォルト動作をブロックしないよう注意: Alt, Ctrl, Meta修飾キーをチェック
- composableで状態管理を共通化する際は、競合条件に注意: フラグやnextTickで制御