はじめに
以前、Spring Boot に Vue.js を統合した開発環境の構築手順を説明する記事を書いたのですが、できることをひとつずつ増やそうの精神でほぼ同じ構成の SPA バージョンにも挑戦したので、こちらの環境構築手順も紹介しようと思います。
記事中のソースコードは以下のリポジトリから参照できます。
誤りなどがありましたら、ご指摘いただけますと幸いです。
環境
- Java 17
- Spring Boot 3.2.2
- gradle-node-plugin 7.0.1
- vue 3.4.21
- vite: 5.2.0
- vue-router: 4.3.0
- Node.js 21.6.1
- IntelliJ IDEA 2023.3.3 (Community Edition)
- Windows 11 Pro
プロジェクト構成概要
前回と同じ点
- バックエンドには
Spring Boot
を使用し、Gradle
プロジェクトとして管理する - フロントエンドには
Vue.js 3
を使用し、Node.js
プロジェクトとして管理する -
Node.js
プロジェクトはGradle
プロジェクトのサブプロジェクトとして扱う - gradle-node-plugin を使って、バックエンドとフロントエンドのビルドサイクルを統合する
- フロントエンドのビルド成果物は
jar
ファイルに同梱し、Spring Boot
の静的リソース配信機能を利用して配信する
前回と異なる点
- バックエンドでは、テンプレートエンジン (Thymeleaf) を使用しない
- ビルドツールを
Webpack
からVite
に変更する - フロントエンドに
Vue Router
を追加する
Spring Boot アプリケーションは、主に RESTful API
を構築するのに使用するため、今回はテンプレートエンジンは不要です。
また、ビルドツールは TypeScript
の導入がスムーズに行えるよう、公式が推奨する Vite
に変更しました。
フロントエンドに使用するライブラリについては、SPA の基本的な機能を実装できる最小限の構成をとるために、Vue Router
のみを導入しています。
環境構築
以下のような手順で環境を構築していきます。
1. Spring Boot プロジェクトを作成する
2. Vite ベースの Vue.js プロジェクトを作成する
3. フロントエンドのビルド成果物をバックエンドに統合する
4. Vue Router を追加する
1. Spring Boot プロジェクトを作成する
まずは、バックエンド用の Spring Boot プロジェクトを作成します。
作成手順は前回と同様ですので、詳細はそちらを参照してください。
ただ、今回は Thymeleaf
を使用しない構成なので、依存関係には最低限 spring-boot-starter-web
を追加するだけで大丈夫です。
2. Vite ベースの Vue.js プロジェクトを作成する
続いて、フロントエンド用のプロジェクトを作成しましょう。
基本的には、前回の「 2. Node.js プロジェクトを作成する 」と同じ内容ですが、今回はビルドツールに Vite
を採用するため、少々手順が変わります。
2.1 create-vite で Vue.js プロジェクトを初期化する
Vite
の Create Vite を使ってフロントエンド用プロジェクトを作成します。
Create Vite
は、Vite ベースの新規プロジェクトを手早く始められるスターターキットです。
まずは、プロジェクトルートにおいて以下のコマンドを実行して、Vite
ベースの Vue.js
プロジェクトを作成してください。
$ npm create vite@latest frontend -- --template vue
ここでは frontend というディレクトリ名にしていますが、お好きな名前にしていただいて構いません (その場合は以降の手順で適宜読み替えてください)。
初期化が完了すると、以下のような構造のディレクトリが生成されているかと思います。
frontend
├── public/ 静的ファイル(画像、フォント等)を格納するディレクトリ
│ └── vite.svg
├── src/
│ ├── assets/ アセット(画像など)を格納するディレクトリ
│ │ └── vue.svg
│ ├── components/ Vue.jsコンポーネントを格納するディレクトリ
│ │ └── HelloWorld.vue
│ ├── App.vue メインのVue.jsコンポーネント
│ ├── main.js アプリのエントリーポイントとなるJavaScriptファイル
│ └── style.css
├── .gitignore
├── index.html アプリのエントリーポイントとなるHTMLファイル
├── package.json プロジェクトの依存関係や設定情報が記載されたファイル
└── vite.config.js Viteの設定ファイル
これらは、Vite
と Vue.js
のベストプラクティスに基づいた標準的なディレクトリ構造になっています。
中核となるファイルは、以下の 3 つです。
-
index.html: アプリのエントリーポイントとなる
HTML
ファイル -
src/App.vue:
Vue.js
アプリ全体の共通レイアウトを定義するコンポーネント -
src/main.js:
Vue.js
アプリの起動処理や設定を行うJavaScript
ファイル
通常、これらのファイルは名前やディレクトリ配置を変更することなく、必要に応じて内容を編集していきます。
また、以下も不可欠な設定ファイルのため、名前やディレクトリ配置は変更しません。
-
package.json: プロジェクトの依存関係や設定情報を記載したファイルで、
npm/yarn
などのパッケージマネージャーで管理される -
vite.config.js:
Vite
の設定ファイルで、ビルド設定やプラグインの設定などを行う
その他のファイルは単なるサンプルなので、開発中に削除しても構いません (ただし、今回はこちらを利用して動作確認を行うため、削除しないでください)。
2.2 Vue.js プロジェクトをサブプロジェクト化する
つぎに、生成された frontend ディレクトリを Gradle
にサブプロジェクトとして認識させるための設定を行います。
プロジェクトルートにある settings.gradle
ファイルに以下を追記してください。
rootProject.name = 'demo'
+ include 'frontend'
2.3 gradle-node-plugin を追加する
frontend ディレクトリをサブプロジェクト化できたので、次はこのフロントエンドプロジェクトを Gradle
と連携させる手順に移りましょう。
連携には、前回と同じく gradle-node-plugin
を使用します。
今回は手順のみを簡潔に示しますので、詳細は前回の記事を参考にしてください。
frontend ディレクトリ直下に、以下のような内容の build.gradle
ファイルを作成します。
plugins {
id("com.github.node-gradle.node") version "7.0.1"
}
// プロジェクトローカルに Node.js をダウンロードする
node {
download.set(true)
version.set("21.6.1")
}
追記
frontend サブプロジェクトの build.gradle には、以下も記述するように書いていましたが、実際には npmInstall
タスクが npm_run_build
よりも先に実行されるため、この設定は不要でした。
// npm run build の前に npm install を実行する
tasks.getByName("npm_run_build") {
dependsOn("npm_install")
}
実行順序の確認には、次のコマンドを利用しました。
$ ./gradlew npm_run_build --dry-run
:frontend:nodeSetup SKIPPED
:frontend:npmSetup SKIPPED
:frontend:npmInstall SKIPPED
:frontend:npm_run_build SKIPPED
追記ここまで
ルートプロジェクトの build.gradle
ファイルに以下を追記してください。
tasks.named('processResources') {
dependsOn(":frontend:npm_run_build")
}
これで、Gradle
によるルートプロジェクトのビルド実行時に、サブプロジェクトであるフロントエンドのビルドが実行されるようになります。
3. フロントエンドのビルド成果物をバックエンドに統合する
3.1 Vite ビルドの成果物の出力先を変更する
Vite
のデフォルト設定では、ビルド成果物がプロジェクトの dist
ディレクトリに出力されますが、この設定ではバックエンドとの連携が難しくなってしまいます。
そのため、Spring Boot
アプリの静的リソース用ディレクトリにビルド成果物を直接出力するよう設定を変更しましょう。
手順 2.1 において Create Vite
で自動生成された frontend/vite.config.js
ファイルに、build
セクションを追加してください。
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
+ build: {
+ outDir: '../src/main/resources/static'
+ },
})
なお、ビルド成果物は必要に応じて .gitignore
に以下を追記して、Git 管理から除外してください。
src/main/resources/static
3.2 SPA のための Controller を作成する
今回の構成では、フロントエンドのビルド成果物は Spring Boot
アプリの静的コンテンツとして提供するため、ルートパスへのアクセス時に、ビルド済みの index.html
ファイルを返すコントローラが必要になります。
具体的には、ルートプロジェクトの src/main/java/
ディレクトリ配下の適切なパッケージに、以下のようなコントローラクラスを定義してください。
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping(path = "/")
public String forward() {
return "forward:/index.html";
}
}
このメソッドでは、ルートパス (/) への GET
リクエストに対して、forward:index.html
を返しています。
forward:
キーワードを使うことで、Spring
が index.html
ファイルをレンダリングするのではなく、静的リソースとして返すことができます。
3.3 動作確認
このあと Vue Router
も追加しますが、ひとまずここでサンプルコードを追加して、ここまでの手順に問題がないかを確認してみましょう。
フロントエンドから axios
を使ってバックエンドにリクエストを送信し、返されたデータを表示する方法を示します。
まずはバックエンド側で、以下のようなコントローラクラスを作成してください。
先ほどと同様、ルートプロジェクトの src/main/java/
配下の適切なパッケージに置いてください。
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
@ResponseBody
public String getMsg() {
return "Hello, World!";
}
}
つぎに frontend ディレクトリに移動して、以下のコマンドを実行し、axios
をインストールしてください。
$ npm install axios@latest
続いて、frountend/src/components/HelloWorld.vue
に、以下のような変更を加えてください (※ 一部省略)。
<script setup>
- import { ref } from 'vue'
+ import { ref, onMounted } from 'vue'
+ import axios from 'axios';
defineProps({
msg: String,
})
const count = ref(0)
+ const msg2 = ref('');
+
+ onMounted(() => {
+ axios.get('/api/hello')
+ .then(response => {
+ msg2.value = response.data;
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ });
+ });
</script>
<template>
<h1>{{ msg }}</h1>
+ <h2>{{ msg2 }}</h2>
<div class="card">
...
</div>
...
</template>
<style scoped>
...
</style>
以上です。以下のコマンドを実行してアプリケーションを起動してください。
$ ./gradlew bootRun
Node.js
プロジェクトのビルドに成功すると、以下のようにルートプロジェクトの src/main/resources/static/
ディレクトリ配下に index.html
などのビルド成果物が生成されているはずです。
ブラウザで http://localhost:8080 にアクセスして、以下のように表示されれば成功です。
4. Vue Router を追加する
4.1 Vue Router をインストールする
続いて、SPA 内で画面遷移や URL 管理を行うために、Vue Router
を追加して、設定を行っていきましょう。
frontend ディレクトリに移動して、以下のコマンドを実行してください。
$ npm install vue-router@latest
4.2 Vue Router を組み込む
つぎに Vue.js
アプリケーションに Vue Router
を組み込む設定を行います。
手順 2.1 において create-vite
で自動生成された frountend/src/main.js
に、以下のような変更を加えてください。
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
+ import router from './router'
- createApp(App).mount('#app')
+ const app = createApp(App)
+ app.use(router)
+ app.mount('#app')
この変更により、Vue Router
が Vue
アプリケーション内でのルーティングを担当するようになります。
4.3 SPA における URL の変更とバックエンド側の対応
SPA では、初回ロード時に必要な静的ファイル一式をサーバから取得し、それ以降の画面遷移やデータ取得は、基本的にクライアント側で処理します。
そのため、基本的にはページ遷移を行っても、サーバに新しいページを要求することはありませんが、リロードや URL の直接入力によってサーバにリクエストが送られた場合、通常の Web サーバではリソースが存在しないため、エラーになってしまいます。
SPA を正しく機能させるためには、サーバは常に index.html を返すように設定する必要があるのです。
ルートプロジェクトの src/main/java/
ディレクトリ配下の適当なパッケージに、以下のような WebMvcConfigurer
を実装したクラスを作成してください。
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
import java.io.IOException;
@Configuration
public class SpaWebMvcConfigurer implements WebMvcConfigurer {
// 静的リソースのハンドリングをカスタマイズする
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
@Override
protected Resource getResource(String resourcePath, Resource location) throws IOException {
Resource resource = location.createRelative(resourcePath);
// 静的リソースが存在しない場合は、index.html を提供する
return resource.exists() ? resource : location.createRelative("index.html");
}
});
}
}
ここでは、addResourceHandlers()
メソッドで静的リソースのハンドリングをカスタマイズすることで、静的リソースへのリクエストを処理し、リソースが存在しない場合は index.html
を提供するように設定しています。
この変更により、SPA が正しく機能し、ブラウザでのページ遷移がスムーズに行われるようになります。
設定は以上です。
4.4 動作確認
せっかくなので、動作確認を行いましょう。
まず、frontend/src/
ディレクトリ直下に views/
ディレクトリを作成し、その中に以下の 2 つの Vue コンポーネントファイルを作成してください。
<template>
<div class="home">
<h1>This is an home page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.home {
min-height: 100vh;
align-items: center;
}
}
</style>
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
align-items: center;
}
}
</style>
これらのファイルは Vue Router
のページコンポーネントとして機能します。
続いて、ルーティングの設定を行います。
frontend/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')
}
]
})
export default router
簡単に説明すると、history
オプションでは、ブラウザの HistoryAPI
を使ったルーティングを指定しています。これにより、ユーザーがブラウザの戻る / 進むボタンを使ってもページ遷移できるようになります。
routes
オプションでは、各ルートの設定を行っています。path
には URL
パスを、name
にはルート名を、component
にはそのルートに対応するページコンポーネントを設定しています。
さいごに、frontend/src/App.vue
ファイルを以下のように編集してください (※ 一部省略)。
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<div>
...
</div>
- <HelloWorld msg="Vite + Vue" />
+ <div>
+ <HelloWorld msg="Vite + Vue" />
+
+ <nav>
+ <RouterLink to="/">Home</RouterLink>
+ <RouterLink to="/about">About</RouterLink>
+ </nav>
+ </div>
+
+ <RouterView />
</template>
<style scoped>
...
+ nav a.router-link-exact-active {
+ color: var(--color-text);
+ }
+ nav a {
+ display: inline-block;
+ padding: 0 1rem;
+ border-left: 1px solid var(--color-border);
+ }
</style>
ここでは、Vue Router
の基本的な機能を使って、アプリケーションのナビゲーション部分と、ページコンテンツの表示を行うための変更を加えています。
RouterLink
コンポーネントは、ナビゲーションリンクを定義するために使用されます。
to
属性に URL パスを指定することで、ルーティングに応じた適切なリンクが生成されます。
そして、RouterView
コンポーネントは、現在のルーティングに対応するページコンポーネントをレンダリングする役割を担います。
これにより、ユーザーがナビゲーションリンクをクリックした際に、正しいページコンテンツが表示されるようになります。
以上です。以下のコマンドを実行してアプリケーションを起動してみましょう。
$ ./gradlew bootRun
ブラウザで http://localhost:8080 にアクセスします。
追加したリンクをクリックして画面遷移を行うと、それに伴って赤枠で囲まれた部分が変化すれば成功です。
さいごに
SPA って難しいんじゃないかと思っていましたが、こうしてできるところから徐々に攻めていくと、案外スルッと馴染めるものですね。
できることをひとつずつ、地道に増やしていけたら、きっといつかはつよつよになれますよね。
それでは、最後までお読みいただき、ありがとうございました!
追記
OpenAPI を導入する記事も書きました!
よろしければ、こちらもどうぞ。