Vue の基本的な使い方 (4) Vue Router ルーティング

以下は Vue のルーティングと Vue Router バージョン 4.x (この時点では 4.1.5)の基本的な使い方についての解説のような覚書です。使用している Vue のバージョンは 3.2.38 です。

Vue Router の使い方は Vite で作成したデフォルトのプロジェクトを使って解説しています。

関連ページ

作成日:2022年11月10日

ルーティング

Vue.js には画面遷移の手段として、リクエストされた URL に応じてコンポーネントを選択して表示するルーティング(Routing)という仕組みが用意されています。

ルーティングを利用することで、初回で単一の Web ページのみを読み込み、動的にコンテンツ(コンポーネント)を切り替えることができます。このような複数の機能を単一のページで構成するアプリを Single Page Application(SPA) と呼びます。

SPA ではルーティングはクライアントサイド(JavaScript)で行われ、History APIhashchange イベントなどのブラウザー API を使用して、アプリケーションの表示の切り替えを管理します。

シンプルなルーティング

ハッシュ(#)を使った単純なルーティングの場合などでは、動的コンポーネントを使って、現在のコンポーネントの状態を変更することができます。

Vite を使って作成したプロジェクト
my-vite-project
├── index.html
├── src
│   ├── App.vue
│   ├── components
│   │   ├── About.vue //http://127.0.0.1:5173/#/about
│   │   ├── Home.vue  //http://127.0.0.1:5173/#/
│   │   └── NotFound.vue //http://127.0.0.1:5173/#/non-existent-path(上記以外)
│   └── main.js
└── vite.config.js 

以下は上記のファイル構成で、URL の # の部分が変化したときに発生する hashchange イベント を利用してコンポーネントを切り替える動的コンポーネントを使った単純なルーティングの例です。

リアクティブな変数 currentPath には、URL のフラグメント(# 記号からの部分)が入っています。

テンプレートのリンク(a 要素)をクリックすると hashchange のイベントリスナーにより currentPathvalue プロパティがリンク先の URL のフラグメントの値で更新されます。

算出プロパティの currentViewcomputed メソッドで routescurrentPath.value からコンポーネント名を生成して返します。例えば routes['/about'] なら About になります。

そしてテンプレートの component 要素(34行目)の is 属性に指定されている currentView が更新されることでコンポーネントを動的に切り替えています。

また、初期状態では currentPath.value の値(window.location.hash)は空なので、26行目では routes[currentPath.value.slice(1) || '/'] として初期状態では currentViewHome になるようにしています。

App.vue
<script setup>
import { ref, computed } from 'vue';
import Home from './components/Home.vue';
import About from './components/About.vue';
import NotFound from './components/NotFound.vue';

//パスとコンポーネント名からなるルーティング情報のオブジェクトを定義
const routes = {
  '/': Home,
  '/about': About
};

//URL のフラグメント(# 記号からの部分)を取得してリアクティブに
const currentPath = ref(window.location.hash);

//hashchange のイベントリスナー
window.addEventListener('hashchange', () => {
  //hashchange イベントでフラグメントを取得して currentPath の value プロパティを更新
  currentPath.value = window.location.hash;
  //console.log(currentPath.value); //About のリンクをクリックすると #/about と出力
});

//currentPath.value と routes からコンポーネント名を生成
const currentView = computed(() => {
  //currentPath.value.slice(1) は「#」を除いた値(該当しなければ NotFound)
  return routes[currentPath.value.slice(1) || '/'] || NotFound;
});
</script>

<template>
  <a href="#/">Home</a> |
  <a href="#/about">About</a> |
  <a href="#/non-existent-path">Broken Link</a>
  <component :is="currentView" />
</template>
About.vue
<template>
  <h1>About</h1>
</template>
Home.vue
<template>
  <h1>Home</h1>
</template>
NotFound.vue
<template>
  <h1>404</h1>
</template>
main.js
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
index.html
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
初期状態での出力
<div id="app" data-v-app="">
  <a href="#/">Home</a> |
  <a href="#/about">About</a> |
  <a href="#/non-existent-path">Broken Link</a>
  <h1>Home</h1>
</div>

上記のようなハッシュベースの単純なルーティングであればルーティングのライブラリは不要ですが、構成が複雑になったり、URL の操作が必要な場合などではルーティング機能を提供するライブラリーを使用するのが簡単です。

Vue Router 概要

Vue は SPA の構築にルーティング機能を提供するライブラリーとして、Vue が公式にサポートする Vue Router ライブラリーを推奨しています。

以下で扱っている Vue Router のバージョンは 4.1.5、Vue のバージョンは 3.2.38、Vite のバージョンは 3.0.9 です。

インストール

Vue Router は CDN で読み込んで利用することも Vite などを使ってプロジェクトを構築して利用することもできます。

Installation

CDN で利用

Vue Router はダウンロードしたり CDN で読み込んで利用することができます。Unpkg.com の以下のリンクは常に vue-router v4 の最新バージョンを指しています。

https://unpkg.com/vue-router@4

https://unpkg.com/vue-router@4.1.5/dist/vue-router.global.js のように @ 以降に特定のバージョンを指定して使用することもできます。

ダウンロードしたり CDN で読み込んで利用する場合は、Vue の後に Vue Router を読み込むと自動的にインストールされます。

<!-- Vue の読み込み -->
<script src="https://unpkg.com/vue@3"></script>
<!-- Vue Router の読み込み -->
<script src="https://unpkg.com/vue-router@4"></script>

以下は Vue と Vue Router を CDN で読み込んで使用する例です(Getting Started)。

テンプレートでは、リンクは <a> タグの代わりに <router-link> を使います。定義したルートとマッチしたコンポーネントが <router-view> へ描画されます。

スクリプトでは、createRouter()routes オプション(ルートの定義)と history オプションを指定してルーターを生成します。

そして Vue のインスタンスを生成し、use() メソッドに生成したルーターを渡して有効化し、Vue のインスタンスをマウントします。

router-sample.html
<body>
<div id="app">
  <nav>
    <!-- ナビゲーションのリンクに router-link コンポーネントを使います -->
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
  </nav>
  <!-- ルートとマッチしたコンポーネントが以下の router-view コンポーネントへ描画されます -->
  <router-view></router-view>
</div>

<!-- Vue の読み込み -->
<script src="//unpkg.com/vue@3"></script>
<!-- Vue Router の読み込み -->
<script src="//unpkg.com/vue-router@4"></script>

<script>
//1.ページを構成するコンポーネントを定義(他のファイルからインポートすることもできます)
const Home = {
  template: `
  <div class="main">
    <h1>This is Home!</h1>
  </div>
  `
}
const About =  {
  template: `
  <div class="main">
    <h1>This is an About page</h1>
  </div>
  `
}

//2.ルートを定義(3.で routes に指定)
const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
]

//3.ルーターを作成する際に、2.で定義したルート(routes)を渡します
const router = VueRouter.createRouter({
  //4.history オプションを指定(ここでは簡単にするためにハッシュ history を使用)
  history: VueRouter.createWebHashHistory(),
  routes, // routes オプションを指定(routes: routes の短縮表記)
})

//5.Vue のインスタンスを生成
const app = Vue.createApp({})
//6.Vue にルーターをインストールして有効化
app.use(router)
//7.Vue のインスタンスをマウント
app.mount('#app')

</script>
</body>

router-link(RouterLink)コンポーネントは、ナビゲーションを有効にするための Vue Router のコンポーネントで、router-view(RouterView)コンポーネントは現在のパスに対してマッチしたコンポーネントを描画する Vue Router のコンポーネントです。

</router-link> のように DOM テンプレートの中ではコンポーネント名はケバブケースで記述しますが、単一ファイルコンポーネントのテンプレート内では、コンポーネント名はパスカルケースにすることが推奨されています。

出力
<div id="app" data-v-app="">
    <nav>
      <a href="#/" class="router-link-active router-link-exact-active" aria-current="page">Home</a>
      <a href="#/about" class="">About</a>
    </nav>
    <div class="main">
      <h1>This is Home!</h1>
    </div>
  </div>

上記のサンプルは、例えば以下のように表示されます。URL の最後は「router-sample.html#/」のようにハッシュが追加されています。また、以下ではブラウザの拡張機能 Vue Devtools も表示しています。

「About」のリンクをクリックすると、 URL の最後は「router-sample.html#/about」のようにハッシュの後に /about が 追加され、以下のように表示されます。

また、Vue Devtools には「Routes」のタブが追加されていて、ルートの情報などを確認できます。

Vite を利用

Vite で作成したプロジェクトに npm install vue-router@4 で Vue Router を追加できます。

以下では Vite を使って Vue Router をあらかじめ追加した Vue プロジェクトを作成します。

以下は Vue プロジェクトを生成(初期化)するコマンド npm init vue@latestcreate-vue)で Vue Router を含む Vue のプロジェクトを新規作成する例です(npm create vue@latest でも同じ)。

途中で Add Vue Router for Single Page Application development? と聞かれた際に、 yes を選択して Vue Router を追加します。

プロジェクト生成ウィザードではデフォルトが No なのでそのまま return キーを押せば、その項目はインストールされません。

% npm init vue@latest  return  //npm init vue でプロジェクトを生成(初期化)

Vue.js - The Progressive JavaScript Framework

✔ Project name: … my-router-project
✔ Add TypeScript? … No / Yes  //そのまま return キーを押せば No(インストールされない)
✔ Add JSX Support? … No / Yes
// 以下は矢印キーで yes を選択して return キーを押して Vue Router を追加
✔ Add Vue Router for Single Page Application development? … No / Yes //→Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes

Scaffolding project in /Applications/MAMP/htdocs/vue/my-router-project2...

Done. Now run: // 準備(Scaffolding)ができたので、続いて以下のコマンドを実行(後述)
  cd my-router-project
  npm install
  npm run dev

プロジェクトの準備(Scaffolding)が完了するとプロジェクト名と同じ名前のフォルダが、コマンドを実行したディレクトリの下に作成されます。

npm create vite@latest を利用する場合

または、npm create vite@latestcreate-vite)でも同様にウィザードを起動して、Vue Router を追加した Vue プロジェクトを作成することもできます。※ 上記を実行した場合は不要です。

create-vite の場合、表示されるウィザードの

  • Select a frameworkVue を選択
  • ? Select a variantCustomize with create-vue を選択
  • Add Vue Router for Single Page Application development?Yes を選択して

Vue Router を追加した Vue プロジェクトを生成します。

create-vue 同様、プロジェクトの準備(Scaffolding)が完了するとプロジェクト名と同じ名前のフォルダが、コマンドを実行したディレクトリの下に作成されます。

% npm create vite@latest  return  //npm create vite でプロジェクトを生成(初期化)
✔ Project name: … my-router-project
? Select a framework: › - Use arrow-keys. Return to submit.
    Vanilla
❯   Vue    //Vue を選択して return キーを押す
    React
    Preact
    Lit
    Svelte
    Others
? Select a variant: › - Use arrow-keys. Return to submit.
    JavaScript
    TypeScript
❯   Customize with create-vue  //これを選択して return キーを押す
    Nuxt
