今日は Vue Router を触ったので、そのメモです。
前回は composable でした。Nuxt はファイルを置くだけでページが増えるらしいのですが、その下地が Vue Router だと知り、先に素の状態で動かしておくことにしました。画像解析の結果を一覧で見て、選んだものを詳細で確認する。その動きを作りたいのが動機です。
まだ途中段階なので、勘違いなどあれば教えていただけるとうれしいです。
今日やったこと
一言でいうと「1画面だけのアプリに、URLで切り替わるページを足した」日です。
- ルーターの設定ファイルを作った
-
<RouterLink>と<RouterView>の役割を確認した - 動的ルート(
/analysis/:id)で詳細ページを作った - クエリパラメータで検索条件をURLに持たせた
- ナビゲーションガードを試した
導入とルーター定義
インストールはこれだけでした。
# Vue3 向けは vue-router@4。3系は Vue2 用。
npm install vue-router@4
ルーターの定義は src/router/index.js にまとめました。この一覧がそのまま画面一覧になります。
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import AnalysisList from '../pages/AnalysisList.vue'
import AnalysisDetail from '../pages/AnalysisDetail.vue'
import NotFound from '../pages/NotFound.vue'
const routes = [
// path は URL、component はそのURLで表示するもの。
// name があると、パス文字列を書かずに遷移できる。
{ path: '/', name: 'list', component: AnalysisList },
// :id は何でも入る「動的な穴」。/analysis/12 も /analysis/abc も一致する。
{ path: '/analysis/:id', name: 'detail', component: AnalysisDetail },
// どれにも一致しなかったときの受け皿。無いとURL打ち間違いで真っ白になる。
{ path: '/:pathMatch(.*)*', name: 'notFound', component: NotFound }
]
export const router = createRouter({
// URL に # が付かない通常の形。Hash 版だと /#/analysis/12 になる。
history: createWebHistory(),
routes
})
作ったルーターは起動時に登録します。
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'
// use(router) を書いて初めて、RouterLink や RouterView が使えるようになる。
createApp(App).use(router).mount('#app')
RouterLink と RouterView
この2つの役割を取り違えて、しばらく画面が出ませんでした。
<!-- App.vue。ここは全ページ共通の枠になる -->
<template>
<nav>
<!-- a タグだとページ全体が再読み込みされ、Vue の状態が全部リセットされる -->
<RouterLink to="/">一覧</RouterLink>
<!-- name で書けば、パスの構造を変えてもここは直さなくていい -->
<RouterLink :to="{ name: 'detail', params: { id: 12 } }">詳細</RouterLink>
</nav>
<!-- 現在のURLに対応したページが、ここに差し込まれる。
書き忘れると、URLは変わるのに画面が変わらない -->
<RouterView />
</template>
RouterLink が「移動する手段」、RouterView が「表示される場所」。この分担が分かってから見通しがよくなりました。
![]() |
💬「URLは変わるのに画面はそのまま、ってやつ、これが原因だったんだね。」 |
Nuxt では pages/ にファイルを置くだけで、この routes 定義が自動生成されるそうです。手で書いておけば、何が省略されたのか分かりそうです。
動的ルートで詳細ページを作る
一覧で選んだ画像の id を URL から受け取ります。今日一番作りたかった部分です。
<script setup>
// pages/AnalysisDetail.vue
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
// useRoute は「今どのURLにいるか」を返す composable。
const route = useRoute()
const item = ref(null)
const load = async (id) => {
const res = await fetch(`/api/analysis/${id}`)
item.value = await res.json()
}
// /analysis/12 → /analysis/13 の移動ではコンポーネントが使い回され、
// onMounted が再実行されない。そこで watch で id を監視して読み直す。
watch(
() => route.params.id,
(id) => load(id),
{ immediate: true } // 初回もすぐ実行する
)
</script>
<template>
<p v-if="!item">読み込み中...</p>
<div v-else>
<h2>{{ item.fileName }}</h2>
<p>スコア:{{ item.score }}</p>
</div>
</template>
一覧側からは useRouter でコードから遷移させます。useRoute(今の場所)と useRouter(移動の操作)は名前が似ていて、ここも混乱しました。
<script setup>
// pages/AnalysisList.vue
import { useRouter } from 'vue-router'
const router = useRouter()
// push は履歴に積む移動。ブラウザの「戻る」で一覧に戻れる。
// 履歴を残したくない場合(ログイン後の画面など)は replace を使う。
const openDetail = (id) => router.push({ name: 'detail', params: { id } })
</script>
これで一覧 → 詳細 → 戻る、という動きがブラウザの機能そのままで使えました。詳細ページのURLを共有できるのもうれしいところです。
クエリで検索条件を持たせる
前回作った useAnalysisList の検索条件を URL に載せます。
<script setup>
// pages/AnalysisList.vue
import { computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAnalysisList } from '../composables/useAnalysisList'
const route = useRoute()
const router = useRouter()
const { items, fetchList } = useAnalysisList()
// /?keyword=桜 の「桜」の部分。無ければ空文字にしておく。
const keyword = computed(() => route.query.keyword ?? '')
// 検索時は直接 fetch せずURLを書き換える。処理の入口が1つにまとまる。
const search = (word) =>
router.push({ name: 'list', query: { keyword: word } })
watch(keyword, (word) => fetchList(word), { immediate: true })
</script>
params は「どのページか」、query は「その中の絞り込み条件」。この使い分けで納得しました。検索結果のURLをブックマークできるのも利点です。
つまずいたところ
一言でいうと「画面は切り替わったのに中身が変わらない」で詰まりました。
1. 同じページ内の移動で再取得されない
/analysis/12 から /analysis/13 へ移動しても onMounted が動かず、前の画像の情報が残ったままになりました。
![]() |
💬「同じ部品を使い回すから、わざわざ作り直さないってことか。」 |
同じコンポーネントを使うルート同士の移動は「作り直し」ではなく「中身の差し替え」なので、watch で route.params を見張るのが定石のようです。公式の動的ルートマッチングにも説明がありました。
2. リロードすると404になる
/analysis/12 を直接開いたりリロードすると404が返ってきました。開発サーバーでは動いていたので焦りました。createWebHistory を使う場合、サーバー側で「どのURLが来ても index.html を返す」設定が要るようです。Vite の開発サーバーは自動でやってくれていて、本番配信では別途設定が必要、という理解でいます。ここは Azure に置くときに確認したいです。
3. router と route の取り違え
useRouter() と useRoute() を何度も書き間違え、router.query と書いて undefined になるのを繰り返しました。暫定的に「r が多いほう(router)が動かすほう」と覚えています。useRoute は現在地を表す読み取り専用の情報、useRouter は push や replace を持つ操作用、という理解です。
4. ガードの戻り値を書き忘れる
ログインしていない場合に一覧へ戻す処理を試したときのコードです。
// 全てのページ遷移の前に呼ばれる。to: 行き先、from: 今いる場所
router.beforeEach((to, from) => {
const isLoggedIn = false // 仮
// 行き先が要ログインで、未ログインなら別の場所へ飛ばす。
if (to.meta.requiresAuth && !isLoggedIn) {
return { name: 'list' } // 行き先を差し替える
}
// それ以外は何も返さない(= そのまま進む)。false を返すと中止。
})
meta はルート定義に自由な情報を持たせる場所で、meta: { requiresAuth: true } のように書きます。最初は条件に合わない場合まで return false していて、全ての遷移が止まりました。
今の理解(メモ)
まだふわっとしていますが、今の頭の中ではこう整理しています。
- ルーターは「URL と コンポーネントの対応表」
-
RouterLinkで移動し、RouterViewの場所に表示される -
:idはparams、?keyword=はquery。前者はページの識別、後者は条件 - 現在地を知るのが
useRoute、動かすのがuseRouter - 同じコンポーネント間の移動では作り直しが起きないので
watchが必要 -
beforeEachは全遷移の前に通る関門
Nuxt との関係でいうと、この routes 定義が pages/ のファイル構成に置き換わるようです。[id].vue が :id、middleware/ が beforeEach に相当するらしいので、今日の設定がそのままファイルの形に変わるだけ、と考えています。
次にやりたいこと
次は状態の共有と Nuxt への移行を進めたいです。
- Pinia を触る。検索条件やログイン状態を複数ページから参照したい
- ネストされたルートで、詳細ページ内にタブ(解析結果 / メタ情報)を作る
- ページ遷移のアニメーションを
<Transition>で付けてみる - Nuxt に移り、
pages/の自動ルーティングが今日の設定とどう対応するか確かめる
![]() |
💬「残りはPiniaだけだね。そろそろNuxtに行けそう。」 |
今日の整理
後から見返せるように整理します。
| 用語 | 役割(今の理解) | 利用イメージ | Nuxt では |
|---|---|---|---|
routes |
URLと画面の対応表 | 一覧・詳細・404 |
pages/ から自動生成 |
RouterLink |
再読み込みなしで移動 | 一覧から詳細へ | <NuxtLink> |
RouterView |
ページの表示場所 |
App.vue の中 |
<NuxtPage> |
params |
どのページかを決める値 | /analysis/12 |
[id].vue |
query |
絞り込み条件 | ?keyword=桜 |
同じ |
useRouter |
移動の操作 | クリックで詳細へ | 同じ |
beforeEach |
遷移前の関門 | ログインチェック | middleware/ |
画面遷移の流れは、こう捉えています。
おわりに
やってみると、Vue Router 自体はそれほど大きい仕組みではありませんでした。ただ「同じコンポーネントは作り直されない」ことやリロードで404になることなど、動かさないと気づけない部分が多かったです。前回と同じで、手を動かした分だけ理解が進みました。
一覧を出して、条件で絞って、詳細を見て、戻る。もともと作りたかった画面の骨格が、これで一通りつながりました。あとは状態の共有さえ分かれば Nuxt に移れそうです。
学習中の内容なので、勘違いや理解不足もあると思います。もし気になる点があれば教えてもらえるとうれしいです。
注意事項
本ブログに掲載している内容は、私個人の見解であり、
所属する組織の立場や戦略、意見を代表するものではありません。
あくまでエンジニアとしての経験や考えを発信していますのでご了承ください。
