1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

HerokuにSpringBoot(WebFlux)でホストするSPAをデプロイする

1
Last updated at Posted at 2020-11-06

概要

Spring BootのWebFluxを利用してSPAをホストする方法をまとめてみました。
HerokuのみでSPAアプリをホストするときの参考になれば。

成果物はGitHubにあります。
https://github.com/seijikohara/springboot2-vue3-heroku

この記事の内容をベースにしているHerokuのアプリも作成してみました。
R2DBC / Flyway / Kotest / SpringMockk / Vue3 / TypeScirptで構築してHeroku(の無料プラン)で公開しています。

利用するフレームワーク・ライブラリ

ビルドツール

サーバサイド

フロントエンド

VueやReactなどお好きなもを。
ここではVue3を利用しました。

プロジェクトの作成

[Spring Initializr]
(https://start.spring.io/#!type=gradle-project&language=kotlin&platformVersion=2.3.5.RELEASE&packaging=jar&jvmVersion=11&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=devtools,webflux,configuration-processor)でGradleプロジェクトを生成しましょう。

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を作成する。
src/main/resources/application.properties
application.index-file=classpath:/static/index.html
  • application.propertiesで指定したindex.htmlResourceを保持する@Configurationを作成する。
com/example/demo/config/ApplicationConfig.kt
@Configuration
@EnableConfigurationProperties(ApplicationProperties::class)
class ApplicationConfig

@ConstructorBinding
@ConfigurationProperties(prefix = "application")
data class ApplicationProperties(
    val indexFile: Resource,
)
  • index.htmlへフォワードするためのハンドラーを用意する。
com.example.demo.handler.IndexHandler
@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がレスポンスを返すルーティングを設定する。
com.example.demo.config.RoutingConfig
@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に出力されたファイルが参照できるか確認してみます。

SpringBootを起動
$ ./gradlew bootRun

このタスクは完了せずにずっと動きます。
ログにNetty started on port(s): 8080が出てきたら、SpringBootの起動は完了しているのでブラウザで http://localhost:8080 へアクセスしてみます。
Vue CLIで作成したWelcomeページが表示されていれば成功です。

image.png

Herokuにデプロイするための設定ファイルを作成

Web Dynoで実行するコマンドをProcfileに記述します。

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が実行されます。

app.json
{
  "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のバージョンを指定します。

system.properties
java.runtime.version=11

Herokuでアプリを起動する

app.jsonを用意したため、Heroku Buttonで簡単にデプロイできます。
実際にHerokuにデプロイして動作することを試してみてください。
Deploy

課題

  • / -> index.htmlのルーティングしかできないので、SPAでHistoryモードが使えない…
    • 解決策募集中です。

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?