本文書ではビルドツールに Vite を利用した Vue3 環境(Composition API)に Vue Router を追加し Vue Router の動作確認を行なっています。本文書を読み終えるとVue Router 4に関する基本的な機能を理解することができます。

Vue Routerとは

Vue Router は Vue.js の公式の Router(ルーター)です。Vue Router を利用することで複数のページから構成されたアプリケーションを構築することができます。ページの更新はすべてJavaScriptによって行われるためページを移動する度にページ全体をリロードする必要がないためページ間をスムーズに移動することができます。Vue.js を利用してシングルページアプリケーション(SPA)を構築するために必須なライブラリです。

Vue.jsのフルスタックフレームワークであるNuxtのルーティングにもVue Routerが利用されています。Nuxtではファイルベースルーティングを採用しており決められたフォルダにファイルを配置すると自動でルーティングが設定されます。Vue Routerでは手動でルーティングを設定していきます。

1つのHTMLページで構成されたアプリケーションをSigle Page Application(SPA)と呼びます。複数のHTMLファイルで構成されたアプリケーションの場合はMulti Page Appication(MPA)と呼ばれブラウザの機能を利用してページの移動を行いますが SPAではJavaScriptの技術を利用してページ内の一部のコンテンツのみ更新することでページの移動を実現しています。
fukidashi

プロジェクトの作成

Vue.js の公式のプロジェクト作成ツールであるcreate-vueを利用するため”npm create vue@latest”コマンドを実行します。create-vue を利用することで Vite を利用した環境で Vue.js のアプリケーションを作成することができます。create-vue では対話的に利用する予定の機能を選択することができます。その中にはVue Routerも含まれており、本文書は Vue Router に注目しているので Vue Router のみ機能の追加を行います。プロジェクト名は vite-vue-router を設定しています。


 % npm create vue@latest

> npx
> create-vue


Vue.js - The Progressive JavaScript Framework

✔ Project name: … vite-vue-router
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … No / Yes
✔ Add Vue DevTools 7 extension for debugging? (experimental) … No / Yes

Scaffolding project in /Users/mac/Desktop/vite-vue-router...

Done. Now run:

  cd vite-vue-router
  npm install
  npm run dev

プロジェクトの作成が完了するとプロジェクト名と同じ名前のフォルダが作成されるのでvite-vue-routerフォルダに移動してnpm installでJavaScriprtライブラリのインストールを行い、npm run devコマンドで開発サーバの起動を行います。


 % cd vite-vue-router
 % npm install
 % npm run dev

  VITE v5.3.5  ready in 703 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

ブラウザを起動してhttp://127.0.0.1:5173/にアクセスするとブラウザ上にはプロジェクトの初期画面が表示されます。

プロジェクトの初期画面
プロジェクトの初期画面

動作確認を行う各種ライブラリのバージョンを package.json で確認しておきます。vue のバージョンは 3.4.29 で、vue-router のバージョンは4.3.3です。vite のバージョンは5です。


{
  "name": "vite-vue-router",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.4.29",
    "vue-router": "^4.3.3"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.5",
    "vite": "^5.3.1"
  }
}

初期設定の確認

Vue.js のアプリケーションは 1 つの HTML ファイルから構成されているので開発サーバhttp://127.0.0.1:5173/にアクセスするとプロジェクトフォルダ直下にあるindex.htmlファイルがブラウザに読み込まれます。


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

index.htmlファイルにはブラウザ上に表示するコンテンツは何も記述されていませんがscriptタグに指定されているmain.jsファイルを利用することでブラウザ上にコンテンツが表示されます。ブラウザからアクセスした場合は必ずこのindex.htmlファイルが読み込まれ、main.jsファイルがダウンロードされてからページが表示されます。

Routerの設定確認

src フォルダにある main.js ファイルを確認します。プロジェクト作成時に Vue Router の追加選択を行なっているので Vue Router を利用するために必要となる初期設定は完了しているので設定内容を確認します。

Vue Router はプラグインとして createApp 関数から作成される Vue インスタンスに登録されます。app.use の引数にはルーティング設定ファイル router/index.js から import した router が指定されています。import では index.js ファイルを指定していませんが router フォルダを指定すると index.js ファイルが読み込まれます。


import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(router)

app.mount('#app')

router フォルダの index.js ファイルでは createRouter 関数を利用して router インスタンスを作成しています。createRouter の引数には history と routes オプションを設定しています。routes オプションでは配列でルーティングの設定を行なっています。デフォルトでは/(ルート)と/about のルーティングが登録されています。ブラウザから/(ルート)または/about にアクセスすることができます。history の設定については後ほど確認します。


import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (About.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import('../views/AboutView.vue')
    }
  ]
})

export default router

/(ルート)にアクセスすると HomeView コンポーネント、/about にアクセスすると AboutView コンポーネントの中身が表示されます。AboutView コンポーネントについては lazy loading(遅延読み込み)の設定が行われているので/about にアクセスが行われた場合など必要になった時に AboutView コンポーネントに関する JavaScript ファイルがダウンロードされるように設定されています。/(ルート), /about ページに以外のルーティングを追加したい場合は routes プロパティ の配列に追加していくことになります。

Appコンポーネントの確認

createApp関数の引数に設定されているAppコンポーネントはルートコンポーネントと呼ばれアプリケーションにアクセスされた場合に必ず実行されるコンポーネントです。

App.vueファイルの中身を下記のように更新します。


<script setup></script>

<template>
  <h1>Hello World</h1>
</template>

ブラウザで確認すると画面中央にHello Worldが表示されます。

Appコンポーネントの更新
Appコンポーネントの更新

中央に文字列が表示されるのはmain.jsファイルでimportされているスタイルシートmain.cssの中の設定が反映されているのでmain.cssファイルのimportの行をコメントしておきます。


// import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(router)

app.mount('#app')

再度ブラウザを確認すると”Hello World”の文字列は左上部に表示されます。

表示位置の変更
表示位置の変更

ルーティングの動作確認

router¥index.jsファイルではルーティングの設定が完了していますが/aboutにアクセスしても”Hello World”が表示されているだけで表示内容は何も変わりません。

/aboutへのアクセス
/aboutへのアクセス

/aboutにアクセスした際にルーティングファイル(router¥index.js)で設定されていたAboutView.vueファイルの内容を表示させるためにはApp.vueファイルにRouterViewコンポーネントを設定する必要があります。


