ScalaJSでJSライブラリを作成する方法

Scala.jsは、Scala開発者にフロントエンドテクノロジーの巨大な世界を開きます。 通常、Scala.jsを使用するプロジェクトはWebまたはnodejsアプリケーションですが、JavaScriptライブラリを作成するだけでよい場合もあります。



このようなScala.jsライブラリの作成にはいくつかの微妙な点がありますが、JS開発者には馴染みがあるようです。 この記事では、 Github APIを操作するための簡単なScala.jsライブラリー( コード )を作成し、イディオムJS APIに焦点を当てます。



しかし、最初に、なぜあなたはそのようなライブラリを作成する必要があるかもしれない理由を尋ねたいと思うでしょうか? たとえば、すでにJavaScriptで記述されたクライアントアプリケーションがあり、Scalaのバックエンドと通信する場合。



Scala.jsを使用してゼロから作成できるとは考えられませんが、次のことを可能にするフロントエンド開発者との対話用のライブラリを作成できます。



これらのすべての利点のおかげで、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にはパターンマッチングはなく、継承の使用はそれほど一般的ではない(そして議論の余地がある)ため、いくつかのオプションがあります。







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ライブラリを作成するためのヒントを次に示します。



これで、これらすべてをエクスポートできることがわかりました。



サンプルコードはGitHubにあります: https : //github.com/vpavkin/scalajs-library-tips





ウラジミール・パブキン

スカラ開発者



All Articles