Utilisation de Scala.js avec NPM et Browserify
Publié: 2022-03-11Si vous utilisez Scala.js, le compilateur du langage Scala vers JavaScript, vous trouverez peut-être que la gestion des dépendances standard de Scala.js est trop restrictive dans le monde JavaScript moderne. Scala.js gère les dépendances avec WebJars, tandis que les développeurs JavaScript gèrent les dépendances à l'aide de NPM. Étant donné que les dépendances produites par NPM sont côté serveur, une étape supplémentaire utilisant Browserify ou Webpack pour générer le code du navigateur est généralement nécessaire.
Dans cet article, je décrirai comment intégrer Scala.js avec la pléthore de modules JavaScript disponibles sur NPM. Vous pouvez consulter ce référentiel GitHub pour un exemple fonctionnel des techniques décrites ici. En utilisant cet exemple et en lisant cet article, vous pourrez collecter vos bibliothèques JavaScript à l'aide de NPM, créer un bundle à l'aide de Browserify et utiliser le résultat dans votre propre projet Scala.js. Tout cela sans même installer Node.js, car tout est géré par SBT.
Gestion des dépendances pour Scala.js
Aujourd'hui, écrire des applications dans des langages qui compilent en JavaScript devient une pratique très courante. De plus en plus de personnes se tournent vers des langages JavaScript étendus, comme CoffeeScript ou TypeScript, ou des transpilers comme Babel, qui vous permettent d'utiliser ES6 aujourd'hui. Dans le même temps, Google Web Toolkit (un compilateur de Java vers JavaScript) est principalement utilisé pour les applications d'entreprise. Pour ces raisons, en tant que développeur Scala, je ne considère plus l'utilisation de Scala.js comme un choix étrange. Le compilateur est rapide, le code produit est efficace et, dans l'ensemble, c'est juste un moyen d'utiliser le même langage à la fois sur le front-end et le back-end.
Cela dit, l'utilisation des outils Scala dans le monde JavaScript n'est pas encore 100% naturelle. Il faut parfois combler le vide entre l'écosystème JavaScript et celui de Scala. Scala.js en tant que langage a une excellente interopérabilité avec JavaScript. Étant donné que Scala.js est un compilateur du langage Scala vers JavaScript, il est très facile d'interfacer le code Scala avec le code JavaScript existant. Plus important encore, Scala.js vous donne la possibilité de créer des interfaces typées (ou façades) pour accéder à des bibliothèques JavaScript non typées (similaire à ce que vous faites avec TypeScript). Pour les développeurs habitués aux langages fortement typés comme Java, Scala ou même Haskell, JavaScript est trop faiblement typé. Si vous êtes un tel développeur, la raison principale est probablement que vous souhaitez utiliser Scala.js pour obtenir un langage (fortement) typé au-dessus d'un langage non typé.
Un problème dans la chaîne d'outils standard Scala.js, qui est basée sur SBT et est encore quelque peu ouvert, est : comment inclure des dépendances, comme des bibliothèques JavaScript supplémentaires, dans votre projet ? SBT standardise sur WebJars, vous êtes donc censé utiliser WebJars pour gérer vos dépendances. Malheureusement, d'après mon expérience, cela s'est avéré insuffisant.
Le problème avec les WebJars
Comme mentionné, la méthode standard Scala.js pour récupérer les dépendances JavaScript est basée sur WebJars. Scala est, après tout, un langage JVM. Scala.js utilise SBT principalement pour créer des programmes, et enfin SBT est excellent pour gérer les dépendances JAR.
Pour cette raison, le format WebJar a été défini exactement pour importer des dépendances JavaScript dans le monde JVM. Un WebJar est un fichier JAR comprenant des ressources Web, où un fichier JAR simple ne comprend que des classes Java compilées. Ainsi, l'idée de Scala.js est que vous devez importer vos dépendances JavaScript en ajoutant simplement des dépendances WebJar, de la même manière que Scala ajoute des dépendances JAR.
Bonne idée, sauf que ça ne marche pas
Le plus gros problème avec les Webjars est qu'une version arbitraire d'une bibliothèque JavaScript aléatoire est rarement disponible en tant que WebJar. Dans le même temps, la grande majorité des bibliothèques JavaScript sont disponibles sous forme de modules NPM. Cependant, il existe un pont supposé entre NPM et WebJars avec un conditionneur automatisé npm-to-webjar
. J'ai donc essayé d'importer une bibliothèque, disponible sous forme de module NPM. Dans mon cas, il s'agissait de VoxelJS, une bibliothèque permettant de créer des mondes de type Minecraft dans une page Web. J'ai essayé de demander la bibliothèque en tant que WebJar, mais le pont a échoué simplement parce qu'il n'y a pas de champs de licence dans le descripteur.
Vous pouvez également faire face à cette expérience frustrante pour d'autres raisons avec d'autres bibliothèques. En termes simples, il semble que vous ne puissiez pas accéder à chaque bibliothèque dans la nature en tant que WebJar. L'obligation d'utiliser WebJars pour accéder aux bibliothèques JavaScript semble être trop restrictive.
Entrez NPM et Browserify
Comme je l'ai déjà souligné, le format de packaging standard pour la plupart des bibliothèques JavaScript est le Node Package Manager, ou NPM, inclus dans n'importe quelle version de Node.js. En utilisant NPM, vous pouvez facilement accéder à presque toutes les bibliothèques JavaScript disponibles.
Notez que NPM est le gestionnaire de packages Node . Node est une implémentation côté serveur du moteur JavaScript V8, et il installe des packages à utiliser côté serveur par Node.js. Tel quel, NPM est inutile pour le navigateur. Cependant, l'utilité de NPM a été prolongée un certain temps pour travailler avec des applications de navigateur, grâce à l'outil omniprésent Browserify, qui est bien sûr également distribué sous forme de package NPM.
Browserify est, tout simplement, un conditionneur pour le navigateur. Il va collecter des modules NPM produisant un « bundle » utilisable dans une application navigateur. De nombreux développeurs JavaScript fonctionnent de cette façon - ils gèrent les packages avec NPM, puis les naviguent pour les utiliser dans l'application Web. N'oubliez pas qu'il existe d'autres outils qui fonctionnent de la même manière, comme Webpack.
Combler le fossé entre SBT et NPM
Pour les raisons que je viens de décrire, je voulais un moyen d'installer des dépendances à partir du Web avec NPM, d'appeler Browserify pour rassembler les dépendances du navigateur, puis de les utiliser avec Scala.js. La tâche s'est avérée un peu plus compliquée que prévu, mais toujours possible. En effet, j'ai fait le travail et je le décris ici.
Pour plus de simplicité, j'ai également choisi Browserify parce que j'ai trouvé qu'il était possible de l'exécuter dans SBT. Je n'ai pas essayé avec Webpack, même si je suppose que c'est possible aussi. Heureusement, je n'ai pas eu à démarrer dans le vide. Plusieurs pièces sont déjà en place :
- SBT prend déjà en charge NPM. Le plugin
sbt-web
, développé pour le Play Framework, peut installer des dépendances NPM. - SBT prend en charge l'exécution de JavaScript. Vous pouvez exécuter les outils Node sans installer Node lui-même, grâce au plugin
sbt-jsengine
. - Scala.js peut utiliser un bundle généré. Dans Scala.js, il existe une fonction de concaténation à inclure dans vos bibliothèques JavaScript arbitraires d'application.
À l'aide de ces fonctionnalités, j'ai créé une tâche SBT qui peut télécharger les dépendances NPM, puis appeler Browserify, produisant un fichier bundle.js
. J'ai essayé d'intégrer la procédure dans la chaîne de compilation, et je peux exécuter le tout automatiquement, mais devoir traiter le regroupement à chaque compilation est tout simplement trop lent. De plus, vous ne modifiez pas les dépendances tout le temps ; par conséquent, il est raisonnable que vous deviez créer manuellement un bundle de temps en temps lorsque vous modifiez les dépendances.
Donc, ma solution était de construire un sous-projet. Ce sous-projet télécharge et empaquete les bibliothèques JavaScript avec NPM et Browserify. Ensuite, j'ai ajouté une commande bundle
pour effectuer la collecte des dépendances. Le bundle résultant est ajouté aux ressources à utiliser dans l'application Scala.js.
Vous êtes censé exécuter ce "bundle" manuellement chaque fois que vous modifiez vos dépendances JavaScript. Comme mentionné, il n'est pas automatisé dans la chaîne de compilation.
Comment utiliser le groupeur
Si vous souhaitez utiliser mon exemple, procédez comme suit : commencez par extraire le référentiel avec la commande Git habituelle.
git clone https://github.com/sciabarra/scalajs-browserify/
Ensuite, copiez le dossier bundle
dans votre projet Scala.js. Il s'agit d'un sous-projet de regroupement. Pour vous connecter au projet principal, vous devez ajouter les lignes suivantes dans votre fichier build.sbt
:
val bundle = project.in(file("bundle")) jsDependencies += ProvidedJS / "bundle.js" addCommandAlias("bundle", "bundle/bundle")
De plus, vous devez ajouter les lignes suivantes à votre fichier project/plugins.sbt
:
addSbtPlugin("com.typesafe.sbt" % "sbt-web" % "1.1.1") addSbtPlugin("com.typesafe.sbt" % "sbt-js-engine" % "1.1.3")
Une fois cela fait, vous avez une nouvelle commande, bundle
, que vous pouvez utiliser pour rassembler vos dépendances. Il générera un fichier bundle.js
sous votre dossier src/main/resources
.
Comment le bundle est-il inclus dans votre application Scala.js ?
La commande bundle
qui vient d'être décrite rassemble les dépendances avec NPM, puis crée un bundle.js
. Lorsque vous exécutez les commandes fastOptJS
ou fullOptJS
, ScalaJS créera un myproject-jsdeps.js
, comprenant toutes les ressources que vous avez spécifiées en tant que dépendance JavaScript, donc également votre bundle.js
. Pour inclure des dépendances groupées dans votre application, vous devez utiliser les inclusions suivantes :