<script setup>
</script>

<template>
  <h1>Hello World</h1>
  <RouterView />
</template>

タグ名はRouterView, router-viewどちらでも利用することができます。本文書ではRouterViewを利用します。


<script setup>
</script>

<template>
  <h1>Hello World</h1>
  <RouterView />
</template>

RouterViewコンポーネントを設定した後に再度/aboutページにアクセスするとHello Worldの下に文字列が表示されます。

RouterViewコンポーネントの設定後
RouterViewコンポーネントの設定後

AboutView.vueファイルを確認するとh1タグで”This is an about page”が設定されていることがわかります。media queryが設定されているのでブラウザの横幅を変えると文字列の表示位置が変わります。


<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>

<style>
@media (min-width: 1024px) {
  .about {
    min-height: 100vh;
    display: flex;
    align-items: center;
  }
}
</style>

router/index.jsで設定したルーティングに対応したコンポーネントを表示させるためにはルートコンポーネントのApp.vueファイルでRouterViewコンポーネントの設定が必須であることがわかります。

http://127.0.0.1:5173/にアクセスするとHomeView.vueファイルに記述されていた内容が表示されます。

/(ルート)へのアクセス
/(ルート)へのアクセス

リンクの設定

/(ルート)と/about のルーティングを設定していますが各ページを確認するためには直接ブラウザで URL を入力する必要があります。ページ間の移動ができるようにリンクの設定を行います。

HTML では通常ページ間のリンクを設定する際には a タグを利用するので a タグを利用してリンクを設定します。


<script setup>
</script>

<template>
  <header>
    <nav>
      <a href="/">Home</a>
      <a href="/about">About</a>
    </nav>
  </header>
  <h1>Hello World</h1>
  <RouterView />
</template>

ページ上部にリンクが表示されページの移動を行うことができますがクリックする度にページ全体が再読み込みされることが確認できます。

aタグの代わりにVue Routerではページ移動にRouterLinkコンポーネントを利用します。RouterLinkではaタグのhref属性の代わりにto属性を利用します。


<script setup>
</script>

<template>
  <header>
    <nav>
      <RouterLink to="/">Home</RouterLink>
      <RouterLink to="/about">About</RouterLink>
    </nav>
  </header>
  <h1>Hello World</h1>
  <RouterView />
</template>
RouterLinkタグではなくroute-linkタグでも設定可能です。
fukidashi

aタグからRouterLinkコンポーネントに設定するとリンクをクリックするとスムーズにページの移動ができることがわかります。RouterLinkではページ全体をリロードするのではなくページの一部のみ更新が行われます。”Hello World”の文字列はページ移動によって何も影響を受けないので更新は行われません。

ここまでの動作確認でVue Routerを利用してページを移動するためには最低以下の5つの設定が必要であることがわかりました。

  • ルーティング設定ファイルの作成(router¥index.js)
  • Router のインスタンスを Vue インスタンスへプラグインとして登録(main.js)
  • ルーティングに対応するページコンポーネントの作成(HomeView.vue, AboutView.vue)
  • App コンポーネントでの RouterView コンポーネントの設定
  • RouterLink コンポーネントによる各ページへのリンク設定

Historyモードの違い

ルーティングの設定ファイル router/index.js の createRouter 関数の引数のオプションで設定されていた history の設定を確認します。主に history では 2 つのモードを選択することができます。一つはハッシュモードでもう一つが HTML5 モードです。デフォルトでは HTML5 モードの createWebHistory が設定されていますがこれをハッシュモードに変更します。


const router = createRouter({
  // history: createWebHistory(import.meta.env.BASE_URL),
  history: createWebHashHistory(),
  routes: [
  //略

ブラウザでURLを確認すると#がついていることがわかります。

Hashモードに設定した場合のURL
Hashモードに設定した場合のURL

ハッシュモードを利用することでサーバ側での設定が不要になります。HTML5 モードの場合はハッシュを利用しない一般的な URL の形式でページにアクセスすることができますがサーバ側での追加設定が必要になります(開発サーバでは特別な設定は必要ありません)。サーバ側での必要な設定については Vue Router のドキュメントにDifferent History modes記載されています。サーバ側で設定する内容を簡単に説明すると/about にアクセスがあった場合にサーバ上の about ページ(about/index.html ファイル)を読み込もうとするのではなくプロジェクトのトップに存在する index.html ファイルを読み込むような設定を行っていきます。

HTML5 モードの createWebHistory 関数の引数に URL を設定することができます。設定した場合にはその URL がルートの URL になるためもしcreateWebHistoryの引数に/about を設定してブラウザから about ページを表示させるためにはブラウザから/about/about にアクセスする必要があります。

Lazy Loading

routes/index.js ファイルのルーティングの設定で HomeView と AboutView では components の指定方法が異なっていました。AboutView では lazy Loading の設定を行なっていると説明しました。では Lazy Loading を利用することでどのような違いがあるのでしょう。

Lazy Loading を設定した場合としない場合の違いを確認します。

HomeView.vue ファイルはデフォルトでは TheWelcome コンポーネントを import しておりアクセスした際にダウンロードするファイルが増えてしまうので HomeView.vue ファイルを更新します。


<script setup></script>

<template>
  <main>Home</main>
</template>

Lazy Loadingを設定していない場合の動作確認を行います。/(ルート)も/aboutも同じ設定を行なっています。


import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';
import AboutView from '../views/AboutView.vue';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
    },
    {
      path: '/about',
      name: 'about',
      component: AboutView,
    },
  ],
});

export default router;

ブラウザのデベロッパーツールのネットワークタブを利用して違いを確認します。

/(ルート)にアクセスするとHomeView.vueファイルとAboutView.vueファイルがダウンロードされていることが確認できます。

HomeView.vue, AboutView.vueファイルの確認
HomeView.vue, AboutView.vueファイルの確認

AboutView.vue ファイルのダウンロードは完了しているのでリンクから/about ページに移動しても追加のダウンロードはありません。

本番環境でのビルドに変化があるか確認するために npm run dev ではなく npm run build コマンドを実行しておきます。実行すると dist/assets フォルダに index.XXX.js ファイルが作成されることがわかります。この後 Lazy Loading を設定した後のビルドファイルと比較します。


 % npm run build

> vite-vue-router@0.0.0 build
> vite build

