🍔

Vite(Vue.js)+SpringBoot2.6でのREST API連携

2022/02/21に公開

はじめに

以前作成したVite(Vue3.2)のToDoアプリが好評だったみたいなので、SpringBootで作成したREST APIと連携する際の実装についても、紹介してみたいと思います。

こうやって、記事をまとめていくと、componentsを事前に用意できれば、ローコード開発でしょ?って思うぐらいに、簡単になってきましたね。Vue3.3(2022.2Q)で登場するReactivity Transformも楽しみにしています。

SpringBootでREST API作成

ToDo用のREST APIを新たに作成します。Javaの開発環境構築は、こちらを参考にしてください。ちなみにDBは、H2を利用しているので、Oracleのセットアップ不要です。Oracleとの連携方法もコメントで追記しています。

プロジェクト作成

F1で「[F1]を押して、「Spring Initializr: Create a Gradle Project」でプロジェクトを作成する。以下の項目以外は、デフォルトのままにします。
・Artifact Id ... todoapp
・Java version ... 17
・dependencies ... 手動で追加
・出力フォルダ ... workspace

REST API作成

ここからは、ひたすら実装していきます。Spring Bootは、特に新しい要素もないので説明は割愛します。

build.gradle
...
dependencies {
	// Spring Web
	implementation 'org.springframework.boot:spring-boot-starter-web'
	// Spring Boot DevTools
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	// Spring Data JDBC
	implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	// H2 Database
	runtimeOnly 'com.h2database:h2'
	// Oracle
	//implementation fileTree(dir: 'lib/ojdbc8.jar', include: ['*.jar'])
	// Lombok
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	// Swagger
	implementation 'org.springdoc:springdoc-openapi-ui:1.6.6'
	implementation 'org.springdoc:springdoc-openapi-data-rest:1.6.6'
	// Test
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
...
src/main/resources/application.yml
spring:
  sql:
    init:
      encoding: UTF-8
      mode: always
  h2:
    console:
      enabled: true
  datasource:
    driver-class-name: org.h2.Driver
    # Oracleの場合、oracle.jdbc.OracleDriver
    url: jdbc:h2:mem:test
    # Oracleの場合、jdbc:oracle:thin:@//database:1521/XEPDB1
    username: sa
    password:
main/java/com/exsample/todo/entity/Todo.java
package com.example.todoapp.entity;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.springframework.data.annotation.Id;

import lombok.Data;

@Data
public class Todo {
    @Id
    private Long id;

    @NotBlank
    @Size(min = 1, max = 20)
    private String title;

    @NotNull
    private Boolean done;
}
main/java/com/exsample/todo/repository/TodoRepository.java
package com.example.todoapp.controller;

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

import java.util.List;

import com.example.todoapp.entity.Todo;
import com.example.todoapp.repository.TodoRepository;

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/todo")
public class TodoController {

    private final TodoRepository todoRepository;

    @GetMapping
    List<Todo> getAll() {
        return todoRepository.findAll();
    }

    @GetMapping("{id}")
    Todo get(@PathVariable("id") Long id) {
        return todoRepository.findById(id).get();
    }

    @PostMapping
    Todo insert(@Validated @RequestBody Todo todo) {
        return todoRepository.save(todo);
    }

    @DeleteMapping("{id}")
    void delete(@PathVariable("id") Long id) {
        todoRepository.deleteById(id);
    }
}
main/java/com/exsample/todo/controller/TodoController.java
package com.example.todoapp.repository;

import java.util.List;

import org.springframework.data.repository.CrudRepository;
import com.example.todoapp.entity.Todo;

public interface TodoRepository extends CrudRepository<Todo, Long> {
    /** 全件取得 */
    List<Todo> findAll();
}
main/java/com/exsample/todo/config/ServletCustomizer.java
package com.example.todoapp.config;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

@Component
public class ServletCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        // vue-routerをHistoryモードで動かすと、/以外が404となってしまうため、index.htmlに飛ばすように修正。
        ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/index.html");
        factory.addErrorPages(error404Page);
    }
}
main/java/com/exsample/todo/config/ServletCustomizer.java
package com.example.todoapp.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // viteとの開発用に、3000からのアクセスを許可する。
        registry.addMapping("/api/**")
                .allowedMethods("*")
                .allowedOrigins("http://localhost:3000");
    }
}
main/resource/data.sql
TRUNCATE TABLE TODO;
INSERT INTO TODO (TITLE, DONE) VALUES ('起きる', 0);
INSERT INTO TODO (TITLE, DONE) VALUES ('歯を磨く', 1);
main/resource/schema.sql
-- Oracleの場合、最初にテーブルを手動追加してから、コメントを外す。
-- DROP TABLE TODO CASCADE CONSTRAINTS;

-- テーブル作成
CREATE TABLE TODO
(
    ID    NUMBER(15) GENERATED ALWAYS AS IDENTITY,
    TITLE VARCHAR2(100 Byte),
    DONE  BOOLEAN    -- Oracleの場合、NUMBER(1)
);
ALTER TABLE TODO ADD CONSTRAINT PK_TODO PRIMARY KEY (ID);

デバック方法

