Как написать JS-библиотеку на ScalaJS

Scala.js открывает огромный мир фронтенд технологий для Scala разработчиков. Обычно проекты, использующие Scala.js, это веб- или nodejs-приложения, но бывают случаи, когда вам просто нужно создать JavaScript-библиотеку.

Есть некоторые тонкости в написании такой Scala.js библиотеки, но они покажутся знакомыми для JS разработчиков. В этой статей мы создадим простую Scala.js библиотеку (код) для работы с Github API и сосредоточимся на идиоматичности JS API.

Но сначала наверняка вы хотите спросить, зачем вообще может понадобиться делать такую библиотеку? Например, если у вас уже есть клиентское приложение написанное на JavaScript и оно общается с бэкендом на Scala.

Вряд ли у вас получится написать ее с чистого листа с помощью Scala.js, но можно написать библиотеку для взаимодействия между вами и фронтенд разработчиками, которая позволит:

  • спрятать сложную или неочевидную клиентсайд логику в ней и предоставить удобное API;
  • в библиотеке вы сможете работать с моделями из backend приложения;
  • изоморфный код из коробки и можете забыть про проблемы синхронизации протоколов;
  • у вас будет публичный API для разработчиков, как у Facebook«s Parse.


Также это отличный выбор для разработки Javascript API SDK, благодаря всем этим преимуществам.

Недавно я столкнулся с тем что у нашего REST JSON API два разных браузерных клиента, поэтому разработка изоморфной библиотеки была хорошим выбором.
Давайте начнем создание библиотеки

Требования: как Scala разработчики мы хотим писать в функциональном стиле и использовать все фишки Scala. В свою очередь API библиотеки должно быть легко понять для 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 — страница только загрузит нашу библиотеку и файл resources/demo.js, для проверки API

API

Цель — упростить взаимодействие с Github API. Для начала мы сделаем только одну фичу — загрузку юзеров и их репозиториев. Итак это публичный метод и парой моделей с результатами ответа. Начнем с модели.

Модель

Определим наши классы вот так:

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 разработчиков?

Для полного описания функционала смотрите Export Scala.js APIs to Javascript.

API для создания объектов.
Давайте посмотрим как оно работает, простое решение экспортировать конструктор.

@JSExport
case class Fork(name: String, /*...*/)]


Но оно не сработает, у вас нет экспортированного конструктора Option, поэтому не получится создать параметр homepage. Есть и другие ограничения для case классов, вы не сможете экспортировать конструкторы с наследованием, вот такой код даже не скомпилируется

@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: можно передать String или вообще обойтись без него:

// JS
var homelessFork = Github().createFork("bar-fork", "Bar", 1);
var fork =         Github().createFork("bar-fork", "Bar", 1, "http://foo.bar");



Замечание касательно кеширования Scala-объектов:

Делать вызов Github() каждый раз не лучшее идея, если вам не нужна ленивость вы можете закешировать их при запуске: