SpringBoot内でReactを同時に実行するための環境を設定したやり方を解説する。
背景
RestAPIの成果をどう見せるか
プログラミングスクールでSpringBootによるWebアプリ開発を学び、それを活用したポートフォリオを作成しようと考えた。ただ、RestAPIが扱うのは情報のやり取りのみであり、それによって何が可能なのかはよく分かりづらい。面倒でもフロントエンドがあった方が成果がよくわかる。
データの入力
フロントエンドが存在しない場合にそのWebアプリで使用しているデータベースを操作するには直接MySQLから操作するか、PostmanのようなAPI開発ツールを使用するかという選択肢になりそうだが、入力ミスが多発しそうなのは明白であり、フロントエンド構築が求められる。
フロントエンドフレームワークの選択
SpringBootにフロントエンドを統合するためにはsrc/main/resources/staticフォルダにHTMLファイルを格納する必要がある。ただし、HTMLでは動的なレンダリングは難しいため、それを可能とするフレームワークを使用する必要がある、つまりコンパイルが必要。
フレームワークとしてはReactを選択した(あまり勉強しないで簡単に構築するためにはVue.jsがよかったかもしれないと少し反省)。
設定方法
Reactの導入
SpringBootプロジェクト内にReactプロジェクトを配置する。インストール等はここでは省略。
project/
├── .git/
├── .gradle/
├── .idea/
├── build/
├── gradle/
├── output/
├── react-app/ ※Reactプロジェクト
└── src/
これ以降は、SpringBootの実行に合わせてフロントエンドも準備されるようにbuild.gradleを設定していく。
build.gradleのタスクの理解
特に設定しないでApplicationを実行したときのタスクは以下の順で実施される。
-
compileJava
compileJavaタスクは、プロジェクト内のJavaソースファイルをコンパイルし、バイトコードを生成する。このタスクは、ソースコードの変更があった場合に実行され、コンパイルされたクラスファイルは後続のビルドプロセスで使用される。 -
processResources
processResourcesタスクは、プロジェクト内のリソースファイル(例えば、プロパティファイルやXMLファイル)を処理し、必要に応じてコピーや変換を行う。このタスクは、リソースが適切に配置され、最終的なアプリケーションパッケージに含まれることを保証する。 -
classes
classesタスクは、compileJavaとprocessResourcesの成果物をまとめて、クラスパスに必要なファイルを生成する。このタスクは、アプリケーションを実行するために必要なクラスファイルやリソースが整っていることを確認し、ビルドプロセスの一部として自動的に実行される。
以上より、processResources以前にsrc/main/resources/staticにReactのコンパイル成果物が格納されていないといけないことになる。
Reactアプリをビルドするタスクの追加
build.gradleに以下のタスクを追加する。これによりreact-app/build内にReactコンパイル成果物を格納できるようになる。実際にreact-appでビルドする際のコマンドは'npm run build'だが、ここでは"npm.cmd"が必要であることに注意が必要。
// Reactアプリをビルドするタスク
task buildReact(type: Exec) {
workingDir "$projectDir/react-app"
commandLine 'npm.cmd', 'run', 'build'
}
project/
├── .git/
├── .gradle/
├── .idea/
├── build/
├── gradle/
├── output/
├── react-app/
│ └── build/ ※reactコンパイル成果物の格納フォルダ
└── src/
└── main/
└── resources/
└── static/
Reactコンパイル成果物をコピーするタスクの追加
先ほどのコンパイル成果物をsrc/main/resources/staticにコピーするタスクをbuild.gradleに追加。このときdependsOnを用いて「buildReact→copyToStatic→processResources」の順にタスクが実行されるようにする。
// Reactアプリをビルドするタスク
task buildReact(type: Exec) {
workingDir "$projectDir/react-app"
commandLine 'npm.cmd', 'run', 'build'
}
// Reactのビルド結果をSpring Bootのstaticフォルダにコピーするタスク
task copyToStatic(type: Copy) {
dependsOn buildReact
from "$projectDir/react-app/build"
into "$projectDir/src/main/resources/static"
}
// パッケージング以前にReact関連タスクを実行する
processResources.dependsOn copyToStatic
project/
├── .git/
├── .gradle/
├── .idea/
├── build/
├── gradle/
├── output/
├── react-app/
│ └── build/ ※reactコンパイル成果物の格納フォルダ
└── src/
└── main/
└── resources/
└── static/ ※reactコンパイル成果物のコピー先
staticを削除するタスクを追加
Reactのコンパイル成果物の詳細は以下の通り。
├── react-app/
│ └── build/ ※reactコンパイル成果物の格納フォルダ
│ ├── asset-manifest.json
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ ├── robots.txt
│ └── static/
│ ├── css/
│ │ ├── main.3572f698.css
│ │ └── main.3572f698.css.map
│ └── js/
│ ├── 453.2a82e7ac.chunk.js
│ ├── 453.2a82e7ac.chunk.js.map
│ ├── main.f4025a23.js
│ ├── main.f4025a23.js.LICENSE.txt
│ └── main.f4025a23.js.map
このうちcss/やjs/内のファイルはindex.htmlに紐づいて表示内容のベースとなるものである。ファイル名のハッシュ部分はキャッシュバスティングのためにファイルが変更された際に、ブラウザが古いファイルを参照しないようにする役割を果たす。
先ほど追加したcopyToStaticでは元々staticフォルダが存在する場合は上書き保存となるが、これによりcss/やjs/内のファイルは毎回新規でコピーされることになるため、放っておくとフォルダ内に大量のファイルが存在することとなる。したがって、コピー前にstaticを消去するタスクを追加する。また、依存関係の変更を忘れてはいけない。
// Reactアプリをビルドするタスク
task buildReact(type: Exec) {
workingDir "$projectDir/react-app"
commandLine 'npm.cmd', 'run', 'build'
}
// static フォルダを削除するタスク
task deleteStatic(type: Delete) {
dependsOn buildReact // Reactのビルドが終わってからコピーを実行
delete "$projectDir/src/main/resources/static"
}
// Reactのビルド結果をSpring Bootのstaticフォルダにコピーするタスク
task copyToStatic(type: Copy) {
dependsOn deleteStatic // staticフォルダを削除してからコピーを実行
from "$projectDir/react-app/build"
into "$projectDir/src/main/resources/static"
}
// パッケージング以前にReact関連タスクを実行する
processResources.dependsOn copyToStatic
テスト時の対応
この段階での以下の各ケースでの実行タスクは以下の通り。
- Application実行
> Task :compileJava UP-TO-DATE
> Task :buildReact
> Task :deleteStatic
> Task :copyToStatic
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE
- Test実行
> Task :compileJava UP-TO-DATE
> Task :buildReact
> Task :deleteStatic
> Task :copyToStatic
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE
> Task :compileTestJava UP-TO-DATE
> Task :processTestResources UP-TO-DATE
> Task :testClasses UP-TO-DATE
Testにおいても先ほど設定したReact関連のタスクが実行されているが、テストにはフロントエンドは含まれていないため、Test実行時にはこれらを実行しないように設定が必要。そこで、build.gradleに以下を追加。
gradle.taskGraph.whenReady { taskGraph ->
if (taskGraph.hasTask(':test')) {
// テスト実行時にReact関連タスクを無効にする
tasks.buildReact.enabled = false
tasks.deleteStatic.enabled = false
tasks.copyToStatic.enabled = false
}
}
再度testを実行すると、タスクリストは下記の通り、React関連の3つのタスクがスキップされるようになった。これでひとまず完成。
> Task :compileJava UP-TO-DATE
> Task :buildReact SKIPPED
> Task :deleteStatic SKIPPED
> Task :copyToStatic SKIPPED
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE
> Task :compileTestJava UP-TO-DATE
> Task :processTestResources UP-TO-DATE
> Task :testClasses UP-TO-DATE
あとがき
今後AWS上にデプロイする際のことを少し考えたが、おそらくここの内容には変更はなさそう。ただ、そもそもの環境構築でひと手間かかりそうな予感がする。
続報あればまた記事投稿します。