環境
MacOS Mojave 10.14.1
scala 2.12.6
sbt 1.2.1
npm 6.4.1
node 8.12.0
基本構造
1. play frameworkのプロジェクトを作成する
なにはともあれ、まずはplayのプロジェクトを作成する。playの公式サイトをみてプロジェクトを作成したいディレクトリで次のコマンドを叩く。(sbtは入っているものとする。)
$ sbt new playframework/play-scala-seed.g8
2. public ディレクトリ内のものを全て削除する。
publicディレクトリ配下のファイルを先に削除しておきます。
$ rm -rf public/*
3. uiディレクトリを作成
プロジェクト配下にuiディレクトリを作成します。
$ mkdir ui
フロントエンドの構築
1. create-react-app
先ほど作成したuiディレクトリ内でcreate-react-appします。create-react-appは入っていない場合は別途インストールが必要です。
$ cd ui
$ create-react-app .
2. ncpとrimrafを追加でインストール。
同じくuiディレクトリ内でnpmまたはyarnでncpとrimrafインストールします。私はnpmでインストールしました。
$ npm install -D ncp rimraf
// または
$ yarn add ncp rimraf -D // こっちは確かめてない。
rimraf : Unixコマンドの rm -rf あたる処理をしてくれるもの
ncp : 再帰的にファイルとディレクトリーのコピーをしてくれるもの
3. package.jsonの修正
create-reacte-appの結果uiディレクトリ配下
にできたpackage.jsonを書き換える。
{
...
"scripts": {
...
"build": "rimraf ../public && react-scripts build && ncp build ../public && rimraf build",
...
},
...
"proxy": "http://localhost:9000",
...
}
scriptsの "build" のところは、
- プロジェクトのルートディレクトリ配下のpublicディレクトリを削除
- reactのソースコードのビルド
- ビルドして出来上がった、buildディレクトリを、publicディレクトリとしてプロジェクトのルートディレクトリにコピー
- 出来上がったbuildディレクトリの削除
を行なっているみたいです。
プロキシはplayの開発モード標準のポートの9000番に合わせておきます。理由は参考サイトに書いてあるけど英語力不足だった。
バックエンドの構築
続いては、play scala側の設定をしていきます。
1. FrontendCommands.scalaの作成
プロジェクトルートディレクトリ配下にあるprojectディレクトリに新たにFrontendCommands.scalaファイルを作成し、以下を記述します。
/**
* Frontend build commands.
* Change these if you are using some other package manager. i.e: Yarn
*/
object FrontendCommands {
val dependencyInstall: String = "npm install"
val test: String = "npm run test"
val serve: String = "npm run start"
val build: String = "npm run build"
}
2. FrontendRunHook.scalaの作成
続いて同じくprojectディレクトリにFrontendRunHook.scalaファイルを作成し、以下を記述
import java.net.InetSocketAddress
import play.sbt.PlayRunHook
import sbt._
import scala.sys.process.Process
/**
* Frontend build play run hook.
* https://www.playframework.com/documentation/2.6.x/SBTCookbook
*/
object FrontendRunHook {
def apply(base: File): PlayRunHook = {
object UIBuildHook extends PlayRunHook {
var process: Option[Process] = None
/**
* Change these commands if you want to use Yarn.
*/
var npmInstall: String = FrontendCommands.dependencyInstall
var npmRun: String = FrontendCommands.serve
// Windows requires npm commands prefixed with cmd /c
if (System.getProperty("os.name").toLowerCase().contains("win")){
npmInstall = "cmd /c" + npmInstall
npmRun = "cmd /c" + npmRun
}
/**
* Executed before play run start.
* Run npm install if node modules are not installed.
*/
override def beforeStarted(): Unit = {
if (!(base / "ui" / "node_modules").exists()) Process(npmInstall, base / "ui").!
}
/**
* Executed after play run start.
* Run npm start
*/
override def afterStarted(addr: InetSocketAddress): Unit = {
process = Option(
Process(npmRun, base / "ui").run
)
}
/**
* Executed after play run stop.
* Cleanup frontend execution processes.
*/
override def afterStopped(): Unit = {
process.foreach(_.destroy())
process = None
}
}
UIBuildHook
}
}
次に、ui-build.sbtファイルをプロジェクトルートディレクトリに作成します。
import scala.sys.process.Process
/*
* UI Build hook Scripts
*/
// Execution status success.
val Success = 0
// Execution status failure.
val Error = 1
// Run serve task when Play runs in dev mode, that is, when using 'sbt run'
// https://www.playframework.com/documentation/2.6.x/SBTCookbook
PlayKeys.playRunHooks += baseDirectory.map(FrontendRunHook.apply).value
// True if build running operating system is windows.
val isWindows = System.getProperty("os.name").toLowerCase().contains("win")
// Execute on commandline, depending on the operating system. Used to execute npm commands.
def runOnCommandline(script: String)(implicit dir: File): Int = {
if(isWindows){ Process("cmd /c set CI=true&&" + script, dir) } else { Process("env CI=true " + script, dir) } }!
// Check of node_modules directory exist in given directory.
def isNodeModulesInstalled(implicit dir: File): Boolean = (dir / "node_modules").exists()
// Execute `npm install` command to install all node module dependencies. Return Success if already installed.
def runNpmInstall(implicit dir: File): Int =
if (isNodeModulesInstalled) Success else runOnCommandline(FrontendCommands.dependencyInstall)
// Execute task if node modules are installed, else return Error status.
def ifNodeModulesInstalled(task: => Int)(implicit dir: File): Int =
if (runNpmInstall == Success) task
else Error
// Execute frontend test task. Update to change the frontend test task.
def executeUiTests(implicit dir: File): Int = ifNodeModulesInstalled(runOnCommandline(FrontendCommands.test))
// Execute frontend prod build task. Update to change the frontend prod build task.
def executeProdBuild(implicit dir: File): Int = ifNodeModulesInstalled(runOnCommandline(FrontendCommands.build))
// Create frontend build tasks for prod, dev and test execution.
lazy val `ui-test` = TaskKey[Unit]("Run UI tests when testing application.")
`ui-test` := {
implicit val userInterfaceRoot = baseDirectory.value / "ui"
if (executeUiTests != Success) throw new Exception("UI tests failed!")
}
lazy val `ui-prod-build` = TaskKey[Unit]("Run UI build when packaging the application.")
`ui-prod-build` := {
implicit val userInterfaceRoot = baseDirectory.value / "ui"
if (executeProdBuild != Success) throw new Exception("Oops! UI Build crashed.")
}
// Execute frontend prod build task prior to play dist execution.
dist := (dist dependsOn `ui-prod-build`).value
// Execute frontend prod build task prior to play stage execution.
stage := (stage dependsOn `ui-prod-build`).value
// Execute frontend test task prior to play test execution.
test := ((test in Test) dependsOn `ui-test`).value
3. FrontendController.scalaの作成
次に、FrontendController.scalaをapp/controllers/配下に作成し、下記のコードを記述します。
package controllers
import javax.inject._
import play.api.Configuration
import play.api.http.HttpErrorHandler
import play.api.mvc._
/**
* Frontend controller managing all static resource associate routes.
* @param assets Assets controller reference.
* @param cc Controller components reference.
*/
@Singleton
class FrontendController @Inject()(assets: Assets, errorHandler: HttpErrorHandler, config: Configuration, cc: ControllerComponents) extends AbstractController(cc) {
def index: Action[AnyContent] = assets.at("index.html")
def assetOrDefault(resource: String): Action[AnyContent] = if (resource.startsWith(config.get[String]("apiPrefix"))){
Action.async(r => errorHandler.onClientError(r, NOT_FOUND, "Not found"))
} else {
if (resource.contains(".")) assets.at(resource) else index
}
}
4. エラーの修正
と、ここまでくれば完成のようだが、なんかIntelliJでbuildしてたら以下のようなエラーが出た。
最初の文字は小文字じゃなきゃだめだと怒られている。
java.lang.IllegalArgumentException: requirement failed: A named attribute key must start with a lowercase letter: Run UI tests when testing application.
ui-build.sbtはそのままコピペだと怒られるみたい。エラーが出てる箇所の文字列の最初の文字を小文字に書き換えてあげると治った。
実行
最後にプロジェクトルートディレクトリで
$ sbt run
をしてあげれば、reactの画面が立ち上がる。
Scala Play React Seed
ここまで、参考[1]のサイトを見ながら進めてきたが、この記事の著者が便利にもscala-play-react-seedなるものを作ってくれている。これを落として使えばいける(はず)。なのだが、最初これをダウンロードして使おうとしたら起動して、ブラウザ画面が立ち上がるところまではいったもののエラーになってしまい、原因がわからなかった。なので、今回は参考[1]の記事を参考に自分でコピペしながらプロジェクトを作成した。まだよく理解しいないことも多いが、色々試しながらscala, play, reactを勉強していこうと思う。
参考
- React with Play Framework 2.6.x :ソースコードなどはここからひっぱっていてます.
- Getting Started with Play Framework
- yohangz/scala-play-react-seed