Single page Application

シングルページアプリケーション

前回の単一ファイルコンポーネント(SFC)を用いてアプリケーションを構築する具体的な方法として、シングルページアプリケーション(SPA)を紐解いていく。

SPAは1つのhtmlファイル上で画面遷移せずにコンテンツの切り替えができるWebアプリケーションの構成スタイルの1つ。
ブラウザに最初に表示されるhtmlファイルに対し、ユーザーの操作に応じてJavaScriptで画面の更新に必要なものだけが部分的に描画される。そのため通常のWebページのようにページ全体を読み込む必要がなく、ユーザー体験がスムーズという利点を持つ。

Vue Router

SPAでは、ページ全体のリロードをせずにURLに応じて中身だけを切り替える。このとき「どのコンポーネントを表示するか」を判断する役割を担うのが、「Vue Router」によるルーティング機能である。

ルーティング機能はSPA構築を行うにあたって必要不可欠な要素であり、Vue.jsの開発環境を構築した後に、Vueの公式拡張ライブラリ「Vue Router」を導入することで、URLに応じたコンポーネントの切り替えが可能となり、SPAとしてのページ遷移が実現できるようになる。

Vue Router のインストール

npm install vue-router@4

Vue 3 用はvue-router@4を使用する。

インストール後は、前回作った作業環境のsrc配下にviewsrouterディレクトリと、それぞれ以下のファイルを追加。

src/
├── views/
│   ├── Home.vue
│   └── About.vue
├── router/
│   └── index.ts
├── App.vue
└── main.ts

ルーター設定ファイル

ルーティングの設定と制御をまとめて管理する仕組み(オブジェクト)のことをルーターと呼び、その定義を次の/src/router/index.tsで行う。

import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

<import>

一、二行目のimportは、設定に必要な関数と型をインポート。
三、四行目でHomeページ用とAboutページ用コンポーネント(<template>, <script>, <style>を含む.vueファイル)をそれぞれインポートしている。

routes

routes配列は、すべてのルート(URLとその表示コンポーネントを結びつける情報)をまとめたリストで、中のオブジェクトが1つの「ルート」。TypeScriptを使用するときはRouteRecordRaw[]で型注釈をする。

