Scala côté frontend grâce à Laminar

Le langage Scala est généralement utilisé dans deux cas : la mise en place de serveurs backend (microservices ou monolithes) et la création de pipeline de données (Spark, Kafka Streams ou autre). Pour beaucoup, ses capacités s’arrêtent ici.

Pourtant, ce n’est pas le cas. Ces dernières années, nous avons vu émerger dans la communauté deux game changers dans des domaines complétement différents :

  • Scala Native qui permet de développer des applications embarquées
  • ScalaJS qui permet de développer des applications frontend

Je n’ai pas encore eu le temps de creuser Scala Native, alors je vous propose aujourd’hui que l'on aborde ScalaJS ensemble afin de vous montrer comment ce dernier peut vous être utile.

Pourquoi ScalaJS ?

ScalaJS offre un avantage considérable comparé aux solutions existantes, son interopérabilité avec les autres librairies Scala. En effet, c’est selon moi l’élément à prendre en compte si l’on considère utiliser ScalaJS en production. Utiliser le même langage côté Frontend et Backend permet de ne pas se répéter et de rester DRY. En effet, généralement lorsque nous utilisons ScalaJS, nous avons trois projets dans notre repository:

  • L’application Frontend
  • L’application Backend
  • La librairie partagée

Les deux applications sont assez communes, ce qui nous intéresse ici, c'est la librairie partagée. Cette dernière a comme particularité d’abriter du code Scala qui va être utilisé par l’application backend en Scala et l’application frontend en ScalaJS. Généralement, nous entreposons dans cette librairie les choses suivantes :

  • Notre domaine, à savoir les case class qui décrivent l’aspect métier qu’essaye de résoudre notre application. Généralement, ce sont des case class partagées entre le front et le back via une API REST.
  • Nos encoders et decoders, qui nous permettent de partager ce domaine entre nos deux applications.
  • Nos descriptions de routes, ces dernières ne sont pas obligatoires, mais je vous encourage fortement à les implémenter en utilisant Tapir, par exemple. Cela va permettre de réutiliser ces descriptions pour créer notre serveur côté Backend et notre client côté Frontend rapidement et facilement.

Comme je l’ai mentionné auparavant, rajouter cette librairie permet de ne pas se répéter dans les points mentionnés précédemment. Ce n’est pas le seul avantage, cette librairie permet de garder une version unifiée de notre système. Ainsi, si vous changez votre domaine en rajoutant un champ, par exemple, ou alors si vous rajoutez un query parameter dans l’une de vos routes, c’est automatiquement répercuté côté back et front en même temps.

On évite les erreurs humaines, on rend notre boucle de développement plus rapide et on utilise le même langage pour toute notre équipe. Voici où réside la puissance de ScalaJS. La question à se poser ensuite est la suivante : est-ce que l’écosystème ScalaJS est assez riche pour pouvoir mettre en place une application frontend ?

L’écosystème de ScalaJS

Je vous ai parlé de la librairie partagée comme étant la solution idéale pour réunir le frontend et le backend, trop longtemps séparés. Il y a juste un seul problème que je n’ai pas mentionné auparavant. Pour que cela fonctionne, il faut que les librairies utilisées soient compatibles avec Scala et ScalaJS.

ScalaJS génère du code JavaScript. Il vous sera impossible d’utiliser une librairie Java dans le projet partagé du moment où ce même morceau de code doit être utilisé côté frontend et backend. Ainsi, si vous utilisez Gson pour la serialization / deserialization de vos JSON, cela n’est pas forcément adapté à la situation. Vous pouvez tout de même utiliser une grande partie du JDK puisque ce dernier a été partiellement réécrit en ScalaJS.

Fort heureusement pour nous, dans le cas des librairies Scala, pléthore de librairies diverses et variées supportent Scala ET ScalaJS, pour ne pas les citer : ZIO, Tapir, Circe, par exemple. Pour une liste un peu plus exhaustive, je vous conseille ce lien : https://www.scala-js.org/libraries/libs.html.

Pour ce qui est du projet Frontend fait en ScalaJS, ce dernier supporte aussi tout l’écosystème JavaScript. Pour cela, il faut utiliser les façades. Pour ceux qui sont familiers avec TypeScript, le principe est le même. Comme JavaScript est typé dynamiquement, cette étape consiste à rajouter des types aux différentes fonctions de ladite librairie. Vous pouvez créer vos propres faces simplement, utiliser ceux déjà écrits par la communautéou alors utiliser directement les types defintions typescript.

Pour saupoudrer le tout, la communauté a aussi créé des librairies exclusivement destinées à ScalaJS. C’est le cas de Laminar, une librairie frontend fait pour Scala que je trouve prometteur.

Créer votre site internet avec Laminar

Laminar est une librairie pour créer des applications Web, au même titre que React ou encore Angular. Outre le fait que cette librairie a été faite pour ScalaJS, je la trouve particulièrement plaisante à utiliser. Elle se base sur un principe peu commun en programmation, le reactive programming.

Sa syntaxe est assez naturelle et ne devrait pas trop dépayser les personnes développant des applications frontend. Voici un exemple :

import com.raquo.laminar.api.L.{*, given}
import org.scalajs.dom

object Main {
	val app = div(
		h1("Hello World", className := "title")
		className := "body"
	)

	renderOnDomContentLoaded(dom.document.querySelector("#app"), app)
}

