【Vue.js】Vue Router で多階層のルート定義を整理する方法|動的ルートとネスト構造の管理

【Vue.js】Vue Router で多階層のルート定義を整理する方法|動的ルートとネスト構造の管理 Vue.js

管理画面のように、画面が増えてくると、ルートの定義も複雑になります。共通のレイアウトの中に子の画面を並べるネスト、ユーザーIDのように変わる部分を持つ動的ルート、ファイルの分割と、整理すべきことが増えます。この記事では、Vue Router 4を前提に、ネストと動的ルートの設計を、つまずきやすい点とあわせて整理します。ルーティングの基本はルーティングによる画面切り替えをご覧ください。

スポンサーリンク

ネストしたルートを定義する

ルートにchildrenを書くと、親子の関係を持つルートを作れます。親のコンポーネントはレイアウトとして、共通の枠を表示し、中の<RouterView>に、子のコンポーネントが表示されます。

JavaScript:router/modules/admin.js
// 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')
      }
    ]
  }
]
Vue:AdminLayout.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はルーティングの動的管理をご覧ください。

JavaScript:componentのない親
// Vue Router 4.1 以降:親に component を書かずに、パスの接頭辞でグループ化できる
{
  path: '/admin',
  children: [
    { path: 'users', component: Users },
    { path: 'settings', component: Settings }
  ]
}
// 親のレイアウトは不要だが、共通のガードや meta をまとめたいときに使える

複数のレイアウトを管理する

管理画面と一般向けの画面で、異なるレイアウトを使いたいときは、トップレベルのルートごとにレイアウトを分けます。ルートの定義は、レイアウト単位でファイルに分割すると、見通しがよくなります。ページごとにレイアウトを切り替える考え方はページごとに異なるレイアウトを切り替える方法でも解説しています。

JavaScript:router/modules/main.js
// 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でまとめます。一般向けのルートを含めるのを忘れないでください。

JavaScript: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として受け取れます。

JavaScript:動的ルートの定義
{
  // :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で受けるのが基本です。

Vue:パラメータの変化を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です。

Vue:onBeforeRouteUpdate
<script setup>
import { onBeforeRouteUpdate } from 'vue-router'

// 同じコンポーネントのまま、パラメータだけが変わるときに呼ばれる
onBeforeRouteUpdate(async (to, from) => {
  if (to.params.id !== from.params.id) {
    // 新しい id で、データを取り直す
  }
})
</script>

watchの使い方はwatchを活用してデータの変化を監視する方法をご覧ください。

パラメータの書き方いろいろ

数字だけに限定する、省略可能にする、繰り返しを受け取る、といった書き方があります。複数のルートが同じURLに一致しうるときは、具体的なものを先に書きます。

JavaScript:パラメータの正規表現・省略可能・繰り返し
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へ転送できます。

JavaScript:redirect
const routes = [
  // 旧URLを新URLへ転送する
  { path: '/old-about', redirect: '/about' },

  // 名前付きルートへの転送もできる
  { path: '/dashboard', redirect: { name: 'AdminDashboard' } }
]

関連記事

よくある質問(FAQ)

Qユーザーを切り替えても、表示が更新されません。
A同じコンポーネントが再利用されるため、onMountedが再実行されないのが原因です。watch(() => route.params.id, …)か、onBeforeRouteUpdateで、パラメータの変化を受けてください。
Q存在しないURLは、どう扱えばよいですか?
A/:pathMatch(.*)*のcatch-allルートを最後に置き、404用のコンポーネントを表示します。トップへredirectすると、ユーザーはリンク切れに気づけません。
Q子ルートのpathを/で始めるとどうなりますか?
A親からの相対ではなく、ルートからのパスとして扱われます。画面は親の中に入れ子にしつつ、URLは入れ子にしたくないときに使えます。
Qルートには、親と子のどちらに名前を付けるべきですか?
A子に付けるのが公式の推奨です。親には名前を付けず、デフォルトの子(path: '')を含む子に名前を付けて、その名前でナビゲーションします。

まとめ

ネストしたルートは、レイアウトを親、画面を子にして、<RouterView>で入れ子にします。子のパスのルールと命名を押さえ、ファイルをレイアウト単位で分割すると、見通しよく管理できます。動的ルートでは、同じコンポーネントが再利用される点に注意して、パラメータの変化をwatchで受けてください。存在しないURLには、専用の404ページを用意しておきましょう。