vite v4.4.9 building for production...
✓ 28 modules transformed.
dist/index.html                  0.42 kB │ gzip:  0.28 kB
dist/assets/index-4d995ba2.css   0.09 kB │ gzip:  0.10 kB
dist/assets/index-ee28faea.js   74.13 kB │ gzip: 29.58 kB

次にLazy Loadingの設定を行います。/aboutではcomponentに関数を利用してimportするコンポーネントを設定します。


import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (About.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import('../views/AboutView.vue'),
    },
  ],
});

export default router;

デベロッパツールのネットワークタブを開いて/(ルート)にアクセスするとHomeView.vueファイルは確認できますがAboutView.vueファイルを確認することはできません。

HomeView.vueファイルのみダウンロード
HomeView.vueファイルのみダウンロード

ダウンロードするファイルを把握しやすくするためネットワークタブに表示されている情報を削除するため消去ボタンを押した後にAboutのリンクをクリックします。クリックするとAboutView.vueファイルのダウンロードが行われていることが確認できます。

AboutView.vueファイルのダウンロード
AboutView.vueファイルのダウンロード

Lazy Loadingの設定を行うとアクセスが行われた時にファイルのダウンロードが行われることがわかりました。

本番環境のビルドファイルを作成するために npm run build コマンドを実行すると Lazy Loading 前とは異なり、index.XXX.js ファイルとは別に AboutView.XXX.js が作成されていることがわかります。


 % npm run build

> vite-vue-router@0.0.0 build
> vite build

vite v4.4.9 building for production...
✓ 29 modules transformed.
dist/index.html                      0.36 kB │ gzip:  0.26 kB
dist/assets/AboutView-4d995ba2.css   0.09 kB │ gzip:  0.10 kB
dist/assets/AboutView-4e9f7188.js    0.23 kB │ gzip:  0.20 kB
dist/assets/index-b17725d4.js       74.98 kB │ gzip: 29.96 kB
✓ built in 917ms

複数のビルドファイルを作成することは Code Splitting と呼ばれます。1 つの JavaScript ファイルではなく複数の JavaScript ファイルに分けることで最初のアクセス時にダウンロードするファイルのサイズの減らすことができます。アクセスがあった場合のみ関連する JavaScript ファイルをダウンロードするので利用しない JavaScript ファイルをダウンロードする必要がなくなります。

ここまでの動作確認で Lazy Loading の設定の違いを Code Splitting を含めて理解することができました。

リンクへのactive classの適用

アプリケーションの中にナビゲーションメニューを設置する場合、現在ユーザがどのページにアクセスしているかわかるように閲覧しているページのリンクにスタイルを加えることは頻繁に行われています。RouterLink を利用しているためアクセスしているページへのスタイル設定を簡単に行うことができます。

RouterLink コンポーネントを利用している場合にはアクセスしているページのリンクに自動で class が追加されます。ブラウザのでデベロッパーツールを利用することで router-link-active と router-link-exact-active クラスが追加されていることが確認できます。

active classの確認
active classの確認

ナビゲーションメニューを設定しているコンポーネントでstyleタグの中にrouter-link-active classを設定することでアクセスしているページのリンクのみにスタイルを適用することができます。classのみ追加されるのでスタイルは自由に設定することが可能です。


<script setup>
</script>

<template>
  <header>
    <nav>
      <RouterLink to="/">Home</RouterLink>
      <RouterLink to="/about">About</RouterLink>
    </nav>
  </header>
  <h1>Hello World</h1>
  <RouterView />
</template>
<style>
.router-link-active {
  font-weight: bold;
}
</style>

router-link-activeクラスを設定後、/aboutページにアクセスするとAboutのリンクの文字列にスタイルが適用されることが確認できます。

Aboutのリンクにスタイルが適用
Aboutのリンクにスタイルが適用

デフォルトではrouter-link-activeと router-link-exact-activeという名前のclassが設定されていますがRouterLinkコンポーネントのactiveClassとexactActiveClass propsを利用することでclassの名前を変更することができます。


<script setup></script>

<template>
  <header>
    <nav>
      <RouterLink activeClass="active" to="/">Home</RouterLink>
      <RouterLink activeClass="active" to="/about">About</RouterLink>
    </nav>
  </header>
  <h1>Hello World</h1>
  <router-view />
</template>
<style>
.active {
  font-weight: bold;
  color: red;
}
</style>

デベロッパーツールを確認するとclass名がactiveClass propsで設定したactiveになっていることが確認できます。

activeClass propsの確認
activeClass propsの確認

RouterLink毎に設定を行わなくてもrouter/index.jsで設定を行うことも可能です。


const router = createRouter({
  linkActiveClass: 'border-indigo-500',
  linkExactActiveClass: 'border-indigo-700',
  // ...
})

Vue.js DevtoolsによるRouter情報の確認

Chrome を利用している場合に Extensions の”Vue.js Devtools”を利用することで Router の情報を確認することができます。”Vue.js Devtools”は開発に利用できる便利な Extensions なのでインストールしていない場合はインストールを行ってください。

“Vue.js Devtools”を確認することでアクセスしているページには exact や active といった情報も表示されます。

Vue.js DevtoolsのRoutes情報
Vue.js DevtoolsのRoutes情報

ダイナミックルーティングの設定

/(ルート)や/aboutのようにURLが変わらないルーティングをスタティックルーティングと呼び/posts/1, /posts/2や/users/1, /users/2のようにURLが変わり対応するページコンポーネントが同じルーティングをダイナミックルーティングと呼びます。

ルーティングの追加

新たにルーティング/usersを追加し/usersにアクセスするとユーザ一覧を表示できるように設定を行います。ユーザ情報は外部リソースのJSONPlaceHolderを利用します。/usersはスタティックルーティングです。

https://jsonplaceholder.typicode.com/usersにアクセスするとユーザ一覧を取得することができます。
fukidashi

router/index.jsファイルにルーティング/usersを追加します。


import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (About.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import('../views/AboutView.vue'),
    },
    {
      path: '/users',
      name: 'users',
      component: () => import('../views/UsersView.vue'),
    },
  ],
});

export default router;

viewsフォルダにUsersView.vueファイルを追加しJSONPlaceHolderから取得したデータをv-forディレクティブを利用して展開します。ライフサイクルフックのonMountedとref関数を使ってリアクティブは変数usersを利用しています。


<script setup>
import { ref, onMounted } from 'vue';