// 必要なパッケージがある場合は以下のように聞かれるのでインストール
Need to install the following packages:
  create-vue@3.3.4
  Ok to proceed? (y) y // y を選択して return キーを押す

Vue.js - The Progressive JavaScript Framework
// 追加する必要があるものは矢印キーで  Yes を選択します(この例では Vue Router のみを Yes に)
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
// 以下で yes を選択して Vue Router を追加
✔ 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 Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes

Scaffolding project in /Applications/MAMP/htdocs/vue/my-router-project...

Done. Now run: // 準備(Scaffolding)ができたので、続いて以下のコマンドを実行
  cd my-router-project
  npm install
  npm run dev

必要なパッケージをインストール

続いてコマンドのレスポンス Done. Now run: の下に記載されている以下のコマンドを実行してプロジェクトの雛形を完成させます。

  • cd コマンドで作成されたプロジェクトのディレクトリに移動
  • npm install コマンドで必要な JavaScriprt パッケージ(依存関係)をインストール
  • npm run dev コマンドで開発サーバを起動
% cd my-router-project  return //プロジェクトディレクトリに移動

% npm install return //必要なパッケージをインストール

added 35 packages, and audited 36 packages in 4s
5 packages are looking for funding
  run `npm fund` for details
found 0 vulnerabilities

% npm run dev  return //開発サーバを起動

> my-router-project@0.0.0 dev
> vite

  VITE v3.1.8  ready in 260 ms

  ➜  Local:   http://127.0.0.1:5173/  //この URL にアクセス
  ➜  Network: use --host to expose

これで、Vue Router が組み込まれたプロジェクトの雛形の作成が完了です。

npm run dev コマンドのレスポンスに出力された http://127.0.0.1:5173/ にアクセスするとプロジェクト直下にある index.html ファイルが読み込まれて以下のような初期画面が表示されます。

About ページのリンクをクリックすると以下のように表示されます。

開発サーバーを終了するには control + c を押します。

プロジェクトのファイル構成

以下は先述の npm create vite@latestnpm install の実行によりデフォルトで生成されるプロジェクトのフォルダーとファイルの例です。

Vue Router を導入したプロジェクトのフォルダー構成(例)
my-router-project //プロジェクトのディレクトリ
├── index.html  //表示用フィル
├── node_modules
├── package-lock.json
├── package.json
├── public
│   └── favicon.ico
├── src
│   ├── App.vue  // メインコンポーネント(Root Component) レンダリングの起点
│   ├── assets
│   │   ├── base.css
│   │   ├── logo.svg
│   │   └── main.css
│   ├── components  // ページで利用するコンポーネントのディレクトリ
│   │   ├── HelloWorld.vue
│   │   ├── TheWelcome.vue
│   │   ├── WelcomeItem.vue
│   │   └── icons
│   ├── main.js  //エントリポイント(ルーターの有効化)
│   ├── router
│   │   └── index.js  //ルーティングの定義とルーターの生成
│   └── views  //ページを構成するコンポーネント(Route Components)のディレクトリ
│       ├── AboutView.vue  // About ページのコンポーネント
│       └── HomeView.vue  // Home ページのコンポーネント
└── vite.config.js  //Vite の設定ファイル

以降では、この構成のままでルーターの機能などを確認しますが、実際の使用では App.vue を編集して不要な部分を削除し、合わせて不要なファイル(aseets のファイルや components のファイルなど)やディレクトリを削除したり、必要なファイルやディレクトリを追加するなどして使用します。

package.json

この例の場合、package.json は以下のようになっています。

  • vue のバージョン:3.2.38
  • vue-router のバージョン:4.1.5
package.json
{
  "name": "my-router-project",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview --port 4173"
  },
  "dependencies": {
    "vue": "^3.2.38",
    "vue-router": "^4.1.5"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^3.0.3",
    "vite": "^3.0.9"
  }
}

実際にインストールされているパッケージのバージョンは package-lock.jsonnpm view xxxx コマンドで確認できます。

vite.config.js

また、vite.config.js は以下のように初期状態では、@ を使ってコンポーネントなど ./src 以下のファイルをインポートできるように ./src@ で表すエイリアス(resolve.alias)が設定されています。

vite.config.js
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    //パスにエイリアスを設定
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

fileURLToPath(url) | new URL(url, import.meta.url) | import.meta

index.html

以下は表示用ファイル index.html です。

アプリをマウントする id 属性が appdiv 要素と、type 属性に module を指定して main.js を読み込んでいる script 要素が記述されています。

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>

AboutView.vue / HomeView.vue

view ディレクトリに配置されている AboutView.vue と HomeView.vue はページを構成するコンポーネント(Route Components)のサンプルで、これらのコンポーネントはルーティングで指定してページを表示する際に使用されるコンポーネントでビュー(View)とも呼ばれます。

以下は About のリンクをクリックした際に表示される About ページのサンプル AboutView.vue です。

src/views/AboutView.vue
<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>

以下は Home のリンクをクリックした際に表示される(http://127.0.0.1:5173/ で表示される)ホームのサンプル HomeView.vue です。

このサンプルのビューは、components ディレクトリ(ページで利用するコンポーネントのディレクトリ)にある TheWelcome コンポーネントを呼び出しています。

そして TheWelcome コンポーネントでは、同じ components ディレクトリにある WelcomeItem コンポーネントなどを呼び出して作成されています。

src/views/HomeView.vue
<script setup>
import TheWelcome from '../components/TheWelcome.vue'
</script>

<template>
  <main>
    <TheWelcome />
  </main>
</template>

以下では生成されたプロジェクトの主なファイルから Vue Router の動作の概要を確認しています。

router/index.js

src/router/index.js では createRouter メソッドでオプションにルーティングの定義などを指定してルーター(Router インスタンス)を生成し、変数 router に格納し、デフォルトエクスポートしています。

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

// createRouter メソッドでルーターを生成
const router = createRouter({
  // History の実装方法を指定(history オプション)
  history: createWebHistory(import.meta.env.BASE_URL),
  // ルーティングの定義(routes オプション)
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('../views/AboutView.vue')
    }
  ]
})

//生成したルーターをデフォルトエクスポート
export default router
createRouter

Vue Router を利用するには、createRouter でルーター(Router インスタンス)を生成します。

createRouter には以下のようなオプションを指定することができます。

createRouter メソッドの主なオプション(RouterOptions
オプション 説明
history History の実装方法(モード)を指定します。Hash Mode を使用する場合は createWebHashHistory メソッドで、HTML5 Mode を使用する場合は createWebHistory メソッドで指定します(history オプション)。
Hash Mode の場合
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    //...
  ],
})
routes ルーティング情報をオブジェクトの配列で定義します。
linkActiveClass 現在のページを表すアクティブなリンクに適用されるクラス名。デフォルトは router-link-active
linkExactActiveClass 完全一致する現在のページを表すアクティブなリンクに適用されるクラス名。デフォルトは router-link-exact-active
scrollBehavior ページ間を移動する際のスクロールの動作を指定する関数。スクロールを遅らせる Promise を返すことができます。
parseQuery クエリを解析するためのカスタム実装。
stringifyQuery クエリオブジェクトを文字列に変換するためのカスタム実装。先頭に ? を付けるべきではありません。
history オプション

createRouter メソッドでルーターのインスタンスを生成する際に指定する history オプションでは、以下のような History の実装方法(モード)を指定することができます(Different History modes)。

モード 説明
Hash Mode URL にハッシュ(#)を使う Hash Mode は createWebHashHistory メソッドを使用して指定します。ハッシュベースの Hash Mode はサーバーでの設定を必要としませんが、検索エンジンではページとして処理されないなど SEO 的に不利になります。
HTML5 Mode ハッシュを利用しない一般的な URL の形式の HTML5 Mode は createWebHistory メソッドを使用して指定します。SEO 的にも問題のない HTML5 Mode の利用が推奨されていますが、サーバーを適切に設定する必要があります(Vite の開発サーバーでは特に設定は必要ありません)。サーバーの設定例は Example Server Configurations で確認できます。
Memory mode 主に SSR(Server Side Rendering)のためのモードです。履歴(History)がないため、戻ったり、進むことができません。

以下は history オプションに HTML5 Mode を指定する例です。引数には基底パス(アプリが配信されているベース URL)を指定しておきます。

const router = createRouter({
  // HTML5 Mode を指定
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    //...
  ],
})
routes オプション(ルーティングの設定・定義)

routes オプション(設定)にはルーティングの情報を route オブジェクトの配列で定義します。

1つの routeオブジェクトが1つのルート(ルーティング)を表します。また、routes オプションの中の各 route オブジェクトはルートレコード(route record)と呼ばれます。

route オブジェクトでは以下のようなプロパティが利用できます。

プロパティ 説明
path リクエスパス(必須)
name ルートの名前(指定しておけば名前付きルートが利用できます)
component このルートによって呼び出されるコンポーネント
components このルートによって呼び出されるコンポーネント(複数)名前付きビュー
children 配下のルーティング(ネストされたルート)の定義
props ルートパラメータ(route params)を props に割り当てるかどうか
redirect リダイレクト先のパスを指定
alias エイリアスを指定
meta ルートのメタ情報(ルートに関する任意の情報)

以下は src/router/index.js を書き換えて、routes オプション(ルーティングの定義)を createRouter() の外で定義した例です(内容的には全く同じです)。

HomeView.vue や AboutView.vue などのページを構成するコンポーネント(Route Component)は、デフォルトでは src/views ディレクトリに配置します。

それぞれの route オブジェクトでは pathcomponent プロパティなどを使ってルーティングの情報を定義します。以下の1つ目の route オブジェクトでは path で指定した '/' にアクセスしたら、component で指定した HomeView コンポーネントを呼び出すように定義されていて、このルートの名前を name プロパティで home として定義しています。

src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'  //ページを構成するコンポーネントをインポート

// ルーティングの情報を route オブジェクトの配列で定義
const routes = [
  //route オブジェクト
  {
    path: '/',  //リクエスパス
    name: 'home',  //このルートの名前
    component: HomeView  //呼び出されるコンポーネント
  },
  //route オブジェクト
  {
    path: '/about',
    name: 'about',
    // Lazy Loading(動的にインポート)
    component: () => import('../views/AboutView.vue')
  }
]

// ルーターを生成
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes //routes: routes の省略形(routes オプションに上記で定義した routes を指定)
})
export default router
コンポーネントの動的インポート

上記の例の場合、HomeView コンポーネントは2行目であらかじめインポートしていますが、AboutView コンポーネントはあらかじめインポートせず、ページにアクセスがある場合に動的にインポート(dynamic imports)するようになっています(17行目の記述)。

コンポーネントを動的にインポートするには、以下のように component(複数の場合は components)オプションにコンポーネントを取得するための関数を渡します。

// component プロパティに、コンポーネントを取得するための関数を渡す
component: () => import('../views/AboutView.vue')

必要であれば、別途関数を定義して渡すこともできます。

// コンポーネントを取得するための関数を定義
const importAboutView = () => import('../views/AboutView.vue');

const router = createRouter({
  // ...,
  routes: [
    // ...,
    {
      path: '/about',
      name: 'about',
      // 別途定義した関数を渡す
      component: importAboutView
    }
  ]
})

