Usando Scala.js con NPM y Browserify
Publicado: 2022-03-11Si usa Scala.js, el compilador del lenguaje Scala para JavaScript, es posible que la administración de dependencias estándar de Scala.js le resulte demasiado limitante en el mundo moderno de JavaScript. Scala.js administra las dependencias con WebJars, mientras que los desarrolladores de JavaScript administran las dependencias mediante NPM. Dado que las dependencias producidas por NPM son del lado del servidor, generalmente se necesita un paso adicional usando Browserify o Webpack para generar el código del navegador.
En esta publicación, describiré cómo integrar Scala.js con la gran cantidad de módulos de JavaScript disponibles en NPM. Puede consultar este repositorio de GitHub para ver un ejemplo práctico de las técnicas descritas aquí. Usando este ejemplo y leyendo esta publicación, podrá recopilar sus bibliotecas de JavaScript usando NPM, crear un paquete usando Browserify y usar el resultado en su propio proyecto Scala.js. Todo esto sin siquiera instalar Node.js, ya que todo es administrado por SBT.
Gestión de dependencias para Scala.js
Hoy en día, escribir aplicaciones en lenguajes que compilan en JavaScript se está convirtiendo en una práctica muy común. Cada vez más personas se están pasando a lenguajes de JavaScript extendidos, como CoffeeScript o TypeScript, o transpiladores como Babel, que le permiten usar ES6 hoy. Al mismo tiempo, Google Web Toolkit (un compilador de Java a JavaScript) se usa principalmente para aplicaciones empresariales. Por estas razones, como desarrollador de Scala, ya no considero que usar Scala.js sea una elección extraña. El compilador es rápido, el código producido es eficiente y, en general, es solo una forma de usar el mismo lenguaje tanto en el front-end como en el back-end.
Dicho esto, usar las herramientas de Scala en el mundo de JavaScript aún no es 100 por ciento natural. A veces tienes que llenar el vacío entre el ecosistema de JavaScript y el de Scala. Scala.js como lenguaje tiene una excelente interoperabilidad con JavaScript. Debido a que Scala.js es un compilador del lenguaje Scala para JavaScript, es muy fácil conectar el código Scala con el código JavaScript existente. Lo que es más importante, Scala.js le brinda la capacidad de crear interfaces con tipo (o fachadas) para acceder a bibliotecas de JavaScript sin tipo (similar a lo que hace con TypeScript). Para los desarrolladores acostumbrados a lenguajes fuertemente tipados como Java, Scala o incluso Haskell, JavaScript está demasiado flojo. Si usted es un desarrollador de este tipo, probablemente la razón principal es que puede querer usar Scala.js para obtener un lenguaje (fuertemente) escrito encima de un lenguaje sin escribir.
Un problema en la cadena de herramientas estándar de Scala.js, que se basa en SBT y aún se deja algo abierta, es: ¿Cómo incluir dependencias, como bibliotecas de JavaScript adicionales, en su proyecto? SBT se estandariza en WebJars, por lo que se supone que debe usar WebJars para administrar sus dependencias. Desafortunadamente, en mi experiencia resultó ser inadecuado.
El problema con los WebJars
Como se mencionó, la forma estándar de Scala.js de recuperar las dependencias de JavaScript se basa en WebJars. Scala es, después de todo, un lenguaje JVM. Scala.js usa SBT principalmente para crear programas y, finalmente, SBT es excelente para administrar dependencias JAR.
Por esta razón, el formato WebJar se definió exactamente para importar dependencias de JavaScript en el mundo JVM. Un WebJar es un archivo JAR que incluye activos web, donde el archivo JAR simple solo incluye clases de Java compiladas. Entonces, la idea de Scala.js es que debe importar sus dependencias de JavaScript simplemente agregando dependencias de WebJar, de manera similar, Scala está agregando dependencias de JAR.
Buena idea, excepto que no funciona
El mayor problema con Webjars es que una versión arbitraria en una biblioteca de JavaScript aleatoria rara vez está disponible como WebJar. Al mismo tiempo, la gran mayoría de las bibliotecas de JavaScript están disponibles como módulos NPM. Sin embargo, existe un supuesto puente entre NPM y WebJars con un empaquetador automatizado npm-to-webjar
. Así que traté de importar una biblioteca, disponible como módulo NPM. En mi caso fue VoxelJS, una librería para construir mundos tipo Minecraft en una página web. Traté de solicitar la biblioteca como WebJar, pero el puente falló simplemente porque no hay campos de licencia en el descriptor.
También puede enfrentar esta experiencia frustrante por otras razones con otras bibliotecas. En pocas palabras, parece que no puede acceder a cada biblioteca en la naturaleza como un WebJar. El requisito que tiene para usar WebJars para acceder a las bibliotecas de JavaScript parece ser demasiado limitante.
Ingrese NPM y Browserify
Como ya señalé, el formato de empaquetado estándar para la mayoría de las bibliotecas de JavaScript es Node Package Manager, o NPM, incluido en cualquier versión de Node.js. Al usar NPM, puede acceder fácilmente a casi todas las bibliotecas de JavaScript disponibles.
Tenga en cuenta que NPM es el administrador de paquetes de Node . Node es una implementación del lado del servidor del motor de JavaScript V8 e instala paquetes para que Node.js los use en el lado del servidor. Tal como está, NPM es inútil para el navegador. Sin embargo, la utilidad de NPM se ha ampliado un tiempo para trabajar con aplicaciones de navegador, gracias a la omnipresente herramienta Browserify, que por supuesto también se distribuye como un paquete de NPM.
Browserify es, en pocas palabras, un empaquetador para el navegador. Recopilará módulos NPM produciendo un "paquete" utilizable en una aplicación de navegador. Muchos desarrolladores de JavaScript trabajan de esta manera: administran paquetes con NPM y luego los navegan para usarlos en la aplicación web. Tenga en cuenta que hay otras herramientas que funcionan de la misma manera, como Webpack.
Llenar la brecha de SBT a NPM
Por las razones que acabo de describir, lo que quería era una forma de instalar dependencias desde la web con NPM, invocar Browserify para recopilar dependencias para el navegador y luego usarlas con Scala.js. La tarea resultó ser un poco más complicada de lo que esperaba, pero aún posible. De hecho, hice el trabajo y lo estoy describiendo aquí.
Para simplificar, elegí Browserify también porque descubrí que es posible ejecutarlo dentro de SBT. No lo he probado con Webpack, aunque supongo que también es posible. Por suerte, no tuve que empezar en el vacío. Hay una serie de piezas ya en su lugar:
- SBT ya es compatible con NPM. El
sbt-web
, desarrollado para Play Framework, puede instalar dependencias de NPM. - SBT admite la ejecución de JavaScript. Puede ejecutar las herramientas de Node sin instalar Node en sí, gracias al complemento
sbt-jsengine
. - Scala.js puede usar un paquete generado. En Scala.js, hay una función de concatenación para incluir en su aplicación bibliotecas de JavaScript arbitrarias.
Usando estas funciones, creé una tarea SBT que puede descargar las dependencias de NPM y luego invocar Browserify, produciendo un archivo bundle.js
. Traté de integrar el procedimiento en la cadena de compilación y puedo ejecutar todo automáticamente, pero tener que procesar el empaquetado en cada compilación es demasiado lento. Además, no cambias las dependencias todo el tiempo; por lo tanto, es razonable que tenga que crear manualmente un paquete de vez en cuando cuando cambie las dependencias.
Entonces, mi solución fue construir un subproyecto. Este subproyecto descarga y empaqueta bibliotecas de JavaScript con NPM y Browserify. Luego, agregué un comando de bundle
para realizar la recopilación de dependencias. El paquete resultante se agrega a los recursos que se utilizarán en la aplicación Scala.js.
Se supone que debe ejecutar este "paquete" manualmente cada vez que cambie sus dependencias de JavaScript. Como se mencionó, no está automatizado en la cadena de compilación.
Cómo usar el paquete
Si desea usar mi ejemplo, haga lo siguiente: primero, consulte el repositorio con el comando habitual de Git.
git clone https://github.com/sciabarra/scalajs-browserify/
Luego, copie la carpeta del bundle
en su proyecto Scala.js. Es un subproyecto para la agrupación. Para conectarse al proyecto principal, debe agregar las siguientes líneas en su archivo build.sbt
:
val bundle = project.in(file("bundle")) jsDependencies += ProvidedJS / "bundle.js" addCommandAlias("bundle", "bundle/bundle")
Además, debe agregar las siguientes líneas a su archivo project/plugins.sbt
:
addSbtPlugin("com.typesafe.sbt" % "sbt-web" % "1.1.1") addSbtPlugin("com.typesafe.sbt" % "sbt-js-engine" % "1.1.3")
Una vez hecho esto, tiene un nuevo comando, bundle
, que puede usar para recopilar sus dependencias. Generará un archivo bundle.js
en su carpeta src/main/resources
.
¿Cómo se incluye el paquete en su aplicación Scala.js?
El comando de bundle
que se acaba de describir recopila dependencias con NPM y luego crea un bundle.js
. Cuando ejecuta los comandos fastOptJS
o fullOptJS
, ScalaJS creará un myproject-jsdeps.js
, incluidos todos los recursos que especificó como una dependencia de JavaScript, por lo tanto, también su bundle.js
. Para incluir dependencias agrupadas en su aplicación, se supone que debe usar las siguientes inclusiones:

<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>
Su paquete ahora está disponible como parte de myproject-jsdeps.js
. El paquete está listo y hemos terminado nuestra tarea (importar dependencias y exportarlas al navegador). El siguiente paso es usar las bibliotecas de JavaScript, que es un problema diferente, un problema de codificación de Scala.js. Para completar, ahora discutiremos cómo usar el paquete en Scala.js y crear fachadas para usar las bibliotecas que importamos.
Uso de una biblioteca JavaScript genérica en su aplicación Scala.js
En resumen, acabamos de ver cómo usar NPM y Browserify para crear un paquete e incluir ese paquete en Scala.js. Pero, ¿cómo podemos usar una biblioteca JavaScript genérica?
El proceso completo, que explicaremos en detalle en el resto del post, es:
- Seleccione sus bibliotecas del NPM e inclúyalas en
bundle/package.json
. - Cárguelos con
require
en un archivo de módulo de biblioteca, enbundle/lib.js
. - Escriba fachadas de Scala.js para interpretar el objeto
Bundle
en Scala.js. - Finalmente, codifique su aplicación utilizando las bibliotecas recién escritas.
Agregar una dependencia
Al usar NPM, debe incluir sus dependencias en el archivo package.json
, que es el estándar.
Entonces, supongamos que desea usar dos bibliotecas famosas como jQuery y Loadash. Este ejemplo es solo para fines de demostración, porque ya existe un excelente contenedor para jQuery disponible como dependencia para Scala.js con un módulo adecuado, y Lodash es inútil en el mundo de Scala. No obstante, creo que es un buen ejemplo, pero tómalo solo como un ejemplo.
Entonces, vaya al sitio web npmjs.com
y ubique la biblioteca que desea usar y seleccione una versión también. Supongamos que elige jquery-browserify
versión 13.0.0 y lodash
versión 4.3.0. Luego, actualice el bloque de dependencies
de sus packages.json
de la siguiente manera:
"dependencies": { "browserify": "13.0.0", "jquery-browserify": "1.8.1", "lodash": "4.3.0" }
Mantenga siempre browserify
, ya que es necesario para generar el paquete. Sin embargo, no está obligado a incluirlo en el paquete.
Tenga en cuenta que si tiene instalado NPM, puede escribir desde el directorio del bundle
:
npm install --save jquery-browserify lodash
También actualizará el package.json
. Si no tiene instalado NPM, no se preocupe. SBT instalará una versión Java de Node.js y NPM, descargará los archivos JAR necesarios y los ejecutará. Todo esto se administra cuando ejecuta el comando de bundle
desde SBT.
Exportación de la biblioteca
Ahora sabemos cómo descargar los paquetes. El siguiente paso es indicarle a Browserify que los reúna en un paquete y los ponga a disposición del resto de la aplicación.
Browserify es un recopilador de require
, que emula el comportamiento de Node.js para el navegador, lo que significa que necesita tener un require
en algún lugar para importar su biblioteca. Debido a que necesitamos exportar esas bibliotecas a Scala.js, el paquete también genera un objeto de JavaScript de nivel superior llamado Bundle
. Entonces, lo que debe hacer es editar lib.js
, que exporta un objeto JavaScript, y requiere todas sus bibliotecas como campos de este objeto.
Si queremos exportar a las bibliotecas Scala.js jQuery y Lodash, en el código esto significa:
module.exports = { "jquery": require("jquery-browserify"), "lodash": require("lodash") }
Ahora, simplemente ejecute el bundle
de comandos y la biblioteca se descargará, recopilará y colocará en el paquete, lista para usarse en su aplicación Scala.js.
Accediendo al paquete
Hasta aquí:
- Instaló el subproyecto del paquete en su proyecto Scala.js y lo configuró correctamente.
- Para cualquier biblioteca que quisiera, la agregó en el
package.json
. - Los requería en
lib.js
. - Ejecutó el comando de
bundle
.
Como resultado, ahora tiene un objeto JavaScript de nivel superior de Bundle
, que proporciona todos los puntos de entrada para las bibliotecas, disponibles como campos de este objeto.
Ahora está listo para usarlo con Scala.js. En el caso más simple, puede hacer algo como esto para acceder a las bibliotecas:
@js.native object Bundle extends js.Object { def jquery : js.Any = js.native def lodash: js.Any = js.native }
Este código le permite acceder a las bibliotecas de Scala.js. Sin embargo, no es la forma en que debe hacerlo con Scala.js, porque las bibliotecas aún no están tipificadas. En su lugar, debe escribir "fachadas" o envoltorios escritos, de modo que pueda usar las bibliotecas de JavaScript originalmente sin escribir de una manera escalar escrita.
No puedo decirle aquí cómo escribir las fachadas, ya que depende de la biblioteca de JavaScript específica que desee envolver en Scala.js. Solo mostraré un ejemplo, para completar la discusión. Consulte la documentación oficial de Scala.js para obtener más detalles. Además, puede consultar la lista de fachadas disponibles y leer el código fuente para inspirarse.
Hasta ahora, cubrimos el proceso para bibliotecas arbitrarias, aún sin mapear. En el resto del artículo me refiero a una biblioteca con una fachada ya disponible, por lo que la discusión es solo un ejemplo.
Envolviendo una API de JavaScript en Scala.js
Cuando usa require
en JavaScript, obtiene un objeto que puede ser muchas cosas diferentes. Puede ser una función o un objeto. Incluso puede ser solo una cadena o un booleano, en cuyo caso se invoca el require
solo para los efectos secundarios.
En el ejemplo que estoy haciendo, jquery
es una función que devuelve un objeto que proporciona métodos adicionales. El uso típico es jquery(<selector>).<method>
. Esta es una simplificación, porque jquery
también permite el uso de $.<method>
, pero en este ejemplo simplificado no voy a cubrir todos esos casos. Tenga en cuenta que, en general, para las bibliotecas de JavaScript complejas, no toda la API se puede asignar fácilmente a tipos estáticos de Scala. Es posible que deba recurrir a js.Dynamic
que proporciona una interfaz dinámica (sin tipo) para el objeto de JavaScript.
Entonces, para capturar solo el caso de uso más común, definí en el objeto Bundle
, jquery
:
def jquery : js.Function1[js.Any, Jquery] = js.native
Esta función devolverá un objeto jQuery. En mi caso, una instancia de un rasgo se define con un solo método (una simplificación, puede agregar el suyo propio):
@js.native trait Jquery extends js.Object { def text(arg: js.Any): Jquery = js.native }
Para la biblioteca de Lodash, modelamos toda la biblioteca como un objeto de JavaScript, ya que es una colección de funciones a las que puede llamar directamente:
def lodash: Lodash = js.native
Donde el rasgo lodash
es el siguiente (también una simplificación, puede agregar sus métodos aquí):
@js.native trait Lodash extends js.Object { def camelCase(arg: js.Any): String = js.native }
Usando esas definiciones, ahora podemos finalmente escribir código Scala usando las bibliotecas jQuery y Lodash subyacentes, ambas cargadas desde NPM y luego navegadas :
object Main extends JSApp { def main(): Unit = { import Bundle._ jquery("#title").text(lodash.camelCase("This is a test")) } }
Puedes consultar el ejemplo completo aquí.
Conclusión
Soy un desarrollador de Scala y me emocioné cuando descubrí Scala.js porque podía usar el mismo lenguaje tanto para el servidor como para el cliente. Dado que Scala es más similar a JavaScript que a Java, Scala.js es bastante fácil y natural en el navegador. Además, también puede utilizar las potentes funciones de Scala como una rica colección de bibliotecas, macros y potentes IDE y herramientas de compilación. Otras ventajas clave son que puede compartir código entre el servidor y el cliente. Hay muchos casos en los que esta característica es útil. Si usa un transpiler para Javascript como Coffeescript, Babel o Typescript, no notará demasiadas diferencias al usar Scala.js, pero aún así hay muchas ventajas. El secreto es tomar lo mejor de cada mundo y asegurarse de que funcionen bien juntos.