かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Gradle multi-project 内に React アプリ(frontend-app)と Spring Boot アプリ(backend-app)を作成する

概要

記事一覧はこちらです。

Gradle multi-project 内に React+Typescript+Tailwind CSS+React Query 構成の frontend 用アプリと、Sping Boot+springdoc-openapi 構成の REST API を提供する backend 用アプリを作成し、以下の内容を実現します。

f:id:ksby:20210428231340p:plain

  • Spring Boot の REST API には @CrossOrigin を付与しません。
  • 開発中も本番も frontend から backend の REST API を呼び出す時の URL はパス名のみの /user/1 にします(http://localhost:8080/user/1 のようにホスト名等は記述しません)。
  • 開発中は CRACOyarn start で起動する webpack-dev-server に proxy を設定して、webpack-dev-server 経由で backend 用アプリの REST API にアクセスします。  ※Configuring the Proxy Manually に記述されている src/setupProxy.js を作成・設定する方法もありますが、Tailwind CSS を使う時に CRACO を入れているので今回は CRACO を使う方法を採用しています。
  • build タスク実行時に yarn build コマンドを実行 → React アプリのファイルを Spring Boot アプリへコピーしてから、Spring Boot アプリの jar ファイルを生成します。

今回作成したソースは ksbysample-react-springboot に置いています。

参照したサイト・書籍

  1. React Query
    https://react-query.tanstack.com/

  2. CRACO - Configuration
    https://github.com/gsoft-inc/craco/blob/master/packages/craco/README.md

  3. webpack - DevServer - devServer.proxy
    https://webpack.js.org/configuration/dev-server/#devserverproxy

  4. Create React App - Proxying API Requests in Development
    https://create-react-app.dev/docs/proxying-api-requests-in-development/

  5. node-gradle / gradle-node-plugin - Usage
    https://github.com/node-gradle/gradle-node-plugin/blob/master/docs/usage.md

目次

  1. ksbysample-react-springboot レポジトリを作成して clone する
  2. Spring Initializr でサンプルプロジェクトを作成して Gradle Wrapper のファイルをコピーする
  3. gradlew init を実行し、build.gradle があるサブプロジェクトを自動認識するよう settings.gradle を変更する
  4. backend-app サブプロジェクトを作成し、REST API を実装する
  5. npx create-react-app frontend-app --template typescript を実行して frontend-app サブプロジェクトを作成する
  6. Tailwind CSS、CRACO をインストールする
  7. React Query をインストールする
  8. frontend-app に REST API を呼び出して取得したデータを表示する処理を実装する
  9. craco.config.js を変更し webpack-dev-server に proxy の設定を追加する
  10. Spring Boot アプリを IDEA から起動、React アプリを yarn start で起動して動作確認する
  11. frontend-app サブプロジェクトで Gradle の build タスクが実行されるようにし、かつ build タスク実行時に yarn build コマンドが実行されるように設定する
  12. backend-app サブプロジェクトで build タスク実行時に frontend-app の build 下のファイルを backend-app/src/main/resources/static にコピーされるように設定する
  13. プロジェクトのルートディレクトリで Gradle の build タスクを実行する
  14. 生成された jar ファイルで Spring Boot アプリを起動して動作確認する

手順

ksbysample-react-springboot レポジトリを作成して clone する

ksbysample-react-springboot レポジトリ を作成し、D:\project-react\ksbysample-react-springboot に clone します。

Spring Initializr でサンプルプロジェクトを作成して Gradle Wrapper のファイルをコピーする

適当なディレクトリに Spring Initializr で Spring Boot のプロジェクトを作成し(Gradle を指定する)、Gradle Wrapper の以下のファイルを D:\project-react\ksbysample-react-springboot の下にコピーします。

gradlew init を実行し、build.gradle があるサブプロジェクトを自動認識するよう settings.gradle を変更する

gradlew init を実行します。

f:id:ksby:20210429104642p:plain

build.gradle があるサブプロジェクトを自動的に認識してくれるよう settings.gradle を以下の内容に変更します。

rootProject.name = 'ksbysample-react-springboot'

rootDir.eachFileRecurse { f ->
    if (f.name == "build.gradle") {
        String relativePath = f.parentFile.absolutePath - rootDir.absolutePath
        String projectName = relativePath.replaceAll("[\\\\\\/]", ":")
        if (projectName != ":buildSrc") {
            include projectName
        }
    }
}

IntelliJ IDEA で D:\project-react\ksbysample-react-springboot を開きます。

backend-app サブプロジェクトを作成し、REST API を実装する

Spring Initializr で D:\project-react\ksbysample-react-springboot の下に backend-app サブプロジェクトを作成します。

f:id:ksby:20210429124224p:plain f:id:ksby:20210429124324p:plain

D:\project-react\ksbysample-react-springboot\backend-app の下は src ディレクトリ、build.gradle 以外のディレクトリ、ファイルを削除します。

build.gradle を以下の内容に変更します。

plugins {
    id 'org.springframework.boot' version '2.4.5'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'ksbysample'
version = '0.0.1-SNAPSHOT'

sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11

[compileJava, compileTestJava]*.options*.encoding = "UTF-8"
[compileJava, compileTestJava]*.options*.compilerArgs = ["-Xlint:all,-options,-processing,-path"]

configurations {
    compileOnly.extendsFrom annotationProcessor

    // annotationProcessor と testAnnotationProcessor、compileOnly と testCompileOnly を併記不要にする
    testAnnotationProcessor.extendsFrom annotationProcessor
    testImplementation.extendsFrom compileOnly
}

repositories {
    mavenCentral()
}

dependencies {
    def lombokVersion = "1.18.20"

    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // for lombok
    // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
}

test {
    useJUnitPlatform()
}

backend-app/src/main/java/ksbysample/backendapp/User.java を作成し、以下の内容を記述します。

package ksbysample.backendapp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

    private String name;

    private int age;

}

backend-app/src/main/java/ksbysample/backendapp/UserApiController.java を作成し、以下の内容を記述します。

package ksbysample.backendapp;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserApiController {

    @GetMapping("/{id}")
    public User findById(@PathVariable String id) {
        return User.builder()
                .name(String.format("Tanaka Taro No.%s", id))
                .age(25)
                .build();
    }

}

npx create-react-app frontend-app --template typescript を実行して frontend-app サブプロジェクトを作成する

D:\project-react\ksbysample-react-springboot で npx create-react-app frontend-app --template typescript コマンドを実行し、frontend-app サブプロジェクトを作成します。

f:id:ksby:20210429131900p:plain f:id:ksby:20210429132002p:plain f:id:ksby:20210429132059p:plain

cd /d/project-react/ksbysample-react-springboot/frontend-app した後 yarn start コマンドを実行し、画面が表示されることを確認します。

Tailwind CSS、CRACO をインストールする

以下のコマンドを実行し Tailwind CSS、CRACO をインストールします(React アプリと Spring Boot アプリを連携するのに Tailwind CSS は必須ではありません)。

  • yarn add tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat
  • yarn add -D postcss@^7 autoprefixer@^9
  • yarn add @tailwindcss/forms
  • yarn add -D @craco/craco

package.json 内の scripts で react-scriptscraco に変更します。

  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    "eject": "craco eject"
  },

