管理画面のように、画面が増えてくると、ルートの定義も複雑になります。共通のレイアウトの中に子の画面を並べるネスト、ユーザーIDのように変わる部分を持つ動的ルート、ファイルの分割と、整理すべきことが増えます。この記事では、Vue Router 4を前提に、ネストと動的ルートの設計を、つまずきやすい点とあわせて整理します。ルーティングの基本はルーティングによる画面切り替えをご覧ください。
ネストしたルートを定義する
ルートにchildrenを書くと、親子の関係を持つルートを作れます。親のコンポーネントはレイアウトとして、共通の枠を表示し、中の<RouterView>に、子のコンポーネントが表示されます。
// router/modules/admin.js
export default [
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
children: [
// path が空文字の子は、/admin そのものを開いたときに表示される(デフォルトの子)
{
path: '',
name: 'AdminDashboard',
component: () => import('@/views/admin/Dashboard.vue')
},
// 子の path は / で始めない(親の /admin からの続きになる) → /admin/users
{
path: 'users',
name: 'AdminUsers',
component: () => import('@/views/admin/Users.vue')
}
]
}
]
<!-- layouts/AdminLayout.vue -->
<template>
<div class="admin">
<aside>
<RouterLink :to="{ name: 'AdminDashboard' }">ダッシュボード</RouterLink>
<RouterLink :to="{ name: 'AdminUsers' }">ユーザー</RouterLink>
</aside>
<main>
<!-- 子ルートのコンポーネントが、ここに表示される -->
<RouterView />
</main>
</div>
</template>
子のパスのルール
- 子の
pathが/で始まらなければ、親のパスの続きになります(usersは/admin/users)。 - 子の
pathが/で始まると、親からの相対ではなく、ルートからのパスとして扱われます。画面の入れ子と、URLの入れ子を切り離したいときに使えます。 - 空文字(
path: '')の子は、親のパスそのものを開いたときに表示される、デフォルトの子です。 - 名前付きルートは、親ではなく子に名前を付けるのが推奨です。公式は、そのほうがナビゲーションの挙動が予測しやすくなる、としています。
親のcomponentを省略する(4.1以降)
Vue Router 4.1以降は、親にcomponentを書かずに、パスの接頭辞でルートをグループ化できます。レイアウトは不要だが、共通のガードやmetaをまとめたいときに便利です。ガードとmetaはルーティングの動的管理をご覧ください。
// Vue Router 4.1 以降:親に component を書かずに、パスの接頭辞でグループ化できる
{
path: '/admin',
children: [
{ path: 'users', component: Users },
{ path: 'settings', component: Settings }
]
}
// 親のレイアウトは不要だが、共通のガードや meta をまとめたいときに使える
複数のレイアウトを管理する
管理画面と一般向けの画面で、異なるレイアウトを使いたいときは、トップレベルのルートごとにレイアウトを分けます。ルートの定義は、レイアウト単位でファイルに分割すると、見通しがよくなります。ページごとにレイアウトを切り替える考え方はページごとに異なるレイアウトを切り替える方法でも解説しています。
// router/modules/main.js:一般ユーザー向けのレイアウト
export default [
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
children: [
{ path: '', name: 'Home', component: () => import('@/views/Home.vue') },
{ path: 'about', name: 'About', component: () => import('@/views/About.vue') }
]
}
]
最後に、すべてのモジュールをrouter/index.jsでまとめます。一般向けのルートを含めるのを忘れないでください。
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import mainRoutes from './modules/main'
import adminRoutes from './modules/admin'
const routes = [
...mainRoutes,
...adminRoutes,
// どれにも一致しなかったURLは、404ページを表示する
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
]
export const router = createRouter({
history: createWebHistory(),
routes
})
末尾の/:pathMatch(.*)*は、どのルートにも一致しなかったURLを受け止める、いわゆる「catch-all」です。ここでトップへredirectするのではなく、404用のページを表示するのがおすすめです。トップへ転送すると、ユーザーはなぜ移動したのか分からず、リンク切れにも気づけません。公式の例も、専用のコンポーネントを表示する形です。
動的ルートとパラメータ
:idのように、コロンで始まる部分が、URLによって変わるパラメータです。/admin/users/123の123が、route.params.idに入ります。()の中に正規表現を書くと、一致する形を絞れます。props: trueを付けると、パラメータをコンポーネントのpropsとして受け取れます。
{
// :id の後ろの (\\d+) は正規表現。数字だけに一致する
path: 'users/:id(\\d+)',
name: 'AdminUserDetail',
component: () => import('@/views/admin/UserDetail.vue'),
props: true // route.params を、コンポーネントの props として渡す
}
同じコンポーネントが再利用される落とし穴
公式が注意している点です。/admin/users/1から/admin/users/2へ移動するとき、Vue Routerは同じコンポーネントのインスタンスを再利用します。このため、onMountedなどのライフサイクルは、再実行されません。onMountedでデータを取得していると、別のユーザーに移動しても、表示が古いままになります。パラメータの変化を、watchで受けるのが基本です。
<!-- views/admin/UserDetail.vue -->
<script setup>
import { ref, watch } from 'vue'
// props: true にしたので、URL の :id が props.id として届く(文字列)
const props = defineProps({ id: String })
const user = ref(null)
// /admin/users/1 → /admin/users/2 に移動しても、同じコンポーネントが再利用される。
// onMounted は再実行されないので、id の変化を watch で受ける
watch(
() => props.id,
async (id) => {
const res = await fetch(`/api/users/${id}`)
user.value = await res.json()
},
{ immediate: true }
)
</script>
もう1つの方法は、ナビゲーションガードのonBeforeRouteUpdateです。
<script setup>
import { onBeforeRouteUpdate } from 'vue-router'
// 同じコンポーネントのまま、パラメータだけが変わるときに呼ばれる
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
// 新しい id で、データを取り直す
}
})
</script>
watchの使い方はwatchを活用してデータの変化を監視する方法をご覧ください。
パラメータの書き方いろいろ
数字だけに限定する、省略可能にする、繰り返しを受け取る、といった書き方があります。複数のルートが同じURLに一致しうるときは、具体的なものを先に書きます。
const routes = [
// 数字だけ
{ path: '/:orderId(\\d+)', component: Order },
// それ以外の文字列(具体的な数字のルートを、先に書く)
{ path: '/:productName', component: Product },
// 省略可能(?): /users と /users/posva の両方に一致
{ path: '/users/:userId?', component: Users },
// 繰り返し(+ は1つ以上、* は0以上): /one/two/three → params.chapters は配列
{ path: '/:chapters+', component: Chapters }
]
redirectで転送する
URLの変更や統合のときは、ルートのredirectで、旧URLから新URLへ転送できます。
const routes = [
// 旧URLを新URLへ転送する
{ path: '/old-about', redirect: '/about' },
// 名前付きルートへの転送もできる
{ path: '/dashboard', redirect: { name: 'AdminDashboard' } }
]
関連記事
- ログインや権限の制御は認証付きルーティングを実装する方法
- ページ遷移中の表示はページ遷移時にローディング画面を表示する方法
よくある質問(FAQ)
onMountedが再実行されないのが原因です。watch(() => route.params.id, …)か、onBeforeRouteUpdateで、パラメータの変化を受けてください。/:pathMatch(.*)*のcatch-allルートを最後に置き、404用のコンポーネントを表示します。トップへredirectすると、ユーザーはリンク切れに気づけません。path: '')を含む子に名前を付けて、その名前でナビゲーションします。まとめ
ネストしたルートは、レイアウトを親、画面を子にして、<RouterView>で入れ子にします。子のパスのルールと命名を押さえ、ファイルをレイアウト単位で分割すると、見通しよく管理できます。動的ルートでは、同じコンポーネントが再利用される点に注意して、パラメータの変化をwatchで受けてください。存在しないURLには、専用の404ページを用意しておきましょう。