サイドバーにある「実行とデバック」から同ボタンを選択し、Javaを選択すると、プログラムが実行されます。その後、構成を保存する場合は、プログラムが実行している状態で、設定ボタンで、再度Javaを選択すると、launch.jsonが作成されます。

動作確認用のURLは以下の通り。新規登録する場合は、POSTの際にidを削除します。

Vite(Vue.js)版のToDoアプリ修正

前回作成したToDoアプリに対して、Vite側にaxiosをinstallして、REST API版に変更していきます。

> npm i axios

REST APIとの連携

REST APIとの連携は、新たにserviceクラスを作成して、その中で管理するようにしていきます。
その際、REST APIでToDoを新規登録するときは、idをnullにしたいので、idの後に?をつけていきます。ロジックの部分が、javaっぽくなってきましたね。

services/TodoService.ts
import axios from "axios";
import { reactive } from "vue";
import { Task } from "../models/Task";

class TodoService {
  // RESTAPI URL
  private RESTAPI_URL = "/api/todo/";
  // タスクリスト
  private tasks: Task[] = reactive([]);

  // タスクを取得する。
  get todoTasks(): Task[] {
    return this.tasks;
  }
  // 全タスクを取得する。
  public getAllTasks(): void {
    axios.get<Task[]>(this.RESTAPI_URL).then((res) => {
      Array.prototype.push.apply(this.tasks, res.data);
    });
  }
  // タスクを追加する。
  public addTask(newTaskTitle: string): void {
    const newTask: Task = {
      title: newTaskTitle,
      done: false,
    };
    axios.post<Task>(this.RESTAPI_URL, newTask).then((res) => {
      this.tasks.push(res.data);
    });
  }
  // タスクを削除する。
  public deleteTask(id?: number): void {
    const index = this.tasks.findIndex((t) => t.id === id);
    if (index !== undefined) {
      this.tasks.splice(index, 1);
      axios.delete(this.RESTAPI_URL + id);
    }
  }
  // タスクのDoneを変更する。
  public doneTask(id?: number): void  {
    const task = this.tasks.find((t) => t.id === id);
    if (task !== undefined) {
      task.done = !task.done;
      axios.post<Task>(this.RESTAPI_URL, task);
    }
  }
}
export default new TodoService();
views/TodoList.vue
<script setup lang="ts">
import todoService from "../services/TodoService";
import TaskAdd from "../components/TaskAdd.vue";
import TaskList from "../components/TaskList.vue";

// taskをすべて取得する。
todoService.getAllTasks();
</script>

<template>
  <h1 class="mt-4">Todo List</h1>
  <div class="row">
    <div class="col-xl-6 col-md-6">
      <TaskAdd @add="(newTaskTitle) => todoService.addTask(newTaskTitle)"></TaskAdd>
      <TaskList :tasks="todoService.todoTasks" @delete="(id) => todoService.deleteTask(id)" @done="(id) => todoService.doneTask(id)"></TaskList>
    </div>
  </div>
</template>
models/Task.ts
// idに?を追加
export interface Task {
  id?: number;
  title: string;
  done: boolean;
}
components/TaskList.vue
// idに?を追加
const emit = defineEmits<{
  (eventName: "done", id?: number): void;
  (eventName: "delete", id?: number): void;
}>();

デバック方法

REST APIの連携する際、データの中身を見たいときもあると思うんで、デバック方法についても紹介します。以前は「Debugger for Chrome」という拡張機能を入れないとデバックできなかったのですが、2021/7/16以降は、VSCodeに標準装備されたため不要になりました。関連記事

  1. サイドバーにある「実行とデバック」から同ボタンを選択し、Chromeを選択する。※launch.jsonのファイルが残っている場合は、ファイルを削除しないとプルダウンが選べません。あと、EdgeはLOCALでしか選択できないようです。
  2. その時に作成されるlaunch.jsonのポート番号を3000に変更する。
  3. 「実行とデバック」で、「Run Script:dev」を実行し、その後「Launch Chrome against localhost」を実行する。
    ※Run Script:devが表示されない場合は、Node.js...からdevを選択する。
  4. 好きなソースの位置にブレークポイントを設定してみて、デバックできるか確認する。ちなみに、デバック中はブレイクポイントは設定できません。

Spring Bootとの連携

Spring Bootと連携する際は、以下の修正を行います。修正後は、「実行とデバック」でNode.js...からbuildを実行すると、jsファイルが生成されますので、Javaで動作確認(http://localhost:8080)してみてください。

  • javaのフォルダ配下に、webフォルダ(どこでもよい)を作成し、ソースを移動する。
  • /apiに対するproxy設定(8080に変更)を追加
  • build時の出力先を、Java側のsrc/main/resources/staticに変更
components/TaskList.vue
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",
  },
  server: {
    proxy: {
      "/api": "http://localhost:8080",
    },
  },
});

おわりに

今回は、いつも仕事で使っているSpring BootでRESTサービスを構築しました。他にもDenoやnode.jsでREST APIを構築すると、Typescriptのみで開発できるので、楽かもしれません。

引き続き、Vite関連の記事は掲載していこうと思いますので、よろしくお願いいたします。

Discussion