Utilizzo di Scala.js con NPM e Browserify
Pubblicato: 2022-03-11Se usi Scala.js, il compilatore del linguaggio Scala per JavaScript, potresti trovare la gestione delle dipendenze standard di Scala.js troppo limitante nel moderno mondo JavaScript. Scala.js gestisce le dipendenze con WebJars, mentre gli sviluppatori JavaScript gestiscono le dipendenze utilizzando NPM. Poiché le dipendenze prodotte da NPM sono lato server, in genere è necessario un passaggio aggiuntivo utilizzando Browserify o Webpack per generare il codice del browser.
In questo post, descriverò come integrare Scala.js con la pletora di moduli JavaScript disponibili su NPM. Puoi controllare questo repository GitHub per un esempio funzionante delle tecniche descritte qui. Usando questo esempio e leggendo questo post, sarai in grado di raccogliere le tue librerie JavaScript usando NPM, creare un bundle usando Browserify e usare il risultato nel tuo progetto Scala.js. Tutto questo senza nemmeno installare Node.js, in quanto tutto è gestito da SBT.
Gestione delle dipendenze per Scala.js
Oggi, scrivere applicazioni in linguaggi che compilano in JavaScript sta diventando una pratica molto comune. Sempre più persone si stanno spostando verso linguaggi JavaScript estesi, come CoffeeScript o TypeScript, o transpiler come Babel, che ti consentono di utilizzare ES6 oggi. Allo stesso tempo, Google Web Toolkit (un compilatore da Java a JavaScript) viene utilizzato principalmente per le applicazioni aziendali. Per questi motivi, come sviluppatore Scala, non considero più l'utilizzo di Scala.js una scelta strana. Il compilatore è veloce, il codice prodotto è efficiente e nel complesso è solo un modo per usare lo stesso linguaggio sia sul front-end che sul back-end.
Detto questo, l'utilizzo degli strumenti Scala nel mondo JavaScript non è ancora naturale al 100%. A volte devi colmare il divario dall'ecosistema JavaScript a quello di Scala. Scala.js come linguaggio ha un'eccellente interoperabilità con JavaScript. Poiché Scala.js è un compilatore per il linguaggio Scala in JavaScript, è molto facile interfacciare il codice Scala con il codice JavaScript esistente. Ancora più importante, Scala.js ti dà la possibilità di creare interfacce (o facciate) tipizzate per accedere a librerie JavaScript non tipizzate (simile a quello che fai con TypeScript). Per gli sviluppatori abituati a linguaggi fortemente tipizzati come Java, Scala o persino Haskell, JavaScript è digitato troppo liberamente. Se sei un tale sviluppatore, probabilmente il motivo principale è perché potresti voler usare Scala.js per ottenere un linguaggio (fortemente) tipizzato sopra un linguaggio non tipizzato.
Un problema nella toolchain standard di Scala.js, che è basata su SBT ed è ancora in qualche modo aperta, è: come includere le dipendenze, come librerie JavaScript aggiuntive, nel tuo progetto? SBT si standardizza su WebJars, quindi dovresti usare WebJars per gestire le tue dipendenze. Purtroppo, secondo la mia esperienza, si è rivelato inadeguato.
Il problema con WebJars
Come accennato, il modo standard di Scala.js per recuperare le dipendenze JavaScript si basa su WebJars. Dopotutto, Scala è un linguaggio JVM. Scala.js utilizza SBT principalmente per la creazione di programmi e, infine, SBT è eccellente nella gestione delle dipendenze JAR.
Per questo motivo, il formato WebJar è stato definito esattamente per l'importazione di dipendenze JavaScript nel mondo JVM. Un WebJar è un file JAR che include risorse Web, in cui un file JAR semplice include solo classi Java compilate. Quindi, l'idea di Scala.js è che dovresti importare le tue dipendenze JavaScript semplicemente aggiungendo dipendenze WebJar, in modo simile Scala sta aggiungendo dipendenze JAR.
Bella idea, solo che non funziona
Il problema più grande con Webjars è che una versione arbitraria su una libreria JavaScript casuale è raramente disponibile come WebJar. Allo stesso tempo, la stragrande maggioranza delle librerie JavaScript è disponibile come moduli NPM. Tuttavia, esiste un presunto ponte tra NPM e WebJars con un packager automatizzato da npm-to-webjar
. Quindi ho provato a importare una libreria, disponibile come modulo NPM. Nel mio caso, era VoxelJS, una libreria per costruire mondi simili a Minecraft in una pagina web. Ho provato a richiedere la libreria come WebJar, ma il bridge non è riuscito semplicemente perché non ci sono campi di licenza nel descrittore.
Potresti anche affrontare questa esperienza frustrante per altri motivi con altre librerie. In poche parole, sembra che tu non possa accedere a ciascuna libreria in natura come WebJar. Il requisito che devi utilizzare WebJars per accedere alle librerie JavaScript sembra essere troppo limitante.
Immettere NPM e Browserify
Come ho già sottolineato, il formato di pacchetto standard per la maggior parte delle librerie JavaScript è il Node Package Manager, o NPM, incluso in qualsiasi versione di Node.js. Utilizzando NPM, puoi accedere facilmente a quasi tutte le librerie JavaScript disponibili.
Si noti che NPM è il gestore di pacchetti Node . Node è un'implementazione lato server del motore JavaScript V8 e installa i pacchetti da utilizzare sul lato server da Node.js. Così com'è, NPM è inutile per il browser. Tuttavia, l'utilità di NPM è stata estesa per un po' di tempo per lavorare con le applicazioni browser, grazie all'onnipresente strumento Browserify, che ovviamente è anche distribuito come pacchetto NPM.
Browserify è, in poche parole, un packager per il browser. Raccoglierà moduli NPM producendo un "bundle" utilizzabile in un'applicazione browser. Molti sviluppatori JavaScript funzionano in questo modo: gestiscono i pacchetti con NPM e successivamente li utilizzano tramite browser per utilizzarli nell'applicazione Web. Tieni presente che ci sono altri strumenti che funzionano allo stesso modo, come Webpack.
Colmare il divario da SBT a NPM
Per i motivi che ho appena descritto, quello che volevo era un modo per installare le dipendenze dal Web con NPM, invocare Browserify per raccogliere le dipendenze per il browser e quindi utilizzarle con Scala.js. Il compito si è rivelato un po' più complicato di quanto mi aspettassi, ma comunque possibile. In effetti, ho fatto il lavoro e lo sto descrivendo qui.
Per semplicità ho scelto Browserify anche perché ho scoperto che è possibile eseguirlo all'interno di SBT. Non ho provato con Webpack, anche se credo sia possibile. Fortunatamente, non ho dovuto iniziare nel vuoto. Ci sono un certo numero di pezzi già in atto:
- SBT supporta già NPM. Il plugin
sbt-web
, sviluppato per Play Framework, può installare dipendenze NPM. - SBT supporta l'esecuzione di JavaScript. Puoi eseguire gli strumenti Node senza installare Node stesso, grazie al plugin
sbt-jsengine
. - Scala.js può utilizzare un bundle generato. In Scala.js è presente una funzione di concatenazione da includere nelle librerie JavaScript arbitrarie dell'applicazione.
Utilizzando queste funzionalità, ho creato un'attività SBT in grado di scaricare le dipendenze NPM e quindi richiamare Browserify, producendo un file bundle.js
. Ho provato a integrare la procedura nella catena di compilazione e posso eseguire il tutto automaticamente, ma dover elaborare il raggruppamento ad ogni compilazione è semplicemente troppo lento. Inoltre, non cambi le dipendenze tutto il tempo; quindi, è ragionevole che tu debba creare manualmente un bundle una volta ogni tanto quando modifichi le dipendenze.
Quindi, la mia soluzione era costruire un sottoprogetto. Questo sottoprogetto scarica e crea pacchetti di librerie JavaScript con NPM e Browserify. Quindi, ho aggiunto un comando bundle
per eseguire la raccolta delle dipendenze. Il bundle risultante viene aggiunto alle risorse da utilizzare nell'applicazione Scala.js.
Dovresti eseguire questo "bundle" manualmente ogni volta che modifichi le tue dipendenze JavaScript. Come accennato, non è automatizzato nella catena di compilazione.
Come utilizzare il bundler
Se vuoi usare il mio esempio, procedi come segue: per prima cosa, controlla il repository con il solito comando Git.
git clone https://github.com/sciabarra/scalajs-browserify/
Quindi, copia la cartella del bundle
nel tuo progetto Scala.js. È un sottoprogetto per il raggruppamento. Per connetterti al progetto principale, dovresti aggiungere le seguenti righe nel tuo file build.sbt
:
val bundle = project.in(file("bundle")) jsDependencies += ProvidedJS / "bundle.js" addCommandAlias("bundle", "bundle/bundle")
Inoltre, devi aggiungere le seguenti righe al tuo file project/plugins.sbt
:
addSbtPlugin("com.typesafe.sbt" % "sbt-web" % "1.1.1") addSbtPlugin("com.typesafe.sbt" % "sbt-js-engine" % "1.1.3")
Una volta terminato, hai un nuovo comando, bundle
, che puoi utilizzare per raccogliere le tue dipendenze. Genererà un file bundle.js
nella tua cartella src/main/resources
.
Come viene incluso il pacchetto nell'applicazione Scala.js?
Il comando bundle
appena descritto raccoglie le dipendenze con NPM e quindi crea un bundle.js
. Quando esegui i comandi fastOptJS
o fullOptJS
, ScalaJS creerà un myproject-jsdeps.js
, includendo tutte le risorse che hai specificato come dipendenza JavaScript, quindi anche il tuo bundle.js
. Per includere le dipendenze in bundle nella tua applicazione, dovresti usare le seguenti inclusioni:

<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>
Il tuo bundle è ora disponibile come parte di myproject-jsdeps.js
. Il pacchetto è pronto e abbiamo in qualche modo terminato il nostro compito (importare le dipendenze ed esportarle nel browser). Il prossimo passo è usare le librerie JavaScript, che è un problema diverso, un problema di codifica Scala.js. Per completezza, discuteremo ora come utilizzare il bundle in Scala.js e creare facciate per utilizzare le librerie che abbiamo importato.
Utilizzo di una libreria JavaScript generica nell'applicazione Scala.js
Per ricapitolare, abbiamo appena visto come utilizzare NPM e Browserify per creare un bundle e includerlo in Scala.js. Ma come possiamo usare una libreria JavaScript generica?
Il processo completo, che spiegheremo in dettaglio nel resto del post, è:
- Seleziona le tue librerie dall'NPM e includile in
bundle/package.json
. - Caricali con
require
in un file del modulo della libreria, inbundle/lib.js
. - Scrivi le facciate Scala.js per interpretare l'oggetto
Bundle
in Scala.js. - Infine, codifica la tua applicazione utilizzando le librerie appena digitate.
Aggiunta di una dipendenza
Usando NPM, devi includere le tue dipendenze nel file package.json
, che è lo standard.
Quindi, supponiamo che tu voglia usare due famose librerie come jQuery e Loadash. Questo esempio è solo a scopo dimostrativo, perché esiste già un eccellente wrapper per jQuery disponibile come dipendenza per Scala.js con un modulo appropriato, e Lodash è inutile nel mondo Scala. Tuttavia, penso che sia un buon esempio, ma prendilo solo come esempio.
Quindi, vai al sito Web npmjs.com
e individua la libreria che desideri utilizzare e seleziona anche una versione. Supponiamo di scegliere jquery-browserify
versione 13.0.0 e lodash
versione 4.3.0. Quindi, aggiorna il blocco delle dependencies
del tuo packages.json
come segue:
"dependencies": { "browserify": "13.0.0", "jquery-browserify": "1.8.1", "lodash": "4.3.0" }
Mantieni sempre browserify
, poiché è necessario per generare il bundle. Tuttavia, non è necessario includerlo nel pacchetto.
Nota che se hai installato NPM, puoi semplicemente digitare dalla directory del bundle
:
npm install --save jquery-browserify lodash
Aggiornerà anche il package.json
. Se non hai installato NPM, non preoccuparti. SBT installerà una versione Java di Node.js e NPM, scaricherà i JAR richiesti e li eseguirà. Tutto questo viene gestito quando si esegue il comando bundle
da SBT.
Esportazione della Libreria
Ora sappiamo come scaricare i pacchetti. Il passaggio successivo consiste nell'istruire Browserify per raccoglierli in un pacchetto e renderli disponibili al resto dell'applicazione.
Browserify è un raccoglitore di require
, che emula il comportamento Node.js per il browser, il che significa che devi avere da qualche parte un require
per importare la tua libreria. Poiché è necessario esportare tali librerie in Scala.js, il bundle genera anche un oggetto JavaScript di primo livello denominato Bundle
. Quindi, quello che devi fare è modificare lib.js
, che esporta un oggetto JavaScript e richiedere tutte le tue librerie come campi di questo oggetto.
Se vogliamo esportare nelle librerie jQuery e Lodash di Scala.js, nel codice questo significa:
module.exports = { "jquery": require("jquery-browserify"), "lodash": require("lodash") }
Ora, esegui semplicemente il bundle
di comandi e la libreria verrà scaricata, raccolta e inserita nel bundle, pronta per essere utilizzata nella tua applicazione Scala.js.
Accesso al pacchetto
Finora:
- Hai installato il sottoprogetto bundle nel tuo progetto Scala.js e lo hai configurato correttamente.
- Per qualsiasi libreria che volevi, l'hai aggiunta nel
package.json
. - Li hai richiesti nel
lib.js
. - Hai eseguito il comando
bundle
.
Di conseguenza, ora hai un oggetto JavaScript di livello superiore Bundle
, che fornisce tutti i punti di ingresso per le librerie, disponibili come campi di questo oggetto.
Ora sei pronto per usarlo con Scala.js. Nel caso più semplice puoi fare qualcosa del genere per accedere alle librerie:
@js.native object Bundle extends js.Object { def jquery : js.Any = js.native def lodash: js.Any = js.native }
Questo codice ti consente di accedere alle librerie da Scala.js. Tuttavia, non è il modo in cui dovresti farlo con Scala.js, perché le librerie non sono ancora tipizzate. Invece, dovresti scrivere "facciate" o wrapper digitati, in modo da poter utilizzare le librerie JavaScript originariamente non tipizzate in modo scalato tipizzato.
Non posso dirti qui come scrivere le facciate poiché dipende dalla specifica libreria JavaScript che vuoi racchiudere in Scala.js. Mostrerò solo un esempio, per completare la discussione. Si prega di controllare la documentazione ufficiale di Scala.js per maggiori dettagli. Inoltre, puoi consultare l'elenco delle facciate disponibili e leggere il codice sorgente per trarre ispirazione.
Finora, abbiamo coperto il processo per le librerie arbitrarie, ancora non mappate. Nel resto dell'articolo mi riferisco a librerie con una facciata già disponibile, quindi la discussione è solo un esempio.
Avvolgimento di un'API JavaScript in Scala.js
Quando usi require
in JavaScript, ottieni un oggetto che può essere molte cose diverse. Può essere una funzione o un oggetto. Può anche essere solo una stringa o un booleano, nel qual caso il require
viene invocato solo per gli effetti collaterali.
Nell'esempio che sto facendo, jquery
è una funzione, che restituisce un oggetto che fornisce metodi aggiuntivi. L'utilizzo tipico è jquery(<selector>).<method>
. Questa è una semplificazione, perché jquery
consente anche l'utilizzo di $.<method>
, ma in questo esempio semplificato non tratterò tutti questi casi. Nota in generale, per le librerie JavaScript complesse non tutte le API possono essere facilmente mappate su tipi Scala statici. Potrebbe essere necessario ricorrere a js.Dynamic
fornendo un'interfaccia dinamica (non tipizzata) all'oggetto JavaScript.
Quindi, per catturare solo il caso d'uso più comune, ho definito nell'oggetto Bundle
, jquery
:
def jquery : js.Function1[js.Any, Jquery] = js.native
Questa funzione restituirà un oggetto jQuery. Un'istanza di un tratto è nel mio caso definita con un unico metodo (una semplificazione, puoi aggiungerne una tua):
@js.native trait Jquery extends js.Object { def text(arg: js.Any): Jquery = js.native }
Per la libreria Lodash, modelliamo l'intera libreria come un oggetto JavaScript poiché è una raccolta di funzioni che puoi chiamare direttamente:
def lodash: Lodash = js.native
Dove il tratto lodash
è proprio il seguente (anche una semplificazione, puoi aggiungere i tuoi metodi qui):
@js.native trait Lodash extends js.Object { def camelCase(arg: js.Any): String = js.native }
Usando queste definizioni, ora possiamo finalmente scrivere il codice Scala usando le librerie jQuery e Lodash sottostanti, entrambe caricate da NPM e poi navigate :
object Main extends JSApp { def main(): Unit = { import Bundle._ jquery("#title").text(lodash.camelCase("This is a test")) } }
Puoi controllare l'esempio completo qui.
Conclusione
Sono uno sviluppatore Scala ed ero entusiasta quando ho scoperto Scala.js perché potevo usare lo stesso linguaggio sia per il server che per il client. Poiché Scala è più simile a JavaScript che a Java, Scala.js è abbastanza semplice e naturale nel browser. Inoltre, puoi anche utilizzare le potenti funzionalità di Scala come una ricca raccolta di librerie, macro e potenti IDE e strumenti di creazione. Altri vantaggi chiave sono la possibilità di condividere il codice tra server e client. Ci sono molti casi in cui questa funzione è utile. Se usi un transpiler per Javascript come Coffeescript, Babel o Typescript, non noterai troppe differenze quando usi Scala.js, ma ci sono comunque molti vantaggi. Il segreto è prendere il meglio di ogni mondo e assicurarsi che funzionino bene insieme.