const users = ref([]);

onMounted(async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  const data = await response.json();
  users.value = data;
});
</script>

<template>
  <h2>Users一覧</h2>
  <ul>
    <li v-for="user in users" :kye="user.id">{{ user.name }}</li>
  </ul>
</template>

ブラウザで確認するとユーザ一覧が表示されます。

ユーザ一覧の取得と表示
ユーザ一覧の取得と表示

ユーザ一覧に表示された名前をクリックすると各ユーザの個別ページにアクセスできるようにRouterLinkを設定します。


<template>
  <h2>Users一覧</h2>
  <ul>
    <li v-for="user in users" :kye="user.id">
      <RouterLink :to="`/users/${user.id}`">{{ user.name }}</RouterLink>
    </li>
  </ul>
</template>

ブラウザ上にはリンクが貼られたユーザ一覧が表示されますがユーザ名に対応するリンク先のルーティングが存在しないためデベロッパーツールのコンソールにはWarninigが表示されています。

ユーザ名にリンクを設定
ユーザ名にリンクを設定

新たにルーティングを設定する必要がありますがこれまでとは異なりリンク先のURLが/users/1, /users/2,…のように同じURLではなくユーザによって異なるidがURLの中に含まれています。idが異なってもページが表示されるようにダイナミックルーティングを利用します。

ダイナミックルーティングの設定

router/index.jsファイルで新たにルーティングを設定します。/users/以下の値は動的に変わるので:idで設定します。動的に変わるidの前には:(コロン)を設定します。


import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
//略
    {
      path: '/users',
      name: 'users',
      component: () => import('../views/UsersView.vue'),
    },
    {
      path: '/users/:id',
      name: 'user',
      component: () => import('../views/UserView.vue'),
    },
  ],
});

export default router;

viewsフォルダにUserView.vueファイルを作成し以下のコードを記述します。


<script setup></script>

<template>
  <h2>User詳細</h2>
</template>

設定後ユーザ一覧のリンクをクリックすると下記の画面が表示されます。どのリンクをクリックしても同じ画面が表示されます。

詳細ページの表示
詳細ページの表示

URLに含まれているidを取得することができればidによってユーザを識別することができるためidの取得方法を確認します。

“Vue.js Devtools”のComponentsを確認することでRoutingの$routeオブジェクトのparamsにidが含まれていることが確認できます。

$routesオブジェクトの確認
$routesオブジェクトの確認

$routeオブジェクトについてはtemplateタグの中でアクセスすることができます。


<script setup></script>

<template>
  <h2>User詳細</h2>
  <p>User Id: {{ $route.params.id }}</p>
</template>

ブラウザで確認するとidが表示されます。URLの/users/3の3と画面上に表示されているUser Idの3が一致します。

$routeオプジェクトのparams.idを表示
$routeオプジェクトのparams.idを表示

scriptタグ内でidを利用したい場合にはuseRouteを利用することができます。


<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route.params);
</script>

<template>
  <h2>User詳細</h2>
  <p>User Id: {{ $route.params.id }}</p>
</template>
Options APIではuserRouteを利用せず、$routeオブジェクトについてはthis.$routeでアクセスすることができます。
fukidashi

ブラウザのデベロッパーツールのコンソールにはparamsオブジェクトが表示されます。idは数値ではなく文字列であることもわかります。


{id: '3'}

idを利用してJSONPlaceHolderにアクセスを行い個別のユーザ情報を取得します。parseInt関数を利用して文字列から数値に変換しています。


<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';

const route = useRoute();
const user = ref([]);

const id = parseInt(route.params.id);

onMounted(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${id}`
  );
  const data = await response.json();
  user.value = data;
});
</script>

<template>
  <h2>User詳細</h2>
  <ul>
    <li>User Id: {{ user.id }}</li>
    <li>User Name: {{ user.name }}</li>
    <li>User Email: {{ user.email }}</li>
  </ul>
</template>

ブラウザ上にはidの値が3のユーザ情報が表示されます。

idを利用したユーザの詳細画面
idを利用したユーザの詳細画面

router¥index.jsファイルでルーティングを設定する際に:idを設定しましたがidは決められた文字列ではなく任意の名前をつけることができます。

idからuserIdに変更します。


{
  path: '/users/:userId',
  name: 'user',
  component: () => import('../views/UserView.vue'),
},

UserView.vueファイルのroute.paramsオブジェクトのidもuserIdに変更する必要があります。


const id = parseInt(route.params.userId);

他のユーザ一覧のリンクからも詳細ページが表示されるか確認するためにUserの詳細ページから一覧ページに戻るためのリンクを設定します。


//略
<template>
  <h2>User詳細</h2>
  <RouterLink to="/users">戻る</RouterLink>
  <ul>
    <li>User Id: {{ user.id }}</li>
    <li>User Name: {{ user.name }}</li>
    <li>User Email: {{ user.email }}</li>
  </ul>
</template>

ブラウザ上には戻るのリンクが表示されます。

戻るリンクの設定
戻るリンクの設定

戻るリンクをクリックしてユーザ一覧に戻り、別のユーザ名をクリックするとクリックしたユーザの詳細ページが表示されることを確認してください。これでダイナミックルーティングの設定は完了です。

パラメータの値の変化の監視

/users/1, /users/2にアクセスするとそれぞれ別のユーザ詳細画面が表示されることがわかったので動作確認のためAppコンポーネントのナビゲーションメニューにリンク(/users/1, /users/2)を追加します。


<script setup></script>

<template>
  <header>
    <nav>
      <router-link to="/">Home</router-link>
      <RouterLink to="/about">About</RouterLink>
      <RouterLink to="/users/1">User1</RouterLink>
      <RouterLink to="/users/2">User2</RouterLink>
    </nav>
  </header>
  <h1>Hello World</h1>
  <RouterView />
</template>
<style>
.router-link-active {
  font-weight: bold;
}
</style>

User1のリンクをクリックするとidの値に1を持つユーザの情報が表示されます。

ナビゲーションメニューからの移動
ナビゲーションメニューからの移動

次にメニューのUser2のリンクをクリックします。idの値に2を持つユーザの情報が表示されるかと思われますがページは更新されずユーザidが1のユーザ情報が表示され続けます。

ページの内容が更新されない
ページの内容が更新されない

原因は UserView コンポーネントが再利用されることでコンポーネントのアンマウント処理が行われず、ライフサイクルフックのマウント処理(onMounted)が実行されないためです。

/users/1 から/users/2 に直接ページ移動してもページを更新させるためには ページを移動時にparams の userId の値が更新された場合にデータ取得の処理が行われるように userId の値の変化を監視する必要があります。userId の値の更新は watch 関数を利用することで監視することができます。


import { ref, onMounted, watch } from 'vue';
//略
onMounted(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${id}`
  );
  const data = await response.json();
  user.value = data;
  watch(route, () => {
    console.log(route.params);
  });
});