ブラウザのデベロッパーツールのネットワークタブで確認すると、初回表示の際に HomeView.vue はダウンロードされますが、AboutView.vue はダウンロードされず、About のリンクをクリックするとダウンロードされるのが確認できます。

また、ビルドすると、以下のように AboutView.0214907f.js と AboutView.4d995ba2.css という AboutView 関連のファイルが別途生成されるのが確認されます。

my-router-project % npm run build  return  //ビルドを実行

> my-router-project@0.0.0 build
> vite build

vite v3.1.8 building for production...
✓ 43 modules transformed.
dist/assets/logo.da9b9095.svg        0.30 KiB
dist/index.html                      0.42 KiB
dist/assets/AboutView.0214907f.js    0.22 KiB / gzip: 0.19 KiB
dist/assets/AboutView.4d995ba2.css   0.08 KiB / gzip: 0.10 KiB
dist/assets/index.be4ca81f.css       4.08 KiB / gzip: 1.28 KiB
dist/assets/index.ef1713f6.js        81.54 KiB / gzip: 32.03 KiB

index.js を以下のように書き換えて、動的インポートを利用しないと、初回表示の際に HomeView.vue と AboutView.vue の両方のファイルがダウンロードされます。

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
// AboutView もインポート
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',
      // HomeView と同じように以下のように書き換える
      component: AboutView
    }
  ]
})

export default router

上記のように書き換えた後にビルドすると、AboutView 関連のファイルは生成されず、index.25027dd1.js と index.cdbbd68c.css にバンドルされてファイルサイズが大きくなります(コンポーネントの数が増えればその差は大きくなります)。

my-router-project % npm run build  return  //ビルドを実行

> my-router-project@0.0.0 build
> vite build

vite v3.1.8 building for production...
✓ 42 modules transformed.
dist/assets/logo.da9b9095.svg    0.30 KiB
dist/index.html                  0.42 KiB
dist/assets/index.cdbbd68c.css   4.16 KiB / gzip: 1.30 KiB
dist/assets/index.25027dd1.js    84.31 KiB / gzip: 33.00 KiB

複数のフィルに分けてビルドすることを Code Splitting と呼びます。

複数のファイルに分けることで、アクセスがあった場合にのみ必要なファイルをダウンロードして、最初のアクセス時にダウンロードするサイズを減らすことができます。

コンポーネントを動的インポートすると Code Splitting が適用され、最初のアクセス時にダウンロードするサイズを減らすことができるので、必要に応じて動的インポートを使用することが推奨されています。

Lazy Loading Routes

main.js

main.js では router/index.js で生成(定義)した Router オブジェクトの router を Vue のインスタンスにインストール(登録)してアプリで利用できるようにしています。

router/index.js からインポートした routeruse() メソッドに渡してプラグインとしてインストールすることで、ルーターが利用できるようになります。

3行目の router のインポートではディレクトリの ./router だけを指定してファイルの index.js を指定していませんが、ディレクトリを指定した場合、ディレクトリ配下の index.js が読み込まれます。

src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'  //ルーターを router/index.js からインポート

import './assets/main.css'  //CSS のインポート

const app = createApp(App)  //Vue のインスタンスを生成

app.use(router)  //Vue のインスタンスにルーターをインストールして有効化

app.mount('#app')

app.use(router) でルーターを有効化すると、任意のコンポーネント内で this.$router でルーター(router オブジェクト)に、this.$route で現在のルート(route オブジェクト)にアクセスできます。

テンプレート内では this なしで、$router$route にアクセスすることができます。

但し、Comosition API setup 内では this にアクセスできないので、useRouter(router オブジェクトを返すメソッド)や useRoute(route オブジェクトを返すメソッド)を使用します。

App.vue

createApp() の引数に渡される App コンポーネントは、アプリケーションにアクセスされた場合にレンダリングの起点として使われるメインコンポーネント(Root Component)で、script setup 構文を使って SFC で記述されています。

※ Root と Route が日本語では同じ「ルート」なので、ここでは Root Component をルートコンポーネントではなく、メインコンポーネントと呼んでいます。

App.vue の <template> では、RouterLink コンポーネントのタグ <RouterLink> を使ってページへのリンクを作成し、RouterView コンポーネントのタグ <RouterView> を使ってルートにマッチしたコンポーネントを表示する領域を作成しています。

src/App.vue
<script setup>
// RouterLink コンポーネントと RouterView コンポーネントをインポート
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" />
    <div class="wrapper">
      <HelloWorld msg="You did it!" />
      <nav>
        <!-- リンクには RouterLink コンポーネントを使い、リンク先を to プロパティに指定 -->
        <!-- デフォルトで <RouterLink> は <a> タグとして描画される -->
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
      </nav>
    </div>
  </header>
  <!-- ルートとマッチしたコンポーネントが RouterView コンポーネントへ描画される -->
  <RouterView />
</template>

<style scoped>
・・・中略・・・
/* 完全一致する現在のページを表すアクティブなリンク */
nav a.router-link-exact-active {
  color: var(--color-text);
}

nav a.router-link-exact-active:hover {
  background-color: transparent;
}
 ・・・中略・・・
</style>

RouterLink コンポーネントや RouterView コンポーネントのタグ名(コンポーネント名)はパスカルケースで記述してあります。

以下のようにケバブケースで記述することもできますが、単一ファイルコンポーネントのテンプレート内では、コンポーネント名は常にパスカルケースにすることが推奨されています。但し、DOM テンプレートの中ではケバブケースです。

<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" />
    <div class="wrapper">
      <hello-world msg="You did it!" />
      <nav>
        <router-link to="/">Home</router-link>
        <router-link to="/about">About</router-link>
      </nav>
    </div>
  </header>
  <router-view />
</template>
RouterView

RouterView コンポーネントは現在のパスに対してマッチした(定義したルーティングに応じた)コンポーネントを描画するコンポーネントで、 <RouterView> または <router-view> タグを使います。

App.vue 抜粋
<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" />
    <div class="wrapper">
      <HelloWorld msg="You did it!" />
      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
      </nav>
    </div>
  </header>
  <!-- ルートにマッチしたコンポーネントが RouterView コンポーネントへ描画される -->
  <RouterView />
</template>

この例の場合、8行目の <RouterLink to="/about">About</RouterLink> のリンクをクリックすると、ルーティングの定義により <RouterView /> の部分に以下の About ページのコンポーネント(AboutView.vue)が描画されます。

src/views/AboutView.vue
<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>

<style>
/* 省略  */
</style>

出力は以下のように <RouterView /> の部分に AboutView コンポーネントが出力されます。

<div id="app" data-v-app="">
  <header data-v-7a7a37b1="">
    <img alt="Vue logo" class="logo" src="/src/assets/logo.svg">
    <div class="wrapper">
      <div class="greetings"><!-- HelloWorld コンポーネントの出力 -->
         ・・・中略・・・
      </div>
      <nav>
        <a href="/" class="">Home</a>
        <a href="/about" class="router-link-active router-link-exact-active" aria-current="page">About</a>
      </nav>
    </div>
  </header>
  <!-- 以下が AboutView コンポーネントの出力 -->
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</div>

Vue Devtools で確認

ブラウザの拡張機能の Vue Devtools を入れていれば、以下のように現在の RouterView やルートなどの情報を簡単に確認できます。

router / route オブジェクト

app.use(router) でルーターを有効化すると、任意のコンポーネント内で this.$router として router オブジェクトのインスタンスにアクセスできます。また、this.$route として現在のルート(route オブジェクト)にアクセスできます。

テンプレート内では単に(this を介さず)、$router$route でアクセスすることができます。

以下は AboutView.vue に記述を追加して router と route オブジェクトをコンソールやテンプレートに出力して確認する例です。

src/views/AboutView.vue
<script>
export default {
  created() {
    //コンポーネント内では this を使ってアクセス
    console.log(this.$router);
    console.log(this.$route);
  }
}
</script>

<template>
  <div class="about">
    <h1>This is an about page </h1>
    <!-- テンプレート内では this なしで、$router や $route にアクセス-->
    <p>{{ $router.options.routes[0].name}}</p> <!-- home と出力-->
    <p>{{ $route.path}}</p> <!-- /about と出力 -->
  </div>
</template>

以下は About ページでのコンソールへの出力例です。それぞれのオブジェクトのプロパティやメソッドが確認できます。

Vue Devtools を使えば、route オブジェクトのプロパティなどを簡単に確認することができます。

Comosition API の場合

但し、Comosition API setup 内では this にアクセスできないので、useRouter() メソッドや useRoute() メソッドを使用します。

  • useRouter :ルーターインスタンス(router オブジェクト)を返します。 テンプレート内で $router を使用するのと同じです。
  • useRoute :現在のルート(route オブジェクト)を返します。テンプレート内で $route を使用するのと同じです。
script setup 構文
<script setup>
//useRouter と useRoute メソッドをインポート
import { useRouter, useRoute } from 'vue-router';
// router インスタンスを取得
const router = useRouter();
//route インスタンスを取得
const route = useRoute();

console.log(router);
console.log(route);

</script>
setup() メソッド
<script>
//useRouter と useRoute メソッドをインポート
import { useRouter, useRoute } from 'vue-router';

export default {
  setup() {
    // router インスタンスを取得
    const router = useRouter();
    // route インスタンスを取得
    const route = useRoute();
    console.log(router);
    console.log(route);
    return {
      //テンプレートで使う場合は返す(但し、テンプレートでは $router と $route でアクセス可能)
      router,
      route
    }
  }
}
</script>

$route のプロパティ

$route はマッチした現在のルートの route オブジェクト(route location)、つまり、現在のルートの情報を表すオブジェクトのインスタンスです。

$route には以下のようなプロパティがあります。

