概要
Spring BootのWebFluxを利用してSPAをホストする方法をまとめてみました。
HerokuのみでSPAアプリをホストするときの参考になれば。
成果物はGitHubにあります。
https://github.com/seijikohara/springboot2-vue3-heroku
この記事の内容をベースにしているHerokuのアプリも作成してみました。
R2DBC / Flyway / Kotest / SpringMockk / Vue3 / TypeScirptで構築してHeroku(の無料プラン)で公開しています。
- https://web-dev-tools.herokuapp.com/ (無料プランなので起動に30秒ほどがかかることあります)
利用するフレームワーク・ライブラリ
ビルドツール
-
Gradle
- Kotlin DSL
サーバサイド
フロントエンド
VueやReactなどお好きなもを。
ここではVue3を利用しました。
プロジェクトの作成
build.gradle.ktsの中身
生成されたbuild.gradle.ktsを変更していきます。
HerokuでのSlugビルド時にnpmを実行してVue3のSPAをビルドするために、com.github.node-gradle.node
プラグインを導入しました。
herokuStageタスクがSlugコンパイル時に実行される想定となります。
Kotlinのバージョンアップをしたりして、最終的に下記の様になりました。
import com.moowork.gradle.node.npm.NpmTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.3.5.RELEASE"
id("io.spring.dependency-management") version "1.0.10.RELEASE"
id("com.github.node-gradle.node") version "2.2.4"
kotlin("jvm") version "1.4.10"
kotlin("plugin.spring") version "1.4.10"
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.springframework.boot:spring-boot-starter-webflux")
developmentOnly("org.springframework.boot:spring-boot-devtools")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
/**
* Node.js
*/
node {
version = "12.19.0"
npmVersion = "6.14.8"
download = true
}
val npmInstallDependencies by tasks.registering(NpmTask::class) {
setArgs(listOf("install"))
setExecOverrides(closureOf<ExecSpec> {
setWorkingDir("./frontend")
})
}
val npmRunBuild by tasks.registering(NpmTask::class) {
// Before buildWeb can run, installDependencies must run
dependsOn(npmInstallDependencies)
setArgs(listOf("run", "build", "--", "--dest", "../src/main/resources/static"))
setExecOverrides(closureOf<ExecSpec> {
setWorkingDir("./frontend")
})
}
/**
* Heroku
*/
val herokuStageBuildFrontend by tasks.registering {
group = "heroku"
dependsOn(npmRunBuild)
doLast {
delete("frontend/node_modules") // npm run buildを実行して不要になったnode_modulesディレクトリを削除してSlugサイズを稼ぐ
}
}
val herokuStageBuild by tasks.registering {
group = "heroku"
dependsOn("bootJar")
mustRunAfter(herokuStageBuildFrontend) // フロントエンドのビルドファイルをboot-jarに含めるため先に実行させる
}
val herokuStage by tasks.registering {
group = "heroku"
dependsOn(herokuStageBuild)
dependsOn(herokuStageBuildFrontend)
}
Vue CLIでVue3のフロントエンドを作成
今度はVue CLIを使ってSPAのフロントエンドを作成します。
Use history mode for router?はNoにしてください。
$ vue create frontend
Vue CLI v4.5.8
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Router, Vuex, CSS Pre-processors, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) No
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with dart-sass)
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In package.json
? Save this as a preset for future projects? No
Gradleでフロントエンドをビルド
./gradlew npmRunBuildコマンドを実行してフロントエンドをビルドしてみます。
ビルドしたファイルはsrc/main/resources/staticに出力されます。.gitignoreでコミットの対象とならない様にしておきましょう。
/を/index.htmlにフォワードする
このままでは、Webアプリを表示させるためにhttps://heroku-app.com/index.htmlと言った様に、URLにindex.htmlの指定が必要です。
https://heroku-app.com/で動作する様にしてみます。
-
application.propertiesを作成する。
application.index-file=classpath:/static/index.html
-
application.propertiesで指定したindex.htmlのResourceを保持する@Configurationを作成する。
@Configuration
@EnableConfigurationProperties(ApplicationProperties::class)
class ApplicationConfig
@ConstructorBinding
@ConfigurationProperties(prefix = "application")
data class ApplicationProperties(
val indexFile: Resource,
)
-
index.htmlへフォワードするためのハンドラーを用意する。
@Component
class IndexHandler(
@Autowired private val applicationProperties: ApplicationProperties
) {
fun getIndexHtml(request: ServerRequest): Mono<ServerResponse> {
return ServerResponse.ok()
.contentType(MediaType.TEXT_HTML)
.bodyValue(applicationProperties.indexFile)
}
}
-
/の時にIndexHandlerがレスポンスを返すルーティングを設定する。
@Configuration
class RoutingConfig(
@Autowired private val indexHandler: IndexHandler,
) {
@Bean
fun apiRouter() = router {
accept(MediaType.ALL).nest {
GET("/", indexHandler::getIndexHtml)
}
}
}
ローカルでSpring Bootを起動してみる
Spring Bootを起動し、src/main/resources/staticに出力されたファイルが参照できるか確認してみます。
$ ./gradlew bootRun
このタスクは完了せずにずっと動きます。
ログにNetty started on port(s): 8080が出てきたら、SpringBootの起動は完了しているのでブラウザで http://localhost:8080 へアクセスしてみます。
Vue CLIで作成したWelcomeページが表示されていれば成功です。
Herokuにデプロイするための設定ファイルを作成
Web Dynoで実行するコマンドをProcfileに記述します。
web: java $JAVA_OPTS -jar build/libs/*.jar --server.port=$PORT com.example.demo.DemoApplication
次に、Herokuの設定情報であるapp.jsonを作成します。
GRADLE_TASKの項目が重要で、Slugコンパイル時に実行するGradleのタスクを設定しています。
GradleのBuildpackを利用した場合、SpringBootの使用を検知すると、デフォルトではbuild -x testが実行されます。
{
"name": "springboot2-vue3-heroku",
"description": "Demo springboot2-vue3-heroku",
"repository": "https://github.com/seijikohara/springboot2-vue3-heroku",
"env": {
"GRADLE_TASK": "herokuStage"
},
"addons": [
{
"plan": "papertrail"
}
],
"stack": "heroku-20",
"buildpacks": [
{
"url": "heroku/gradle"
}
]
}
今回はJava11を利用したいため、system.propertiesを作成し、Herokuが利用するJavaのバージョンを指定します。
java.runtime.version=11
Herokuでアプリを起動する
app.jsonを用意したため、Heroku Buttonで簡単にデプロイできます。
実際にHerokuにデプロイして動作することを試してみてください。
課題
-
/->index.htmlのルーティングしかできないので、SPAでHistoryモードが使えない…- 解決策募集中です。
