【Vue + Viteで始めるSPA開発入門】HTMLをコンポーネントに変換するまで

# はじめに
この記事ではVueを用いて、静的なHTMLをもとにSPA化および共通部品化を行う手順を示します。

## Vueとは
Webアプリのフロントエンドフレームワークで、HTML内にフレームワーク独自のタグやスクリプトを記述した「コンポーネント」を作成することで、部品の再利用を促すことができるものです。
Reactに比べ学習コストが低い（らしい）点を、ChatGPTが推していました。

# この記事でやらないこと、説明しないこと
* CSSなどを用いた画面の装飾
* アクセシビリティ対応
* Vueのリアクティビティーを用いた実装
* SPAの説明

# SPA化 ＆ Vue使用前のHTML
今回Vueで実装する画面のモックは以下の通りです。
```html:index.html
<html>
    <head>
        <meta charset="UTF-8">
        <title>Sample</title>
    </head>
    <body>
        <h1>お届け先</h1>
        <div>
            <label>お名前</label>
            <div>
                <label for="family-name">姓</label>
                <input type="text" id="family-name" name="family-name" autocomplete="family-name" placeholder="姓">
                <label for="given-name">名</label>
                <input type="text" id="given-name" name="given-name" autocomplete="given-name" placeholder="名">
            </div>
        </div>
        <br/>
        <div>
            <label>お届け先住所</label>
            <div>
                <div>
                    <label for="postal-code">郵便番号</label>
                    <input type="text" id="postal-code" name="postal-code" autocomplete="postal-code" placeholder="郵便番号">
                </div>
                <div>
                    <label for="address-line1">都道府県</label>
                    <input type="text" id="address-line1" name="address-line1" autocomplete="address-line1" placeholder="都道府県">
                </div>
                <div>
                    <label for="address-line2">市区町村</label>
                    <input type="text" id="address-line2" name="address-line2" autocomplete="address-line2" placeholder="市区町村">
                </div>
                <div>
                    <label for="address-line3">番地</label>
                    <input type="text" id="address-line3" name="address-line3" autocomplete="address-line3" placeholder="番地">
                </div>
            </div>
        </div>
        <br/>
        <button type="button" id="next-button" onclick="location.href='./bill.html'">次へ</button>
    </body>
</html>
```
```html:bill.html
<html>
    <head>
        <meta charset="UTF-8">
        <title>Sample</title>
    </head>
    <body>
        <h1>請求先</h1>
        <div>
            <label>お名前</label>
            <div>
                <label for="family-name">姓</label>
                <input type="text" id="family-name" name="family-name" autocomplete="family-name" placeholder="姓">
                <label for="given-name">名</label>
                <input type="text" id="given-name" name="given-name" autocomplete="given-name" placeholder="名">
            </div>
        </div>
        <br/>
        <div>
            <label>請求書送付先住所</label>
            <div>
                <div>
                    <label for="postal-code">郵便番号</label>
                    <input type="text" id="postal-code" name="postal-code" autocomplete="postal-code" placeholder="郵便番号">
                </div>
                <div>
                    <label for="address-line1">都道府県</label>
                    <input type="text" id="address-line1" name="address-line1" autocomplete="address-line1" placeholder="都道府県">
                </div>
                <div>
                    <label for="address-line2">市区町村</label>
                    <input type="text" id="address-line2" name="address-line2" autocomplete="address-line2" placeholder="市区町村">
                </div>
                <div>
                    <label for="address-line3">番地</label>
                    <input type="text" id="address-line3" name="address-line3" autocomplete="address-line3" placeholder="番地">
                </div>
            </div>
        </div>
        <br/>
        <button type="button" id="back-button" onclick="history.back()">戻る</button>
        <button type="button" id="submit-button">登録</button>
    </body>
</html>
```
同一の入力フォームを持つ2画面を、Vueでコンポーネント化していきます。

# 実行環境情報
|項目|バージョン|
|:--|--:|
|OS|macOS Sequoia 15.4.1|
|Node.js|v23.11.0|
|npm|10.9.2|
|Vite|6.3.5|
|Vue|3.5.13|
|Vue Router|4.5.1|

# 前提条件
* Node.jsをインストール済みであること

# Vue使用前の事前準備

