プログラマブル深海魚

目立たないけど華やかに

Play FrameworkでTypeScript, SCSSを動かす

Play FrameworkでTypeScript, SCSSを動かしてみたので、
備忘録も兼ねて記録を残します。

目次

sbtプラグインを追加する

sbtには、sbt-webというWeb開発用のプラグイン向けライブラリがあり、
プロジェクトでSbtWebを有効にしてsbt-web用プラグインを追加すれば、対応するファイルがコンパイルされるようになります。
https://github.com/sbt/sbt-web

TypeScript, SCSS(Sass)にも、それぞれプラグインが作られています。
https://github.com/platypii/sbt-typescript
https://github.com/irundaia/sbt-sassify

以下のようにplugins.sbtにプラグインの記述を追加して、

addSbtPlugin("com.github.platypii" % "sbt-typescript" % "4.6.4")
addSbtPlugin("io.github.irundaia" % "sbt-sassify" % "1.5.2")

SbtWebをプロジェクトで有効にします。

lazy val proj = (project in file("."))
  .settings(
    name := "SampleProject"
  )
  .enablePlugins(PlayScala, SbtWeb)

これで、assetsタスクでTypeScriptとSCSS(Sass)のコンパイルが有効になります。
(runで実行中のときも、ファイルの変更を検知して再コンパイルしてくれる)

プラグインの設定