watch関数の引数にrouteオブジェクトを指定することでをUser1のリンクをクリックするとコンソールには{userId:’1′}が表示され、User2のリンクをクリックすると{userId:’2′}が表示されます。watchによりuserIdの更新を検知していることがわかります。

watchで検知した後もJSONPlaceHolderへのfetchが行われるので新たにfetchUser関数を作成してfetchの処理を記述します。


<script setup>
import { ref, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';

const route = useRoute();
const user = ref([]);

let id = parseInt(route.params.userId);

const fetchUser = async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${id}`
  );
  const data = await response.json();
  user.value = data;
};

onMounted(() => {
  fetchUser();
  watch(route, () => {
    id = parseInt(route.params.userId);
    fetchUser();
  });
});
</script>

<template>
  <h2>User詳細</h2>
  <RouterLink to="/users">戻る</RouterLink>
  <ul>
    <li>User Id: {{ user.id }}</li>
    <li>User Name: {{ user.name }}</li>
    <li>User Email: {{ user.email }}</li>
  </ul>
</template>

ページへアクセスした際のmount処理でfetchUser関数を実行し、watchでparamsオブジェクトのuserIdに更新があった場合にfetchUser関数を実行します。

watchを追加後はUser1にアクセスした後にUser2のリンクをクリックするとidの値に2を持つユーザの情報が表示されるようになります。

/users/1ページから/users/2ページへの直接移動でページを更新させるためRouterLinkコンポーネントにkeyを設定する方法があります。keyの値が変わることでコンポーネントの再作成を行うことができます。


<script setup></script>

<template>
  <header>
    <nav>
      <router-link to="/">Home</router-link>
      <RouterLink to="/about">About</RouterLink>
      <RouterLink to="/users/1">User1</RouterLink>
      <RouterLink to="/users/2">User2</RouterLink>
    </nav>
  </header>
  <h1>Hello World</h1>
  <RouterView :key="$route.params.userId" />
</template>
<style>
.router-link-active {
  font-weight: bold;
}
</style>

上記ではparams.userIdをkeyに設定していますがuserIdが変わると$routeオブジェクトのpathプロパティが更新されるのでuserIdの代わりに$routeオブジェクトのpathプロパティやfullPathプロパティを利用することもできます。


<RouterView :key="$route.path" />
//or
<RouterView :key="$route.fullPath" />

propsの利用

ダイナミックルーティングではURLに含まれる動的な値(id, userId)をuseRouterやrouteオブジェクトを利用して取得しましたがpropsを使うことでも取得することもできます。

propsでidを渡すためにはルーティングの追加設定が必要となります。ルーティングにpropsプロパティを追加して値をtrueに設定します。propsをtrueにするとroute.paramsの値をpropsで渡すことができます。この設定がない場合はpropsでroute.paramsの値を渡すことはできません。


{
  path: '/users/:userId',
  name: 'user',
  component: () => import('../views/UserView.vue'),
  props: true,
},

UserViewコンポーネントではpropsを受け取るためdefinePropsを利用します。受け取ったpropsは定義したprops変数にオブジェクトとして保存されるのでprops.userIdでダイナミックルーティングのidを取得することができます。


<script setup>
import { ref, onMounted } from 'vue';

const props = defineProps({
  userId: String,
});

const user = ref([]);

const id = parseInt(props.userId);

const fetchUser = async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${id}`
  );
  const data = await response.json();
  user.value = data;
};

onMounted(() => {
  fetchUser();
});
</script>

<template>
  <h2>User詳細</h2>
  <RouterLink to="/users">戻る</RouterLink>
  <ul>
    <li>User Id: {{ user.id }}</li>
    <li>User Name: {{ user.name }}</li>
    <li>User Email: {{ user.email }}</li>
  </ul>
</template>

ルーティングでpropsをtrueにするとroute.paramsの値をpropsで渡せることがわかりましたがroute.paramsではなく他の値を渡したい場合はtrueではなくオブジェクトで以下のように行うことができます。下記の設定を行なった場合にはpropsにはroute.paramsの値は含まれません。


{
  path: '/users/:userId',
  name: 'user',
  component: () => import('../views/UserView.vue'),
  props: {
    first_name: 'John',
    last_name: 'Doe',
  },
},

propsで受け取るコンポーネントではidと同様にdefinePropsの設定で受け取ることができます。


const props = defineProps({
  first_name: String,
  last_name: String,
});

idのように動的に変わる値については関数を利用して渡すこともできます。これで任意の値とidを同時にpropsで渡すことができます。


{
  path: '/users/:userId',
  name: 'user',
  component: () => import('../views/UserView.vue'),
  props: (route) => ({
    userId: route.params.userId,
    first_name: 'John',
    last_name: 'Doe',
  }),
},

propsで受け取るコンポーネントではdefinePropsの設定で受け取ることができます。


const props = defineProps({
  first_name: String,
  last_name: String,
  userId: String,
});

ネスト化の設定

ルーティングはネスト化することができます。ネスト化によってUserViewコンポーネントに一つ下の階層を持たせることでき、/users/1/profileにアクセスした場合にユーザの詳細画面を表示させながらprofileに対応する別のコンポーネントの内容を表示させることができます。

ネスト化する場合は階層化させるためルーティングに親子関係を持たせるのでルーティングの設定でchildrenを利用します。


import ProfileView from '../views/ProfileView.vue';
//略
{
  path: '/users/:userId',
  name: 'user',
  component: () => import('../views/UserView.vue'),
  props: (route) => ({
    userId: route.params.userId,
    first_name: 'John',
    last_name: 'Doe',
  }),
  children: [
    {
      path: 'profile',
      component: ProfileView,
    },
  ],
},

childrenは配列で設定を行うので1つのルーティングに複数のchildrenを持たせることができます。ここではProfileViewというコンポーネントを設定しています。childrenに設定したコンポーネントの内容を表示させるためには/users/:id/profileでアクセスする必要があります。

