Vue.jsをSpringBootアプリに組み込む

# Vue.jsプロジェクトをjarに内包して実行

フロントエンドはVue.js、バックエンドはJava（SpringBoot）でアプリを作りたいけど、
Vue.jsとJavaでプロジェクトを分けたくない…というシーンに遭遇したので書いておきます

ビルド済みVue.jsリソースをjarファイルに内包してSpringBootの組み込みTomcatで実行します

# 開発環境
* Windows10Pro
* AdoptOpenJDKJ9 1.8.0_265
* npm 6.14.6
* vue-cli 4.5.4

# Vue.js＋SpringBootプロジェクトを作成

IntelliJでSpringBootプロジェクトを作成します

以下のコマンドで作成したSpringBootプロジェクトの中にVue.jsプロジェクトを作成します

```
cd C:\Users\risab\git\demo
vue create web
```

これでVue.js＋SpringBootプロジェクトができました　画像のwebディレクトリがVue.jsプロジェクトです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530205/5e3f7483-36f3-5d6a-d66c-bdb269367e99.png)

# アプリケーションを実装する

今回はVue.jsで作成した簡単な画面にSpringBootで作成したRESTAPIのレスポンスを表示するアプリを作成します

### RESTAPI

`/blog/entries/latest`にアクセスすると最新のブログタイトルと記事内容が返される
RESTAPIコントローラーを作成しました

```demo\src\main\java\com\example\demo\controller\BlogController.java
package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/blog")
public class BlogController {

    /**
     * 最新ブログ記事を取得
     * @return タイトルと内容のMap
     */
    @RequestMapping(value = "/entries/latest", method = RequestMethod.GET)
    public Entry getLatestEntry() {
        Entry entryData = new Entry();
        // TODO DBから最新の記事データを取得
        entryData.setTitle("VueRouterを使ってみた");
        entryData.setContent("VueRouterで簡単にページルーティングができました");
        return entryData;
    }

    /**
     * ブログ記事クラス
     */
    private class Entry{
        private String title;
        private String content;

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getContent() {
            return content;
        }

        public void setContent(String content) {
            this.content = content;
        }
    }
}

```
IntelliJでSpringBootアプリを作成するとアプリ起動に必要な以下のクラスは自動で生成されています

<details><summary>DemoApplication.java</summary><div>

```demo\src\main\java\com\example\demo\DemoApplication.java
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}
```
</div></details>

<details><summary>ServletInitializer.java</summary><div>

```demo\src\main\java\com\example\demo\ServletInitializer.java
package com.example.demo;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(DemoApplication.class);
    }

}

```
</div></details>


### アプリ画面
Vue.js+Vuetify+VueRouterで簡単な画面を2つ作成しました

トップページは画面タイトルと「BLOG」ボタンだけのシンプルな画面です

```demo\web\src\pages\TopPage.vue
<template>
  <v-app>
    <v-app-bar
        absolute
        color="teal lighten-3"
        dark
    >
      <v-toolbar-title>トップページ</v-toolbar-title>
    </v-app-bar>
    <v-container class="ml-0 mt-16">
      <v-row>
        <v-col cols="4">
          <v-btn
              color="brown lighten-3"
              width="100"
              v-on:click="clickBlogBtn"
          >
            blog
          </v-btn>
        </v-col>
      </v-row>
    </v-container>
  </v-app>
</template>

<script>
export default {
  name: "TopPage",
  methods: {
    clickBlogBtn: function () {
      this.$router.push("/blogPage")
    }
  }
}
</script>

<style>
</style>

```

ブログページは先ほど作成したRESTAPIを呼び出してブログ記事データを表示します

```demo\web\src\pages\BlogPage.vue
<template>
  <v-app>
    <v-app-bar
        absolute
        color="teal lighten-3"
        dark
    >
      <v-toolbar-title>tech blog</v-toolbar-title>
    </v-app-bar>
    <v-container class="ml-0 mt-16">
      <v-row>
        <v-col cols="4">
          <v-btn
              color="blue-grey lighten-4"
              width="100"
              v-on:click="clickTopPageBtn"
          >
            top page
          </v-btn>
        </v-col>
      </v-row>
      <v-row>
        <v-card class="ml-3" color="yellow lighten-5">
          <v-card-title>{{ blogPost.title }}</v-card-title>
          <v-card-text>
            {{ blogPost.content }}
          </v-card-text>
        </v-card>
      </v-row>
    </v-container>
  </v-app>
</template>

<script>
export default {
  name: "BlogPage",
  data: () => ({
    // APIから取得した記事データをバインド
    blogPost: {
      title: "",
      content: ""
    }
  }),
  mounted() {
    // RESTAPI呼び出し
    const el = this
    this.axios.get("/blog/entries/latest")
        .then(response => {
          el.blogPost = response.data
        })
  },
  methods: {
    clickTopPageBtn: function () {
      this.$router.push("/")
    }
  }
}
</script>

<style>
</style>
```

以下のファイルも修正・作成しました　main.jsは変更不要です

<details><summary>App.vue</summary><div>

```demo\web\src\App.vue
<template>
  <v-app>
    <router-view/>
  </v-app>
</template>

<script>
export default {
  name: 'App'
};
</script>
```
</div></details>

<details><summary>router.js</summary><div>

```demo\web\src\router.js
import Vue from "vue"
import Router from "vue-router"

import TopPage from "@/pages/TopPage";
import BlogPage from "@/pages/BlogPage";

Vue.use(Router)

export default new Router({
    mode: "history",
    routes: [
        {
            path: "/",
            name: "トップページ",
            component: TopPage
        },
        {
            path: "/blogPage",
            name: "ブログ",
            component: BlogPage
        }
    ]
})

```
</div></details>
これでアプリケーションの作成は完了です

# ビルド済みVue.jsの出力先を変更

この時点でwebディレクトリで `npm run build` するとweb\dist\配下にビルド済みVue.jsリソースが出力されます
**web\package.json**を修正してJavaから見える場所に出力するようにします

scripts.buildを以下のように修正してSpringBootプロジェクトの静的リソースディレクトリに出力するようにします

```demo\web\package.json
{
  "name": "demo",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build --dest ../src/main/resources/static/",
    "lint": "vue-cli-service lint"
  },
```

上記の通りpackage.jsonを修正してビルドすると以下のようになります

```
cd C:\Users\risab\git\demo\web
npm run build
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530205/ab069f8a-1aa0-e8c9-0a91-3eeec4cc74a3.png)

# jarを生成する

jarファイルはgradleで生成するので、以下の記述をbuild.gradleの末尾に追加します
Main-Classには`@SpringBootApplication`を付けているクラスを指定します

```demo\build.gradle
jar {
    manifest {
        attributes 'Main-Class': 'DemoApplication'
    }
```

以下のコマンドを実行するとbuild\libs\配下にjarファイルが生成されます

```
cd C:\Users\risab\git\demo
gradlew bootJar
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530205/8182623e-a428-c96f-f3e5-f541b46d0734.png)

あとは生成されたjarファイルを実行するだけです！

```
cd C:\Users\risab\git\demo\build\libs
java -jar demo-0.0.1-SNAPSHOT
```

`Started DemoApplication in 2.893 seconds (JVM running for 3.378)`のように出力されていればSpringBootアプリケーションが起動できています

SpringBootの組み込みTomcatを利用しているので、ブラウザで`http://localhost:8080/`にアクセスします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530205/5ffced9a-f1cd-f7a4-fe1c-714dcf4a3dc8.png)

Vue.jsからJavaで作ったRESTAPIも実行できています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530205/88759484-eb70-18d1-9f35-76a887796fc3.png)