sbt-typescriptは、プロジェクトのルートディレクトリにtsconfig.jsonを作成し、
"compilerOptions"の下にコンパイラオプションを記述することができます。
コンパイラオプションの一覧はこちら(https://www.typescriptlang.org/docs/handbook/compiler-options.html
今回は、SourceMapを生成するようにしたかったので、以下の設定を入れました。

{
  "compilerOptions": {
    "sourceMap": true,
    "mapRoot": "/assets",
    "sourceRoot": "/assets"
  }
}

sbt-sassifyは、build.sbtからSassKeysを通して設定することができます。
sbt-sassifyを使う上で実は1点うまくいかなかった点があって、assetsのルート直下ではなくサブディレクトリ(たとえば/assets/styles)にSCSSを配置すると、
SourceMapの対応付けがおかしくなってしまいます("sources"が"styles/<ファイル名>"となってほしいところが"<ファイル名>"となる)
そこで、今回は"SassKeys.assetRootURL"をサブディレクトリに設定しました(ただし、この方法はSCSSをディレクトリ分けすると使えない……)

SassKeys.assetRootURL := "/assets/styles"

テンプレートからの参照

作成したTypeScriptやSCSSは、テンプレートから参照する必要があります。
TypeScriptやSCSSをコンパイルして生成されたJavaScript/CSSを参照するには、
アセット用のルーティングを作成し、controller.routes.Assetsに作成されるアセット用のリバースコントローラを利用します。

routesに以下の記述でルーティングを追加します。

GET /assets/*file controllers.Assets.at(path="/public", file)

これによりcontroller.routes.Assetsにリバースコントローラが生成され、以下のように@Assets.at("<生成したファイルの相対パス>")により参照できるようになります。

@import controllers.routes.Assets

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Test</title>
  <link href="@Assets.at("styles/test.css")" rel="stylesheet">
  <script type="module" src="@Assets.at("scripts/test.js")"></script>
</head>
<body>
  ......
</body>
</html>

実際に動かす

実際に簡単なページを作って動かしてみます。

まずは、テンプレートとControllerを作成します。

@import controllers.routes.Assets

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Test</title>
  <link href="@Assets.at("styles/test.css")" rel="stylesheet">
  <script type="module" src="@Assets.at("scripts/test.js")"></script>
</head>
<body>
<main>
  <input id="testBtn" type="button" value="TEST">
</main>
</body>
</html>
import play.api.i18n.I18nSupport
import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents}

import javax.inject.Inject

class TestController @Inject()(cc: ControllerComponents)() extends AbstractController(cc) with I18nSupport {
  def test(): Action[AnyContent] = Action { implicit request =>
    Ok(views.html.Test())
  }
}

次に、/assets/scriptsと/assets/stylesにそれぞれTypeScriptとSCSSを作成します。

document.querySelector('#testBtn').addEventListener('click', (event: Event) => {
  alert('TEST!');
});
body {
  margin: auto;
  width: 60vw;
}
input[type="button"] {
  background-color: #efefef;
  border: 1px solid #efefef;
  border-radius: unset;
  width: 100%;
  height: 40px;
  line-height: 1.2;
  &:focus {
    outline: none;
  }
  &:hover {
    color: #fdfdfd;
    background-color: #777777;
    border-color: #777777;
    cursor: pointer;
  }
}

最後に、routesにルーティングを定義します。

GET /test TestController.test()

これで動くようになるので、実際に動かしてみます。

TESTボタンが表示されている
実際の画面

ボタンを押すと、こちらのようにポップアップが表示されます。

TEST!と表示される
ポップアップ

クライアント側のライブラリ依存関係を管理する

JavaScriptCSSを書く上で、サードパーティのライブラリ(例えばnode.jsやjQueryなど)を使用する場合があると思います。
そうした場合、WebJarsというクライアント側のライブラリをMaven等で管理できるようにしたサービスを利用することができます。

build.sbtに以下のようにwebjarsのライブラリの依存関係を追加することで、クライアント側のライブラリについても依存関係を管理できます。
TypeScriptのコンパイル時に必要な情報も、これによって追加することができます。

libraryDependencies += "org.webjars.npm" % "jquery" % "3.6.4"
libraryDependencies += "org.webjars.npm" % "types__jquery" % "3.5.16"
libraryDependencies += "org.webjars.npm" % "types__sizzle" % "2.3.3"

注: @types/jqueryを追加すると@types/sizzleを解決できなくてエラーになりました。[0,)というversionの表記を解釈できていない模様。
範囲指定のバージョンには対応しているみたいですが、[0,)という記述には対応できていない?
https://github.com/sbt/sbt/issues/2647

また、@typesなど特殊な文字を含むライブラリを追加する場合、npmパッケージとWebJarsでの名前が若干異なるので、
以下の設定も追加する必要があるようです。

resolveFromWebjarsNodeModulesDir := true

WebJarsで追加したライブラリをPlayで扱うために、webjars-playを依存関係に追加します。

libraryDependencies += "org.webjars" %% "webjars-play" % "2.8.18"

webjars-playにWebJars用のルーティングが用意されているため、routesに以下を追加してインクルードします。

-> /webjars webjars.Routes

これで、リバースルーティングにより、@org.webjars.play.routes.WebJarAssets.at("<ライブラリ名>/<ライブラリのバージョン>/<パッケージ内のパス>")で参照できるようになります。

@import org.webjars.play.routes.WebJarAssets
<script src="@WebJarAssets.at("jquery/3.6.4/dist/jquery.min.js")"></script>

jQueryを使えるようになりました。

$('#testBtn').on('click', (event: JQuery.Event) => {
  alert('TEST!');
});

おわり

今回はTypeScriptとSCSS(Sass)を使いましたが、CoffeeScriptやLESS等でもほぼ同様の手順で利用可能なはずです。
想像していたよりは手軽に導入ができました。素のJavaScriptCSSを書くよりは絶対書きやすいと思うので、使っていきます。

参考文献

Assets - 2.8.x
https://www.playframework.com/documentation/2.8.x/Assets

Scala Routing - 2.8.x
https://www.playframework.com/documentation/2.8.x/ScalaRouting

webjars/webjars-play
https://github.com/webjars/webjars-play

platypii/sbt-typescript: An sbt plugin for compiling typescript
https://github.com/platypii/sbt-typescript

irundaia/sbt-sassify: sbt-web plugin for Sass files
https://github.com/irundaia/sbt-sassify

WebJars - Documentation
https://www.webjars.org/documentation

Play Framework with WebJars で管理画面をサクッと作ってみる | | AI tech studio
https://cyberagent.ai/blog/tech/scala/3334/

java - Play Framework: Arrow ("->") in routing - Stack Overflow
https://stackoverflow.com/questions/31009785/play-framework-arrow-in-routing