Là où l’application se démarque, c’est sur son côté réactif. En effet, Laminar se base sur une autre librairie écrite par le même auteur (raquo) nommé Airstream. Le principe est le suivant : l’état de votre application est contenu dans des streams. Nos différents éléments Html peuvent alors mettre à jour ces streams ou alors récupérer l’état de ces streams pour automatiquement s'actualiser.

Implémentation d’un générateur de nombre aléatoire

Imaginons que nous voulons créer une application qui tire un nombre aléatoire et que nous voulons l’afficher. L’état de l’application est ce nombre aléatoire. Comme nous voulons à la fois le mettre à jour et récupérer son état, nous devons créer un Var.

val state: Var[Int] = Var(0)

Nous allons maintenant créer le composant qui va s’abonner à cette valeur :

val display: Div = div(
	child <-- state.signal.map(span(_)),
  className := "display"
)

Ce dernier est un div qui va contenir un span contenant la valeur du Var à l’intérieur. À tout moment, si la valeur du Var change, alors le span sera de nouveau généré. L’opérateur ←- permet ici de consommer le signal.

Ce nombre ne risque pas de se générer tout seul ! Alors, il faut aussi un composant pour mettre à jour cette valeur :

val generator: Button = button(
  "Generate a new number",
  onClick.map { _ => Random.nextInt() } --> state,
  className := "generator"
)

Ce qui nous intéresse ici, c'est ce qui se passe lorsqu’on clique sur ce bouton. onClick renvoie l’événement de la souris, ce dernier ne nous intéresse pas, donc on map cette valeur pour, à la place, générer un nombre aléatoire. Tout comme <-- permet de consommer une valeur, —-> permet de produire une valeur, en l’occurrence, le nombre aléatoire.

Et c’est tout ! Les deux composants communiquent à présent ensemble. L’un actualise l’état de l’application. L’autre le consomme et se met à jour lorsque l’état change. Il faut un certain temps d’adaptation pour manipuler les streams avec aisance, mais croyez-moi, cela en vaut la chandelle.

J’ai créé spécialement pour cet article un repository qui implémente le code ci-dessus : https://github.com/univalence/laminar-article. Ce dernier peut aussi vous être utile pour mettre en place une codebase ScalaJS comportant notamment le préprocesseur SCSS et Vite. Cette codebase est en très grande partie inspirée du talk de Sebastien Doeraene que nous avons notamment eu la chance d’écouter lors du Scala.IO 2022.

Les tradeoffs

Cet avant-gout devrait déjà vous avoir partiellement convaincu. J’ai personnellement été bluffé de voir à quel point le développement frontend devenait plaisant et intuitif avec Laminar. Je n’ai bien sûr qu’effleuré la surface et je vous conseille d’aller creuser dans la documentation si cette dernière vous intéresse.

Maintenant, afin que vous optiez pour la solution qui convient le plus à votre projet, il est de mon devoir de vous faire part des points négatifs d’un tel environnement de développement. Nous avons longuement vu pourquoi opter pour ScalaJS. Voici certaines choses qui pourraient vous manquer si jamais vous sautiez le pas.

Le point le plus important selon moi, c’est le tooling. Ce dernier n’est pas très optimisé pour ScalaJS. Il suffit de voir le repository de l’exemple ci-dessus pour se rendre compte qu’il faut tout de même pas mal de code custom pour rattacher les pièces ensemble. De plus, certains outils très intéressant n’existent pas pour ScalaJS, ce qui me manque personnellement, c'est le support de Storybook. Cet outil est devenu un incontournable du développement frontend et permet véritablement à une équipe d’avoir plus de visibilité sur ses composants et de créer un design system déclinable et efficace.

L’écosystème étant encore très jeune, il manque de librairies et de frameworks. Comme dit au-dessus, ScalaJS est compatible avec toutes les libraires Javascript à condition d’avoir une façade. Cependant, comme utiliser une librairie en Java dans un projet Scala n’est pas agréable, il en va de même pour une librairie Javascript dans un projet ScalaJS. Cette fonctionnalité permet de dépanner le développeur en lui assurant, par exemple, de toujours trouver chaussure à son pied. Cependant, on ne lésinerait pas sur plus de librairies directement faites en ScalaJS, tel que Laminar, adoptant ainsi les conventions implicites du langage, tel que la composition, l’omniprésence des fonctions anonymes ou encore l’intégration avec le standard librairie de Scala.

Ces tradeoffs ne sont pas une fatalité en soi. Ils ne sont que la conséquence d’un écosystème encore assez jeune et ambitieux. Néanmoins, la stabilité reste une métrique très importante à prendre en compte lorsqu’on choisit une technologie pour un projet professionnel et je devais souligner ces points.

Conclusion

Depuis que j’ai vu la présentation sur ScalaJS et Laminar de Sébastien Doeraene au Scala.IO 2022, j’ai été conquis par la solution. Elle réunit dans une seule et même stack notre code frontend et backend, elle permet d’avoir un code typé, dry et safe. Selon moi, ScalaJS a réussi son coup, à savoir permettre à des développeurs Scala de coder des applications frontend sans que cela soit un supplice.

La technologie reste assez jeune et vous ne retrouverez pas tous les outils présents dans l’écosystème Javascript. Mais c’est largement suffisant pour construire des projets d’ampleurs sans trop de soucis. Essayer c’est l’adopter !