Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Isomorphic web development with scala and scala.js

19,109 views

Published on

isomorphic tokyo meetupで発表した資料です

Published in: Engineering
  • Be the first to comment

Isomorphic web development with scala and scala.js

  1. 1. Isomorphic Web development with Scala & Scala.js TanUkkii isomorphic tokyo meetup 2015/4/30
  2. 2. I am ... • @TanUkkii007 on Twitter • Web frontend engineer • だったけどゲーム開発が辛くてサーバーサイドを Scalaで開発する人に • Scala業務歴4ヶ月
  3. 3. Agenda 1. 開発環境を共有する 2. コードを共有する 3. アーキテクチャを共有する クライアントーサーバー間で
  4. 4. Motivation • Scala + Akka + SprayでAPIサーバーを開発 • StrongLoopのApi Exprolerみたいなのを作りたい • Scala.jsでisomorphicにつくる! REST/HTTP server build on Akka Actors
  5. 5. Why Scala.js? • クライアントーサーバーで同一の開発環境 • クライアントーサーバーでコードの共有 • 片手間クライアント開発
  6. 6. 1. Sharing development environment: Building applications with sbt • プロジェクト定義 • Scalaのバージョン • 依存ライブラリ • コンパイルオプション Java/Scalaのビルドツール 設定 タスク name := “your_project_name”
 
 scalaVersion := "2.11.6"
 
 libraryDependencies ++= Seq(
 "com.typesafe.akka" %% “akka-actor" % "2.3.10"
 ) ! scalacOptions in ThisBuild ++= Seq("-feature") build.sbt • 依存ライブラリの解決 • コンパイル • テスト • REPLの起動
  7. 7. Multi-project build - client - server - root import sbt._
 import Keys._
 
 object IsomorphicBuild extends Build {
 
 lazy val root = project.in(file(“.")) 
 lazy val server = Project(“server", file(“server"))
 
 lazy val client = Project("client", file(“client”)) ! } project/Build.scala sbtではサブプロジェクトを複数定義できる ↓サーバーもクライアントも サブプロジェクトとして定義 相似のプロジェクト構造ができる→
  8. 8. import sbt._
 import Keys._
 import org.scalajs.sbtplugin.ScalaJSPlugin
 import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
 
 
 object IsomorphicBuild extends Build {
 
 lazy val root = project.in(file(".")).aggregate(server, client)
 
 lazy val server = Project(“server", file(“server"))
 
 
 lazy val client = Project("client", file(“client")).enablePlugins(ScalaJSPlugin) .settings(
 persistLauncher in Compile := true,
 skip in packageJSDependencies := false
 )
 } project/Build.scala Make Scala.js project ! addSbtPlugin(“org.scala-js" % "sbt-scalajs" % "0.6.2") project/plugins.sbt Scala.jsプラグインを追加 clientプロジェクトで Scala.jsプラグインを有効化
  9. 9. Make Scala.js project isomorphic ! import sbt._
 import Keys._
 import org.scalajs.sbtplugin.ScalaJSPlugin
 import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
 
 object IsomorphicBuild extends Build {
 
 lazy val root = project.in(file(".")).aggregate(server, client)
 
 lazy val server = Project(“server", file(“server"))
 
 
 lazy val client = Project("client", file(“client")).dependsOn(server).enablePlugins(ScalaJSPlugin) .settings(
 unmanagedSourceDirectories in Compile += (sourceDirectory in server).value/"main"/"scala"/"jp.isomorphic.example" / “shared", packageJSDependencies in Compile := {
 val base = (packageJSDependencies in Compile).value
 IO.copyFile(base, (baseDirectory in server).value / "src/main/resources/js" / base.getName)
 base
 },
 persistLauncher in Compile := true,
 skip in packageJSDependencies := false
 ).settings(Seq(fastOptJS, fullOptJS) map { packageJSKey =>
 crossTarget in (Compile, packageJSKey) :=
 (baseDirectory in server).value / "src/main/resources/js"
 })
 } project/Build.scala サーバーからクライアントに クラスパスを通す (Scalaコンパイルが可能に) コンパイル対象に サーバー側のソースの一部を追加 (Scala.jsコンパイルが可能に) Scala.jsのコンパイル結果を サーバー側にコピー
  10. 10. Using CrossProject to build isomorphic project structure -shared - js - jvm import sbt.Keys._
 import sbt._ import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
 ! ! object ApplicationBuild extends Build {
 
 lazy val root = project.in(file("."))
 
 
 lazy val sharedProject = crossProject.in(file("."))
 .settings()
 .jvmSettings()
 .jsSettings()
 )
 
 
 lazy val js: Project = sharedProject.js.settings()
 
 
 lazy val jvm: Project = sharedProject.jvm.settings()
 }
 ! ScalaJSPluginの CrossProjectを使えば 簡単にisomorphicな プロジェクト構造を作れる scalajs-spa-tutorialを参照scalajs-cross-compile-example,
  11. 11. 2. Sharing codes between Client and Server • Scala.jsで利用可能なライブラリ • シリアライゼーションによるScalaデータ型の通信 • 型安全なAPIの呼び出し • マクロ
  12. 12. Available Libraries • DOM • jQuery • React.js • AngularJS www.scala-js.orgにもっと多く載っている JSライブラリの 型付けされたインターフェース • Scalaz • NICTA/rng Scalaライブラリのポート • Scala.Rx • Monifu • autowire • uPickle 最初からクロスコンパイル前提で 作られたライブラリ※Scalaは型だけ提供。実装はJS。 ※本家のクロスコンパイルできない 部分を修正してJSを提供 ※ScalaとJSを提供
  13. 13. Pickling (serialization) • クラス階層情報の喪失 Scala.jsの大問題:可逆的なJSONのシリアライズ リフレクションを使わずにコンパイル時に少ないコードで解決しなければならない。 サーバー クライアント JSON {"fruits": [{"color": "yellow"}, {"color": "red"}]} {"points": [{"x": 1, "y": 2}, {"x": 1, "y": 2}]} null [[]] val fruits: List[Fruit] = List(Banana("yellow"), Apple("red")) val p = Point(1,2); val points = List(p, p) val option: Option[Option[Int]] = None 通信 • 参照同一性の喪失 • Optionの扱い Scalaデータ型 Scalaデータ型
  14. 14. Cross-compiled pickling libraries きれいな JSON形式 クラス階 層の保持 参照同一 性の保持 Anyの解 決 uPickle ○ 難しいことは忘れて きれいなJSONを吐くことに注力 Optionが配列として表現される Prickle ○ ○ 可逆性を高めるため メタ情報をJSONに保持させている Scala.js Pickling ○ ○ 型の登録処理が事前に必要 サポートがあまりよくない
  15. 15. Binary serialization with BooPickle and XHR2 def request(method: String, url: String, body: ArrayBuffer): Future[ByteBuffer] = {
 val promise = Promise[ByteBuffer]
 val xhr = new XMLHttpRequest()
 xhr.onreadystatechange = { (e: Event) =>
 if (xhr.readyState == 4) { if (xhr.status >= 200 && xhr.status < 300) {
 val byteBuffer = TypedArrayBuffer.wrap(xhr.response.asInstanceOf[ArrayBuffer])
 promise.success(byteBuffer)
 } else promise.failure(AjaxException(xhr))
 }}
 xhr.open(method, url)
 xhr.responseType = "arraybuffer"
 xhr.setRequestHeader("Content-Type", "application/octet-stream")
 xhr.setRequestHeader("Accept", "application/octet-stream")
 xhr.send(body)
 promise.future
 } val byteBuffer = Pickle.intoBytes(SampleRequest("Hello"))
 request(method, url, byteBuffer.arrayBuffer()).map(Unpickle[SampleResponse].fromBytes(_)) • JSONではなく、バイナリにシリアライズ • XHR level2のバイナリサポートを利用 • JSのArrayBufferを使う • メディアタイプは application/octet-stream • クラス階層、参照の同一性も復元可能
  16. 16. Client-server communication with Autowire object AutowireClient extends autowire.Client[String, Reader, Writer]{
 override def doCall(req: Request): Future[String]
 } trait MyApi {
 def sampleRequest(id: Int): SampleResponse
 } object MyApiImpl extends MyApi{
 def sampleRequest(id: Int) = SampleResponse(id)
 } AutowireClient[MyApi].sampleRequest(1).call().map(println) path("api" / Segments){ s =>
 extract(_.request.entity.asString) { e =>
 complete {
 AutowireServer.route[MyApi](MyApiImpl)(
 autowire.Core.Request(s, upickle.read[Map[String, String]](e)))
 }}} object AutowireServer extends autowire.Server[String, Reader, Writer]{
 val routes = AutowireServer.route[MyApi](MyApiImpl)
 } Autowire マクロマジック!! AutowireはAjaxにおける クライアントーサーバー間のAPIの煩雑さを RPCスタイルのメソッド呼び出しで 解決するライブラリ • なぜかRPCは自動で解決される • API呼び出しにおける間違いは コンパイル時に発見される 1. sharedでRPCインターフェースを定義 2. serverでRPCインターフェースを実装 3. serverでルーティング部分関数をマクロにより生成 4. serverでルーティング部分関数を呼び出してレスポンスを返す 5. clientでAjaxの通信の仕方を実装 6. clientでRPC関数を呼び出す
  17. 17. Macro performing macro expansion AutowireServer.route[jp.isomorphic.example.MyApi] (<empty> match {
 case autowire.Core.Request(Seq("jp", "isomorphic", "example", "MyApi", "sampleRequest"), (args$macro$1 @ _)) => autowire.Internal.doValidate({
 <synthetic> <artifact> val x$2 = autowire.Internal.read[String, Int](args$macro$1, scala.util.Left(autowire.Error.Param.Missing("id")), "id", ((x$1) => AutowireServer.read[Int](x$1)));
 Nil.$colon$colon(x$2)
 }) match {
 case scala.$colon$colon((id @ (_: Int @unchecked)), Nil) => scala.concurrent.Future.successful(MyApiImpl.sampleRequest(id)).map(((x$3) => AutowireServer.write(x$3)))
 case _ => $qmark$qmark$qmark
 }
 }: autowire.Core.Router[String]) 種明かし:-Ymacro-debug-liteコンパイルオプションでマクロを展開する package jp.isomorphic.example trait MyApi {
 def sampleRequest(id: Int): SampleResponse
 } マクロは ←から、関数のシグニチャに対して パターンマッチをかける部分関数 を作っていた ※Scalaのマクロは抽象構文木を操るすごいやつです
  18. 18. 3. Sharing Application Architecture Scalaでフロントエンドのアプリケーションをいざ書くときに 今までやってきたことをScalaでどう表現するとよいか迷う オブザーバーパターン → ? アーキテクチャをシェアして コンテクストスイッチのコストを減らそう!
  19. 19. Common Practice: Unidirectional Data Flow • Tell, don t ask. • Fire and forget. Flux Scalajs SPA Tutorialがこの共通点からアプローチしている • unidirectional data flow • message passing 複雑さに対抗する手段のコンセプトは同じ
  20. 20. Flux in Scala つまりStoreがActorになった Scalajs SPA Tutorial* image from
  21. 21. trait Actor {
 type Receive = PartialFunction[Any, Unit]
 
 def receive: Receive
 
 def !(message: Any) = {
 receive(message)
 }
 } trait Dispatcher {
 var actors = Set.empty[Actor]
 
 def dispatch(message: Any) = {
 actors.foreach { actor =>
 actor ! message
 }
 }
 
 def register(actor: Actor) = {
 actors = actors + actor
 }
 
 def unregister(actor: Actor) = {
 if (actors.contains(actor)) {
 actors - actor
 }
 }
 } Dispatcherは Actorプログラミングにおける Recipient Listパターン var Dispatcher = { _listeners: [], register: function(callback) { this._listeners.push(callback); return this; }, unregister: function(callback) { var index = this._listeners.indexOf(callback); if (index !== -1) { delete this._listeners[index]; } return this; }, dispatch: function(...args) { this._listeners.forEach(callback => callback.apply(this, args) ); return this; } }; ※Recipient List: メッセージを複数のアクターに拡散 し仕事を分散して処理する際に、拡散先の受信者である アクター参照の一覧を保持しているもの
  22. 22. object SampleDispatcher extends Dispatcher
 
 object SampleActorProtocol {
 case class Foo(message: String)
 case class Bar(message: String)
 }
 
 object SampleActor extends Actor {
 import SampleActorProtocol._
 
 SampleDispatcher.register(this)
 
 def receive: Receive = {
 case Foo(message) => println(message)
 case Bar(message) => println(message)
 }
 } ! import SampleActorProtocol._
 SampleDispatcher.dispatch(Foo("Hello")) Dispatcher.register(function(payload) { if (payload.type === "FOO") { console.log(payload.message); } else if (payload.type === "BAR") { console.log(payload.message); } }); ! Dispatcher.dispatch({type: "FOO", message: "Hello"}); ! ! ! ! ! ! ! ! ! ! ! • Dispatcherに関数ではなくActorオブジェクトを登録する • payloadを処理する関数はActorのReceive部分関数に相当 • payloadはメッセージ
  23. 23. Conclusion • Scala + Scala.jsでクライアントーサーバーを高度 に統合できる • (ただし他のシステムとのinteropが犠牲になるかも • みんなScala.jsを使おう!!!

×