ProfileView.vueファイルをviewsフォルダの中に作成して以下の内容を記述します。


<template>
  <h2>プロフィール</h2>
</template>

ルーティングへのchildrenの追加とProfileView.vueを設定することで/users/:id/profileにアクセスすることが可能になります。しかしブラウザから/users/2/profileにアクセスしてもProfileView.vueの内容は表示されません。

ProfileViewの内容が表示されない
ProfileViewの内容が表示されない

ProfileView.vueファイルの内容を表示させるためには親であるUserView.vueファイルにRouterViewコンポーネントを設定する必要があります。UserView.vueを表示させるために親のApp.vueファイルにRouterViewを設定したのと同じ理由です。


<template>
  <h2>User詳細</h2>
  <RouterLink to="/users">戻る</RouterLink>
  <ul>
    <li>User Id: {{ user.id }}</li>
    <li>User Name: {{ user.name }}</li>
    <li>User Email: {{ user.email }}</li>
  </ul>
  <RouterView />
</template>

RouteViewコンポーネントを追加することでProfileView.vueの内容が表示されます。表示されたページは下記のようなネスト化された構造になります。

ネスト化されたルーティング
ネスト化されたルーティング

名前付きルーティング

RouterLinkコンポーネントのto属性にはURLを設定していましたがURLではなくルーティングで設定したnameの値を利用することができます。

各ルーティングにはpathとcomponentだけではなくnameプロパティも設定したのでこの値を利用してリンクを設定します。


const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (About.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import('../views/AboutView.vue'),
    },
    //略

RouterLinkコンポーネントではtoを以下のように変更することができます。


<RouterLink :to="{ name: 'home' }">Home</RouterLink>
<RouterLink :to="{ name: 'about' }">About</RouterLink>

名前付きルーティングを利用する利点はアプリケーションの設計に変更があり/aboutのパスが/aboutusに変更になったとしてもルーティングで設定したnameの値は変わらないのでコンポーネントで設定したリンクの設定は必要ありません。設定を行うのはルーティングファイルであるrouter/index.jsファイルのpathの設定のみになります。名前付きルーティングを利用することで急なpathの変更にも柔軟に対応することができます。

ダイナミックルーティングの設定を行なっている/user/:idページへのリンクはparamsプロパティを設定することで名前付きルーティングを利用することができます。


<RouterLink :to="{ name: 'user', params: { userId: 1 } }"
  >User1</RouterLink
>
<RouterLink :to="{ name: 'user', params: { userId: 2 } }"
  >User2</RouterLink
>

メソッドによるページの移動

ページ間を移動する際はRouterLinkコンポーネントを利用していました。RouterLinkコンポーネントではなくrouterオブジェクトのもつメソッドを利用してページ間の移動を行うことができます。

UserView.vueファイルではユーザ一覧ページ(UsersView.vue)に戻すためにRouterLinkコンポーネントを利用していましたがrouterオブジェクトのpushメソッドを利用することができます。pathプロパティにURLを設定することでボタンをクリックするとpathで指定したURLに移動できます。


<template>
  <h2>User詳細</h2>
  <!-- <RouterLink to="/users">戻る</RouterLink>-->
  <button @click="$router.push({ path: '/users' })">戻る</button>
  <ul>
    <li>User Id: {{ user.id }}</li>
    <li>User Name: {{ user.name }}</li>
    <li>User Email: {{ user.email }}</li>
  </ul>
  <RouterView />
</template>

pathではなく名前付きルーティングを利用することもできます。


<button @click="$router.push({ name: 'users' })">戻る</button>

pushメソッド以外にもbackメソッド、goメソッドがあります。backメソッドではそのページにくる前に閲覧したページに戻ることができます。goメソッドは引数に数値を設定することができ、一つ前のページの場合は-1、2つの前のページに戻る場合は-2と設定することができます。goメソッドは戻るだけではなく進むこともできます。一度ページを戻った後にまた元のページに戻りたい場合はgo(1)と設定することで元のページに戻ることができます。


<button @click="$router.back()">戻る</button>
<button @click="$router.go(-1)">戻る</button>
<button @click="$router.go(1)">進む</button>

ブラウザはページ閲覧の履歴をhistory APIとして持っているのでその機能を利用しています。ブラウザにChromeを利用している場合は左上に表示されている矢印を長押しすることで履歴を確認することができます。

ブラウザの履歴を確認
ブラウザの履歴を確認

各ページに異なるtitleがつけられていないのですべて同じtitleが表示されています。履歴からページを選択するとそのページに移動することができます。

存在しないページへのアクセス

ルーティングに設定していない存在しないページ(URL)へアクセスがあった場合に”Not Found”ページが表示されるように設定を行なっていきます。

現在の設定でURLに適当な文字列を入力するとエラーが発生するわけではなくAppコンポーネントに設定した内容のみ表示されます。そのためページが存在しないことをユーザに伝えるためにNotFound.vueファイルをviewsフォルダに作成します。NotFound.vueファイルではユーザに伝えたいメッセージを設定してください。


<template>
  <h2>Not Found</h2>
  <p>アクセスしたページは存在しません。</p>
  <div><RouterLink :to="{ name: 'home' }">ホーム</RouterLink>に戻る</div>
</template>

NotFound.vueファイルを作成後、ルーティングの設定が必要となります。


//略
import NotFound from '../views/NotFound.vue';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
    },
 //略
    { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
  ],
});

設定後存在しないページにアクセスするとNotFound.vueで設定した内容が表示されます。設定方法についてはVue Routerのドキュメントに記載させています。

NotFoundページの表示
NotFoundページの表示

RedirectとAliasの設定

Redirectの設定

/homeにアクセスがあった場合に/(ルート)にリダイレクトさせたい場合にredirectを利用することができます。ルーティングを利用してredirectの設定を行います。

ルーティングではpathにURL(ユーザからアクセスされるURL), redirectにリダイレクトさせたいURLを設定します。


//略
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
    },
    {
      path: '/home',
      redirect: '/',
    },
//略

設定後、ブラウザから/homeにアクセスすると/にリダイレクトされることが確認できます。名前付きルーティングを利用することもできます。


//略
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
    },
    {
      path: '/home',
      redirect: { name: 'home' },
    },
//略

Aliasの設定