プロパティ 説明
fullPath クエリや hash を含む完全なパス(URL)
hash ハッシュ(# 以降の文字列)
matched 現在のルートのネストを含むルートレコード(routes の配列内のオブジェクトのコピー)。
meta マッチしたルートレコードのメタ情報(ルートに関する任意の情報)
name マッチしたルートの名前
params ルートの path に含まれるパラメータ情報
path 現在のルートのパス
query クエリ文字列(? 以降の文字列)の key/value ペアを保持するオブジェクト
redirectedFrom リダイレクト元の情報

プログラムによるナビゲーション(ページ遷移)

ページ間の移動には RouterLink コンポーネントのリンクを利用できますが、プログラム(コード)でページを移動するには router オブジェクトの メソッド を利用することができます。

Programmatic Navigation

router オブジェクトのページ遷移メソッド(ナビゲーションメソッド)には以下のようなものがあります。

ナビゲーションメソッド
メソッド 概要
push 別のページ(URL)に移動(遷移)
replace 指定したパスに置き換え(新しい history エントリを追加しないで遷移)
go history スタックの中で指定された分だけ遷移(負の値で前に戻る)
forward 次のページへ遷移
back 前のページに遷移

ナビゲーションメソッドは、ナビゲーションが終了するまで待機して Promise を返すので、ナビゲーションが成功したか失敗したかを確認することができます(詳細:Navigation Handling)。

push メソッド

標準的な JavaScript の場合、ページの移動には location.href を利用できますが location.href = 'path' はページ全体を再読込するため、ルーター環境では使用しません。

ルーター環境でページを移動(異なる URL へ遷移)するには router.push メソッドが使えます。

push メソッドは history スタックに新しいエントリを追加するので、ユーザーがブラウザの戻るボタンをクリックした時に前の URL に戻れるようになります。

また、<RouterLink> をクリックした時に内部的にこのメソッドが呼ばれるので、<RouterLink :to="..."> をクリックすることは router.push(...) を呼ぶことと同じです。

コンポーネント内では this.$router として router オブジェクトのインスタンスにアクセスできるので、this.$router.push() で呼び出します。

以下はテンプレートに Home に戻るボタンを追加し、push を使ったイベントリスナを設定した例です。

AboutView.vue
<script>
export default {
  methods: {
    goToHome() {
      this.$router.push('/');
    }
  }
}
</script>

<template>
  <div class="about">
    <h1>This is an about page </h1>
    <button type="button" v-on:click="goToHome">Home</button>
  </div>
</template>

テンプレートでは router オブジェクトを $router で参照できるので、イベントリスナを別途設定せずに以下のように v-on:click に直接記述することもできます。

AboutView.vue
<template>
  <div class="about">
    <h1>This is an about page</h1>
    <button type="button" v-on:click="$router.push('/')">Home</button>
  </div>
</template>

Composition API では setup 内で this にアクセスできないので、useRouter を利用します。

AboutView.vue
<script setup>
// useRouter をインポート
import { useRouter } from 'vue-router';
// router オブジェクトを取得
const router = useRouter();
// イベントハンドラを定義
const goToHome = () => router.push('/');
</script>

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <div>
     <button type="button" v-on:click="goToHome">Home</button>
    </div>
  </div>
</template>

以下のように router をインポートして、router.push() として利用することもできます。

AboutView.vue (テンプレートは同じなので省略)
<script setup>
// router をインポート
import router from '@/router';
// イベントハンドラを定義
const goToHome = () => router.push('/');
</script>

以下は <script setup> を使わずに setup() で記述する例です。

<script>
// useRouter をインポート
import { useRouter } from 'vue-router';
export default {
  setup() {
    // router オブジェクトを取得
    const router = useRouter();
    const goToHome = () => router.push('/');
    return { goToHome };
  }
}
</script>
push() の引数

引数は文字列のパス、または、location を記述するオブジェクト(routeLocation)を指定できます。

// 文字列パス
router.push('/users/eduardo')

// オブジェクト
router.push({ path: '/users/eduardo' })

// 名前付きルート(ルーターが URL を構築するためのパラメータ params 付き)
router.push({ name: 'user', params: { username: 'eduardo' } })

// パスが /register?plan=private となるクエリ(query)を指定
router.push({ path: '/register', query: { plan: 'private' } })

// パスが /about#team になるようにハッシュ(hash)を指定
router.push({ path: '/about', hash: '#team' })

replace メソッド

replace メソッドは push メソッド同様、ページを移動しますが、新しい history エントリを追加しないで遷移します(指定したパスに置き換えます)。

そのため、ユーザーがブラウザの戻るボタンをクリックした時に前の URL に戻れません。

RouterLink で以下のように replace 属性を指定したのと同じです。

<RouterLink to="パス" replace>xxxx</RouterLink>
<RouterLink v-bind:to="式(オブジェクト)" replace>xxxx</RouterLink>

また、push メソッドで引数にオブジェクト(routeLocation)を指定する際に、replace: true を渡しても同じことになります。

router.push({ path: '/home', replace: true });
// 以下と同じ
router.replace({ path: '/home' });
// または
router.replace('/home');

go メソッド

go メソッドは history スタック(履歴)の中でどのくらい進めるか(戻るか)を表す整数を引数として受け取り、指定された分だけ遷移します(負の値で前に戻ります)。

// 1つ先へ進める。router.forward() と同じ
router.go(1)

// 1つ前に戻す。router.back() と同じ
router.go(-1)

// 3つ先へ進める
router.go(3)

// もし指定した数の履歴が存在しない場合、サイレントに失敗します(エラーは出ません)
router.go(-100)
router.go(100)

パラメータを使った動的ルーティング

パス(URL)の末尾にパラメータの文字列(params)を指定して、その値をルーター経由でコンポーネントに引き渡すことができます。

route オブジェクトの path プロパティにパラメータを指定

ルーティング情報を表す route オブジェクトの path プロパティに、コロン : を使ってパラメータの文字列(params の値)を指定します。

path プロパティに指定した :文字列 の部分を動的セグメントと呼びます。

以下は router/index.js の routes オプションに UserView コンポーネントへのルートを追加した例です。

この例の場合、path オプションに指定した /user/:uid:uid の部分が動的に変わる部分(動的セグメント)で、これにより /user/001 や /user/12 などの URL は同じルートにマッチします。

ルートがマッチした時、この動的セグメントの値はそのルートがマッチしたコンポーネント内で this.$route.params.xxxx として利用可能になります。

$routeroute オブジェクトを参照します。また、xxxx の部分はこの場合、uid になります。

router/index.js
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',
      component: () => import('../views/AboutView.vue')
    },
    //以下のルート(route オブジェクト)を追加
    {
      path: '/user/:uid',  //  :uid が動的セグメント
      name: 'user',
      component: () => import('../views/UserView.vue')
    }
  ],
})

export default router

route オブジェクトの path に指定したパラメータを受け取るコンポーネント

上記の routes オプションに追加したルートに対応するコンポーネント UserView.vue を追加します。

ルートがマッチした時、このコンポーネントでは動的セグメントの値を this.$route.params.uid として取得できます。テンプレートの中では単に $route.params.uid で参照できます(this は不要)。

src/views/UserView.vue(パラメータを受け取るコンポーネント)
<template>
  <div class="user">
    <h1>User : {{ $route.params.uid}}</h1>
  </div>
</template>

リンクにパラメータを渡す

リンクにパラメータの値を指定してコンポーネントに渡します。

App コンポーネントに UserView へのリンクを追加し、パラメータの値(001)を指定します。

src/App.vue
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" />
    <div class="wrapper">
      <HelloWorld msg="You did it!" />
      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
        <!-- リンクを追加し、パラメータの値を指定 -->
        <RouterLink to="/user/001">User 001</RouterLink>
      </nav>
    </div>
  </header>
  <RouterView />
</template>

追加したリンク「User 001」をクリックすると、以下のように表示され、パラメータの値が UserView コンポーネントに渡されていることが確認できます。

複数のパラメータ(動的セグメント)を指定

1 つのルートが複数の動的セグメントを持つこともでき、複数のパラメータを指定することができます。

前述の例では、1つの動的セグメント :uid を指定しましたが、/user/:uid/:name のように複数指定することもできます。

router/index.js 抜粋
{
  path: '/user/:uid/:name',  //:uid と :name を配置
  name: 'user',
  component: () => import('../views/UserView.vue')
}
src/views/UserView.vue 抜粋
<template>
  <div class="user">
    <h1>User : {{ $route.params.uid }}</h1>
    <h2>Name : {{ $route.params.name }}</h2>
  </div>
</template>
src/App.vue 抜粋
<nav>
  <RouterLink to="/">Home</RouterLink>
  <RouterLink to="/about">About</RouterLink>
  <RouterLink to="/user/001/foo">User 001</RouterLink>
</nav>

パラメータの変更を監視

Vue Router の場合、例えば、/user/001 から /user/002 へ移動するようなパラメータだけが異なるページ遷移では同じコンポーネントインスタンスが再利用されます。

このため、コンポーネントのアンマウント処理が行われず、ライフサイクルフックが呼ばれません。

Reacting to Params Changes

以下はパラメータ uid を受け取って created() ライフサイクルフックでその値を更新して出力するように前述の UserView.vue を書き換えた例です。

src/views/UserView.vue
<script>
export default {
  data() {
    return {
      //uid を受け取ってデータを作成
      userId: this.$route.params.uid
    }
  },
  created() {
    //created フックで値を更新(但し、これは呼ばれないので更新されない)
    this.userId = this.$route.params.uid;
  },
}
</script>

<template>
  <div class="user">
    <h1>User : {{ $route.params.uid }}</h1>
    <p>User ID  : {{ userId }}</p>
  </div>
</template>

以下は上記のスクリプト部分を Composition API の script setup で書き換えたものです。

Composition API では、setup に定義した処理が beforeCreatecreated に相当するので created はありません。

src/views/UserView.vue
<script setup>
//ref メソッドをインポート
import { ref } from 'vue';
//useRoute メソッドをインポート
import { useRoute } from 'vue-router';
//route インスタンス($route)を取得
const route = useRoute();
//ref メソッドでリアクティブな変数 userId を定義
const userId = ref(route.params.uid);
</script>

<template>
  <div class="user">
    <h1>User : {{ $route.params.uid }}</h1>
    <p>User ID  : {{ userId }}</p>
  </div>
</template>

ルートの定義は以下のように動的セグメントとして :uid を指定しています。

router/index.js 抜粋
{
  path: '/user/:uid',  //:uid を配置
  name: 'user',
  component: () => import('../views/UserView.vue')
}

メニューには、User 009 へのリンクを追加しています。

src/App.vue 抜粋
<nav>
  <RouterLink to="/">Home</RouterLink>
  <RouterLink to="/about">About</RouterLink>
  <RouterLink to="/user/001/foo">User 001</RouterLink>
  <RouterLink to="/user/009/foo">User 009</RouterLink>
</nav>

上記の設定で、/user/001 から /user/009 へリンクをクリックしてページを移動すると、直接 $route.params.uid で出力している部分は更新されますが、created() が呼ばれないため userId の部分は更新されません。

$route オブジェクトを watch

同じコンポーネントでパラメーター変更を検知するためには(上記の問題を回避するには)、 $route オブジェクトを監視(watch)します。

以下は Options API の例です。$route オブジェクトを watch オプションを使って監視して、変更があれば userId を更新します。

<script>
export default {
  data() {
    return {
      userId: this.$route.params.uid
    }
  },
  created() {
    this.userId = this.$route.params.uid;
  },
  // watch オプションを追加
  watch: {
    $route(to) {
      this.userId = to.params.uid;
    }
  },
}
</script>

以下は Composition API の watch メソッドで書き換えたものです。

<script setup>
// watch をインポート
import { watch, ref } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const userId = ref(route.params.uid);

// watch メソッド
watch(
  route, // $route オブジェクトを監視
  (to) => {
    userId.value = to.params.uid;
  }
)
</script>

上記では $route オブジェクト全体を監視していますが、この例の場合、以下のように $route オブジェクトのプロパティや値を監視するように書き換えても同じ結果になります。

//$route オブジェクトの params プロパティを監視する場合
watch(
  () => route.params, // params プロパティを監視
  (toParams) => {
    userId.value = toParams.uid;
  }
)
//または uid のみ監視する場合
watch(
  () => route.params.uid, // uid を監視
  (toUid) => {
    userId.value = toUid;
  }
)

以下は、{JSON} Placeholder という JSON データを返してくれる API にアクセスして、ダミーのユーザーデータを取得してページに表示する例です。

ユーザーデータを取得する URL は「https://jsonplaceholder.typicode.com/users/id」になるので、「id」の部分をパラメータの値を整数に変換して指定しています。

