Vue Router の基本 — URL とコンポーネントを対応させる
ここまでの 14 章は 1 つの画面の中の話でした。この章から画面の切り替えに入ります。Vue Router は URL とコンポーネントの対応表を持ち、URL が変わったら対応するコンポーネントを <RouterView> の位置へ差し替えます。
この章のバージョンは vue-router 5.2.0 です。
この章で学ぶこと
- ルート定義の書き方と、URL の一致のさせ方
route.paramsを使わずにコンポーネントへ値を渡す方法- パラメータだけが変わる遷移でコンポーネントが作り直されない理由
router-link-activeとrouter-link-exact-activeが付く条件の違い- 遅延ローディングとコード分割の現行の書き方
対応表を作る
ルーターは 1 つのファイルにまとめるのが慣例です。
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import HomeView from './views/HomeView.vue'
const routes: RouteRecordRaw[] = [
{ path: '/', name: 'home', component: HomeView },
{
path: '/users/:id',
name: 'user',
// 動的 import にしておくとバンドラがチャンクを分ける
component: () => import('./views/UserView.vue'),
props: true,
},
{
path: '/settings',
component: () => import('./views/SettingsView.vue'),
children: [
{ path: '', name: 'settings', component: () => import('./views/ProfileTab.vue') },
{ path: 'security', name: 'security', component: () => import('./views/SecurityTab.vue') },
],
},
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('./views/NotFoundView.vue') },
]
export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})
createApp(App).use(router).mount('#app') で登録すると、<RouterView> と <RouterLink> がどこでも使えるようになります。
createWebHistory() は履歴 API を使うので URL に # が入りません。ただし配信するサーバー側の設定が要ります (Vite の開発サーバーは既定で面倒を見てくれるので、開発中は気づきません)。/users/42 を直接開いたりリロードしたりするとリクエストはサーバーへ行くので、公式ガイドの言う「静的ファイルに一致しない URL には index.html を返す」設定が無いとサーバーの 404 になります。上の catch-all ルートが受け止めるのはアプリまで届いた URL だけです。20 章でデプロイと合わせて扱います。
name を付けておくと、パスの文字列ではなく名前で遷移できます。パスを変えたときに書き換える場所が 1 か所で済みます。
<RouterLink :to="{ name: 'user', params: { id: 42 } }">ユーザー 42</RouterLink>
プログラムから遷移するときは useRouter() を使います。useRoute() と名前が似ていますが、useRouter() が操作する側、useRoute() が現在の状態を読む側です。
const router = useRouter()
const route = useRoute()
router.push({ name: 'user', params: { id: 42 } }) // 履歴を 1 つ増やす
router.replace('/login') // 現在の履歴を置き換える
router.back() // 1 つ戻る
URL の一致のさせ方
:id のように書いた部分がパラメータになり、route.params.id で読めます。ほかに 2 つの記法をよく使います。
| 書き方 | 一致するもの |
|---|---|
/users/:id | /users/42 |
/users/:userId? | /users と /users/42 の両方 |
/:pathMatch(.*)* | どれにも一致しなかった URL (アプリ内の 404 用) |
? は「0 個か 1 個」です。ただし条件があります。セグメントの中身が「任意パラメータ 1 つだけ」でないと、末尾スラッシュの無い URL には一致しません。
実測するとこうなります。
| ルート | セグメントの中身 | /users | /users/ |
|---|---|---|---|
/users/:uid? | 任意パラメータ 1 つ | 一致する | (未確認) |
/users/:uid?:name? | 任意パラメータ 2 つ | 一致しない | 一致する |
/users/:uid?-:name? | 任意パラメータ 2 つ + 区切り | 一致しない | (未確認。/users/- には一致する) |
区切りの有無は関係ありません。 2 行目は区切りが無くても一致しません。任意パラメータを 2 つ並べた時点で、セグメントごと省略できる形ではなくなります。省略できる形にしたいなら、セグメントにパラメータを 1 つだけ置きます。
コンポーネントへ値を渡す
useRoute() を使うとコンポーネントが URL の形に依存します。テストでもルーターを組み立てる必要が出てきます。props オプションを使うと、この依存を切れます。
3 つのモードがあり、実測でもそれぞれ期待どおりに渡りました。
props の書き方 | 渡るもの |
|---|---|
true | route.params がそのまま props になる |
オブジェクト ({ label: '固定' }) | 書いた値がそのまま入る。静的な値向け |
関数 (route => ({ query: route.query.q })) | 戻り値のオブジェクトが props になる。クエリや型変換に使う |
名前付きビュー (components: で複数のビューを持つルート) ではビューごとに指定します (props: { default: true, sidebar: false })。単数の component: に同じ書き方をするとオブジェクトモードと解釈され、default という名前の props がそのまま渡ってしまいます。
関数モードには注意があります。公式ガイドは「ルート変更時にしか評価されないので、状態を持たせない」としています。コンポーネントの状態から props を作りたいなら、ラップするコンポーネントを挟みます。
パラメータだけが変わる遷移
/users/1 から /users/2 へ移ると、URL は変わりますが対応するコンポーネントは同じです。Vue Router はここでコンポーネントを作り直しません。実測するとこうなります。
| 見るもの | /users/1 → /users/2 |
|---|---|
onMounted | 1 回だけ。2 回目は走らない |
テンプレートの route.params.id | 追随して 2 になる |
onMounted の中で取った値 | 古いまま残る |
つまりテンプレートに直接書いた分は勝手に更新されますが、マウント時に 1 回だけ実行した処理は取り残されます。データを取り直したいなら watch で拾います。
<script setup lang="ts">
import { ref, watch } from 'vue'
const props = defineProps<{ id: string }>()
const user = ref<{ name: string } | null>(null)
async function load(id: string) {
const res = await fetch(`/api/users/${id}`)
user.value = await res.json()
}
// パラメータだけが変わる遷移ではコンポーネントが作り直されないので watch で拾う
watch(() => props.id, load, { immediate: true })
</script>
<template>
<section>
<h1>{{ user?.name ?? '読み込み中' }}</h1>
</section>
</template>
{ immediate: true } を付けているので初回も走り、onMounted は要りません。5 章の watch がそのまま使えます。
watch(() => props.id, load) の load は呼び出しの引数として評価されます。load を watch より後ろで const 宣言していると、その時点では初期化されていないので ReferenceError になります。immediate を付けているかどうかに関係なく起きます。watch は渡すものを全部宣言した後に置いてください。
レイアウトを共有する
children を書くと入れ子のルートになります。親のコンポーネントの中に <RouterView> を置くと、そこへ子が入ります。
<template>
<div class="settings">
<nav>
<RouterLink :to="{ name: 'settings' }">プロフィール</RouterLink>
<RouterLink :to="{ name: 'security' }">セキュリティ</RouterLink>
</nav>
<RouterView />
</div>
</template>
子の path を空文字にすると、親のパスちょうど (/settings) に一致する既定の子になります。実測すると、/settings では既定の子が入り、/settings/security へ遷移しても親の要素は同じままで子だけが入れ替わりました。
既定の子を表示している間は、{ name: 'settings' } へのリンクが exact-active になります。空の path を持つ子は親と同じ URL に一致するので、リンクの判定でも最も深い一致として扱われます。
リンクが「今いる場所」になる条件
<RouterLink> は現在地に応じて 2 つのクラスを付けます (custom を付けて v-slot で中身を自分で描く形では付きません)。判定の違いはこうです。
| クラス | 付く条件 |
|---|---|
router-link-active | リンクが指すルートの一番深いレコードが現在のルートの一致リストに含まれている、かつ現在のパラメータがリンクのパラメータを包含している |
router-link-exact-active | そのレコードが現在の一致リストの最後で、パラメータも完全に一致している |
一番深いレコードが見つからないときの例外が 1 つあります。リンクが「親と同じパスの子」(空の path を持つ子) を指している場合は、その親のレコードで判定します。 /settings/security を表示しているとき { name: 'settings' } へのリンクに active が付くのはこの経路です。ただし現在地そのものが同じパスの子であれば、この例外は使われません。
つまり /settings/security を表示しているとき、/settings へのリンクは active のまま exact-active ではありません。パンくずやタブの親側をハイライトしたいときはこの差を使います。
パラメータの条件も効きます。/users/1 を表示しているとき、/users/2 へのリンクにはどちらも付きません。一致レコードは同じでも、現在のパラメータがリンクのパラメータを包含していないためです。実測でも /users/1 へのリンクだけに active が付きました。
クラス名は 3 段で決まります。
| 指定する場所 | 例 |
|---|---|
| リンクごと | <RouterLink active-class="is-open"> |
| ルーター全体 | createRouter({ linkActiveClass: 'nav-on' }) |
| 指定なし | router-link-active |
上のものが下を置き換えます。active-class を渡すと router-link-active は付きません。ユーティリティクラス中心の CSS では全体の既定を差し替えるのが楽です。
遅延ローディングとコード分割
component にコンポーネントの Promise を返す関数を渡すと、そのルートを初めて訪れたときにだけ読み込まれます。実測すると、初回訪問で 1 回呼ばれ、別のルートへ移って戻ってきても呼ばれませんでした (成功した結果がキャッシュされます)。
返すのは Promise です。 コンポーネントそのものを返す関数を書くと、開発ビルドでは診断メッセージが出て救済されます。この救済は開発ビルドの分岐の中にあるので、本番ビルドでは同じコードが通りません (本番の挙動は実装を読んで確かめたもので、実行しての確認はしていません)。関数コンポーネントをルートに置きたいときも遅延ローディングと区別が付かないので、公式ガイドは displayName を付けるよう案内しています。
{ path: '/users/:id', component: () => import('./views/UserView.vue') }
公式ガイドは「すべてのルートで動的 import を使うのが基本」としています。Vite でも webpack でも、これだけでコード分割に乗ります。
チャンクをまとめたいときの書き方はバンドラごとに違います。
| バンドラ | 書き方 |
|---|---|
| Vite (Rollup) | vite.config.ts の build.rollupOptions.output.manualChunks |
| webpack | import(/* webpackChunkName: "group-user" */ './UserView.vue') |
webpackChunkName のコメントは Vite では効きません。 公式ガイドは Vite と webpack を別の節に分け、このコメントを webpack 側にだけ載せています。create-vue で作ったプロジェクトは Vite なので、こちらは使えません。
もう 1 つ、混同しやすい点があります。ルートの遅延ローディングは 13 章の非同期コンポーネントとは別の仕組みです。 13 章で見たとおり、ルートの遅延ローディングは現時点では <Suspense> を起動しません。公式ガイドは「ルートコンポーネントに非同期コンポーネントを使わないこと」と明記しています。ルートには素の関数を渡し、非同期コンポーネントはその中で使います。
まとめ
- ルーターは
createRouter({ history, routes })で作り、app.use(router)で登録します。createWebHistory()は静的ファイルに一致しない URL へindex.htmlを返すサーバー設定が前提で、catch-all ルートが受け止めるのはアプリまで届いた URL だけです nameを付けておけば{ name: 'user', params: { id: 42 } }で遷移でき、パスの変更に強くなりますuseRouter()が操作する側、useRoute()が現在の状態を読む側です:uid?は「0 個か 1 個」です。ただしセグメントの中身が「任意パラメータ 1 つだけ」でないと、末尾スラッシュの無い URL には一致しません。区切りの有無は関係ありません。/users/:uid?:name?も/usersに一致せず、/users/になら一致しますpropsオプションでrouteへの依存を切れます。true(params をそのまま) / オブジェクト (静的な値) / 関数 (クエリや型変換) の 3 モード。ビューごとの指定はcomponents:を持つルートの話で、単数のcomponent:に書くとオブジェクトモードになります- 関数モードはルート変更時にしか評価されません。コンポーネントの状態から props を作りたいならラップするコンポーネントを挟みます
- パラメータだけが変わる遷移ではコンポーネントが作り直されません。
onMountedは 1 回しか走らないので、データの取り直しはwatchで拾います watchに渡すコールバックは呼び出しの引数として評価されるので、watchより後ろでconst宣言するとimmediateの有無に関係なくReferenceErrorになりますchildrenで入れ子のルートになり、親の<RouterView>に子が入ります。子のpathを空文字にすると親のパスちょうどに一致する既定の子になります。その子を表示している間だけそのリンクがexact-activeになり、兄弟へ遷移するとactiveだけが残ります。兄弟へ遷移しても親の要素は同じままですrouter-link-activeは「リンクの一番深いレコードが現在の一致リストに含まれている」と「現在のパラメータがリンクのパラメータを包含している」の両方で付きます。router-link-exact-activeはそれが一致リストの最後で、パラメータも完全一致であることまで要求します。リンクが空のpathを持つ子を指すときは親のレコードで判定します。子を表示している間は親のリンクが active のままで、/users/1表示中の/users/2へのリンクにはどちらも付きません- クラス名はリンクごと → ルーター全体 → 既定の 3 段で、上が下を置き換えます
componentにコンポーネントの Promise を返す関数を渡すと初回訪問時にだけ読み込まれ、以降は成功した結果がキャッシュされます。Promise でないものを返す関数は開発ビルドでしか救済されません- チャンクのまとめ方はバンドラごとに違います。
webpackChunkNameのコメントは Vite では効きません - ルートの遅延ローディングは非同期コンポーネントとは別の仕組みです。ルートコンポーネントに非同期コンポーネントを使ってはいけません
- Vue Router — Dynamic Route Matching — パラメータの書き方
- Vue Router — Routes' Matching Syntax —
?と正規表現の記法 - Vue Router — Passing Props to Route Components —
propsの 3 モード - Vue Router — Lazy Loading Routes — 動的 import とチャンクのまとめ方
次に読む
- ナビゲーションガード — 遷移を止める / 逸らす
- Pinia — 画面を跨ぐ状態