Aliasを利用することで1つのルーティングに対して複数のURLを設定することができます。/aboutだけではなく/aboutusでも同じ内容を表示させたい場合にAliasを利用することができます。下記では/aboutのルーティングに/aboutusのAliasを設定しています。


//略
{
  path: '/about',
  name: 'about',
  alias: '/aboutus',
  // route level code-splitting
  // this generates a separate chunk (About.[hash].js) for this route
  // which is lazy-loaded when the route is visited.
  component: () => import('../views/AboutView.vue'),
},
//略

設定後は/aboutだけではなく/aboutusでもアクセス可能となりAboutViewコンポーネントの内容が表示されます。

ナビゲーションガードの設定

ナビゲーションガードは主に認証が完了しているかどうかによってアクセスできるルーティングを制限する際に利用される機能です。

ナビゲーションガードにはルーティング全体で設定を行う Global Guards から個別のルーティングで設定を行う Per-Route Guards、コンポーネントで設定で行う In-Component Guards などいくつか種類があります。設定する場所が異なるだけで基本的な利用方法は同じです。初めてルーティングを学習した人にとって最初にナビゲーションガードは難しく感じられるかもしれないのでシンプルな例を利用してどのようなものか確認していきます。

Global Before Guards

Global Before Guards は名前に Global(グローバル)が入っている通りルーティング全体に適用されるナビゲーションガードです。createRouter 関数から作成される router インスタンスが持つ beforeEach メソッド(router.beforeEach)を使って設定を行います。

beforeEach は 2 つの引数を取り一つは to、もう一つは from です。to, from もどのナビゲーションガードでも利用するオブジェクトなので to, from がそれぞれどのような値を持っているか確認を行います。

下記のコードは to, from の値をコンソールを出力させるだけでナビゲーションには何も影響を与えません。


//略
router.beforeEach((to, from) => {
  console.log('to:', to);
  console.log('from:', from);
});

export default router;

/(ルート)ページから/aboutページへの移動を行なった場合のto, fromの値です。/(ルート)ページから/aboutページに移動したのでfromには/(ページ)の情報、toには/aboutページの情報が含まれます。

/(ルート)から/aboutへページ移動
/(ルート)から/aboutへページ移動

beforeEach では return false を実行することでナビゲーションをキャンセルすることができます。return false を設定しなければそのままナビゲーションが継続します(ページを移動できる)。return false ではナビゲーションのキャンセルを行うので各ルーティングで設定したコンポーネントの内容は表示されません。


//略
router.beforeEach((to, from) => {
  console.log('to:', to);
  console.log('from:', from);

  return false;
});

export default router;

/aboutにアクセスするとAppコンポーネントの内容のみ表示されRouterViewの部分の内容は表示されません。

ページの内容が表示されない
ページの内容が表示されない

to, fromの中身, falseを戻すことでナビゲーションをキャンセルできることがわかったのでどのページにアクセスがあっても/(ルート)にリダイレクトされる方法を確認します。returnに名前付きルーティングでhomeを設定しています。


//略
router.beforeEach((to, from) => {
  console.log('to:', to);
  console.log('from:', from);

  return { name: 'home' };
});

export default router;

動作確認を行うと以下の警告メッセージがブラウザのコンソールに表示されます。/(ルート)にリダイレクトしても再度/(ルート)にリダイレクトされるといった無限ループに陥ります。


vue-router.mjs:35 [Vue Router warn]: Detected an infinite redirection in a navigation guard when going from "/" to "/". Aborting to avoid a Stack Overflow. This will break in production if not fixed.
vue-router.mjs:35 [Vue Router warn]: Unexpected error when starting the router: Error: Infinite redirect in navigation guard

上記の問題を避けるためにtoオブジェクトのnameの名前がhomeでない場合のみ/(ルート)にリダイレクトさせます。


//略
router.beforeEach((to, from) => {
  console.log('to:', to);
  console.log('from:', from);

  if (to.name !== 'home') {
    return { name: 'home' };
  }

});

export default router;

上記の設定を行うどのページにアクセスしても/(ルート)にリダイレクトされるようになります。このように Global Before Guards を設定することですべてのルーティングに対する設定を行うことができます。

例えば認証しているかどうかを保持する isAuthenticated という値をチェックし、もし isAuthenticated が false の場合は/(ルート)にリダイレクトさせるということが可能になります。ここでは isAuthenticated の値を true, false で手動で変更しますが通常はログイン処理を実行して isAuthenticated の値を更新させます。


//略
router.beforeEach((to, from) => {
  const isAuthenticated = true;

  if (!isAuthenticated && to.name !== 'home') {
    return { name: 'home' };
  }

});

isAuthenticated が true の場合は認証が完了しているという意味をもたせているのでどのページにもアクセスすることができます。isAuthenticated を false に変更するとどのページにアクセスしても/(ルート)にリダイレクトされることになります。

以前のバージョンのVue RouterではbeforeEachの第3引数にnextを利用していましたが現在のバージョンではオプションになっており利用することも可能ですが利用する必要はありません。
fukidashi

グローバルに設定できるナビゲーションガードにはbeforeEachメソッドの他にもbeforeResolvedやafterEachなどのメソッドがあります。

meta Fields

Global Guards を利用するとすべてのルーティングに適用されることがわかりました。

/about のルーティングのみ認証のチェックを行いたい場合には meta fileds を利用することができます。meta fields はルーティングに下記のように設定することができます。オブジェクトのプロパティの名前は任意でここでは requireAuth で値は true に設定しています。


//略
{
  path: '/about',
  name: 'about',
  component: () => import('../views/AboutView.vue'),
  meta: { requiresAuth: true },
},
//略

meta fieldsの設定はtoオブジェクトに保存されるのでGlobal before Guardsの中で確認を行います。


//略
router.beforeEach((to, from) => {
  console.log(to);
  const isAuthenticated = false;

  if (!isAuthenticated && to.name !== 'home') {
    return { name: 'home' };
  }
});
//略

/aboutのルーティングのみにmeta fieldsを設定したので/aboutページに移動する際のtoオブジェクトのmetaプロパティに値が含まれていることが確認できます。/aboutページ以外への移動時はmetaプロパティは{}です。

設定したmeta fieldsの確認
設定したmeta fieldsの確認

requireAuth が true の値を持っている場合のみ認証のチェックを行わせるため if 文の条件に meta fields の値を含めることができます。


