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

# はじめに

こんにちは。バックエンドエンジニアを目指して奮闘中のひろえです。

突然ですが、未経験・独学の身では、何から勉強を始めて、何に重点を置くかって大事ですよね。
私自身は `Java` や `Spring Boot` から学び、バックエンドに力を入れてきました。
しかし、ポートフォリオを作るうえでもフロントエンドやインフラに関しても最低限メジャーな技術は押さえておきたいところ。

ということで、できるだけ簡単にフロントエンドを始めるべく、希望をざっと挙げてみました。

- フロントエンドはほとんど触ったことがないから最小構成から始めたい
- Vue.js の単一ファイルコンポーネント (SFC) を使ってみたい
- 静的アセットは `jar` ファイルに同梱し、`Spring Boot` の静的リソース配信機能を利用して配信したい
- `CI/CD` 環境や本番環境でもシンプルに環境構築できるようにしたい

希望を叶えるため、あれこれ調べているうちに以下の記事に出会いました。

▼ [領域展開「Gradle」で Node.js を制する](https://qiita.com/aucfan-engineer/items/fc84490d9fef48e3f136)
▼ [Spring Boot ウェブアプリにフロントエンド技術を含める webpack ビルド](https://qiita.com/h1romas4/items/8345e313b351c992df08)

いずれの記事もとても分かりやすく丁寧に説明してくださっているので、こちらを読んでいただければ十分なのですが、フロントエンドに関する知識が少なすぎて詰まる部分があったので、この記事ではよりシンプルな構成で環境構築する方法をご紹介しようと思います。

記事中のソースコードは以下のリポジトリから参照できます。

https://github.com/hiroe-mkk/springboot-vue-tips

誤りなどがあればご指摘いただけますと幸いです。

# 環境

- Java 17.0.5
- Spring Boot 3.2.2
- gradle-node-plugin 7.0.1
- Vue.js 3.4.18
- Node.js 21.6.1
- IntelliJ IDEA 2023.3.3 (Community Edition)
- Windows 11 Pro

# プロジェクト構成概要

バックエンドには `Gradle` プロジェクト、フロントエンドには `Node.js` プロジェクトを使用し、`Node.js` プロジェクトは `Gradle` プロジェクトのサブプロジェクトとして扱います。
そして [gradle-node-plugin](https://github.com/node-gradle/gradle-node-plugin) というプラグインで両ビルドサイクルを統合する、というのが今回のポイントです。

このプラグインは `Gradle` で `Node.js` プロジェクトを管理するためのもので、`Gradle` のビルドの一環として任意の `Node.js` スクリプトを実行させることができます。
今回の場合は、リソースを処理する `Gradle` タスクを開始する前に `Node.js` プロジェクトのビルドを実行させるように構成することで、バックエンドとフロントエンドのビルドサイクルの統合を実現しています。

なお、`gradle-node-plugin` は、ローカルの `.gradle` ディレクトリに `Node.js` と `npm` 自動的にインストールする機能も提供しています。
`Spring Initializr` で作成した `Spring Boot` プロジェクトは `Gradle Wrapper` を同梱するため、これによって JDK を用意するだけで済むようになります。

※ Java の Web アプリケーションにおいて静的リソースをどのように構成するかについては、以下の記事が勉強になりました。

▼ [Java ウェブアプリプロジェクトに JavaScript/TypeScript などの静的アセットをどう配置するか](https://blog.kengo-toda.jp/entry/2019/12/01/005852)

今回紹介する構成は、この記事における 「1.1. サーバサイドを Maven/Gradle で、フロントエンドを Maven/Gradle で包んだ npm/Yarn で開発するケース」に該当します。

# 環境構築

以下のような手順で環境を構築していきます。

**1. Spring Boot プロジェクトを作成する**
**2. Node.js プロジェクトを作成する**
**3. Vue.js 開発環境を構築する**

## 1. Spring Boot プロジェクトを作成する

はじめに [Spring Initializer](https://start.spring.io/) で Spring Boot プロジェクトを生成します。

![2024-02-09-194957.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671461/f3d0dfe4-fe27-e36d-64be-7680ab5ad177.png)

ビルドツールは `Gradle - Groovy` を、言語は `Java` を選択します。
依存関係は最低限 `spring-boot-starter-web` と `spring-boot-starter-thymeleaf` が追加されていれば問題ありません。
メタデータについては必要に応じて変更してください。

GENERATE ボタンをクリックしてプロジェクトをダウンロードします。
ダウンロードが完了したら、zip ファイルを展開し、お好みのエディタで開いてください。

以下のようなディレクトリ構成となっていると思います。

![2024-02-09-195244.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671461/4abb13e9-91ad-9ca1-d278-7bdacff7193d.png)

`settings.gradle` と `build.gradle` は以下のような状態です。

```gradle:settings.gradle
rootProject.name = 'demo'
```

```gradle:build.gradle
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.2'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}
```

## 2. Node.js プロジェクトを作成する

続いては `Node.js` プロジェクトを作成します。

### 2.1 フロントエンドサブプロジェクトを作成する

`Node.js` プロジェクトは `Gradle` サブプロジェクトとして扱うため、まずはそのための設定を行いましょう。

まず、プロジェクトルートに新規ディレクトリを作成します。
ここではディレクトリ名を frontend としますが、お好きな名前にしていただいて構いません (その場合は以降適宜読み替えてください)。

ディレクトリの作成が完了したら、`Gradle` にサブプロジェクトとして認識させるために `settings.gradle` ファイルを以下のように変更します。

```diff_gradle:setting.gradle
rootProject.name = 'demo'
+ include 'frontend'
```

※ Gradle のマルチプロジェクトに関しては以下をご参照ください。

▼ [Multi-Project Build Basics (gradle.org)](https://docs.gradle.org/8.5/userguide/partr3_multi_project_builds.html)

### 2.2 gradle-node-plugin を追加する

つぎに、frontend ディレクトリ直下に frontend サブプロジェクト専用の `build.gradle` ファイルを作成してください。

![2024-02-10-101700.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671461/1ec0a817-ecfd-c094-08c8-e29377f816c4.png)

frontend サブプロジェクトにおいて `gradle-node-plugin` を利用できるようにするために、この `build.gradle` ファイルを以下のように編集します。

```gradle:build.gradle (frontend サブプロジェクト)
plugins {
    id("com.github.node-gradle.node") version "7.0.1"
}

// プロジェクトローカルに Node.js をダウンロードする
node {
    download.set(true)
    version.set("21.6.1")
}
```

`node` ブロックでは、frontend ディレクトリ配下に指定したバージョンの `Node.js` (とそれにバンドルされた `npm` ) をダウンロードする設定をしています。

他にも設定可能な項目がいくつかありますので、詳細については以下をご参照ください。

▼ [gradle-node-plugin/docs/usage.md at 7.0.2 (github.com)](https://github.com/node-gradle/gradle-node-plugin/blob/7.0.2/docs/usage.md#configuring-the-plugin)

**追記**

frontend サブプロジェクトの build.gradle には、以下も記述するように書いていましたが、実際には `npmInstall` タスクが `npm_run_build` よりも先に実行されるため、この設定は不要でした。

```gradle:build.gradle (frontend サブプロジェクト)
// npm run build の前に npm install を実行する
tasks.getByName("npm_run_build") {
    dependsOn("npm_install")
}
```

実行順序の確認には、次のコマンドを利用しました。

```bash
$ ./gradlew npm_run_build --dry-run

:frontend:nodeSetup SKIPPED
:frontend:npmSetup SKIPPED
:frontend:npmInstall SKIPPED
:frontend:npm_run_build SKIPPED
```

**追記ここまで**

### 2.3 バックエンドとフロントエンドのビルドプロセスを統合する

続いて、ルートプロジェクトの `build.gradle` ファイルに以下を追加してください。

```gradle: build.gradle (ルートプロジェクト)
tasks.named('processResources') {
    dependsOn(":frontend:npm_run_build")
}
```

これにより、ルートプロジェクトにおいてリソース処理を開始する前に frontend サブプロジェクトのビルドが実行されるようになります。

### 2.4 Node.js プロジェクトとして初期化する

最後に frontend サブプロジェクトを Node.js プロジェクトとして初期化します。

frontned ディレクトリに移動して以下のコマンドを実行してください。

```
$ npm init -y
```

コマンドの実行が完了すると、以下のような内容の `package.json` が自動的に作成されているかと思います。

```json:package.json
{
  "name": "frontend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
}
```

## 3. Vue.js 開発環境を構築する

それでは、`Vue.js` 開発環境を構築していきましょう。

Vue.js で単一ファイルコンポーネントを使用するためには、`Webpack` などのビルドツールを導入する必要があるため、`Webpack` -> `Vue.js` の順で設定を行います。

ここでも引き続き frontend ディレクトリで作業してください。

### 3.1 Webpack をインストールする

まず、以下のコマンドを実行し、`Webpack` と `webpack-cli` をインストールします。

```
$ npm install --save-dev webpack webpack-cli
```

インストールが完了すると `node_modules/` ディレクトリと `package-lock.json` ファイルが新たに作成されているかと思います。

![2024-02-10-110801.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671461/cc71fcb9-3ccb-e930-8e1c-0b2f8f1f0c29.png)

また、`package.json` ファイルには `devDependencies` ブロックが追記されていると思うので確認してください。

```package.json
"devDependencies": {
  "webpack": "^5.90.1",
  "webpack-cli": "^5.1.4"
}
```

このファイルには `npm run build` コマンドで `Webpack` のビルドを実行させるために、`scripts` ブロックに `build` スクリプトを追加しておきます (test スクリプトは削除しました)。

```json:package.json
"scripts": {
  "build": "webpack"
}
```

つぎに frontend ディレクトリ直下に `webpack.config.js` ファイルを作成し、以下のように編集してください。

![2024-02-10-110818.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671461/662671be-2def-a167-0f68-b11d89201ae7.png)

```javascript:webpack.config.js
const Webpack = require('webpack');
const path = require('path');

module.exports = {
  mode: 'development',
  output: {
	// ビルド成果物はルートプロジェクトの src/main/resources/static/dist に出力する
    path: path.resolve(__dirname, '../src/main/resources/static/dist'),
    // Webpack によって生成される各バンドルのファイル名 ( [name] はエントリポイント名で置き換え)
	filename: 'js/[name].bundle.js',
    // ビルド成果物を出力する前に、出力ディレクトリをクリーンアップする
	clean: true,
  },
  resolve: {
    alias: {
      // .js ファイルや .vue ファイル内で @src エイリアスを使用して src ディレクトリを参照する
      '@src': path.resolve(__dirname, 'src/'),
    },
  },
};
```

`Node.js` プロジェクト (frontend サブプロジェクト) のビルド成果物を `Gradle` がリソースとして認識するディレクトリ `src/main/resources/static/dist/` に出力するように設定しているのがポイントです。

なお、`node_modules/` と `Node.js` プロジェクトのビルド成果物に関しては、必要に応じて `.gitignore` に以下を追記して Git 管理から除外してください。

```gitignore:.gitignore (ルートプロジェクト)
frontend/node_modules
src/main/resources/static/dist
```

### 3.2 Vue.js をインストールする

`Webpack` の設定が完了したので、続いて `Vue.js` の設定を行っていきましょう。

以下のコマンドを frontend ディレクトリで実行してください。

```
$ npm install vue
$ npm install --save-dev vue-loader vue-template-compiler vue-style-loader css-loader
```

インストールが完了すると `package.json` の依存関係が以下のように変更されています。

```json:package.json
"devDependencies": {
  "css-loader": "^6.10.0",
  "vue-loader": "^17.4.2",
  "vue-style-loader": "^4.1.3",
  "vue-template-compiler": "^2.7.16",
  "webpack": "^5.90.1",
  "webpack-cli": "^5.1.4"
},
"dependencies": {
  "vue": "^3.4.18"
}
```

`webpack.config.js` を以下のように編集して、`Vue.js` のための基本的な設定を行います。

```diff_javascript:webpack.config.js
const Webpack = require('webpack');
const path = require('path');
+ const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
  mode: 'development',
+  entry: {
+	// 共通のライブラリなどは vendor.bundle.js としてひとまとめにする
+    vendor: ['vue'],
+  },
  output: {
    // ビルド成果物はルートプロジェクトの src/main/resources/static/dist に出力する
    path: path.resolve(__dirname, '../src/main/resources/static/dist'),
    // Webpack によって生成される各バンドルのファイル名 ([name]はエントリーポイントの名前で置き換え)
    filename: 'js/[name].bundle.js',
    // ビルド成果物を出力する前に、出力ディレクトリをクリーンアップする
    clean: true,
  },
+  plugins: [
+    // .vue ファイルをバンドルするために必要なプラグイン
+    new VueLoaderPlugin(),
+  ],
+  module: {
+    rules: [
+      // .vue ファイルをビルドおよびバンドルする
+      {
+        test: /\.vue$/,
+        loader: 'vue-loader',
+      },
+      // .vue ファイル内の style をビルドおよびバンドルする
+      {
+        test: /\.vue\.css$/,
+        use: ['vue-style-loader', 'css-loader'],
+      },
+    ],
+  },
  resolve: {
    alias: {
+      // ESM (ECMAScript Modules) のバンドラー向けビルドを使用する
+      vue$: 'vue/dist/vue.esm-bundler.js',
      // .js ファイルや .vue ファイル内で @src エイリアスを使用して src ディレクトリを参照する
      '@src': path.resolve(__dirname, 'src/'),
    },
  },
};
```

適宜コメントを入れましたが、詳細な説明を省略したので、不明な点があれば以下の記事がおすすめです。

▼ [Webpack 5 を使った vue.js の環境構築と SFC の利用方法](https://reffect.co.jp/vue/webpack-vue-single-file-component)

こちらの記事は、単一ファイルコンポーネントを利用する場合の Vue.js の開発環境構築手順が丁寧に説明されていて分かりやすかったです。

なお、Babel や Jest などの他のライブラリについても同じ手順で設定可能なので、ぜひやってみてください。

環境構築は以上となります。
以降は、この環境における単一ファイルコンポーネントの利用方法について簡単に説明していきます。

## おまけ. アプリケーション作成

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

```java:IndexController.java
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {
    @GetMapping
    public String display() {
        return "index";
    }
}
```

同じくルートプロジェクトの `src/main/resources/templates/` ディレクトリ直下に `index.html` ファイルを作成します。

```html:index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <div id="main"><example/></div>

    <script th:src="@{/dist/js/vendor.bundle.js}"></script>
    <script th:src="@{/dist/js/index.bundle.js}"></script>
</body>
</html>
```

以降は frontend サブプロジェクトでの作業です。

まず、エントリーポイントとなる `JavaScript` ファイルを作成します。
frontend ディレクトリ直下に `src/main/` ディレクトリを作成し、以下のような内容の `index.js` ファイルを作成してください。

```javascript:index.js
import { createApp } from 'vue';
import example from '@src/main/components/example.vue';

createApp({
  components: {
    example: example,
  },
}).mount('#main');
```

つぎに、このファイルでインポートされている単一ファイルコンポーネントを作成します。
同じく `src/main/` ディレクトリの直下に `components/` ディレクトリを作成し、以下のような `example.vue` ファイルを作成してください。

```vue:example.vue
<template>
  <div class="message">{{ message }}</div>
</template>

<script>
export default {
  data () {
    return {
      message: 'Hello Vue Component'
    }
  }
}
</script>

<style scoped>
.message {
  font-weight: bold;
}
</style>
```

以上です。

最終的なディレクトリ構成は以下の通りです。

![2024-02-10-170744.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671461/7014da46-3714-a78c-5ef0-756d96a54167.png)

それでは、ルートプロジェクトにおいて、以下のコマンドを実行してアプリケーションを実行しましょう。

```
$ ./gradlew bootRun
```

Node.js プロジェクトのビルドに成功すると、ルートプロジェクトの `src/main/resources/static/dist/js/` ディレクトリに `index.bundle.js` と `vendor.bundle.js` が生成されているはずです。

![2024-02-10-183547.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671461/044f36dc-7282-8af8-66cf-19e50189f7d9.png)

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

![2024-02-10-184037.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671461/4eda81cd-5ed3-7280-d8a6-b617943081c1.png)

# さいごに

今回はシンプルな構成、簡単な環境構築にこだわってみましたが、パフォーマンスはどうなんだろうとか、ファイルサイズは削減できるところはないだろうかとか、悩みだしたらきりがないですね。
メリデメを考慮して環境構築ができるようになりたいものです。

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

# 追記

SPA バージョンの記事も書きました！

https://qiita.com/hiroe-mkk/items/cf9617f49cc59c458124

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