プロパティ 役割
path URLのパス(例:/about
name ルートに付ける名前(例:About
component 表示する Vue コンポーネント(例:About.vue

必須なのはpathcomponentのみ。nameは必須でないが、つけておくとパスを直書きすることなく名前でそのコンポーネントに遷移できるようになる。特に、動的ルートやプログラムによる画面遷移を行う予定があるなら、最初からnameを付けるのがベストプラクティス。この他にも多数プロパティがあるので必要になったら検索。

router

createRouter(...)は、アプリケーションにルーティング機能を追加するためのルーターを作成する関数。引数として渡すオブジェクトに、ルーターの動作設定を記述する。

createRouter({
  history: createWebHistory(),  // ルーターの履歴モードを指定
  routes: routes               // 先に定義したroutes(ルート)を渡す(routesだけでも可)
})
historyオプション
メソッド URLの見え方 説明
createWebHistory() /about HTML5 History API を使う(一般的でSEOに強い)
createWebHashHistory() /#/about ハッシュベースのURL(古いブラウザや静的サーバー向け)
createMemoryHistory() 状態のみ(URLなし) 主にテスト用・SSRで使われる

export

外部でルーターを使えるようにするためにexportする。これは「デフォルトエクスポート」と「名前付きエクスポート」2つの方法があるが基本はデフォルトで行う。

const router = createRouter({ ... })
export default router
export const router = createRouter({ ... }) // 名前付きexport

ルーターを登録

/src/main.tsに以下のように追記。

import { createApp } from 'vue'
import App from './App.vue'
import router from './router' ← ここ追加

createApp(App)
.use(router) ← ここ追加
.mount('#app')

ルーターが/src/router/index.tsで名前付きエクスポートされていたらimportは以下に変更

import { router } from './router'

ルートごとに表示を切り替える

テンプレート<router-view />を設置。これが「ページの切り替え部分」になる。ボタンのクリックイベントや、処理完了後に自動でページを遷移したいときなど、何かしらの処理を挟んで遷移したい場合は、JavaScriptのコード内からページ遷移を行うためのメソッドrouter.push()を使用する。

ちなみに遷移先(表示されるコンポーネント)は/src/router/index.tsで設定した/src/views/配下の.vueファイルになる。

<template>
  <div id="app">
    <h1>My Vue App</h1>
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </nav>
    <router-view />
  </div>
</template>
<!-- src/views/Home.vue -->
<script setup>
import { useRouter } from 'vue-router';

const router = useRouter();

function goToAbout() {
  router.push({ name: 'about' });
}
</script>

<template>
  <div>
    <h2>Home Page</h2>
    <button @click="goToAbout">Aboutへ移動</button>
  </div>
</template>
<!-- src/views/About.vue -->
<template>
  <div>
    <h2>About Page</h2>
  </div>
</template>

<router-link>

「他のページ(ルート)に移動するリンク」を作るためのコンポーネント。

<!-- パスで指定 -->
<router-link to="/about">About</router-link>

<!-- 名前付きルートで指定 -->
<router-link :to="{ name: 'about' }">About</router-link>

<!-- 動的パラメータ付き -->
<router-link :to="{ name: 'user', params: { id: 123 } }">ユーザー詳細</router-link>

現在のルートにマッチしているときに追加されるクラス名を指定できるactive-classなど専用の属性もある。

<router-link to="/home" active-class="is-active">Home</router-link>

router.push()

JavaScriptのコード内からページ遷移を行うためのメソッド。

// 名前付きルートへの遷移
router.push({ name: 'about' })

// パス指定で遷移
router.push('/about')

// 動的ルート + パラメータ
router.push({ name: 'user', params: { id: 42 } })

状態管理(Pinia)

状態管理とは、データをグローバルな状態として、どこからでも使えて、保存や編集できるようにする方法である。

Vueでは、refreactiveを使って、データ(状態)を保持できるが、これはローカル(コンポーネント内または親子間)な範囲に限られている。そのため、先のルーターを使ってページの切り替えなどをする場合はVue 3向けの公式状態管理ライブラリ「Pinia」を使うことで、どのコンポーネントからでもアクセス・更新可能な状態管理が実現できる。

Pinia のインストールと登録

npm install pinia

まずはインストール。その後、/src/main.tsに以下のように追記。

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia' ← ここ追加

createApp(App)
.use(router)
.use(createPinia()) ← ここ追加
.mount('#app')

ストアを作成

Piniaでは、「ストア(store)」単位で状態を定義する。イメージ的にはストアという箱の中に、アプリ全体で扱いたいデータや、その操作方法(メソッドなど)を保存しておき、必要なときにそれを取り出して使用する感じ。ストアの格納場所は/src/stores/が一般的で、ファイル名は端的に中身が分かるものにする。

<!-- /src/stores/counter.ts -->
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  // ストアの中身を定義するオブジェクト
  state: () => ({ ... }),  // 状態(データ)
  getters: { ... },        // 算出プロパティ(オプション)
  actions: { ... }         // メソッド(オプション)
})
  • useCounterStoreは、ストアを使うための関数名。「use + ストア名 + Store」という名前を付けるのが一般的。
  • defineStore()の第一引数('counter')は、ストアのID。Piniaが内部でこのストアを識別するためのユニークな名前(アプリ内で重複しない)。
  • defineStore()の第二引数はストアの中身を定義するオブジェクト。

オブジェクトスタイルでストアを定義する

ストアの中身をstategettersactionsというプロパティを持つオブジェクトで定義する方法。

export const useCounterStore = defineStore('counter', {
  // 状態(カウント値を管理)
  state: () => ({
    count: 0,
    name: 'Vue学習中'
  }),

  // ゲッター(状態を加工した値)
  getters: {
    doubleCount: (state) => state.count * 2
  },

  // アクション(状態を変更する関数)
  actions: {
    increment() {
      this.count++
    },
    reset() {
      this.count = 0
    }
  }
})
第2引数に設定できる主なプロパティ
state 保持したい「状態(データ)」を返す関数として定義する。
getters 計算された状態。Vueの算出プロパティのようなもの。
actions ストアの状態を変更する関数。

setupスタイルでストアを定義する

setupスタイルは、defineStore()の第2引数は「関数」の戻り値として、Composition APIのsetup()と同じように、ref()reactive()で状態を、関数としてアクションを定義し、returnでまとめて返す。

export const useCounterStore = defineStore('counter', {
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0);
  const name = ref('Vue学習中');

  const doubleCount = computed(() => count.value * 2);

  function increment() {
    count.value++;
  }

  function reset() {
    count.value = 0;
  }

  return {
    count,
    name,
    doubleCount,
    increment,
    reset
  }
})

コンポーネントでストアを使う

オブジェクトスタイルと、setupスタイルどちらを使ってもコンポーネントでの使い方は同じ。どちらのスタイルを使うかは自由だが、アプリ内でスタイルは統一したほうが良い。

<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore();
</script>

<template>
  <p>Count: {{ counter.count }}</p>
  <p>Double: {{ counter.doubleCount }}</p>
  <button @click="counter.increment">+1</button>
</template>

補足

ルーターを実装している途中、/src/router/index.tsで以下のようなエラーが出た。

モジュール '../views/Home.vue' またはそれに対応する型宣言が見つかりません。

これは、Vue + TypeScript プロジェクトで .vueファイルを TypeScript が認識できていないことが原因で出るエラーらしい。
これを解消するには、TypeScript に「.vue ファイルはVueコンポーネントですよ」と教える型定義ファイルが必要。プロジェクト直下(srcの外)にenv.d.tsを作成し以下を記述する(環境構築の際に自動で生成されることもある)。

declare module '*.vue' {
  import { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}