router.beforeEach((to, from) => {
  const isAuthenticated = false;

  if (to.meta.requiresAuth && !isAuthenticated && to.name !== 'home') {
    return { name: 'home' };
  }
});

/about のみアクセスすると/(ルート)にリダイレクトされます。認証のチェックを行いたいルーティングのみに meta fields を適用することでアクセス制限を行うことができます。

ルーティング全体ではなく個別のルーティングに対してナビゲーションガードを設定することはできないのかと疑問を持つ人もいるかと思います。そのような場合に利用できるのが次に説明する Per Route Guard です。

Per Route Guard

router インスタンスの beforeEnter メソッドはルーティング全体に適用されますが個別のルーティング毎にナビゲーションガードを設定したい場合には Per-Route Guard を利用することができます。

例えば about ページのみアクセスの制限をかけたい場合には beforeEnter プロパティをルーティングに追加することで設定することができます。


//略
{
  path: '/about',
  name: 'about',
  component: () => import('../views/AboutView.vue'),
  beforeEnter: (to, from) => {
    const isAuthenticated = false;

    if (!isAuthenticated && to.name !== 'home') {
      return { name: 'home' };
    }
  },
},
//略

isAuthenticatedがfalseの場合はaboutページにアクセスすると/(ルート)にリダイレクトされますが他のページにはアクセス可能です。

beforeEnterをダイナミックルーティングに設定した場合/users/1から/users/2に移動してもbeforeEnterは実行されません。beforeEnterは異なるルーティング間の移動のみ実行されます。
fukidashi

ナビゲーションガードの利用方法の例として認証以外で利用してみましょう。ナビゲーションガードの中ではto、fromオブジェクトにアクセスすることができるのでtoの中身を書き換えることもできます。URLに必要ないQuery Parametersなどがついている時はナビゲーションガードで削除することもできます。

URLへのqueryは下記のように設定を行うことができます。


<RouterLink :to="{ name: 'about', query: { name: 'John' } }"
  >About<RouterLink
>

Query ParametersをつけることでURLは/about?name=Johnとなります。

aboutに追加されたQuery Parametersはナビゲーションガードを使って下記のように削除することができます。console.logを利用して削除前のQuery Parametersの値を確認しておきます。


{
  path: '/about',
  name: 'about',
  component: () => import('../views/AboutView.vue'),
  beforeEnter: (to, from) => {
    console.log(to.query)
    if (Object.keys(to.query).length) return { path: to.path, query: {} };
  },
},

リンクをクリックした時は/about?name=JohnであったURLがナビゲーションガードによってページを開いた時には/aboutとなります。コンソールには削除前のQuery Parametersが表示されます。ナビゲーションガードはこのように利用することもできます。


{name: 'John'}

In-Component Gurads

Composition APIではコンポーネントの中でonBeforeRouteLeave, onBeforeRouteUpdateの2つのナビゲーションガードの関数を利用することができます。

onBeforeRouteLeave

onBeforeRouteLeave はページを離れる前に実行されるので例としてページを移動する前の確認メッセージに利用することができます。

AboutView コンポーネントを利用して動作確認を行います。onBeforeRouteLeave を vue-router から import して window.confirm メソッドを使って確認ダイアログを表示させます。表示される確認ダイアログのキャンセルを選択すると return false を戻すことでナビゲーションをキャンセルさせページ移動を行わせません。


<script setup>
import { onBeforeRouteLeave } from 'vue-router';
onBeforeRouteLeave((to, from) => {
  const answer = window.confirm(
    'Do you really want to leave? you have unsaved changes!'
  );
  // cancel the navigation and stay on the same page
  if (!answer) return false;
});
</script>

<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>

設定後、ブラウザで動作確認を行います。/aboutページにアクセスして他のページに移動しようとするとonBeforeRouteLeaveが実行され、確認ダイアログが表示されます。

onBeforeRouteLeaveによる確認ダイアログの表示
onBeforeRouteLeaveによる確認ダイアログの表示

“OK”ボタンをクリックするとそのまま別のページに移動しますが、”キャンセル”ボタンをクリックするとそのページに止まります。

onBeforeRouteUpdate

onBeforeRouteUpdateは/users/1から/users/2のように同じコンポーネントを利用したページ移動で実行される関数です。/aboutページから/users/1ページに移動しても実行されることはありません。

/users/1から/users/2に直接移動した際に同じコンポーネントを利用するためライフサイクルフックが実行されないという問題がありました。その解決策として本記事ではwatch関数を利用した場合とRouteViewコンポーネントにkeyを設定する方法を説明しました。

3つ目の方法としてonBeforeRouteUpdateを利用することができます。App.vueファイルのRouteViewにkeyを設定している場合は削除しておいてください。


<script setup>
import { ref, onMounted } from 'vue';
import { onBeforeRouteUpdate } from 'vue-router';

const props = defineProps({
  first_name: String,
  last_name: String,
  userId: String,
});

const user = ref([]);

const fetchUser = async (userId) => {
  const id = parseInt(userId);
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${id}`
  );
  const data = await response.json();
  user.value = data;
};
onMounted(() => {
  console.log('onMounted');
  fetchUser(props.userId);
});
onBeforeRouteUpdate(async (to, from) => {
  console.log('onBeforeRouteUpdate');
  fetchUser(to.params.userId);
});
</script>

<template>
  <h2>User詳細</h2>
  <!-- <RouterLink to="/users">戻る</RouterLink>-->
  <button @click="$router.back()">戻る</button>
  <ul>
    <li>User Id: {{ user.id }}</li>
    <li>User Name: {{ user.name }}</li>
    <li>User Email: {{ user.email }}</li>
  </ul>
  <RouterView />
</template>

/aboutページから/users/1にアクセスするとコンソールには”onMounted”が表示されます。/users/1から/users/2に移動すると’onBeforeRouteUpdate’のみ表示され画面にはユーザid1を持つユーザ情報が表示されます。/users/1から/users/2に移動すると’onBeforeRouteUpdate’のみが表示され画面にはユーザid2を持つユーザ情報が表示されます。動作確認からonBeforeRouteUpdateは同じコンポーネント間での移動で実行されるということがわかりました。

ここまでの説明を読み終えればVue Router 4の基本的な理解はできていると思いますのでぜひVue Router 4を利用してシングルページアプリケーションの構築にチャレンジしてみてください。