2018年3月1日に Spring Boot 2.0 がリリースされてから半年がたち[参考文献1]、Kotlin の正式サポートや Spring WebFlux によるリアクティブ Web プログラミングといったパラダイムシフトが徐々に広まりをみせつつある今日このごろです。 そしてこれらバックエンド技術と、進化を続ける JavaScript フレームワークによるフロントエンド技術を組合せて Web アプリやシステムを開発するというのはよくあるパターンになっています。一方で、この組合せの境界を言語的にまたぐアプローチやフレームワークもあり、これはこれで開発チームの構成によってはすごくフィットすることがあります。
ところでフロントエンド界隈は生き残っているものでは Angular 派, React 派, Vue.js 派の 3 大派閥があり[参考文献2]、また最近では Mithril など新進気鋭なものも見かけるようになりました。 そしてそれぞれのフレームワークをベースにしたフレームワークも多数登場しており[参考文献3]、有名所では SSR (Server Side Rendering) を実現する Next.js (React ベース) や Nuxt.js (Vue.js ベース) などが挙げられるでしょう。
今回は DB 操作などを WebFlux, SPA (Single Page Application) 画面を Vue.js を用い、更に Vue.js のビルド結果のサーブにも WebFlux を用いる構成を考えてみます (もちろん、他のフレームワークにも応用が効きます)。この構成の利点は 3 つあります:
- フロントエンドコードとバックエンドコードをひとまとめに管理できる
- フロントエンド開発者とバックエンド開発者の双方が手軽に両者結合したテストを実行できる
- 1つのアプリケーション (単一の jar ファイル) としてデプロイできる
欠点としては、そもそも別々にデプロイしたい (フロントのサーブとバックエンドを別々にスケールしたい) 場合にはハマらないのと、ディレクトリ構成が少し複雑になることが挙げられます。
基本戦略
Spring WebFlux は Gradle, Vue.js を npm でビルドし、双方のディレクトリレイアウトの慣習を一切破らずに開発したいとします。
すると問題になるのが src
の扱いです。Gradle と npm で src 内のディレクトリ構成が全く違うので、ここは分けるのが賢明です。
もちろん src
改名という荒療治でも解決はできますが、ここは Gradle のサブプロジェクト機能を最大限に活用してみましょう。
概念上はこんな構成になります:
app
├── backend
│ ├── gradle サブプロジェクト
│ └── src
│ └── webflux コード
├── gradle ルートプロジェクト
├── npm ファイル
└── src
└── vue.js コード
それぞれ、次のような役割を振ります:
- npm: Vue.js のビルド
- gradle ルートプロジェクト: gradle-node-plugin を用いた npm ビルドの呼び出し
- gradle サブプロジェクト: WebFlux のビルド
プロジェクトの作成
用意するものは以下の 3 つです:
npm
コマンド (Node.js についてくる)vue
コマンド (npm install -g @vue/cli
)gradle
コマンド (Homebrew や Chocolatey で導入が楽)
⚠️ Vue CLI の npm パッケージはバージョン 2 までが vue-cli
で 3 からが @vue/cli
です[参考文献4]。今回は 3 ベースで説明しています。
$ vue create webflux-vuejs-demo
💡 このコマンドは与えられた名前でディレクトリを作り、その中にプロジェクトファイルを生成してくれます。
実行すると、次のように聞かれます。
Vue CLI v3.0.0-rc.8
? Please pick a preset:
❯ default (babel, eslint)
Manually select features
キーボードの ⬆️ と ⬇️ で切り替えられます。ここは default のまま Enter をターンっとタイプします。 また、 “Your connection to the default npm registry seems to be slow.” と出て、別の npm リポジトリをお勧めされることがあります。私の場合、 taobao.org とか・・・中国っぽい。
実行が終わると、次のコマンドで試すよう促されます。実際に動かしてみましょう。
$ cd webflux-vuejs-demo
$ npm run serve
ブラウザで http://localhost:8080/ にアクセスすると…
めでたく表示されました!って、満足してはいけません。話はここからです・・・。
さきほど npm を叩いたのと同じディレクトリで、以下のコマンドを実行します。
$ gradle init wrapper
build.gradle
ほかいくつかのファイルが生成されました。ここで .gitignore
に以下を追加します。
# Gradle
.gradle
build
build.gradle
を編集します。元からあるコメント行はいらないので消してしまいましょう。
plugins {
id "com.moowork.node" version "1.2.0"
}
node {
version = '10.7.0'
download = true
}
これで、 npm と同じことが gradle からできるようになりました。
npm | gradle |
---|---|
npm install | ./gradlew npm_install |
npm run lint | ./gradlew npm_run_lint |
npm run build | ./gradlew npm_run_build |
npm run serve | ./gradlew npm_run_serve |
さらに、よく使うタスクについては、以下のように別途定義することで ./gradlew runBuild
のように呼び出せます。
task runBuild(type: NpmTask) {
args = ['run', 'build']
}
💡 ./gradlew tasks --all
で叩けるタスク一覧とルールを確認することができます。
WebFlux の構成
gradle-node-plugin (com.moowork.node) の素晴らしさを体感したら、いよいよ WebFlux の構成です。
先の基本戦略に従い、 backend
サブプロジェクトを作成します。
$ mkdir backend
$ vi backend/build.gradle
backend/build.gradle
:
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.2.51'
id 'org.jetbrains.kotlin.plugin.spring' version '1.2.51'
id 'org.springframework.boot' version '2.0.3.RELEASE'
id 'io.spring.dependency-management' version '1.0.6.RELEASE'
}
repositories {
jcenter()
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
compile 'org.jetbrains.kotlin:kotlin-reflect'
compile 'org.springframework.boot:spring-boot-starter-webflux'
testCompile 'org.springframework.boot:spring-boot-starter-test'
}
compileKotlin {
kotlinOptions {
jvmTarget = '1.8'
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = '1.8'
}
}
bootJar {
baseName = 'webflux-vuejs-demo'
archiveName = baseName + '.' + extension
}
そしてプロジェクトルートにある settings.gradle
に以下を追記します。
include 'backend'
Router Function を実装します。Kotlin で書いてみました。
backend/src/main/kotlin/com/github/mikan/demo/App.kt
:
package com.github.mikan.demo
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.annotation.Bean
import org.springframework.core.io.Resource
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.HandlerFunction
import org.springframework.web.reactive.function.server.RequestPredicates
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.RouterFunctions
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono
@SpringBootApplication
class App
fun main(args: Array<String>) {
SpringApplication.run(App::class.java, *args)
}
@Component
class IndexHandler {
@Value("classpath:/static/index.html")
private lateinit var indexHtml: Resource
@Bean
fun indexRoutes(): RouterFunction<ServerResponse> {
return RouterFunctions.route(RequestPredicates.GET("/"), HandlerFunction { get(it) })
}
fun get(request: ServerRequest): Mono<ServerResponse> {
return ServerResponse.ok().contentType(MediaType.TEXT_HTML).syncBody(indexHtml)
}
}
💡 Spring Boot の Welcome Page 機能により本来は自動的に /static/index.html
が /
にマッピングされますが、 WebFlux ではそれが実現されていません[参考文献5]。そのため参考文献に示されているワークアラウンドとして Resource
として取得して手作業でルーティングしています。今後のアップデートで不要になる可能性があります。
Vue.js ビルド成果物の連携
先程 Kotlin のコードに classpath:/static/index.html
とあったように、Vue.js のビルド成果物を backend に渡さないとこのコードは動きません。
といっても、実際これを実現するのは簡単です。まずはディレクトリを作成します。
$ mkdir -p backend/src/main/resources/static
.gitignore
に以下を追記します。
# Frontend output for backend
/backend/src/main/resources/static
package.json
の build
を次のように修正します。
- "build": "vue-cli-service build",
+ "build": "vue-cli-service build --dest backend/src/main/resources/static",
これで準備ができました。いよいよビルドして実行してみましょう!
$ ./gradlew npm_run_build
$ ./gradlew bootRun
結果は同じですね。ですが、確かに WebFlux で動いています。あとはフロントエンド・バックエンド二手に分かれてガシガシ開発するのみです。
参考までに、今回作成した一式のサンプルを GitHub に公開してみました。ご利用ください。
Vue Router の history mode の使用
Vue.js には素晴らしいルーティング機構 Vue Router が付属しており、2つの動作モード hash と history があります[参考文献6]。 hash モードは URL に /#/ を入れることで完全な URL をシミュレートしていますが、これを取り除く手段として HTML5 の History API 機能を用いた history モードが提供されています。この場合、最初の一発目だけ WebFlux が index.html をサーブし、その後は JavaScript の世界でページ遷移が実現されます (ページごとに WebFlux に取りにいきません) 。
その一発目が 404 にならないようにするには、ありったけのパスを index.html に降ってあげる必要があります。 シンプルな解決策としては、 (使わない) パスパラメーターでパターンを定義してしまうというアイデアがあります。
@Bean
fun indexRoutes(): RouterFunction<ServerResponse> {
val handler = HandlerFunction { get(it) }
return RouterFunctions.route(RequestPredicates.GET("/"), handler)
.andRoute(RequestPredicates.GET("/page"), handler)
.andRoute(RequestPredicates.GET("/page/{sub1}"), handler)
.andRoute(RequestPredicates.GET("/page/{sub1}/{sub2}"), handler)
}
WebFlux は他の静的リソース (js や css 等) も提供しますから、トップレベルをパスパラメーターにすることはできません (例では page
としています)。そして、他に API エンドポイントなども作ることを考えると、それらと競合しないように URL 設計する必要があります。
Quasar Framework の使用
Vue.js ベースのフレームワークの一つに Quasar Framework (くえーさーふれーむわーく) があります[参考文献7]。 こちらで開発する際、プロジェクト作成時のコマンドと、先のビルド成果物の連携の設定手段が変わってきます。 作り方を順を追って説明します。
追加で必要なもの:
quasar
コマンド (npm install -g quasar-cli
)vue init
コマンド (npm install -g @vue/cli-init
)
手順:
$ quasar init webflux-quasar-demo
? Project name (internal usage for dev) webflux-quasar-demo
? Project product name (official name) Quasar App
? Project description A Quasar Framework app
? Author xxx <xxx@xxx.xxx>
? Check the features needed for your project: ESLint
? Pick an ESLint preset Standard
? Cordova id (disregard if not building mobile apps) org.cordova.quasar.app
? Should we run `npm install` for you after the project has been created? (recommended) NPM
手順の統一のため、最後の質問を NPM
にしています。それ以外はデフォルトのまま Enter です。
Gradle プロジェクトの構成についてはこれまでの説明と一緒ですが、ビルド成果物の出力場所の設定箇所が package.json
から quasar.conf.js
に変わり、次のように build
セクションに distDir
としてパスを追加することになります。
...
build: {
+ distDir: 'backend/src/main/resources/static',
scopeHoisting: true,
vueRouterMode: 'history',
...
そして、これまでと同様 ./gradlew npm_run_build
でビルドするために、 package.json
に build
スクリプトを追加します。
"scripts": {
+ "build": "quasar build",
"lint": "eslint --ext .js,.vue src",
"test": "echo \"No test specified\" && exit 0"
},
これで、 ./gradlew npm_run_build bootRun
で WebFlux サーブされます。
あともうひとつ。quasar init
コマンドは色々なファイルを生成しますが、その中に .editorconfig
があります。
しかしこれは全てのファイルに 2 スペースインデントを強制するため、4 スペースが標準な Kotlin がひどいことになります。
そこで、以下のように修正します。
- [*]
+ [*.{js,vue}]
charset = utf-8
indent_style = space
indent_size = 2
IntelliJ だとデフォルトで .editorconfig をサポートしているので、存在を忘れるとなぜ 2 スペースになるのかわからず混乱します 😱
参考までに、今回作成した Quasar 版のサンプルも GitHub に公開してみました。ご利用ください。
Happy hacking!
参考文献
- Spring Boot 2.0 goes GA
- kamranahmedse/developer-roadmap: Roadmap to becoming a web developer in 2018
- 25+ Best Vue.js Frameworks » CSS Author
- Overview | Vue CLI 3
- Welcome page(classpath:/static/index.html) not being resolved for / uri in webflux project · Issue #9785 · spring-projects/spring-boot
- HTML5 History モード | Vue Router
- Quasar Framework