メインコンテンツまでスキップ

Vue Router の基本 — URL とコンポーネントを対応させる

ここまでの 14 章は 1 つの画面の中の話でした。この章から画面の切り替えに入ります。Vue Router は URL とコンポーネントの対応表を持ち、URL が変わったら対応するコンポーネントを <RouterView> の位置へ差し替えます。

この章のバージョンは vue-router 5.2.0 です。

この章で学ぶこと

  • ルート定義の書き方と、URL の一致のさせ方
  • route.params を使わずにコンポーネントへ値を渡す方法
  • パラメータだけが変わる遷移でコンポーネントが作り直されない理由
  • router-link-activerouter-link-exact-active が付く条件の違い
  • 遅延ローディングとコード分割の現行の書き方

対応表を作る

ルーターは 1 つのファイルにまとめるのが慣例です。

src/router.ts
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 の書き方渡るもの
trueroute.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
onMounted1 回だけ。2 回目は走らない
テンプレートの route.params.id追随して 2 になる
onMounted の中で取った値古いまま残る

つまりテンプレートに直接書いた分は勝手に更新されますが、マウント時に 1 回だけ実行した処理は取り残されます。データを取り直したいなら watch で拾います。

views/UserView.vue
<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呼び出しの引数として評価されますloadwatch より後ろで const 宣言していると、その時点では初期化されていないので ReferenceError になります。immediate を付けているかどうかに関係なく起きます。watch は渡すものを全部宣言した後に置いてください。

レイアウトを共有する

children を書くと入れ子のルートになります。親のコンポーネントの中に <RouterView> を置くと、そこへ子が入ります。

views/SettingsView.vue
<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.tsbuild.rollupOptions.output.manualChunks
webpackimport(/* 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 では効きません
  • ルートの遅延ローディングは非同期コンポーネントとは別の仕組みです。ルートコンポーネントに非同期コンポーネントを使ってはいけません
関連リファレンス

次に読む