関連ページ:Fetch API fetch() の使い方

この例の場合も、パラメータの変更を watch しないと、ページの内容が更新されません。

前述の UserView.vue を以下のように書き換えます。ルートの定義(index.js)やリンク(App.vue)は前述の例と同じです。

src/views/UserView.vue
<script setup>
//watch, ref メソッドをインポート
import { watch, ref } from 'vue';
//useRoute メソッドをインポート
import { useRoute } from 'vue-router';
//route インスタンスを取得
const route = useRoute();
//ref メソッドでユーザー情報を入れるリアクティブな変数(ref)を初期化
const user = ref({});
//パラメータ uid の文字列を整数に変換
let userId = parseInt(route.params.uid);
//API から fetch() でユーザー情報を取得して変数に代入する関数
const fetchUser = async (url) => {
  //API からユーザー情報を取得
  const response = await fetch(url);
  //取得したレスポンスを json に変換
  const data = await response.json();
  //user(ref)の value プロパティに、取得したデータを代入
  user.value = data;
};
//API の URL(ユーザーの ID 部分を除く)
const targetUrl = 'https://jsonplaceholder.typicode.com/users/';
//引数に渡す URL を userId を使って作成して上記関数を実行
fetchUser(targetUrl + userId);

// params を監視して更新(以下の記述がないとページ遷移の際に更新されない)
watch(
  () => route.params,
  (toParams) => {
    // 変更があれば uid を整数に変換して fetchUser() を実行
    userId =  parseInt(toParams.uid);
    fetchUser(targetUrl + userId);
  }
)
</script>

<template>
  <div class="user">
    <h1>User : {{ route.params.uid}}</h1>
    <ul>
      <li>Name : {{ user.name }}</li>
      <li>Email : {{ user.email }}</li>
      <li>Phone : {{ user.phone }}</li>
    </ul>
  </div>
</template>

例えば、以下のように表示されます。watch メソッドで route.params を監視しているので、/user/001 から /user/009 へリンクをクリックしてページを移動すれば、ユーザーの情報も更新されます。

beforeRouteUpdate ナビゲーションガード

watch を使用する代わりに beforeRouteUpdate ナビゲーションガードを利用することもできます。

Composition API の場合は、onBeforeRouteUpdate を使用します。

src/views/UserView.vue 抜粋(5行目〜15行目は前述の例と同じ)
<script setup>
import { ref } from 'vue';
//onBeforeRouteUpdate メソッドをインポート
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
const route = useRoute();
const user = ref({});
let userId = parseInt(route.params.uid);
const fetchUser = async (url) => {
  const response = await fetch(url);
  const data = await response.json();
  user.value = data;
};

const targetUrl = 'https://jsonplaceholder.typicode.com/users/';
fetchUser(targetUrl + userId);

// watch の代わりに beforeRouteUpdate ナビゲーションガードを利用
onBeforeRouteUpdate(async (to) => {
  userId =  parseInt(to.params.uid);
  fetchUser(targetUrl + userId);
});
</script>
<script>
import { ref } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';

export default {
  setup() {
    const route = useRoute();
    const user = ref({});
    let userId = parseInt(route.params.uid);
    const fetchUser = async (url) => {
      const response = await fetch(url);
      const data = await response.json();
      user.value = data;
    };

    const targetUrl = 'https://jsonplaceholder.typicode.com/users/';
    fetchUser(targetUrl + userId);

    onBeforeRouteUpdate(async (to) => {
      userId =  parseInt(to.params.uid);
      await fetchUser(targetUrl + userId);
    });
    return {
      route,
      user
    }
  }
}
</script>

関連項目:ナビゲーションガード

404 Not Found Route

ルーティングに設定していない(存在しない)ページの URL へアクセスがあった場合、コンソールに警告は出力されますが、エラーが発生するわけではなく App コンポーネントに設定した内容のみ表示されます。

存在しないページの URL へアクセスがあった場合に、Not Found(404)のページが表示されるようにするには以下のようなルートを設定します。

src/router/index.js
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
    },
    ・・・中略・・・
    // Not Found(404)へのルート
    {
      path: '/:pathMatch(.*)*',  //すべてに一致するパス
      name: 'NotFound',
      component: () => import('../views/NotFoundView.vue')
    },
  ],
})
export default router

通常のパラメータは、/ で区切られた URL フラグメント間の文字のみに一致します。

何かに一致させたい場合は、パラメータの直後(末尾)に括弧 ( ) を追加してその中に正規表現を指定することで、パラメータに独自の正規表現を使用できます。

path: '/:pathMatch(.*)*'(.*) はすべてに一致し、リクエストされた URL のパスの / 以降の値は $route.params.pathMatch で取得できます。:pathMatch(.*)* の最後の *繰り返しを意味するパラメータで、:pathMatch(.*)*:pathMatch(.*) の繰り返しです。

上記の Not Found(404)へのルートの定義で component に指定した以下のようなコンポーネントを作成すれば、存在しない URL にアクセスされた際に表示されます。

src/views/NotFoundView.vue
<template>
  <div class="not-found">
    <h1>404 Not Found</h1>
    <p>指定したページは存在しません。</p>
    <p><RouterLink v-bind:to="{ name: 'home' }">ホーム</RouterLink></p>
    <!-- <p>:pathMatch(.*)* の値:{{ $route.params.pathMatch }}</p>(確認用)-->
  </div>
</template>

history オプションに HTML5 Mode モード を使用する場合は、正しいサーバの設定も必要になります(Vite の開発サーバーでは特に設定は必要ありません)。

例えば、存在しないパス「/xxx/yyy/zzz」にアクセスされると NotFoundView コンポーネントが表示され、pathMatch(.*)* の値は [ "xxx", "yyy", "zzz" ] になります(6行目のコメントを外すと確認できます)。

Catch all / 404 Not found Route

ルートのマッチング構文

Vue Router では、ルートのパラメータに *? などの特定の文字やカスタム正規表現パターンを使ったマッチング構文により、複雑なパスを表現できるようになっています。

Routes' Matching Syntax

正規表現パターンを使ったマッチング

パラメータの末尾に正規表現パターンを括弧 ( ) で括って指定すると、その正規表現パターンに合致した値だけがマッチします。

内部的には、例えば :userId というパラメータの指定は :userId([^/]+) と同じことで、([^/]+) の括弧の中は「スラッシュ以外の少なくとも1文字」というような正規表現になります。

例えば /:orderId/:productName というルートは、全く同じ URL がマッチします。

この2つを分けてルーティングさせるには、パスに固定のセクションを追加する方法があります。

const routes = [
  //  /o/3549 や /o/foo などにマッチ
  { path: '/o/:orderId', ... },
  // /p/books や /p/123 などにマッチ
  { path: '/p/:productName', ... },
]

但し、場合によっては /o/p などの固定のセクションを追加したくないこともあります。

例えば、orderId は常に数値で、productName は何でもよいというような場合は、以下のようにパラメーターに続く括弧内にカスタム正規表現を指定できます。

以下の場合、/25 のような数値は /:orderId にマッチし、それ以外は /:productName にマッチします。ルート配列の順序は重要ではありません。

const routes = [
  // /:orderId -> /25 や /123 などの数値のみにマッチ( \\d+ は1つ以上の数値 )
  { path: '/:orderId(\\d+)', ... },
  // /:productName -> 何にでもマッチ
  { path: '/:productName', ... },
]

(\\d+)\d は数値を意味し、+ は1つ以上を意味します。また、\ は文字列リテラルでは利用できないので、\\のようにバックスラッシュでエスケープします。

例えば、以下のようにルートを定義すると、(\\d{1,3}) は1〜3桁の数値を表すので、/user/009 や /user/12、 /user/3 にはマッチしますが、/user/foo や /user/1234 にはマッチしません。

{
  path: '/user/:uid(\\d{1,3})',
  name: 'user',
  component: () => import('../views/UserView.vue')
},

関連ページ:JavaScript の正規表現/ RegExp

繰り返しパラメータ(*)

/first/second/third のような複数セクションのルートにマッチさせるには、*+ を使います。

  • * :0回以上の値にマッチ
  • + :1回以上の値にマッチ
const routes = [
  // /:chapters ->  /one, /one/two, /one/two/three, などにマッチ(/ にはマッチしない)
  { path: '/:chapters+', ... },
  // /:chapters ->  /, /one, /one/two, /one/two/three,などにマッチ
  { path: '/:chapters*', ... },
]

例えば、/:chapters* の場合、/one/two/three にマッチし、$route.params.chapters の値は [ "one", "two", "three" ] になります。

*+ を指定した場合、/one/two/three のような複数のセクションからなるパスのパラメータの値は / で分割された結果が配列として返されます。

また、*+ は括弧で指定した正規表現の後に指定することもできます。

以下は数値のセクションの繰り返しのみにマッチします。

const routes = [
  //  /1, /1/2, /1/2/3, などにマッチ
  { path: '/:chapters(\\d+)+', ... },
  //  /, /1, /1/2, /1/2/3 などにマッチ
  { path: '/:chapters(\\d+)*', ... },
]

オプショナルパラメータ(?)

? を使うことでパラメーターをオプション(任意)とすることもできます。? は0回または1回の繰り返しを意味し、{0,1} と同じ意味です。

const routes = [
  //  /users や /users/posva にマッチ
  { path: '/users/:userId?' },
  //  /users や /users/42 にマッチ
  { path: '/users/:userId(\\d+)?' },
]

sensitive/strict オプション

デフォルトでは、すべてのルートは大文字と小文字を区別せず、また、末尾のスラッシュの有無に関わらずルートにマッチします。 例えば /users は、/users/users//Users/ にマッチします。

このデフォルトの動作は、sensitivestrict オプションを使用して設定できます。これらは、router レベル(全てのルートに適用)と route レベル(個々のルートに適用)の両方で設定できます。

  • sensitive : 大文字と小文字を区別するかどうか
  • strict : 末尾のスラッシュの有無を区別するかどうか

以下の場合、 4行目のルートは、/users/posva にマッチしますが、router レベルの strict: true により /users/posva/ にマッチせず、route レベルの sensitive: true により /Users/posva にもマッチしません。

5行目のルートは、router レベルの strict: true により /users/Users、および /users/42 にはマッチしますが、/users/ または /users/42/ にはマッチしません。

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/users/:id', sensitive: true, ... },
    { path: '/users/:id?', ... },
  ],
  strict: true, // 全てのルートに適用(router レベル)
})

ルートの優先順位

ルートはより明示的に指定されたものが優先して適用されます。

例えば、/users/:id/:product/:name の場合、固定値が入っている /users/:id の方がより明示的にパスを表しているので、優先順位が高くなります。

※ ルートの優先順位はルートの記述の順序には関係ありません。

ルートの定義が複雑になってきた場合は、Path Ranker ツールなどを利用して、ルートの優先順位を確認することができます。

また、Vue Devtools の Routes タブでもスコアを確認することができます。

名前付きルート(Named Routes)

createRouter メソッドでルーター(Router インスタンス)を生成する際に routes オプションの name プロパティでそのルートに名前を設定していれば、名前を使ってルートを特定することができます。