プロジェクトのルートディレクトリ直下に craco.config.js を作成し、以下の内容を記述します。

module.exports = {
  style: {
    postcss: {
      plugins: [
        require('tailwindcss'),
        require('autoprefixer'),
      ],
    },
  },
}

npx tailwindcss init -p を実行して tailwind.config.js、postcss.config.js を作成した後、tailwind.config.js を以下の内容に変更します。

module.exports = {
  purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),
  ],
}

frontend-app/src/index.css を以下の内容に変更します。

@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind forms;

ここに書いた内容については React+Tailwind CSS+Storybook のプロジェクトを作成する 参照。

React Query をインストールする

React アプリから Spring Boot アプリの REST API を呼び出すのに React Query を使用します。yarn add react-query コマンドを実行してインストールします。

frontend-app に REST API を呼び出して取得したデータを表示する処理を実装する

frontend-app/src/index.tsx を以下のように変更します。

import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';

import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const queryClient = new QueryClient();

ReactDOM.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
  • 以下の行を追加します。
    • import { QueryClient, QueryClientProvider } from 'react-query';
    • const queryClient = new QueryClient();
  • <App /> の前後に <QueryClientProvider client={queryClient}> ... </QueryClientProvider> を追加します。

frontend-app/src/App.tsx を以下の内容に変更します。REST API を呼び出す時の URL は /user/1 固定です。

