Pedro Palma Ramosの記事「 Scala.jsとReactを使用したWebアプリケーションの構築-パート1 」の翻訳
私は、Webアプリケーションを開発するScalaプログラマーとして、通常、きちんとした機能的でタイプセーフなScalaバックエンドからJavaScriptで記述されたフロントエンドへの移行を嫌います。 幸いなことに、私たちの(常にではないが)Webの標準言語の強力で成熟した代替手段があります。
Scala.jsは、 SébastienDoeraeneによって作成されたScala実装で、JVMバイトコードではなくJavaScriptでScalaコードをコンパイルします。 ScalaとJavaScriptコード間の完全な双方向相互運用性をサポートしているため、JavaScriptライブラリとフレームワークを使用してScalaでフロントエンドWebアプリケーションを開発できます。 また、サーバー側用に開発されたフロントエンドモデルとビジネスロジックを再利用できるため、通常のScala Webアプリケーションと比較してコードの重複を減らすことができます。
一方、 ReactはJavaScriptでユーザーインターフェイスを作成するためのWebフレームワークであり、Facebookや他の企業によって開発およびサポートされています。 ユーザーイベントに応答してアプリケーションの状態を更新することと、指定された状態に基づいて視覚化をレンダリングすることの間の明確な分離を促進します。 したがって、Reactフレームワークは、Scalaでプログラミングするときに使用される機能パラダイムに特に適しています。
ReactをScala.jsで直接使用できますが、幸いなことに、 David Barriはscalajs-reactを作成しました。これは、Scala.jsでタイプセーフで使いやすいようにReactのラッパーセットを提供するScalaライブラリです。 また、 Callbackクラスなどのいくつかの便利な抽象化も定義します。Reactフレームワークで実行する必要がある複合的な反復可能なサイド計算です。
この記事は、e.nearでscalajs-reactを使用してフロントエンドWebアプリケーションを作成する方法を説明するチュートリアルの最初の部分です。 Scala.jsでクリーンなプロジェクトを作成することに焦点を当てており、2番目の部分では、Scala.jsとJVMの「標準」Scalaコードの両方を組み合わせます。 あなたは経験豊富なScalaユーザーであり、少なくともHTMLとBootstrapの基本に精通していると思います。 JavaScriptまたはReactフレームワークの以前の経験は必要ありません。
最終結果は、Spotify APIを使用してアーティストを検索し、アルバムとトラックを表示するシンプルなWebアプリケーションになります(こちらをご覧ください )。 単純であるにもかかわらず、この例では、ユーザー入力への応答、Ajaxを介したREST APIの呼び出し、表示の更新など、Scala.js ReactでWebアプリケーションを開発する方法のアイデアを提供します。
この記事で使用されているコードは、 https://github.com/enear/scalajs-react-guide-part1で完全に利用可能です。
カスタマイズ
Scala.jsプロジェクトを開始する簡単な方法は、GITを使用してSébastienDoeraeneによって作成されたアプリケーションテンプレートを複製することです 。
build.sbtファイルにscalajs-react
へのリンクを追加する必要があります。
libraryDependencies ++= Seq( "com.github.japgolly.scalajs-react" %%% "core" % "0.11.3" ) jsDependencies ++= Seq( "org.webjars.bower" % "react" % "15.3.2" / "react-with-addons.js" minified "react-with-addons.min.js" commonJSName "React", "org.webjars.bower" % "react" % "15.3.2" / "react-dom.js" minified "react-dom.min.js" dependsOn "react-with-addons.js" commonJSName "ReactDOM", "org.webjars.bower" % "react" % "15.3.2" / "react-dom-server.js" minified "react-dom-server.min.js" dependsOn "react-dom.js" commonJSName "ReactDOMServer" )
SBTのScala.jsプラグインは、 jsDependencies
パラメーターを追加します。 これにより、SBTはWebJarsを使用してJavaScriptの依存関係を管理できます。 <project-name>-jsdeps.js
ファイル<project-name>-jsdeps.js
コンパイルされます。
コードをコンパイルするには、SBT内でfullOptJS
(開発用の中程度の最適化)またはfullOptJS
(本番用の完全最適化) fullOptJS
を使用できます。 アーティファクト<project-name>-fastopt/fullopt.js
および<project-name>-launcher.js
ます。 1つ目はコンパイルされたコードを含み、2つ目は単にmainメソッドを呼び出すスクリプトを含みます。
また、Reactがレンダリングされたコンテンツを貼り付ける空の<div>
タグを持つHTMLファイルも必要です。
<!DOCTYPE html> <html> <head> <title>Example Scala.js application</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> </head> <body> <div class="app-container" id="playground"> </div> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> <script type="text/javascript" src="./target/scala-2.12/scala-js-react-guide-jsdeps.js"></script> <script type="text/javascript" src="./target/scala-2.12/scala-js-react-guide-fastopt.js"></script> <script type="text/javascript" src="./target/scala-2.12/scala-js-react-guide-launcher.js"></script> </body> </html>
Reactコンポーネントの構築
Scala.jsのエントリポイントは、JSApp JSApp
を継承するオブジェクトによって決まりJSApp
。 これにより、オブジェクトとそのメインメソッドが完全な名前でJavaScriptにエクスポートされます。
object App extends JSApp { @JSExport override def main(): Unit = { ReactDOM.render(TrackListingApp.component(), dom.document.getElementById("playground")) } }
scalajs-react
は、単一ページのアプリケーションで複数のReactコンポーネントを管理するためのRouterクラスを提供しますが、アプリケーションは1つのReactコンポーネントのみで構成されているため、このチュートリアルの範囲を超えています。
object TrackListingApp { val component = ReactComponentB[Unit]("Spotify Track Listing") .initialState(TrackListingState.empty) .renderBackend[TrackListingOps] .build
すべてのReactコンポーネントは、引数や状態の関数としてHTMLを返すrender
メソッドを定義する必要があります。 このコンポーネントは引数を必要としないため、 Unit
型のパラメーターUnit
ますが、 TrackListingState
型の状態のオブジェクトが必要TrackListingState
。 このコンポーネントのレンダリングをTrackListingOps
クラスに委任します。ここでは、コンポーネントの状態を制御するメソッドを記述することもできます。
アプリケーションの状態は次のように保存されます。
case class TrackListingState( artistInput: String, // albums: Seq[Album], // tracks: Seq[Track] // ) object TrackListingState { val empty = TrackListingState("", Nil, Nil) }
Album
クラスとTrack
クラスは、次のセクションで定義されます。
Reactコンポーネントを作成する他の方法については、 こちらをご覧ください 。
REST APIコール
SpotifyパブリックAPIの 3つのメソッドを使用します 。
方法 | エントリーポイント | 予定 | 戻り値 |
---|---|---|---|
ゲット | / v1 / search?type = artist | アーティストを探す | アーティスト |
ゲット | / v1 /アーティスト/ {id} /アルバム | アーティストアルバムを取得する | アルバム* |
ゲット | / v1 /アルバム/ {id} /トラック | アルバムから曲を取得する | トラック* |
このAPIはオブジェクトをJSON形式で返し、JavaScriptを使用して解析できます。 ScalaとJavaScriptモデルの間のインターフェースとなるファサードのタイプを定義することにより、Scala.jsでこれを利用できます。 これを行うには、特性を@js.native
マークし、 @js.native
から継承します。
@js.native trait SearchResults extends js.Object { def artists: ItemListing[Artist] } @js.native trait ItemListing[T] extends js.Object { def items: js.Array[T] } @js.native trait Artist extends js.Object { def id: String def name: String } @js.native trait Album extends js.Object { def id: String def name: String } @js.native trait Track extends js.Object { def id: String def name: String def track_number: Int def duration_ms: Int def preview_url: String }
最後に、 Ajax Scala.jsオブジェクトを使用してSpotify APIを非同期的に呼び出すことができます(便宜上、Futureを返すため、これらのコールバックのすべてで混乱しないようにします )。
object SpotifyAPI { def fetchArtist(name: String): Future[Option[Artist]] = { Ajax.get(artistSearchURL(name)) map { xhr => val searchResults = JSON.parse(xhr.responseText).asInstanceOf[SearchResults] searchResults.artists.items.headOption } } def fetchAlbums(artistId: String): Future[Seq[Album]] = { Ajax.get(albumsURL(artistId)) map { xhr => val albumListing = JSON.parse(xhr.responseText).asInstanceOf[ItemListing[Album]] albumListing.items } } def fetchTracks(albumId: String): Future[Seq[Track]] = { Ajax.get(tracksURL(albumId)) map { xhr => val trackListing = JSON.parse(xhr.responseText).asInstanceOf[ItemListing[Track]] trackListing.items } } def artistSearchURL(name: String) = s"https://api.spotify.com/v1/search?type=artist&q=${URIUtils.encodeURIComponent(name)}" def albumsURL(artistId: String) = s"https://api.spotify.com/v1/artists/$artistId/albums?limit=50&market=PT&album_type=album" def tracksURL(albumId: String) = s"https://api.spotify.com/v1/albums/$albumId/tracks?limit=50" }
JavaScriptコードとやり取りするその他の方法については、Scala.jsのドキュメントを参照できます 。
HTMLレンダリング
次に、状態の関数としてTrackListingOps
クラスのrender
メソッドを定義しrender
。
class TrackListingOps($: BackendScope[Unit, TrackListingState]) { def render(s: TrackListingState) = { <.div(^.cls := "container", <.h1("Spotify Track Listing"), <.div(^.cls := "form-group", <.label(^.`for` := "artist", "Artist"), <.div(^.cls := "row", ^.id := "artist", <.div(^.cls := "col-xs-10", <.input(^.`type` := "text", ^.cls := "form-control", ^.value := s.artistInput, ^.onChange ==> updateArtistInput ) ), <.div(^.cls := "col-xs-2", <.button(^.`type` := "button", ^.cls := "btn btn-primary custom-button-width", ^.onClick --> searchForArtist(s.artistInput), ^.disabled := s.artistInput.isEmpty, "Search" ) ) ) ), <.div(^.cls := "form-group", <.label(^.`for` := "album", "Album"), <.select(^.cls := "form-control", ^.id := "album", ^.onChange ==> updateTracks, s.albums.map { album => <.option(^.value := album.id, album.name) } ) ), <.hr, <.ul(s.tracks map { track => <.li( <.div( <.p(s"${track.track_number}. ${track.name} (${formatDuration(track.duration_ms)})"), <.audio(^.controls := true, ^.key := track.preview_url, <.source(^.src := track.preview_url) ) ) ) }) ) }
特にBootstrapに慣れていない場合、コードは複雑に見えるかもしれませんが、これは型付きHTMLにすぎないことに注意してください。 タグと属性は、それぞれオブジェクト<
および^
メソッドとして記述されます(最初にjapgolly.scalajs.react.vdom.prefix_<^._
をインポートする必要があります)。
奇妙な矢印( -->
および==>
)は、 コールバックコールバックとして定義されているイベントハンドラーをバインドするために使用されます。
-
-->
単純なCallback
引数を受け入れます。 -
==>
関数(ReactEvent => Callback)
受け入れます。これは、呼び出されたイベントからキャプチャされた値を処理する必要がある場合に便利です。
仮想DOMの作成方法の詳細については、scalajs-reactのドキュメントを参照してください。
イベントへの反応
イベントハンドラを定義するためだけに残ります。
TrackListingOps
クラスの定義をもう一度見てみましょう。
class TrackListingOps($: BackendScope[Unit, TrackListingState]) {
$ constructor引数は、 modState
とmodState
を使用してアプリケーションの状態を更新するためのインターフェイスを提供します。 更新の記録を短くするために、すべてのステータスフィールドにレンズを定義できます。
val artistInputState = $.zoom(_.artistInput)((s, x) => s.copy(artistInput = x)) val albumsState = $.zoom(_.albums)((s, x) => s.copy(albums = x)) val tracksState = $.zoom(_.tracks)((s, x) => s.copy(tracks = x))
覚えているように、3つのイベントハンドラーを使用します。
- アーティスト名が変更されたときの
updateArtistInput
-
updateTracks
は、新しいアルバムが選択されたとき、 -
searchForArtist
は、検索ボタンがクリックされたとき。
updateArtistInput
から始めましょう。
def updateArtistInput(event: ReactEventI): Callback = { artistInputState.setState(event.target.value) }
modState
とmodState
はすぐに更新を実行しませんが、対応するCallbackコールバックを返すため、ここで適しています。
updateTracksメソッドの場合、アルバム内の曲のリストをロードする必要があるため、非同期コールバックを使用する必要があります。 幸いなことに、 Callback.future
メソッドを使用してFuture[Callback]
を非同期Callback
変換できます。
def updateTracks(event: ReactEventI) = Callback.future { val albumId = event.target.asInstanceOf[HTMLSelectElement].value SpotifyAPI.fetchTracks(albumId) map { tracks => tracksState.setState(tracks) } }
最後に、3つのAPIメソッドすべてを使用し、状態を完全に更新するsearchForArtist
メソッドを定義します。
def searchForArtist(name: String) = Callback.future { for { artistOpt <- SpotifyAPI.fetchArtist(name) albums <- artistOpt map (artist => SpotifyAPI.fetchAlbums(artist.id)) getOrElse Future.successful(Nil) tracks <- albums.headOption map (album => SpotifyAPI.fetchTracks(album.id)) getOrElse Future.successful(Nil) } yield { artistOpt match { case None => Callback(window.alert("No artist found")) case Some(artist) => $.setState(TrackListingState(artist.name, albums, tracks)) } } }
おわりに
このポイントに到達すると、Scala.jsの純粋に機能的な構造を使用して、Webアプリケーションのフロントエンドをモデル化できるようになります。 興味のある方は、 Scala.jsとscalajs-reactのドキュメントを確認してください。
チュートリアルの第2部は、Scalaで本格的なWebアプリケーションを作成し、データモデルと一般的なビジネスロジックをバックエンドとフロントエンドで再利用する方法に専念します。