Przegląd Scala.js


Dlaczego warto zainteresować się Scala.js? Ponieważ oferuje możliwości języka Scala – a co za tym idzie, jego wysoki poziom rozbudowania, umożliwiający odwzorowanie technik programowania obiektowego i funkcyjnego. Co więcej, dzięki odpowiednim importom, przy pomocy Scala.js możemy pisać tak jak w JavaScript – nie odczuwając przy tym większej różnicy.

Mój artykuł adresowany jest zarówno do developerów frontend, którym do projektu nie wystarcza JavaScript i potrzebują silnego typowania, jak i do programistów backendowych, którzy chcieliby stworzyć przeglądarkowy projekt w Scali.

Osobiście zainteresowałem się tematem Scala.js jako programista frontend, znudzony nieco prostotą JavaScriptu i nienasycony możliwościami TypeScriptu. Jednocześnie mam pewne doświadczenie w backendzie, dlatego też postaram się przedstawić Scala.js pod kątem użyteczności dla programistów z obu światów.

Czym jest Scala.js?

Scala.js to projekt rozwijany przez zespół EPFL – ten sam, który tworzy język Scala. Głównym produktem jest tu kompilator Scala do JavaScript (ES5).

Jeśli chodzi o frontend, Scala.js doskonale komponuje się z React, ale też z Angular, Ember, jQuery, a w mniejszym stopniu z AngularJS. Ogólnie rzecz biorąc, możemy użyć dowolnego frameworka czy też biblioteki, nawet jeśli nie ma do nich jeszcze bindingów (interface’u). Te jednak możemy napisać sami – o tym poniżej.

Jeśli szukamy zupełnie nowych wrażeń, możemy spróbować Udash – w pełni scalowego połączenia Scalatags, ScalaCSS (UdashCSS) wraz z kompletną implementacją API REST lub RPC – składającego się na naprawdę interesujący full-stack framework.

Oczywiście Scala.js znajdzie zastosowanie wszędzie tam, gdzie sprawdza się JavaScript, czyli także w backendzie lub cloudzie.

Korzyści

