isomorphic web development with scala and scala.js

23
Isomorphic Web development with Scala & Scala.js TanUkkii isomorphic tokyo meetup 2015/4/30

Upload: tanukkii

Post on 15-Jul-2015

9.920 views

Category:

Engineering


2 download

TRANSCRIPT

Page 1: Isomorphic web development  with scala and scala.js

Isomorphic Web development

with Scala & Scala.jsTanUkkii

isomorphic tokyo meetup 2015/4/30

Page 2: Isomorphic web development  with scala and scala.js

I am ...• @TanUkkii007 on Twitter

• Web frontend engineer

• だったけどゲーム開発が辛くてサーバーサイドをScalaで開発する人に

• Scala業務歴4ヶ月

Page 3: Isomorphic web development  with scala and scala.js

Agenda

1. 開発環境を共有する

2. コードを共有する

3. アーキテクチャを共有する

クライアントーサーバー間で

Page 4: Isomorphic web development  with scala and scala.js

Motivation

• Scala + Akka + SprayでAPIサーバーを開発

• StrongLoopのApi Exprolerみたいなのを作りたい

• Scala.jsでisomorphicにつくる!

REST/HTTP server build on Akka Actors

Page 5: Isomorphic web development  with scala and scala.js

Why Scala.js?

• クライアントーサーバーで同一の開発環境

• クライアントーサーバーでコードの共有

• 片手間クライアント開発

Page 6: Isomorphic web development  with scala and scala.js

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の起動

Page 7: Isomorphic web development  with scala and scala.js

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ではサブプロジェクトを複数定義できる

↓サーバーもクライアントも サブプロジェクトとして定義

相似のプロジェクト構造ができる→

Page 8: Isomorphic web development  with scala and scala.js

import sbt._import Keys._import org.scalajs.sbtplugin.ScalaJSPluginimport 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プラグインを有効化

Page 9: Isomorphic web development  with scala and scala.js

Make Scala.js project isomorphic

!import sbt._import Keys._import org.scalajs.sbtplugin.ScalaJSPluginimport 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のコンパイル結果を サーバー側にコピー

Page 10: Isomorphic web development  with scala and scala.js

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,

Page 11: Isomorphic web development  with scala and scala.js

2. Sharing codes between Client and Server

• Scala.jsで利用可能なライブラリ

• シリアライゼーションによるScalaデータ型の通信

• 型安全なAPIの呼び出し

• マクロ

Page 12: Isomorphic web development  with scala and scala.js

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を提供

Page 13: Isomorphic web development  with scala and scala.js

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データ型

Page 14: Isomorphic web development  with scala and scala.js

Cross-compiled pickling libraries

きれいなJSON形式

クラス階層の保持

参照同一性の保持

Anyの解決

uPickle ○難しいことは忘れて

きれいなJSONを吐くことに注力 Optionが配列として表現される

Prickle ○ ○ 可逆性を高めるため メタ情報をJSONに保持させている

Scala.js Pickling ○ ○ 型の登録処理が事前に必要

サポートがあまりよくない

Page 15: Isomorphic web development  with scala and scala.js

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 • クラス階層、参照の同一性も復元可能

Page 16: Isomorphic web development  with scala and scala.js

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関数を呼び出す

Page 17: Isomorphic web development  with scala and scala.js

Macroperforming 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のマクロは抽象構文木を操るすごいやつです

Page 18: Isomorphic web development  with scala and scala.js

3. Sharing Application Architecture

Scalaでフロントエンドのアプリケーションをいざ書くときに

今までやってきたことをScalaでどう表現するとよいか迷う

オブザーバーパターン → ?

アーキテクチャをシェアして コンテクストスイッチのコストを減らそう!

Page 19: Isomorphic web development  with scala and scala.js

Common Practice: Unidirectional Data Flow

• Tell, don’t ask.

• Fire and forget.

Flux

Scalajs SPA Tutorialがこの共通点からアプローチしている

• unidirectional data flow

• message passing

複雑さに対抗する手段のコンセプトは同じ

Page 20: Isomorphic web development  with scala and scala.js

Flux in Scala

つまりStoreがActorになったScalajs SPA Tutorial* image from

Page 21: Isomorphic web development  with scala and scala.js

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: メッセージを複数のアクターに拡散し仕事を分散して処理する際に、拡散先の受信者である

アクター参照の一覧を保持しているもの

Page 22: Isomorphic web development  with scala and scala.js

object SampleDispatcher extends Dispatcherobject 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はメッセージ

Page 23: Isomorphic web development  with scala and scala.js

Conclusion

• Scala + Scala.jsでクライアントーサーバーを高度に統合できる

• (ただし他のシステムとのinteropが犠牲になるかも

• みんなScala.jsを使おう!!!