import { FC } from 'react';
import { useQuery } from 'react-query';

type User = {
  name: string;
  age: number;
};

const getUser = async () => (await fetch('/user/1')).json();

const App: FC = () => {
  const { data: user } = useQuery<User>(['user', 1], () => getUser());

  return (
    <div className="pt-4 pl-4 text-red-600 text-4xl font-extrabold">
      {user?.name}{user?.age} 歳です。
    </div>
  );
};

export default App;

craco.config.js を変更し webpack-dev-server に proxy の設定を追加する

frontend-app/craco.config.js に devServer: { ... } を追加して、/user/... でアクセスされたら Spring Boot アプリに転送するようにします。

module.exports = {
  style: {
    postcss: {
      plugins: [require('tailwindcss'), require('autoprefixer')],
    },
  },
  devServer: {
    proxy: [
      {
        context: ['/user'],
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    ],
  },
};

Spring Boot アプリを IDEA から起動、React アプリを yarn start で起動して動作確認する

Spring Boot アプリを IDEA から起動します。

f:id:ksby:20210429200623p:plain

React アプリを yarn start コマンドで起動すると、

f:id:ksby:20210429200733p:plain f:id:ksby:20210429200844p:plain

ブラウザ上に REST API から取得したデータが表示されました。

f:id:ksby:20210429200959p:plain

Spring Boot アプリ、React アプリを停止します。

frontend-app サブプロジェクトで Gradle の build タスクが実行されるようにし、かつ build タスク実行時に yarn build コマンドが実行されるように設定する

サブプロジェクトに build.gradle を作成すれば Gradle multi-project のサブプロジェクトとして認識されるので、frontend-app の直下に build.gradle を作成し、以下の内容を記述します。

plugins {
    id "base"
    id "com.github.node-gradle.node" version "3.0.1"
}

task yarnBuild(type: YarnTask) {
    args = ["build"]
}
build.dependsOn yarnBuild
  • base プラグインを追加し、frontend-app サブプロジェクトで build、clean タスクが実行できるようにします。
  • gradle-node-plugin プラグインを追加し、build タスク実行時に yarn build コマンドが実行されるようにします。

IDEA の Gradle Tool Window で Reload ボタンをクリックすると frontend-app が Gradle のサブプロジェクトとして認識・表示され、その下に build、clean タスクが表示されます。

f:id:ksby:20210429212926p:plain:w300

backend-app サブプロジェクトで build タスク実行時に frontend-app の build 下のファイルを backend-app/src/main/resources/static にコピーされるように設定する

backend-app/build.gradle の一番下に以下の記述を追加します。

..........

clean.delete fileTree("src/main/resources/static").include("**/*")
task copyToStatic(type: Copy) {
    from project(":frontend-app").file("build")
    into "src/main/resources/static"
}
copyToStatic.dependsOn ":frontend-app:build"
processResources.dependsOn copyToStatic
  • clean タスク実行時に src/main/resources/static の下をクリアするように設定します。
  • build タスク実行時に、processResources タスクの前で copyToStatic タスクを実行し、frontend-app の build ディレクトリの下にあるディレクトリ・ファイル一式を backend-app/src/main/resources/static の下にコピーします。

IDEA の Gradle Tool Window で Reload ボタンをクリックしておきます。

プロジェクトのルートディレクトリで Gradle の build タスクを実行する

IDEA の Gradle Tool Window から clean → build タスクを実行すると、

f:id:ksby:20210429214412p:plain:w300

:frontend-app:yarnBuild タスクの後に :backend-app:copyToStatic タスクが実行されて、

f:id:ksby:20210429214724p:plain

backend-app/src/main/resources/static の下に React アプリのファイルがコピーされています。

f:id:ksby:20210429215139p:plain:w300

生成された jar ファイルで Spring Boot アプリを起動して動作確認する

コマンドプロンプトで backend-app/build/libs の下に移動してから java -jar backend-app-0.0.1-SNAPSHOT.jar コマンドを実行して Spring Boot アプリを起動した後、

f:id:ksby:20210429215514p:plain

ブラウザから http://localhost:8080/ にアクセスすると REST API から取得したデータが表示されました。

f:id:ksby:20210429215654p:plain

履歴

2021/04/29
初版発行。