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配下にviewsとrouterディレクトリと、それぞれ以下のファイルを追加。
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) |
必須なのはpathとcomponentのみ。nameは必須でないが、つけておくとパスを直書きすることなく名前でそのコンポーネントに遷移できるようになる。特に、動的ルートやプログラムによる画面遷移を行う予定があるなら、最初からnameを付けるのがベストプラクティス。この他にも多数プロパティがあるので必要になったら検索。
router
createRouter(...)は、アプリケーションにルーティング機能を追加するためのルーターを作成する関数。引数として渡すオブジェクトに、ルーターの動作設定を記述する。
createRouter({
history: createWebHistory(), // ルーターの履歴モードを指定
routes: routes // 先に定義したroutes(ルート)を渡す(routesだけでも可)
})
| メソッド | 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では、refやreactiveを使って、データ(状態)を保持できるが、これはローカル(コンポーネント内または親子間)な範囲に限られている。そのため、先のルーターを使ってページの切り替えなどをする場合は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()の第二引数はストアの中身を定義するオブジェクト。
オブジェクトスタイルでストアを定義する
ストアの中身をstate、getters、actionsというプロパティを持つオブジェクトで定義する方法。
export const useCounterStore = defineStore('counter', {
// 状態(カウント値を管理)
state: () => ({
count: 0,
name: 'Vue学習中'
}),
// ゲッター(状態を加工した値)
getters: {
doubleCount: (state) => state.count * 2
},
// アクション(状態を変更する関数)
actions: {
increment() {
this.count++
},
reset() {
this.count = 0
}
}
})
statet>
| 保持したい「状態(データ)」を返す関数として定義する。 |
|---|---|
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
}