Kilka bardziej „miękkich” argumentów na rzecz stosowania Scala.js w projektach informatycznych:

  • bezpieczeństwo typów, a w konsekwencji błędy takie jak x is not defined, x is not a function są eliminowane już na poziomie kompilatora i nie powodują zablokowania przeglądarki podczas runtime aplikacji,
  • standaryzacja – używamy jednego języka programowania w zespole; brak zbędnych parserów i błędów związanych z niezgodnością modeli frontendowych i backendowych,
  • biblioteki Scala – właściwie wszystko, co Scala oferuje innym językom (Java), jest też dostępne w Scala.js, m.in. pełne programowanie funkcyjne: Scalaz, Each, future.scala, tryt.scala, RAII.scala, Shapeless, Cats, Monocle, Quicklens, a także typowo webowe, jak scalatags (do tworzenia kodu html – backend lub obiektów DOM – frontend), ScalaCSS itp. Warto też zwrócić uwagę na serializer JSON Pickle. (https://www.scala-js.org/libraries/libs.html)
  • mniej zależności – paczki npm można ściągnąć za pomocą sbt (sbt-web i sbt-js-engine),
  • wiele istniejących bindingów (fasad dla bibliotek JS) i łatwość tworzenia nowych (sbt-js-engine),
  • jest to projekt twórców Scala,
  • aplikacje można stworzyć jednym poleceniem z szablonów Gitter8, np.:
sbt new vmunier/akka-http-scalajs.g8

Podobieństwa i różnice względem JavaScript

Funkcje JavaScript oraz API DOM w Scala.js są odwzorowane 1:1. Scala.js używa natywnego API silnika przeglądarki, innymi słowy, utrzymuje standard ECMA. Oto przykład, który można skopiować do scalafiddle.io:

import org.scalajs.dom.{console, Event, XMLHttpRequest}
import scala.scalajs.js.JSON

val xhr = new XMLHttpRequest()

xhr.open("GET",
 "https://jsonplaceholder.typicode.com/posts/1"
)
xhr.onload = { (e: Event) =>
 if (xhr.status == 200) {
 val r = JSON.parse(xhr.responseText)
 console.log(r)
 }
}
xhr.send()

Dokumentacja na scala-js.org jest utrzymywana na wysokim poziomie, ponieważ tworzy ją ten sam zespół, co scala-lang.org. Znajdziemy tu też tutorial do Reacta, korzystający z bindingów scalajs-react. Jest też odnośnik do scalafiddle.io. Warto zerknąć na lewą kolumnę, gdzie dostępne są biblioteki. Jest to tylko drobny fragment tego, co można dodać do projektu, lecz wywołanie console.log’a z funktorem daje naprawdę dużo radości:

import org.scalajs.dom.console

import scala.scalajs.js._, scala.scalajs.js.timers._
import cats._ , cats.instances._ , cats.implicits._

val addRandom : Double => Double = _ + Math.floor(Math.random() * 10)
def addedRandom(la: List[Double]) = {
 Apply[List].ap(List(addRandom))(la)
}

var x = 0;

def setme {
 console.log(addedRandom(List(1, 11, 111, 1111)).toString)
 x += 1
 if (x < 5) {
 setTimeout(1000) (setme)
 }
}

setTimeout(1000) (setme)

Jak stworzyć stronę internetową?

Frameworki Scala

Wybrałem dwa najbardziej interesujące frameworki jeśli chodzi o przejrzystość, dostępność dokumentacji, kompletność i estetykę stron internetowych. Ale jeśli interesuje nas pełna lista na rok 2016, to zachęcam do zerknięcia na interesujący slajd.

Scala Play

Najkrótszą drogą do stworzenia witryny internetowej z dodatkiem JavaScript będzie Scala Play, który zapewni nam solidny szkielet MVC. Dodatkowo, jeśli zależy nam np. na GraphQL, jako swego rodzaju transporcie dla naszych zapytań RPCowych, możemy wypróbować połączenie Scala Play z Sangrią (try.sangria-graphql.org).

Tworzenie aplikacji z szablonu Gitter8:

sbt new vmunier/play-scalajs.g8

Polecam też tutorial Grega Dorrella, który ukończymy uruchamiając i naprawiając testy na trzech kolejnych gałęziach projektu: 

  1. Nauczymy się korzystać ze Scala Play i Twirla (analogiczne do Twig w PHP).
  2. Dowiemy się czegoś o Autowire (biblioteka RPC, inną biblioteką do tego zastosowania jest Sloth).
  3. Dołożymy mapę Google.

Udash

Podobnie jak Play, Udash ma swojego Gitter8. Choć może nie w takim natężeniu – możemy natomiast wybrać, czy tworzymy tylko frontend, czy całą aplikację. Dodatkowo, Udash oferuje API websocketowe i z tego powodu znalazł się w tym artykule. O ile Play jest frameworkiem Scala, który można dostosować do Scala.js, o tyle Udash jest już od początku pisany z myślą o byciu full-stackiem.

Tworzenie aplikacji z szablonu:

sbt new UdashFramework/udash.g8

Przy tworzeniu wybieramy wersję aplikacji (z backendem lub bez, rodzaj API: REST bądź RPC).

Moim zdaniem jest to bardzo ciekawa opcja, jeśli zaczynaliśmy przygodę z programowaniem od backendu i nie mamy (lub nie zamierzamy zdobywać) wiedzy o JavaScript ani o frameworkach frontendowych.

Adaptacja frameworków webowych

ReactJS

Do dyspozycji mamy kilka bindingów, ale dwa z nich zasługują na największą uwagę:

  • scalajs-react
  • slinky

scalajs-react

Po głębszej analizie strony projektu na githubie, trafimy na trzy przykłady, które miło nas zaskoczą:

  • Scastie – aplikacja do uruchamiania kodu Scala w wybranej wersji języka wraz z bibliotekami
  • Scalafiddle.io – tego narzędzia chyba nie trzeba przedstawiać 🙂
  • Weather app – stworzona na backendzie Play 

Pewnym minusem jest wykonanie strony – nie tyle w sensie estetyki, co stromej krzywej nauki. Poza tym dokumentacja jest częściowo dostępna na githubie.

Ponadto, bindingi są dość nietypowe. Kod w JavaScript:

class Hello extends React.Component {
 render() {
 return React.createElement("div", null, "Hello ", this.props.name);
 }
}

ReactDOM.render(React.createElement(Hello, {name: "World"}), root);

Ten sam komponent w Scala.js:

val Hello = ScalaComponent.builder[String]("Hello")
 .render($ => <.div("Hello ", $.props))
 .build

Hello("World").renderIntoDOM(root)

…wygląda, jak zupełnie inny framework.

Slinky

Prawdopodobnie nie jestem jedynym frontendowcem, który po przeanalizowaniu powyższego kodu chciałby wyłączyć komputer. Pocieszę jedynie, że ja jestem w gorszej sytuacji, bo zdążyłem już przesiedzieć tydzień nad udowadnianiem sobie, że te narzędzia są naprawdę przydatne 🙂 . Na szczęście, po sprawdzeniu Slinky odetchnąłem z ulgą.

Nasz projekt w Slinky tworzymy za pomocą Gitter8, jak przystało na aplikację sbt:

sbt new shadaj/create-react-scala-app.g8

analogicznie do https://github.com/facebook/create-react-app – lub dodajemy do istniejącego projektu (szczegóły na stronie).

Zwróćmy jedynie uwagę na to, że nie uruchamiamy aplikacji po prostu przez:

sbt run

a raczej, w zależności od potrzeb, (development, production), za pomocą:

sbt ";fastOptJS::startWebpackDevServer;~fastOptJS

tudzież:

sbt fullOptJS::webpack

Można to przeoczyć za pierwszym razem i wpaść w zakłopotanie, ponieważ informacja jest w repo pod prawie niewidocznym linkiem.

Kod wygląda bliźniaczo podobnie do oryginału, piszemy właściwie tylko adnotacje:

@react class Hello extends React.Component {
 case class Props(name: String) // muszą tu być, ew type Props = Unit

override def render(): ReactElement = {
 div("Hello ", props.name)
 }
}

ReactDOM.render(Hello("World"), root);

I to by było, w skrócie, na tyle.

Angular

Obsługa frameworków Google wypada nieco skromniej niż w przypadku React. W przypadku Angular, nie ma jeszcze Webpacka, za to mamy do dyspozycji System.js i lite-server pod development. Niestety, dokumentacja jest raczej szczątkowa, a i aplikację lepiej uruchamiać na gotowym frameworku, np. Scala Play. Nie mniej jednak muszę przyznać, że sam kod wygląda na bardzo przemyślany – co jest wielką zaletą tego binda, i daje nadzieję na przyszłość:

import angulate2.std._
import angulate2.platformBrowser.BrowserModule
import org.something.AppComponent

@NgModule(
 imports = @@[BrowserModule],
 declarations = @@[AppComponent],
 bootstrap = @@[AppComponent]
)
class AppModule {}

//org/something/app.component
@Component(
 selector = "my-app",
 template = "<h1>{{greeting}}</h1>
)
class AppComponent {
 val greeting = "Hello Angular!"
}

Natomiast w Angular z TypeScript, zrobilibyśmy to tak:

import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';

@NgModule({
 imports: [BrowserModule],
 declarations: [AppComponent],
 bootstrap: [AppComponent]
})
export class AppModule {}

//app.component
@Component({
 selector: "my-app",
 template: "<h1>{{greeting}}</h1>
})
export class AppComponent {
 greeting = "Hello Angular!"
}

Czyli jesteśmy naprawdę bardzo blisko – powiedziałbym: nawet bliżej, niż w przypadku Slinky.

AngularJS

Jedyny binding, jakiego doszukałem się dla AngularJS, przypomina nieco założeniami react-scalajs, tj. posiada wszystkie jego wady. Co za tym idzie, obawiam się, że dla wprawionych koderów AngularJS będzie to jedynie przeszkoda, ponieważ odchodzące od oryginału bindy niczego raczej nie ułatwiają. Autor zapomniał też, że w wersji 1.5 doszły komponenty i w repo na próżno szukać Component.scala mimo, że bindy obsługują wersję 1.6.

Przykładowo, w AngularJS stworzenie dyrektywy o nazwie my-customer wygląda następująco:

.directive(‘myCustomer’, MyCustomerDirective);
function MyCustomerDirective() {
 return {
 restrict: ‘E’, //wiem, że od tego jest komponent
 templateUrl: ‘my-customer-iso.html’
 scope: {
 customerInfo: ‘=info’,
 title: ‘@’,
 close: ‘&onClose’
 }
 }
}

Typ scope ustawia się traitami, a kod:

@injectable("myCustomer")
class CustomerDirective extends ElementDirective
 with TemplatedDirective with IsolatedScope {
 
 override val templateUrl = "my-customer-iso.html"

bindings ++= Seq(
 "customerInfo" := "info",
 "title" :@ "",
 "close" :& "onClose"
 )
}

…raczej nie przypomina tego z JavaScript.

Tworzymy bindingi

Ponieważ w naszych scalowych aplikacjach chcemy używać przeróżnych bibliotek pisanych pod JavaScript, który to nie ma silnego typowania, musimy stworzyć pewne fasady. W przypadku Webpacka będzie to scalajs-bundler.

Znalazłem też ciekawy artykuł mówiący o tym, jak używać Browserify do instalowania zależności bez konieczności posiadania NodeJS. A jeśli zajrzymy do podlinkowanego na stronie repo, znajdziemy tam bindy do lodasha i jquery, bardziej w postaci bazy do tworzenia własnych.

Podsumowanie

Chyba najlepiej i najpewniej wypada połączenie Scala.js z React przy użyciu Slinky. Dostajemy w prezencie Webpacka i możliwość serwowania wersji livereload lub tworzenia wersji produkcyjnej, testów itp. Co więcej, kod najbardziej przypomina ten „oryginalny”.

Godnym uwagi jest też Angulate2, choć wymaga trochę więcej pracy. Ten framework chyba już po prostu tak ma, że jest wiecznie niedokończony – i nie mówię tu o wersji pod Scala.js.

Uwagę backendowców zwróciłbym na Udash, ponieważ jest to framework napisany w Scali, skoncentrowany na API – posiada także bardzo dobry, dynamiczny frontend pisany z myślą o pełnej integracji RPC.