src/router/index.js 抜粋
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  //routes オプション
  routes: [
    ・・・中略・・・
    {
      path: '/about',
      name: 'about',  //name プロパティに名前を設定
      component: () => import('../views/AboutView.vue')
    },
    {
      path: '/user/:uid',
      name: 'user',  //name プロパティに名前を設定
      component: () => import('../views/UserView.vue'),
    },
  ],
})

名前を付けたルートにリンクするには、RouterLink コンポーネントの to プロパティ(属性)にオブジェクトを渡して、name プロパティと必要なプロパティを指定します。

以下の2行目の RouterLink の場合、上記ルーティングの path にパラメータ(:uid)を指定しているので、オブジェクトの name プロパティと params プロパティを指定します。

<RouterLink :to="{ name: 'about' }">About</RouterLink>
<RouterLink :to="{ name: 'user', params: { uid: '003' }}">User 003</RouterLink>

これは push() メソッドを呼び出すときに引数に渡すオブジェクトと同じです。

<button type="button" v-on:click="$router.push({ name: 'about' })">About</button>
<button
  type="button"
  v-on:click="$router.push({ name: 'user', params: { uid: '003' } })">
  User 003
</button>

上記の RouterLinkpush() メソッドのどちらの場合もルーターはそれぞれ /about と /user/003 のパスにナビゲーションします。

名前付きルートを利用すると、コンポーネントのパスが変更になった場合でも、コンポーネントで設定したそれぞれの RouterLinkpush() の設定は影響を受けないという利点があります(ルーティングの定義 routes/index.js の path の設定の変更のみで済みます)。

Named Routes

ネストされたルート(Nested Routes)

以下のようなネストしたコンポーネントを作成して、これに対してルートを設定することができます。

<RouterView> の中で描画されるコンポーネントも、コンポーネントを描画するための <RouterView> を持つ(入れ子にする)ことができます。

以下は App.vue のテンプレートです。ここの <RouterView /> はトップレベルの RouterView で、トップレベルのルートに対してマッチしたコンポーネントが描画されます。

そして、マッチして描画されたコンポーネントも、同様に(ネストされた)<RouterView /> を持つことができます。

src/App.vue 抜粋
<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" />
    <div class="wrapper">
      <HelloWorld msg="You did it!" />
      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
        <RouterLink to="/user/001">User 001</RouterLink>
      </nav>
    </div>
  </header>
  <RouterView /><!-- トップレベルの RouterView -->
</template>

以下は UserView コンポーネントのテンプレートに <RouterView /> を1つ追加した例です。

src/views/UserView.vue
<template>
  <div class="user">
    <h1>User : {{ $route.params.uid}}</h1>
    <RouterView /><!-- ネストされた RouterView -->
  </div>
</template>

このネストされた RouterView にコンポーネントを描画するには、ルーティングの設定(routes オプション)でネストされたルート(nested route)を設定する必要があります。

children オプションでネストされたルート(nested route)を設定

ネストされたルート(ここでは子ルートと呼ぶことにします)は children オプションを使用します。

children オプションは routes オプション同様、ルート設定オブジェクト(route オブジェクト)の配列で、子ルートを必要なだけ指定することができます。

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

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    ・・・中略・・・
    {
      path: '/user/:uid',
      name: 'user',
      component: () => import('../views/UserView.vue'),
      //children オプション(route オブジェクトの配列で指定)
      children: [
        {
          // /user/:uid/profile にマッチすると
          // UserProfile が UserView の RouterView に描画される
          path: 'profile',
          component:  () => import('../views/UserProfile.vue'),
        },
        {
          // /user/:uid/posts にマッチすると
          // UserPosts が UserView の RouterView に描画される
          path: 'posts',
          component:  () => import('../views/UserPosts.vue'),
        },
      ],
    },
  ],
})
export default router

子ルートの path は、先頭に / を付けずに親ルートからの相対パスとして指定します。上記の path: 'profile'/user/:uid/profile にマッチします。

先頭に / を付けると、親ルートからの相対パスではなく、絶対パスとみなされてしまいます。

子コンポーネント(ネストされるコンポーネント)を作成

以下は子ルートに指定した描画する子コンポーネント(UserProfile と UserPosts)の例です。

src/views/UserProfile.vue
<template>
  <div class="user-profile">
    <h2>User Profile</h2>
  </div>
</template>

<style>
.user-profile {
  margin: 20px 0;
  padding: 20px;
  border: 1px solid rgb(125, 161, 138);
  background-color: rgb(194, 248, 229);
}
</style>
src/views/UserPosts.vue
<template>
  <div class="user-posts">
    <h2>User Posts</h2>
  </div>
</template>

<style>
.user-posts {
  margin: 20px 0;
  padding: 20px;
  border: 1px solid rgb(211, 136, 213);
  background-color: rgb(252, 233, 247);
}
</style>

/user/001/profile にアクセスすると以下が表示されます。

/user/001/posts にアクセスすると以下が表示されます。

既定で描画するコンポーネント(空のネストされたパス)

この状態で、/user/001 にアクセスすると、いずれの子ルートもマッチしないので、以下のようにネストされたコンポーネントは描画されません。

空のネストされたパスを指定することで、既定の子ルートとして指定したコンポーネントを描画することができます。

以下では、既定で描画するネストされたコンポーネント UserHome のルートを追加しています。

また、以下の設定(状態)で親のルートの name プロパティを指定していると警告が表示されるため、コメントアウトして無効にしています(関連:ネストされた名前付きルート)。

src/router/index.js 抜粋
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    ・・・中略・・・
    {
      path: '/user/:uid',
      //name: 'user',  // この場合 name を指定していると警告が表示される
      component: () => import('../views/UserView.vue'),
      children: [
        //既定の子ルートを追加
        {
          //空のネストされたパスを指定
          path: '',
          component:  () => import('../views/UserHome.vue'),
        },
        {
          path: 'profile',
          component:  () => import('../views/UserProfile.vue'),
        },
        {
          path: 'posts',
          component:  () => import('../views/UserPosts.vue'),
        },
      ],
    },
  ],
})
src/views/UserHome.vue(既定で描画するネストされたコンポーネントを追加)
<template>
  <div class="user-home">
    <h2>User Home</h2>
  </div>
</template>

/user/001 にアクセスすると、以下のように UserHome が既定でネストされて描画されます。

ネストされた名前付きルート

以下のような親ルートが(name プロパティが指定されている)名前付きルートで、空のパスを指定したネストされたルートがあると、「user という名前のルートには、名前のない子と空のパスがあります。 その名前を使用しても空のパスの子はレンダリングされないため、代わりに名前を子に移動することをお勧めします。これが意図的なものである場合は、子ルートに名前を追加して警告を削除します。」のような警告がコンソールに出力されます。

src/router/index.js 抜粋
const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
      ・・・中略・・・
      {
        path: '/user/:uid',
        name: 'user',  // 警告が表示される
        component: () => import('../views/UserView.vue'),
        children: [
          {
            //空のネストされたパスを指定
            path: '',
            component:  () => import('../views/UserHome.vue'),
          },
          {
            path: 'profile',
            component:  () => import('../views/UserProfile.vue'),
          },
          {
            path: 'posts',
            component:  () => import('../views/UserPosts.vue'),
          },
        ],
      },
    ],
  })
  

この状態で、例えば以下のような name プロパティを使ったリンクをクリックしても UserHome は描画されません。

<RouterLink :to="{ name: 'user', params: { uid: '009' }}">User 009</RouterLink>

解決方法としては警告に表示されているように、親の name プロパティを子に移動します。これにより、上記の name プロパティを使ったリンクが機能します。

src/router/index.js 抜粋
const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
      ・・・中略・・・
      {
        path: '/user/:uid',
        component: () => import('../views/UserView.vue'),
        children: [
          {
            //空のネストされたパス
            path: '',
            name: 'user',  // 親の name プロパティを子に移動
            component:  () => import('../views/UserHome.vue'),
          },
          {
            path: 'profile',
            component:  () => import('../views/UserProfile.vue'),
          },
          {
            path: 'posts',
            component:  () => import('../views/UserPosts.vue'),
          },
        ],
      },
    ],
  })
  

または、以下のように親と子の両方のルートに name プロパティを指定すれば警告は表示されません。

但し、:to="{ name: 'user', params: { uid: '009' }}" でリンクすると、/user/009 にナビゲートされ、子コンポーネントの UserHome は描画されませんが、そのページで再読込すると、子コンポーネントの UserHome が描画されてしまいます。

{
  path: '/user/:uid',
  name: 'user', // name プロパティを指定
  component: () => import('../views/UserView.vue'),
  children: [
    {
      //空のネストされたパス
      path: '',
      name: 'user-home', // name プロパティを指定
      component:  () => import('../views/UserHome.vue'),
    },
    {
      path: 'profile',
      component:  () => import('../views/UserProfile.vue'),
    },
    {
      path: 'posts',
      component:  () => import('../views/UserPosts.vue'),
    },
  ],
},

名前付きビュー(Named Views)

ネストをさせずに同時に複数の RouterView を配置することで、複数のビュー(ルートにマッチするコンポーネントの描画)を表示することができます。

但し、それぞれのビューを区別するために、RouterViewname 属性を使って任意の名前を付ける必要があります(名前を指定しない RouterView はその name 属性の値として default が付与されます)。

また、ルートの定義(routes オプション)では複数のコンポーネントを割り当てられるように、呼び出されるコンポーネントの指定を component ではなく、components (複数形) オプションを使用します。

components オプションでは「名前:コンポーネント」の形式で指定します。

src/router/index.js
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',
      //components オプションに描画する複数のコンポーネントを指定してインポート
      components: {
        // この例では動的インポートを使用
        default: () => import('../views/AboutView.vue'), //名前のない RouterView
        sub: () => import('../views/FooView.vue'),
        footer: () => import('../views/BarView.vue')
      }
    },
    {
      path: '/user/:uid',
      name: 'user',
      //複数のコンポーネントを指定してインポート
      components: {
        default: () => import('../views/UserView.vue'), //名前のない RouterView
        sub: () => import('../views/BazView.vue')
      }
    },
  ],
})
export default router
src/App.vue
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" />
    <div class="wrapper">
      <HelloWorld msg="You did it!" />
      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
        <RouterLink to="/user/001">User 001</RouterLink>
        <RouterLink to="/user/009">User 009</RouterLink>
      </nav>
    </div>
  </header>
  <!-- RouterView に name 属性を使って名前を指定 -->
  <RouterView /> <!-- name 属性を指定しない RouterView の名前は default になる -->
  <RouterView name="sub" />
  <RouterView name="footer" />
</template>
src/views/AboutView.vue
<template>
  <div class="about">
    <h1>This is an about page </h1>
  </div>
</template>

以下は name="sub" の RouterView に描画されるコンポーネントの例です。

src/views/FooView.vue
<template>
  <div class="foo">
    <h2>This is Foo section. </h2>
  </div>
</template>

<style>
.foo {
  border: 1px solid rgb(139, 182, 148);
  margin: 50px 0 10px;
  padding: 30px;
  background-color: rgb(192, 248, 204);
  grid-column-start: 1;
  grid-column-end: 3;
}
</style>
src/views/BarView.vue(style は省略)
<template>
<div class="bar">
  <h2>This is Bar section. </h2>
