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


りラゞミヌル・パブキン
スカラ開発者

Source: https://habr.com/ru/post/J272625/


All Articles