[公式のクイックスタート](https://ja.vuejs.org/guide/quick-start.html)では`npm create vue@latest`コマンドでプロジェクトを作成するよう記載されていますが、公式でも謳われている通り「Vite」の存在が基盤にあることを明示的に理解するため、**あえて公式手順とは異なる方法で進行**します。

## Viteとは
フロントエンド開発体験を向上させることを目的とした、高速で動作するローカル開発サーバーです。
ソースの変更を監視して即時ビルドするため、デバッグやコード修正時にリアルタイムで画面を見ながら動作を確認できます。
本記事ではVueを使用しますが、Reactでも使えるライブラリです。

## Viteプロジェクト作成
Vueでの開発着手前に、まずは以下のコマンドでプロジェクトを作成します。
```shell:zsh
npm create vite@latest
```
コマンド実行中に色々問われるので、それぞれ以下を入力・選択します。
* Ok to proceed? : y
* Project name : UseVue
* Package name : usevue
* Select a framework : Vue
* Select a variant : JavaScript

実際のコンソールは以下の通り。
```plaintext:zsh
Need to install the following packages:
create-vite@6.5.0
Ok to proceed? (y) y


> npx
> create-vite

│
◇  Project name:
│  UseVue
│
◇  Package name:
│  usevue
│
◇  Select a framework:
│  Vue
│
◇  Select a variant:
│  JavaScript
│
◇  Scaffolding project in /Users/yoda//UseVue...
│
└  Done. Now run:

  cd UseVue
  npm install
  npm run dev

npm notice
npm notice New major version of npm available! 10.9.2 -> 11.3.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.3.0
npm notice To update run: npm install -g npm@11.3.0
npm notice
```

コマンド実行後のファイル構成は以下のようになります。
```plaintext
.
├── NotUseVue
│   ├── bill.html
│   └── index.html
└── UseVue
    ├── index.html
    ├── package.json
    ├── public
    │   └── vite.svg
    ├── README.md
    ├── src
    │   ├── App.vue
    │   ├── assets
    │   │   └── vue.svg
    │   ├── components
    │   │   └── HelloWorld.vue
    │   ├── main.js
    │   └── style.css
    └── vite.config.js

7 directories, 12 files
```

## 依存パッケージのインストール
プロジェクト作成時点では、依存するパッケージがダウンロードされていないので、以下のコマンドを実行します。
```shell:zsh
cd UseVue 
npm install
```
ダウンロード完了後、`ls`コマンドを実行して`node_modules`ディレクトリが作成されていることを確認します。
```shell:zsh
ls
index.html		package-lock.json	public			src
node_modules	package.json		README.md		vite.config.js
```

## DRY RUN

正しくプロジェクトに必要な一式が揃ったかを確認するため、以下のコマンドで開発サーバーを起動します。
```shell:zsh
npm run dev
```
コマンド実行後、コンソールに出力されるURLへアクセスし、下図のような画面が表示されることを確認します。
![スクリーンショット 2025-05-13 11.17.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3551450/81406d86-a9ef-4bfe-81ee-23af0d0615a7.png)
確認できたら、コンソールで`control + C`を押してサーバーを停止します。

## Vue Routerのインストール
VueではSPAのルーティング制御[^1]を行う場合、Vue Routerを利用すると便利なので、以下のコマンドでインストールします。
```shell:zsh
npm install vue-router
```

# 実装
あとは淡々と実装を進めます。

## 共通部品作成
まずは、共通利用されている入力要素をコンポーネント化します。`<template>`タグでHTMLを書き出す点がポイントです。
なお、本記事では触れませんがJavaScriptやスタイルを同一ファイル内に記述する[「単一ファイルコンポーネント」](https://ja.vuejs.org/guide/introduction.html#single-file-components)も作成できます。
```html:src/components/InputName.vue
<template>
    <div>
        <label>お名前</label>
        <div>
            <label for="family-name">姓</label>
            <input type="text" id="family-name" name="family-name" autocomplete="family-name" placeholder="姓">
            <label for="given-name">名</label>
            <input type="text" id="given-name" name="given-name" autocomplete="given-name" placeholder="名">
        </div>
    </div>
</template>
```
```html:src/components/InputAddress.vue
<template>
    <div>
        <div>
            <label for="postal-code">郵便番号</label>
            <input type="text" id="postal-code" name="postal-code" autocomplete="postal-code" placeholder="郵便番号">
        </div>
        <div>
            <label for="address-line1">都道府県</label>
            <input type="text" id="address-line1" name="address-line1" autocomplete="address-line1" placeholder="都道府県">
        </div>
        <div>
            <label for="address-line2">市区町村</label>
            <input type="text" id="address-line2" name="address-line2" autocomplete="address-line2" placeholder="市区町村">
        </div>
        <div>
            <label for="address-line3">番地</label>
            <input type="text" id="address-line3" name="address-line3" autocomplete="address-line3" placeholder="番地">
        </div>
    </div>
</template>
```

## 画面作成
続いて、親となる画面を作成します。これも`<template>`タグで書き出すところは先ほどのコンポーネントと変わりありませんが、独自の`<inputName>`タグを用い、`<script>`内でコンポーネントとして設定している点がポイントです。
```html:src/views/Destination.vue
<template>
    <h1>お届け先</h1>
    <inputName></inputName>
    <br/>
    <div>
        <label>お届け先住所</label>
        <inputAddress></inputAddress>
    </div>
    <br/>
    <button type="button" id="next-button" @click="() => $router.push({path: './Bill'})">次へ</button>
</template>

<script>
    import inputName from '../components/InputName.vue'
    import inputAddress from '../components/InputAddress.vue'
    export default {
        components: {
            inputName,
            inputAddress
        }
    }
</script>
```
```html:src/views/Bill.vue
<template>
    <h1>請求先</h1>
    <inputName></inputName>
    <br/>
    <label>請求書送付先住所</label>
    <inputAddress></inputAddress>
    <br/>
    <button type="button" id="back-button" @click="() => $router.push({path: './'})">戻る</button>
    <button type="button" id="submit-button">登録</button>
</template>

<script>
    import inputName from '../components/InputName.vue'
    import inputAddress from '../components/InputAddress.vue'
    export default {
        components: {
            inputName,
            inputAddress
        }
    }
</script>
```
:::note
`@click`属性はイベントハンドラの省略記法です。
本記事ではボタンクリック時に実行するスクリプトをインラインで実装しています。
:::

## ルーティング設定追加
次に、ルーティング[^1]の設定を実装します。
```javascript:src/router.js
import { createRouter, createWebHistory } from 'vue-router'
import Bill from './views/Bill.vue'
import Destination from './views/Destination.vue'

export const router = createRouter({
    history: createWebHistory(),
    routes: [
        {
            path: '/',
            component: Destination
        },
        {
            path: '/bill',
            component: Bill
        }
    ]
})
```

## メイン実装修正
プロジェクト作成時、自動的に作成される`App.vue`と、`main.js`、`index.html`を以下のように変更します。
```html:src/App.vue
<template>
  <router-view/>
</template>

<script>
export default {
  name: 'App'
}
</script>
```
```javascript:src/main.js
import { createApp } from 'vue'
import { router } from './router.js'
import App from './App.vue'

const app = createApp(App)
app.use(router)
app.mount('#app')
```
```html:index.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue Sample</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
```

## ファイル構成
実装後のファイル構成は以下のようになります。
```plaintext:zsh
├── index.html
└── src
    ├── App.vue
    ├── assets
    │   └── vue.svg
    ├── components
    │   ├── InputAddress.vue
    │   └── InputName.vue
    ├── main.js
    ├── router.js
    └── views
        ├── Bill.vue
        └── Destination.vue
```
:::note warn
node_modulesディレクトリなど、一部省略しています。
:::

# 動作確認
実装が完了したので、再びコマンドラインからアプリを起動します。
```shell:zsh
npm run dev
```

## 実際の画面
ブラウザで開発サーバーにアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3551450/2806bd58-d6bf-4213-902f-32454700683f.png)
きちんと入力フォームが画面に表示できています。
「次へ」ボタンを押すと...
![スクリーンショット 2025-05-13 16.14.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3551450/4184639e-4c5d-4c3e-bf5b-6d7b2dc265ed.png)
画面とURLの両方が切り替わりました。これでSPA化、コンポーネント化は完了です :exclamation:

# 終わりに
ざっくりと使ってみた感触では、PHPのIncludeと似た感じに思えました。
ルーティング設定で少しつまずいたところもありましたが、ChatGPTの言っていた通り、「実装するだけであれば」、比較的容易に習得できそうです。
公式手順に従うと、ユニットテストやE2Eテストのライブラリも初回プロジェクト作成時に導入可能に見えるので、それらテストの実装難度は今後改めて確認してみたいところです。

## 今回実装したコード等
以下GitHubに置いてありますので、興味のある方はこちらもご参考ください。

https://github.com/Taketo-Yoda/VueLearning/tree/main/20250513_CreateSPA

# 参考にさせていただいた記事

https://zenn.dev/15bluefortran/articles/e81ebee62896a2

https://qiita.com/hshota28/items/765cf903f055754f7557

https://qiita.com/Engineer_Grotle/items/39864e0ababf354c69de

[^1]:ブラウザのURLとウェブサイトの表示（コンポーネントやコンテンツ）を対応させる仕組み。