</div>
</template>
src/views/BazView.vue(style は省略)
<template>
<div class="baz">
  <h2>This is Baz section. </h2>
</div>
</template>

例えば、/about にアクセスすると以下が表示されます。

ネストされた名前付きビュー

ネストされたビューを持つ名前付きビューを使用して複雑なレイアウトを作成することができます。

src/router/index.js
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',
      component: () => import('../views/AboutView.vue')
    },
    {
      path: '/user/:uid',
      name: 'user',
      component: () => import('../views/UserView.vue'),
      //ネストされたルート
      children: [
        {
          path: 'profile',
          name: 'user-profile',
          //ネストされた名前付きビュー
          components: {
            default:   () => import('../views/FooView.vue'),
            content: () => import('../views/UserProfile.vue'),
          }
        },
        {
          path: 'posts',
          name: 'user-posts',
          //ネストされた名前付きビュー
          components: {
            default:   () => import('../views/BarView.vue'),
            content: () => import('../views/UserPosts.vue'),
          }
        },
      ],
    },
  ],
})
export default router
src/App.vue
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" />
    <div class="wrapper">
      <HelloWorld msg="You did it!" />
      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
        <RouterLink to="/user/001">User 001</RouterLink>
        <RouterLink to="/user/009">User 009</RouterLink>
      </nav>
    </div>
  </header>
  <RouterView /><!-- トップレベルの RouterView -->
</template>
src/views/UserView.vue
<template>
  <div class="user">
    <h1>User : {{ $route.params.uid}}</h1>
    <RouterLink :to="`/user/${$route.params.uid}/profile`">Profile</RouterLink> |
    <RouterLink :to="`/user/${$route.params.uid}/posts`">Posts</RouterLink>
    <!-- ネストされた名前付きビュー -->
    <RouterView /><!-- default -->
    <RouterView name="content" />
  </div>
</template>

この例の場合、それぞれのルートに名前(name オプション)を付けているので、上記のリンクは以下のように記述することもできます。

<RouterLink :to="{ name: 'user-profile', params: { uid: $route.params.uid }}">
  Profile
</RouterLink> |
<RouterLink :to="{ name: 'user-posts', params: { uid: $route.params.uid }}">
  Posts
</RouterLink>
src/views/UserProfile.vue
<template>
  <div class="user-profile">
    <h2>User Profile</h2>
  </div>
</template>
src/views/UserPosts.vue
<template>
  <div class="user-posts">
    <h2>User Posts</h2>
  </div>
</template>
src/views/FooView.vue(style は省略)
<template>
    <div class="foo">
      <h2>This is Foo section. </h2>
    </div>
  </template>;
src/views/BarView.vue(style は省略)
<template>
  <div class="bar">
    <h2>This is Bar section. </h2>
  </div>
  </template>
  

パラメータを props として渡す

コンポーネントで $route を使用する(パラメータを $route オブジェクト経由で受け渡しする)と、特定の URL(ルート経由)でしかそのコンポーネントを使用できないため、ルート(route)との密結合が作成され、コンポーネントの柔軟性が制限されます。

これは必ずしも悪いことではありませんが、props オプションを使って、コンポーネントをルーターから切り離すことができます。

パラメータをコンポーネントの props に引き渡すには、ルーティングの定義で props オプションを追加し、コンポーネント側では対応する props を定義します。

これにより、コンポーネントをどこからでも使用できるようになり、コンポーネントの再利用とテストが容易になります。

Boolean モード

propstrue に設定すると、route.params がコンポーネントのプロパティとして設定されます。

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

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    ・・・中略・・・
    {
      path: '/user/:uid',
      name: 'user',
      component: () => import('../views/UserView.vue'),
      props: true  //props オプションを追加
    },
  ],
})
export default router

UserView コンポーネント側では、props を定義します。これにより、テンプレートでは $route.params.uid ではなく、uid でアクセスできるようになります。

src/views/UserView.vue
<script>
export default {
  // props を定義
  props: {
    uid: String
  }
}
</script>

<template>
  <div class="user">
    <h1>User : {{ uid }}</h1><!-- props の uid でアクセス -->
  </div>
</template>

script setup 構文では defineProps で定義します。

src/views/UserView.vue 抜粋
<script setup>
// script setup では defineProps で定義
const props = defineProps({
  uid: String
});
</script>

Object モード

props がオブジェクトの場合、コンポーネントプロパティとしてそのまま設定されます。固定値で props を指定する場合に便利です。

src/router/index.js 抜粋
{
  path: '/user/:uid',
  name: 'user',
  component: () => import('../views/UserView.vue'),
  props: { uid: 100 }  //オブジェクトとして固定値を渡す
}

この例の場合、ルートの定義で props: { uid: 100} のように数値(Number)として指定しているので、props も数値として定義します。この場合、URL に /user/001 でアクセスしても、uid は固定で 100 になります。

src/views/UserView.vue
<script setup>
const props = defineProps({
  uid: Number  // 数値に
});
</script>

<template>
  <div class="user">
    <h1>User : {{ uid }}</h1><!--  uid は数値の 100 で固定 -->
  </div>
</template>

Function モード

props を返す関数を作成することができます。これにより、パラメータの型を変換するなどが可能です。

$route.params の戻り値は文字列ですが、例えば、UserView コンポーネントが uid プロパティ(props)を数値(Number)として受け取る場合、以下のように props を返す関数を使うことで型を変換することができます。

props を返す関数は、引数として $route オブジェクトを受け取り、戻り値として props をオブジェクトで返します。また、オブジェクトを表す { } が関数ブロックとして認識されてしまうため、戻り値全体を括弧 ( ) で囲んでいます(オブジェクトリテラルの返却)。

src/router/index.js 抜粋
{
  path: '/user/:uid',
  name: 'user',
  component: () => import('../views/UserView.vue'),
  //props を返す関数で数値(Number)に変換
  props: routes => ({
    uid: Number(routes.params.uid)
  })
},
src/views/UserView.vue
<script setup>
const props = defineProps({
  uid: Number
});
</script>

<template>
  <div class="user">
    <h1>User : {{ uid }}</h1><!--  uid は数値(例: 009 は 9 に) -->
  </div>
</template>

名前付きビュー

名前付きビューを使用する場合、名前付きビューごとに props オプションを定義する必要があります。

以下は Vue Router ドキュメント(Passing Props to Route Components/Named views)から

const routes = [
  {
    path: '/user/:id',
    // 複数のコンポーネント(名前付きビュー)を指定(components は複数形)
    components: {
      default: User,  // default は名前のない RouterView
      sidebar: Sidebar
    },
    //名前付きビューごとに props オプションを定義(定義しなくても問題ない???)
    props: { default: true, sidebar: false }
  }
]

リダイレクト

あるパスがリクエストされたときに、redirect プロパティを使って異なるパスにリダイレクトすることができます。

以下は /home にアクセスがあった場合に / にリダイレクトする例です。

src/router/index.js 抜粋
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      // /home にアクセスがあった場合に / にリダイレクト
      path: '/home',
      redirect: '/',
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('../views/AboutView.vue')
    },
    {
      path: '/user/:uid',
      name: 'user',
      component: () => import('../views/UserView.vue'),
    },
  ],
})

redirect プロパティを指定する場合、レンダリングするコンポーネントがないため、component プロパティを省略できます(但し、そのルートに childrenredirect プロパティがある場合は、component プロパティも必要です)。

オブジェクトで指定

リダイレクト先をオブジェクトで指定することもできます。以下は、前述の例を name プロパティを指定したオブジェクトで書き換えた例です。

routes: [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/home',
    //オブジェクトで指定(上記ルートの name:'home' のパス '/' へリダイレクト)
    redirect: {name: 'home'},
  },
  ・・・
],

関数を使った動的なリダイレクト

関数は引数としてターゲットルート(リクエストされたルートのオブジェクト)を受け取り、リダイレクト先のパスを返します。

以下は /search/screens にアクセスされたら /search?q=screens にリダイレクトする例です。

to.params.searchText/search/screens の場合、screens になります。query はクエリ文字列(? 以降の文字列)の key=value を指定します。

const routes = [
  {
    path: '/search/:searchText',
    redirect: to => {
      // 引数の to はターゲットルートのオブジェクト
      // リダイレクト先のパスを返す
      return { path: '/search', query: { q: to.params.searchText } }
    },
  },
  {
    path: '/search',
    // ...
  },
]

Redirect and Alias

エイリアス

リダイレクトと似た仕組みにエイリアス(alias)があります。エイリアスは特定のパスに別のパスでもアクセスできるようにする仕組みです。alias を利用することで1つのルーティングに対して複数の URL を設定することができます。

以下は /about に対して /aboutus からでもアクセスできるようにし、/user/:uid に対して /u/:uid からでもアクセスできるようにエイリアスを設定する例です。

src/router/index.js
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',
      //エイリアスを設定
      alias: '/aboutus',
      component: () => import('../views/AboutView.vue')
    },
    {
      path: '/user/:uid',
      name: 'user',
      //エイリアスを設定
      alias: '/u/:uid',
      component: () => import('../views/UserView.vue'),
    },
  ],
})
export default router

※ SEO 的には、エイリアスを使う場合は重複コンテンツ対策が求められます。

meta フィールド

ルートを定義する際に meta プロパティ(Route Meta Fields)を含めることができます。

meta は、ルートに関する任意の情報を表すことができる route オブジェクトのプロパティで { key:value } の形式で指定し、任意の名前(key)と値(value)を指定することができます。

以下は /about のルートに meta プロパティ { myMeta: 'foo' } を設定して、その値をナビゲーションガードを使って出力する例です。

ナビゲーションガードの引数に渡される route オブジェクト(tofrom) のプロパティ(.meta)として、それらをコンソールに出力しています。

値は名前(key 名)を使って、この場合 to.meta.myMetafrom.meta.myMeta で取得できます。

この場合、/about にアクセスすると、beforeEachbeforeEnter によりそれぞれ以下が出力され、

  • beforeEach to.meta.myMeta: foo
  • beforeEach from.meta.myMeta: undefined
  • beforeEnter to.meta: {myMeta: 'foo'}
  • beforeEnter to.meta.myMeta: foo

/about から他のページに移動するとグローバルガードの beforeEach から以下が出力されます。

  • beforeEach to.meta.myMeta: undefined
  • beforeEach from.meta.myMeta: foo
src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    ・・・中略・・・
    {
      path: '/about',
      name: 'about',
      //meta プロパティを設定
      meta: { myMeta: 'foo' },
      component: () => import('../views/AboutView.vue'),
      // beforeEnter ナビゲーションガードでコンソールに出力
      beforeEnter: (to) => {
        console.log('beforeEnter to:', to.meta); //オブジェクト
        console.log('beforeEnter to.meta.myMeta:', to.meta.myMeta); //値
      },
    },
    ・・・中略・・・
  ],
});

// beforeEach グローバルナビゲーションガードでコンソールに出力
router.beforeEach((to, from) => {
  console.log('beforeEach to.meta.myMeta:',  to.meta.myMeta);
  console.log('beforeEach from.meta.myMeta:', from.meta.myMeta);
});
export default router

