2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SpringBootで包み込むVue.js開発環境 カンタン構築 SPA編

Last updated at Posted at 2024-04-15

はじめに

以前、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 プロジェクトを初期化する

ViteCreate 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の設定ファイル

これらは、ViteVue.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 ファイルに以下を追記してください。

setting.gradle
rootProject.name = 'demo'
+ include 'frontend'

2.3 gradle-node-plugin を追加する

frontend ディレクトリをサブプロジェクト化できたので、次はこのフロントエンドプロジェクトを Gradle と連携させる手順に移りましょう。

連携には、前回と同じく gradle-node-plugin を使用します。
今回は手順のみを簡潔に示しますので、詳細は前回の記事を参考にしてください。

frontend ディレクトリ直下に、以下のような内容の build.gradle ファイルを作成します。

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 よりも先に実行されるため、この設定は不要でした。

build.gradle (frontend サブプロジェクト)
// 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 ファイルに以下を追記してください。

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 セクションを追加してください。

vite.config.js
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 管理から除外してください。

.gitignore (ルートプロジェクト)
src/main/resources/static

3.2 SPA のための Controller を作成する

今回の構成では、フロントエンドのビルド成果物は Spring Boot アプリの静的コンテンツとして提供するため、ルートパスへのアクセス時に、ビルド済みの index.html ファイルを返すコントローラが必要になります。

具体的には、ルートプロジェクトの src/main/java/ ディレクトリ配下の適切なパッケージに、以下のようなコントローラクラスを定義してください。

IndexController.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:キーワードを使うことで、Springindex.html ファイルをレンダリングするのではなく、静的リソースとして返すことができます。

3.3 動作確認

このあと Vue Router も追加しますが、ひとまずここでサンプルコードを追加して、ここまでの手順に問題がないかを確認してみましょう。

フロントエンドから axios を使ってバックエンドにリクエストを送信し、返されたデータを表示する方法を示します。

まずはバックエンド側で、以下のようなコントローラクラスを作成してください。
先ほどと同様、ルートプロジェクトの src/main/java/ 配下の適切なパッケージに置いてください。

HelloController.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 に、以下のような変更を加えてください (※ 一部省略)。

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 などのビルド成果物が生成されているはずです。

3-3-dir.png

ブラウザで http://localhost:8080 にアクセスして、以下のように表示されれば成功です。

3-3.png

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 に、以下のような変更を加えてください。

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 RouterVue アプリケーション内でのルーティングを担当するようになります。

4.3 SPA における URL の変更とバックエンド側の対応

SPA では、初回ロード時に必要な静的ファイル一式をサーバから取得し、それ以降の画面遷移やデータ取得は、基本的にクライアント側で処理します。

そのため、基本的にはページ遷移を行っても、サーバに新しいページを要求することはありませんが、リロードや URL の直接入力によってサーバにリクエストが送られた場合、通常の Web サーバではリソースが存在しないため、エラーになってしまいます。

SPA を正しく機能させるためには、サーバは常に index.html を返すように設定する必要があるのです。

ルートプロジェクトの src/main/java/ ディレクトリ配下の適当なパッケージに、以下のような WebMvcConfigurer を実装したクラスを作成してください。

SpaWebMvcConfigurer.java
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 コンポーネントファイルを作成してください。

HomeView.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>
AboutView.vue
<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 ファイルを作成してください。

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 ファイルを以下のように編集してください (※ 一部省略)。

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 にアクセスします。

追加したリンクをクリックして画面遷移を行うと、それに伴って赤枠で囲まれた部分が変化すれば成功です。

4-4-home.png

4-4-about.png

さいごに

SPA って難しいんじゃないかと思っていましたが、こうしてできるところから徐々に攻めていくと、案外スルッと馴染めるものですね。
できることをひとつずつ、地道に増やしていけたら、きっといつかはつよつよになれますよね。

それでは、最後までお読みいただき、ありがとうございました!

追記

OpenAPI を導入する記事も書きました!

よろしければ、こちらもどうぞ。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?