<script src="target/scala-2.11/myproject-jsdeps.js"></script> <script src="target/scala-2.11/myproject-fastopt.js"></script> <script src="target/scala-2.11/myproject-launcher.js"></script>
Votre bundle est maintenant disponible dans le cadre de myproject-jsdeps.js
. Le bundle est prêt et nous avons quelque peu terminé notre tâche (importation des dépendances et exportation vers le navigateur). La prochaine étape consiste à utiliser les bibliothèques JavaScript, qui est un problème différent, un problème de codage Scala.js. Pour être complet, nous allons maintenant discuter de l'utilisation du bundle dans Scala.js et créer des façades pour utiliser les bibliothèques que nous avons importées.
Utilisation d'une bibliothèque JavaScript générique dans votre application Scala.js
Pour récapituler, nous venons de voir comment utiliser NPM et Browserify pour créer un bundle, et inclure ce bundle dans Scala.js. Mais comment utiliser une bibliothèque JavaScript générique ?
Le processus complet, que nous expliquerons en détail dans la suite du post, est :
- Sélectionnez vos bibliothèques dans le NPM et incluez-les dans le
bundle/package.json
. - Chargez-les avec
require
dans un fichier de module de bibliothèque, dansbundle/lib.js
. - Écrivez des façades Scala.js pour interpréter l'objet
Bundle
dans Scala.js. - Enfin, codez votre application en utilisant les bibliothèques nouvellement typées.
Ajouter une dépendance
En utilisant NPM, vous devez inclure vos dépendances dans le fichier package.json
, qui est la norme.
Supposons donc que vous souhaitiez utiliser deux bibliothèques célèbres telles que jQuery et Loadash. Cet exemple est uniquement à des fins de démonstration, car il existe déjà un excellent wrapper pour jQuery disponible en tant que dépendance pour Scala.js avec un module approprié, et Lodash est inutile dans le monde Scala. Néanmoins, je pense que c'est un bon exemple, mais ne le prenez qu'à titre d'exemple.
Alors, allez sur le site Web npmjs.com
et localisez la bibliothèque que vous souhaitez utiliser et sélectionnez également une version. Supposons que vous choisissiez jquery-browserify
version 13.0.0 et lodash
version 4.3.0. Ensuite, mettez à jour le bloc de dependencies
de votre packages.json
comme suit :
"dependencies": { "browserify": "13.0.0", "jquery-browserify": "1.8.1", "lodash": "4.3.0" }
Conservez toujours browserify
, car il est nécessaire pour générer le bundle. Cependant, vous n'êtes pas obligé de l'inclure dans le bundle.
Notez que si vous avez installé NPM, vous pouvez simplement taper depuis le répertoire du bundle
:
npm install --save jquery-browserify lodash
Il mettra également à jour le package.json
. Si vous n'avez pas installé NPM, ne vous inquiétez pas. SBT installera une version Java de Node.js et NPM, téléchargera les fichiers JAR requis et les exécutera. Tout cela est géré lorsque vous exécutez la commande bundle
à partir de SBT.
Exportation de la bibliothèque
Nous savons maintenant comment télécharger les packages. L'étape suivante consiste à demander à Browserify de les rassembler dans un bundle et de les rendre disponibles pour le reste de l'application.
Browserify est un rassembleur de require
, émulant le comportement Node.js pour le navigateur, ce qui signifie que vous devez avoir quelque part une require
pour importer votre bibliothèque. Étant donné que nous devons exporter ces bibliothèques vers Scala.js, le bundle génère également un objet JavaScript de niveau supérieur nommé Bundle
. Donc, ce que vous devez faire est de modifier le lib.js
, qui exporte un objet JavaScript, et d'exiger toutes vos bibliothèques en tant que champs de cet objet.
Si nous voulons exporter vers les bibliothèques Scala.js jQuery et Lodash, cela signifie dans le code :
module.exports = { "jquery": require("jquery-browserify"), "lodash": require("lodash") }
Maintenant, exécutez simplement le groupe de commandes et la bibliothèque sera téléchargée, rassemblée et placée dans le groupe, prête à être utilisée dans votre application bundle
.
Accéder au forfait
Jusque là:
- Vous avez installé le sous-projet bundle dans votre projet Scala.js et l'avez configuré correctement.
- Pour toute bibliothèque que vous vouliez, vous l'avez ajoutée dans le
package.json
. - Vous les avez requis dans le
lib.js
. - Vous avez exécuté la commande
bundle
.
En conséquence, vous avez maintenant un objet JavaScript de niveau supérieur Bundle
, fournissant tous les points d'entrée pour les bibliothèques, disponibles sous forme de champs de cet objet.
Vous êtes maintenant prêt à l'utiliser avec Scala.js. Dans le cas le plus simple, vous pouvez faire quelque chose comme ceci pour accéder aux bibliothèques :
@js.native object Bundle extends js.Object { def jquery : js.Any = js.native def lodash: js.Any = js.native }
Ce code vous permet d'accéder aux bibliothèques de Scala.js. Cependant, ce n'est pas la façon dont vous devriez le faire avec Scala.js, car les bibliothèques ne sont toujours pas typées. Au lieu de cela, vous devriez écrire des « façades » typées, ou wrappers, afin de pouvoir utiliser les bibliothèques JavaScript non typées à l'origine de manière typée .
Je ne peux pas vous dire ici comment écrire les façades car cela dépend de la bibliothèque JavaScript spécifique que vous souhaitez envelopper dans Scala.js. Je ne montrerai qu'un exemple, pour compléter la discussion. Veuillez consulter la documentation officielle de Scala.js pour plus de détails. Aussi, vous pouvez consulter la liste des façades disponibles et lire le code source pour vous inspirer.
Jusqu'à présent, nous avons couvert le processus pour les bibliothèques arbitraires, toujours non mappées. Dans le reste de l'article, je fais référence à une bibliothèque avec une façade déjà disponible, donc la discussion n'est qu'un exemple.
Wrapper une API JavaScript dans Scala.js
Lorsque vous utilisez require
en JavaScript, vous obtenez un objet qui peut être très différent. Il peut s'agir d'une fonction ou d'un objet. Il peut même s'agir simplement d'une chaîne ou d'un booléen, auquel cas l' require
n'est invoquée que pour les effets secondaires.
Dans l'exemple que je fais, jquery
est une fonction, renvoyant un objet qui fournit des méthodes supplémentaires. L'utilisation typique est jquery(<selector>).<method>
. Il s'agit d'une simplification, car jquery
permet également l'utilisation de $.<method>
, mais dans cet exemple simplifié, je ne vais pas couvrir tous ces cas. Notez qu'en général, pour les bibliothèques JavaScript complexes, toutes les API ne peuvent pas être facilement mappées sur des types Scala statiques. Vous devrez peut-être recourir à js.Dynamic
fournissant une interface dynamique (non typée) à l'objet JavaScript.
Donc, pour capturer uniquement le cas d'utilisation le plus courant, j'ai défini dans l'objet Bundle
, jquery
:
def jquery : js.Function1[js.Any, Jquery] = js.native
Cette fonction renverra un objet jQuery. Une instance d'un trait est dans mon cas définie avec une seule méthode (une simplification, vous pouvez ajouter la vôtre):
@js.native trait Jquery extends js.Object { def text(arg: js.Any): Jquery = js.native }
Pour la bibliothèque Lodash, nous modélisons toute la bibliothèque en tant qu'objet JavaScript puisqu'il s'agit d'un ensemble de fonctions que vous pouvez appeler directement :
def lodash: Lodash = js.native
Où le trait lodash
est juste comme suit (également une simplification, vous pouvez ajouter vos méthodes ici):
@js.native trait Lodash extends js.Object { def camelCase(arg: js.Any): String = js.native }
En utilisant ces définitions, nous pouvons enfin écrire du code Scala en utilisant les bibliothèques jQuery et Lodash sous-jacentes, toutes deux chargées à partir de NPM puis explorées :
object Main extends JSApp { def main(): Unit = { import Bundle._ jquery("#title").text(lodash.camelCase("This is a test")) } }
Vous pouvez consulter l'exemple complet ici.
Conclusion
Je suis un développeur Scala et j'étais ravi lorsque j'ai découvert Scala.js car je pouvais utiliser le même langage pour le serveur et le client. Étant donné que Scala ressemble plus à JavaScript qu'à Java, Scala.js est assez simple et naturel dans le navigateur. De plus, vous pouvez également utiliser les fonctionnalités puissantes de Scala en tant que riche collection de bibliothèques, de macros et d'IDE puissants et d'outils de construction. D'autres avantages clés sont que vous pouvez partager du code entre le serveur et le client. Il existe de nombreux cas où cette fonctionnalité est utile. Si vous utilisez un transpileur pour Javascript comme Coffeescript, Babel ou Typescript, vous ne remarquerez pas trop de différences lors de l'utilisation de Scala.js, mais il y a quand même de nombreux avantages. Le secret est de tirer le meilleur de chaque monde et de s'assurer qu'ils fonctionnent bien ensemble.