以下は meta プロパティを設定したルートのコンポーネント(AboutView.vue)で、route オブジェクトのプロパティとして出力する例です。

Composition API では、route オブジェクトにアクセスするには useRoute() を使います。テンプレート内では $route.meta でアクセスすることがでできます。

src/views/AboutView.vue
<script setup>
//useRoute メソッドをインポート
import { useRoute } from 'vue-router';
//route インスタンスを取得(Options API では this.$route でアクセス可能)
const route = useRoute();
//route オブジェクトのプロパティとして出力
console.log(route.meta);  // {myMeta: 'foo'} と出力される
console.log(route.meta.myMeta);   //foo と出力される
</script>

<template>
  <div class="about">
    <h1>This is an about page </h1>
    <p>{{ $route.meta.myMeta }}</p> <!-- foo -->
    <p>{{ route.meta.myMeta }}</p> <!-- foo -->
  </div>
</template>

以下は、requiresAuth という名前の「認証が必要かどうか」を表す meta プロパティを設定し、ナビゲーションガードを使って、requiresAuth の値と、すでに認証されているかどうかを表す変数 isLoggedIn の値によって、ページへのアクセスを制御する例です。

この場合、/user/:uid/posts のルートは meta: { requiresAuth: false } なのでリダイレクトされませんが、/user/:uid/profile のルートは、requiresAuth: true で、且つ isLoggedInfalse なので、37行目の return により /user/:uid にリダイレクトされます。

例えば、/user/003/posts にはアクセスできますが、/user/003/profile にアクセスしようとすると、/user/003 にリダイレクトされます。

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

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

    {
      path: '/user/:uid',
      name: 'user',
      component: () => import('../views/UserView.vue'),
      children: [
        {
          path: 'profile',
          // meta に requiresAuth: true を設定
          meta: { requiresAuth: true },
          component:  () => import('../views/UserProfile.vue'),
        },
        {
          path: 'posts',
          // meta に requiresAuth: false を設定
          meta: { requiresAuth: false },
          component:  () => import('../views/UserPosts.vue'),
        },
      ],
    },
  ],
});

//すでに認証されているかどうかを表す変数
const isLoggedIn = false;

//グローバルナビゲーションガード beforeEach
router.beforeEach((to) => {
  // meta の requiresAuth が true で、isLoggedIn が false の場合はリダイレクト
  if (to.meta.requiresAuth && !isLoggedIn) {
    return { path: `/user/${to.params.uid}` };
  }
});
export default router

アニメーションの適用

RouterViewRouterLink と同様、スロット機能(スコープ付きスロット)に対応しています。

<RouterView> をスロットを使って書き換えると、以下のように記述することができます。

<RouterView v-slot:default="slotProps">
  <component v-bind:is="slotProps.Component"></component>
</RouterView>

スロットプロパティに分割代入を使い、デフォルトスロットしかないので v-slot の省略記法を使うと以下のようにも記述できます(v-bind も省略形を使い、タグも閉じタグを省略する記法にしています)。

スロットに渡すコンテンツは、<component> 要素の is 属性にスロットプロパティから取得した Component を指定して動的に表示します。

<RouterView v-slot="{ Component }">
  <component :is="Component" />
</RouterView>

スロットプロパティを受け取る v-slot 属性には以下のプロパティを持つオブジェクトが渡されます。

プロパティ 説明
Component 現在のルートで描画するコンポーネント
route 現在のルートの情報(route オブジェクト)$route のプロパティ

以下はスロットプロパティの内容をコンソールに出力するボタンを App.vue に追加して確認する例です。

src/App.vue
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
//コンソールに出力するメソッドの定義
const log = (val) => {
  console.log(val);
}
</script>

<template>
  <header>
  ・・・中略・・・
  </header>

  <RouterView v-slot="slotProps">
    <component :is="slotProps.Component"></component>
    <button @click="log(slotProps)">Check</button>
  </RouterView>
</template>

Check ボタンをクリックすると、以下のようにスロットプロパティの内容をコンソールで確認できます。

アニメーションを適用

<RouterView> 要素を <transition> 要素で囲むことで、ルーティングの際に描画するコンテンツにアニメーション(transition)を適用することができます。

この例ではアニメーションの際にカクカクしないように、トランジションモード mode="out-in" を指定しています。

src/App.vue 抜粋
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <header>
 ・・・中略・・・
  </header>

  <!-- アニメーションを適用 -->
  <RouterView v-slot="{ Component }">
    <transition mode="out-in" >
      <component :is="Component" />
    </transition>
  </RouterView>
</template>

<style scoped>
/* アニメーションの CSS(デフォルトのトランジションクラス) */
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0.1;
}
・・・中略・・・
</style>

transition 要素に name 属性を指定

transition 要素に name 属性を指定して、デフォルトのトランジションクラスのクラス名(.v-enter-active などの v の部分)をカスタマイズできます。

<RouterView v-slot="{ Component }">
  <transition mode="out-in" name="fade-in">
    <component :is="Component" />
  </transition>
</RouterView>

上記の場合、name 属性に fade-in を指定しているので、トランジションクラスのクラス名は fade-in-xxxx になります。

<style scoped>
/* アニメーションの CSS(name 属性が fade-in のトランジションクラス) */
.fade-in-enter-active,
.fade-in-leave-active {
  transition: opacity 0.5s ease;
}

.fade-in-enter-from,
.fade-in-leave-to {
  opacity: 0.1;
}
・・・中略・・・
</style>

再利用されるコンポーネントへのアニメーションの適用

Vue は、類似したコンポーネントを自動的に再利用するため、パラメータを使った動的ルーティングなどのページ遷移では、アニメーションがトリガーされません。

そのような場合にアニメーションを強制的にトリガーするには、<component> 要素に key 属性を追加します。key 属性の値には、スロットプロパティから取得した routepath を利用することができます。

<RouterView v-slot="{ Component, route }">
  <transition mode="out-in" name="fade-in">
    <component :is="Component" :key="route.path"/>
  </transition>
</RouterView>

上記のように設定すると、例えば、/user/001 から /user/007 へのような遷移でもアニメーションが適用されます。

ルート単位でのアニメーション

これまでの例の場合、全てのルートに対して同じアニメーションを適用しますが、ルートにより異なるアニメーションを適用することもできます。

ルートにより異なるアニメーションを適用するには、各ルートの meta フィールドにアニメーションの種類を表す文字列を設定し、その値を使って transition 要素の name 属性を動的に変更してアニメーションを切り替えます。

以下の例では、meta プロパティに animation というキーにアニメーションの種類を表す文字列を設定しています。この例ではキー名を animation としていますが、任意の名前を付けられます。

src/router/index.js
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',
      // meta プロパティにアニメーションの種類を表す文字列を設定
      meta: { animation: 'slide-in' },
      component: HomeView
    },
    {
      path: '/about',
      name: 'about',
      // meta プロパティ
      meta: { animation: 'fade-in' },
      component: () => import('../views/AboutView.vue'),
    },
    {
      path: '/user/:uid',
      name: 'user',
      // meta プロパティ
      meta: { animation: 'rotate' },
      component: () => import('../views/UserView.vue'),
    },
  ],
});
export default router

transition 要素の name 属性に、meta プロパティの値(route.meta.animation)を指定すれば、ルートによりアニメーションが動的に切り替わります。

以下では、ルートに meta プロパティが設定されていない場合は、fade-in のアニメーションを適用するように指定しています。

そして CSS で meta プロパティに指定した名前を使ったトランジションクラスを使ってアニメーションを定義します。

src/App.vue
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <header>
  ・・・中略・・・
  </header>

  <RouterView v-slot="{ Component, route }">
    <transition mode="out-in" v-bind:name="route.meta.animation || 'fade-in'">
      <component :is="Component" :key="route.path"/>
    </transition>
  </RouterView>

</template>

<style scoped>
/* fade-in */
.fade-in-enter-active,
.fade-in-leave-active {
  transition: opacity 0.5s ease;
}
.fade-in-enter-from,
.fade-in-leave-to {
  opacity: 0.1;
}
/* rotate */
@keyframes rotate {
  0% {
    transform: rotate(360deg);
  }
  10% {
    transform: rotate(160deg);
  }
  100% {
    transform: rotate(0deg);
  }
}
.rotate-enter-active {
  animation : rotate 0.5s;
}
/* slide-in */
@keyframes slide-in {
  0% {
    transform: translateX(100%);
  }
  10% {
    transform: translateX(30%);
  }
  100% {
    transform: translateX(0);
  }
}
.slide-in-enter-active {
  animation : slide-in 0.5s;
}
.slide-in-leave-active {
  animation : slide-in 0.2s reverse;
}
・・・中略・・・
</style>

Transitions

ルーティング時のスクロールの動作

createRouter メソッドでルーターインスタンスを生成する際に scrollBehavior オプションを使って、ページ移動(遷移)時のスクロールの動作を指定することができます。

const router = createRouter({
  history: createWebHashHistory(import.meta.env.BASE_URL),
  routes: [
    ・・・中略・・・
  ],

  //scrollBehavior オプション
  scrollBehavior (to, from, savedPosition) {
    // 遷移後のスクロール位置を返す
  }
})

scrollBehavior は以下の引数を取り、戻り値として遷移後のスクロール位置(を表すオブジェクト)を返します。

引数 説明
to 遷移先のルート情報(route オブジェクト
from 遷移元のルート情報(route オブジェクト)
savedPosition 前回のスクロール位置を表すオブジェクト。popstate ナビゲーション (ブラウザの戻る/進むボタンが使用された) 時のみ利用可能。

スクロール位置を表すオブジェクトは以下のようなプロパティを使って指定することができます。

プロパティ 説明
top left top:トップからの垂直軸上のピクセル、left:左端からの水平軸上のピクセル。el と一緒に使用する場合は、その要素からの相対距離。例 { top: 0 }
el CSS セレクタや DOM 要素。例 el: '#main'el: to.hash
savedPosition 前回のスクロール位置を表すオブジェクト
behavior スムーススクロールには smooth を指定

以下は、savedPosition が存在する場合(「戻る」を利用した場合)はその位置に移動し、そうでなければ、ハッシュ # がある場合はハッシュで指定された要素の位置へ、なければトップへ移動する例です。

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

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
  ・・・中略・・・
  ],
  scrollBehavior(to, from, savedPosition) {
    //前回のスクロール位置があれば
    if (savedPosition) {
      //その位置を保持
      return savedPosition;
    } else {
      //ハッシュがある場合は
      if (to.hash){
        return {
          //ハッシュで指定された要素の位置へ
          el: to.hash,
          //スムーススクロール
          behavior: 'smooth',
        }
        //上記以外は先頭へ
      } else {
        return { top: 0 }
      }
    }
  }
});
export default router

スクロールを遅延させる

例えば、ルーティングの際にアニメーションを適用する場合では、スクロールする前にアニメーションが終了するのを待ちたいかもしれません。

スクロールを遅延させるには、スクロール位置を返す Promise を返します。

以下はスクロールする前に500ミリ秒待機する例です。

scrollBehavior(to, from, savedPosition) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ left: 0, top: 0 })
    }, 500)
  })
},

Scroll Behavior