このようなScala.jsライブラリの作成にはいくつかの微妙な点がありますが、JS開発者には馴染みがあるようです。 この記事では、 Github APIを操作するための簡単なScala.jsライブラリー( コード )を作成し、イディオムJS APIに焦点を当てます。
しかし、最初に、なぜあなたはそのようなライブラリを作成する必要があるかもしれない理由を尋ねたいと思うでしょうか? たとえば、すでにJavaScriptで記述されたクライアントアプリケーションがあり、Scalaのバックエンドと通信する場合。
Scala.jsを使用してゼロから作成できるとは考えられませんが、次のことを可能にするフロントエンド開発者との対話用のライブラリを作成できます。
- 複雑または非自明なクライアント側のロジックを隠し、便利なAPIを提供します。
- ライブラリでは、バックエンドアプリケーションからモデルを操作できます。
- 同形コードはそのままで使用でき、プロトコル同期の問題を忘れることができます。
- FacebookのParseのような公開開発者APIがあります。
これらのすべての利点のおかげで、Javascript API SDKの開発にも最適です。
最近、REST JSON APIには2つの異なるブラウザークライアントがあるという事実に出会いました。そのため、同形ライブラリの開発は良い選択でした。
ライブラリの作成を始めましょう
要件:Scala開発者として、機能的なスタイルで記述し、すべてのScalaチップを使用したいと考えています。 同様に、ライブラリ開発者は、JS開発者にとって理解しやすいものでなければなりません。
ディレクトリ構造から始めましょう 。これは、Scalaアプリケーションの通常の構造と同じです。
+-- build.sbt +-- project ¦ +-- build.properties ¦ L-- plugins.sbt +-- src ¦ L-- main ¦ +-- resources ¦ ¦ +-- demo.js ¦ ¦ L-- index-fastopt.html ¦ L-- scala L-- version.sbt
resources/index-fastopt.html
ページはAPIを確認するためにライブラリと
resources/demo.js
ファイルのみをダウンロードします
API
目標は、Github APIとの対話を簡素化することです。 最初に、ユーザーとそのリポジトリのロードという1つの機能のみを実行します。 したがって、これはパブリックメソッドであり、回答結果を含むいくつかのモデルです。 モデルから始めましょう。
モデル
クラスを次のように定義します。
case class User(name: String, avatarUrl: String, repos: List[Repo]) sealed trait Repo { def name: String def description: String def stargazersCount: Int def homepage: Option[String] } case class Fork(name: String, description: String, stargazersCount: Int, homepage: Option[String]) extends Repo case class Origin(name: String, description: String, stargazersCount: Int, homepage: Option[String], forksCount: Int) extends Repo
複雑なことは何もありません。
User
はいくつかのリポジトリがあり、リポジトリはオリジナルまたはフォークにすることができますが、JS開発者向けにこれをエクスポートするにはどうすればよいですか?
機能の詳細については、「 Scala.js APIをJavascriptにエクスポートする」を参照してください。
オブジェクトを作成するためのAPI。
それがどのように機能するか、コンストラクタをエクスポートする簡単なソリューションを見てみましょう。
@JSExport case class Fork(name: String, /*...*/)]
ただし、機能しません。エクスポートされた
Option
コンストラクターがないため、
homepage
パラメーターを作成できません。 ケースクラスには他の制限があり、継承を使用してコンストラクターをエクスポートすることはできません。そのようなコードはコンパイルされません。
@JSExport case class A(a: Int) @JSExport case class B(b: Int) extends A(12) @JSExport object Github { @JSExport def createFork(name: String, description: String, stargazersCount: Int, homepage: UndefOr[String]): Fork = Fork(name, description, stargazersCount, homepage.toOption) }
ここでは、
js.UndefOr
の助けを借りて、JSスタイルのオプションのパラメーターを処理します
js.UndefOr
を渡すことも、まったく行わないこともできます。
// JS var homelessFork = Github().createFork("bar-fork", "Bar", 1); var fork = Github().createFork("bar-fork", "Bar", 1, "http://foo.bar");
Scalaオブジェクトのキャッシュに関する注意:
Github()
毎回呼び出すことはお勧めできません。怠lazが必要ない場合は、起動時にキャッシュできます。
<!--index-fastopt.html--> <script> var Github = Github()
フォーク名を取得しようとすると、
undefined
取得
undefined
ます。 そうです、エクスポートされていません。モデルのプロパティをエクスポートしましょう。
String
、
Boolean
または
Int
などのネイティブ型には問題がありません。次のようにエクスポートできます。
sealed trait Repo { @JSExport def name: String // ... }
クラスのケースフィールドは、
@(JSExport@field)
アノテーション
@(JSExport@field)
を使用してエクスポートできます。
forks
プロパティの例:
case class Origin(name: String, description: String, stargazersCount: Int, homepage: Option[String], @(JSExport@field) forks: Int) extends Repo
オプション
しかし、あなたはそれを推測しました、
homepage: Option[String]
問題があります
homepage: Option[String]
。 エクスポートすることもできますが、
Option
から値を取得することは役に立ちません。jsは開発者が何らかのメソッドを呼び出す必要がありますが、
Option
については何もエクスポートされていません。
一方、Scalaコードがシンプルで直感的なままであるように、
Option
を維持したいと思います。 簡単な解決策は、特別なjs getterをエクスポートすることです。
import scala.scalajs.js.JSConverters._ sealed trait Repo { //... // , JS def homepage: Option[String] @JSExport("homepage") def homepageJS: js.UndefOr[String] = homepage.orUndefined }
試してみましょう:
console.log("fork.name: " + fork.name); console.log("fork.homepage: " + fork.homepage);
私たちはお気に入りの
Option
を残して、JS用のきれいで美しいAPIを作りました。 やった!
一覧
User.repos
は
List
であり、エクスポートに問題があります。 解決策は同じで、JS配列としてエクスポートするだけです:
@JSExport("repos") def reposJS: js.Array[Repo] = repos.toJSArray // JS user.repos.map(function (repo) { return repo.name; });
サブタイプ
Repo
特性にはまだ1つの問題があります。 コンストラクターをエクスポートしないため、JS開発者は、どの
Repo
サブタイプを扱っているかを把握できません。
Javascriptにはパターンマッチングはなく、継承の使用はそれほど一般的ではない(そして議論の余地がある)ため、いくつかのオプションがあります。
-
hasForks: Boolean
メソッドまたはhasForks: Boolean
メソッドを作成します。 これは正常ですが、十分に一般化されていません。 -
type: String
追加type: String
すべてのサブタイプのtype: String
プロパティ。
2つの方法を選択します。抽象化してプロジェクト全体で簡単に使用できます。typeプロパティをエクスポートするmixinを宣言しましょう。
trait Typed { self => @JSExport("type") def typ: String = self.getClass.getSimpleName } </code> , <code>type</code> Scala. <source lang="scala"> sealed trait Repo extends Typed { // ... }
...そしてそれを使用します:
// JS fork.type // "Fork"
定数を保存しておけば、少し安全にできます(ここでコンパイラが役立ちます)。
class TypeNameConstant[T: ClassTag] { @JSExport("type") def typ: String = classTag[T].runtimeClass.getSimpleName }
このヘルパーを使用して、
GitHub
オブジェクトで必要な定数を宣言できます。
@JSExportAll object Github { //... val Fork = new TypeNameConstant[model.Fork] val Origin = new TypeNameConstant[model.Origin] }
これにより、Javascriptの行を避けることができます。例
// JS function isFork(repo) { return repo.type == Github.Fork.type }
これが、サブタイプの操作方法です。
エクスポートするオブジェクトを変更できない場合はどうすればよいですか?
この場合、おそらく、クロスコンパイルされたモデルのクラスまたはインポートされたライブラリからオブジェクトをエクスポートしています。 メソッドは
Option
と
List
で同じですが、違いが1つあります-JSの観点から受け入れられるラッパークラスと変換を実装する必要があります。
ここでは、エクスポート(
Scala => JS
)およびインスタンス化(
JS => Scala
)にのみjs置換を使用することが重要です。すべてのビジネスロジックは、純粋なScalaクラスによってのみ実装する必要があります。
Commit
というクラスがある
Commit
ます。これは変更できません。
case class Commit(hash: String)
エクスポート方法は次のとおりです。
object CommitJS { def fromCommit(c: Commit): CommitJS = CommitJS(c.hash) } case class CommitJS(@(JSExport@field) hash: String) { def toCommit: Commit = Commit(hash) }
次に、たとえば、管理するコードの
Branch
クラスは次のようになります。
case class Branch(initial: Commit) { @JSExport("initial") def initialJS: CommitJS = CommitJS.fromCommit(initial) }
コミットはJS環境では
CommitJS
オブジェクトとして表されるため、
Branch
のファクトリメソッドは次のようになります。
@JSExport def createBranch(initial: CommitJS) = Branch(initial.toCommit)
もちろん、これは優れた方法ではありませんが、コンパイラによってチェックされます。 そのため、このようなライブラリを、値クラスのプロキシとしてだけでなく、不必要な詳細を隠してAPIを簡素化するファサードとして見ることを好みます。
アヤックス
実装
簡単にするために、ネットワークリクエストにはscalajs-domライブラリの
Ajax
拡張を使用します。 エクスポートを中断して、APIを実装してみましょう。
物事を複雑にしないために、AJAXに関連するすべてのものを
API
オブジェクトに配置します。これには、ユーザーのロードとリポジトリのロードの2つのメソッドがあります。
APIをモデルから分離するDTOレイヤーも作成します。 メソッドの結果は
Future[String \/ DTO]
になり、
DTO
は要求されたデータのタイプであり、
String
はエラーを表します。
object API { case class UserDTO(name: String, avatar_url: String) case class RepoDTO(name: String, description: String, stargazers_count: Int, homepage: Option[String], forks: Int, fork: Boolean) def user(login: String) (implicit ec: ExecutionContext): Future[String \/ UserDTO] = load(login, s"$BASE_URL/users/$login", jsonToUserDTO) def repos(login: String) (implicit ec: ExecutionContext): Future[String \/ List[RepoDTO]] = load(login, s"$BASE_URL/users/$login/repos", arrayToRepos) private def load[T](login: String, url: String, parser: js.Any => Option[T]) (implicit ec: ExecutionContext): Future[String \/ T] = if (login.isEmpty) Future.successful("Error: login can't be empty".left) else Ajax.get(url).map(xhr => if (xhr.status == 200) { parser(js.JSON.parse(xhr.responseText)) .map(_.right) .getOrElse("Request failed: can't deserialize result".left) } else { s"Request failed with response code ${xhr.status}".left } ) private val BASE_URL: String = "https://api.github.com" private def jsonToUserDTO(json: js.Any): Option[UserDTO] = //... private def arrayToRepos(json: js.Any): Option[List[RepoDTO]] = //... }
コードの逆シリアル化は非表示であり、興味深いものではありません。コードが200でない場合、
load
メソッドはエラー文字列を返します。そうでない場合、応答をJSONに変換してからDTOに変換します
これで、APIレスポンスをモデルに変換できます。
import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue object Github { // ... def loadUser(login: String): Future[String \/ User] = { for { userDTO <- EitherT(API.user(login)) repoDTO <- EitherT(API.repos(login)) } yield userFromDTO(userDTO, repoDTO) }.run private def userFromDTO(dto: API.UserDTO, repos: List[API.RepoDTO]): User = //.. }
ここでは、monadトランスフォーマーを使用して
Future[\/[..]]
、DTOをモデルに変換します。
すばらしい、機能的なScalaコードのように見えます。 次に、ライブラリのユーザー向けの
loadUser
メソッドに
loadUser
ましょう。
未来を共有する
ここで質問があります。Javascriptで非同期呼び出しを処理する一般的に受け入れられている方法は何ですか? js開発者は存在しないので、笑っています。 コールバック、イベントエミッター、プロミス、ファイバー、ジェネレーター、非同期/待機がすべて使用されていますが、何を選択する必要がありますか? PromisesはScala Futureに最も近い実装だと思います。 約束は非常に人気があり、多くの最新のブラウザですぐにサポートされています。 最初に、約束についてコードに伝える必要があります。 これは「Typed Facade」と呼ばれます。 これは自分で簡単に行うことができますが、scalajs-domは既に実装されています。 実装を自分で行いたい人の例は次のとおりです。
trait Promise[+A] extends js.Object { @JSName("catch") def recover[B >: A]( onRejected: js.Function1[Any, B]): Promise[Any] = js.native @JSName("then") def andThen[B]( onFulfilled: js.Function1[A, B]): Promise[Any] = js.native @JSName("then") def andThen[B]( onFulfilled: js.Function1[A, B], onRejected: js.Function1[Any, B]): Promise[Any] = js.native }
まあ、
Promise.all
ようなメソッドを持つコンパニオンオブジェクト。 ここで、この特性を拡張するだけです。
@JSName("Promise") class Promise[+R]( executor: js.Function2[js.Function1[R, Any], js.Function1[Any, Any], Any] ) extends org.scalajs.dom.raw.Promise[R]
したがって、
Future
を
Promise
に変換するだけです。 暗黙のクラスを使用してこれを行います。
object promise { implicit class JSFutureOps[R: ClassTag, E: ClassTag](f: Future[\/[E, R]]) { def toPromise(recovery: Throwable => js.Any) (implicit ectx: ExecutionContext): Promise[R] = new Promise[R]((resolve: js.Function1[R, Unit], reject: js.Function1[js.Any, Unit]) => { f.onSuccess({ case \/-(f: R) => resolve(f) case -\/(e: E) => reject(e.asInstanceOf[js.Any]) }) f.onFailure { case e: Throwable => reject(recovery(e)) } }) } }
回復機能は、「落ちた」
Future
を「落ちた」
Promise
変えます。 条項の左側も約束を破棄します。
それでは、約束を友人のフロントエンドと共有しましょう。いつものように、元のメソッドの隣の
Github
オブジェクトに追加します。
def loadUser(login: String): Future[String \/ User] = //... @JSExport("loadUser") def loadUserJS(login: String): Promise[User] = loadUser(login).toPromise(_.getMessage)
ここで、エラーが発生した場合、例外からエラーのあるプロミスを削除します。 これで、APIをテストできます。
// JS Github.loadUser("vpavkin") .then(function (result) { console.log("Name: ", result.name); }, function (error) { console.log("Error occured:", error) }); // Name: Vladimir Pavkin
これで、Futureと私たちが慣れ親しんでいるすべてのものを使用できるようになりました-それでも、慣用的なJS APIとしてエクスポートできます。
結論Scala.jsを使用してJavascriptライブラリを作成するためのヒントを次に示します。
- 起動時にエクスポートされたオブジェクトをキャッシュします。
- シームレスタイプをそのままエクスポートします 。
-
Option
、List
およびその他のScalaピースをエクスポートしないでください。js.UndefOr
およびjs.Array
変換するゲッターを使用します。 - コンストラクターをエクスポートしないでください。 JSに優しい工場を使用してください。
- JSフレンドリーとは、
js.*
を受け入れることを意味しjs.*
型を標準のScala型に変換します。 - 文字列フィールド
type
を合計タイプにミックスします。 -
Future
をJS Promise
としてエクスポートします。 - Scalaで最初に書いてください。 Scala開発者としての自己表現に限定せず、言語機能を最大限に活用してください。
これで、これらすべてをエクスポートできることがわかりました。
サンプルコードはGitHubにあります: https : //github.com/vpavkin/scalajs-library-tips
ウラジミール・パブキン
スカラ開発者