Usando Scala.js com NPM e Browserify

Publicados: 2022-03-11

Se você usa Scala.js, o compilador da linguagem Scala para JavaScript, pode achar o gerenciamento de dependência padrão de Scala.js muito limitado no mundo JavaScript moderno. Scala.js gerencia dependências com WebJars, enquanto desenvolvedores JavaScript gerenciam dependências usando NPM. Como as dependências produzidas pelo NPM são do lado do servidor, geralmente é necessária uma etapa adicional usando Browserify ou Webpack para gerar o código do navegador.

Neste post, descreverei como integrar Scala.js com a infinidade de módulos JavaScript disponíveis no NPM. Você pode verificar este repositório do GitHub para obter um exemplo funcional das técnicas descritas aqui. Usando este exemplo e lendo esta postagem, você poderá coletar suas bibliotecas JavaScript usando NPM, criar um pacote usando Browserify e usar o resultado em seu próprio projeto Scala.js. Tudo isso sem sequer instalar o Node.js, pois tudo é gerenciado pelo SBT.

Browserify e Scala.js

A magia do Browserify também pode funcionar muito bem no mundo Java!
Tweet

Gerenciando dependências para Scala.js

Hoje, escrever aplicativos em linguagens que compilam para JavaScript está se tornando uma prática muito comum. Mais e mais pessoas estão migrando para linguagens JavaScript estendidas, como CoffeeScript ou TypeScript, ou transpiladores como Babel, que permitem que você use o ES6 hoje. Ao mesmo tempo, o Google Web Toolkit (um compilador de Java para JavaScript) é usado principalmente para aplicativos corporativos. Por essas razões, como desenvolvedor Scala, não considero mais usar Scala.js uma escolha estranha. O compilador é rápido, o código produzido é eficiente e, no geral, é apenas uma maneira de usar a mesma linguagem tanto no front-end quanto no back-end.

Dito isso, usar as ferramentas Scala no mundo JavaScript ainda não é 100% natural. Às vezes, você precisa preencher a lacuna do ecossistema JavaScript para o Scala. Scala.js como linguagem tem uma excelente interoperabilidade com JavaScript. Como o Scala.js é um compilador da linguagem Scala para JavaScript, é muito fácil fazer a interface do código Scala com o código JavaScript existente. Mais importante ainda, Scala.js oferece a capacidade de criar interfaces tipadas (ou fachadas) para acessar bibliotecas JavaScript não tipadas (semelhante ao que você faz com TypeScript). Para desenvolvedores acostumados a linguagens fortemente tipadas como Java, Scala ou mesmo Haskell, o JavaScript é muito vagamente tipado. Se você é um desenvolvedor assim, provavelmente o principal motivo é porque você pode querer usar Scala.js para obter uma linguagem (fortemente) tipada em cima de uma linguagem não tipada.

Um problema na cadeia de ferramentas Scala.js padrão, que é baseada no SBT e ainda permanece um pouco aberta, é: Como incluir dependências, como bibliotecas JavaScript adicionais, em seu projeto? O SBT padroniza em WebJars, então você deve usar WebJars para gerenciar suas dependências. Infelizmente, na minha experiência, provou ser insuficiente.

O problema com WebJars

Conforme mencionado, a maneira Scala.js padrão de recuperar dependências de JavaScript é baseada em WebJars. Afinal, Scala é uma linguagem JVM. Scala.js usa o SBT principalmente para construir programas e, finalmente, o SBT é excelente no gerenciamento de dependências JAR.

Por esse motivo, o formato WebJar foi definido exatamente para importar dependências JavaScript no mundo JVM. Um WebJar é um arquivo JAR que inclui ativos da Web, onde o arquivo JAR simples inclui apenas classes Java compiladas. Então, a ideia do Scala.js é que você deve importar suas dependências JavaScript simplesmente adicionando dependências WebJar, de forma semelhante Scala está adicionando dependências JAR.

Boa ideia, exceto que não funciona

O maior problema com Webjars é que uma versão arbitrária em uma biblioteca JavaScript aleatória raramente está disponível como WebJar. Ao mesmo tempo, a grande maioria das bibliotecas JavaScript está disponível como módulos NPM. No entanto, existe uma suposta ponte entre o NPM e o WebJars com um npm-to-webjar automatizado. Então tentei importar uma biblioteca, disponível como módulo NPM. No meu caso, foi o VoxelJS, uma biblioteca para construir mundos do tipo Minecraft em uma página da web. Tentei solicitar a biblioteca como um WebJar, mas a ponte falhou simplesmente porque não há campos de licença no descritor.

A biblioteca WebJar não está funcionando

Você também pode enfrentar essa experiência frustrante por outros motivos com outras bibliotecas. Simplificando, parece que você não pode acessar cada biblioteca em estado selvagem como um WebJar. O requisito de usar WebJars para acessar bibliotecas JavaScript parece ser muito limitante.

Digite NPM e Browserify

Como já mencionei, o formato de empacotamento padrão para a maioria das bibliotecas JavaScript é o Node Package Manager, ou NPM, incluído em qualquer versão do Node.js. Ao usar o NPM, você pode acessar facilmente quase todas as bibliotecas JavaScript disponíveis.

Observe que o NPM é o gerenciador de pacotes do Node . O Node é uma implementação do lado do servidor do mecanismo JavaScript V8 e instala pacotes para serem usados ​​no lado do servidor pelo Node.js. Como está, o NPM é inútil para o navegador. No entanto, a utilidade do NPM foi estendida por um tempo para trabalhar com aplicativos de navegador, graças à onipresente ferramenta Browserify, que também é distribuída como um pacote NPM.

Browserify é, simplesmente, um empacotador para o navegador. Ele coletará módulos NPM produzindo um “pacote” utilizável em um aplicativo de navegador. Muitos desenvolvedores de JavaScript trabalham dessa maneira - eles gerenciam pacotes com NPM e depois os navegam para usá-los no aplicativo da web. Lembrando que existem outras ferramentas que funcionam da mesma forma, como o Webpack.

Preenchendo a lacuna do SBT para o NPM

Pelas razões que acabei de descrever, o que eu queria era uma maneira de instalar dependências da web com NPM, invocar o Browserify para reunir dependências para o navegador e usá-las com Scala.js. A tarefa acabou sendo um pouco mais complicada do que eu esperava, mas ainda possível. Na verdade, eu fiz o trabalho e estou descrevendo-o aqui.

Por simplicidade, escolhi o Browserify também porque descobri que é possível executá-lo dentro do SBT. Eu não tentei com o Webpack, embora eu ache que seja possível também. Felizmente, não tive que começar no vácuo. Já existem várias peças:

  • O SBT já suporta NPM. O plugin sbt-web , desenvolvido para o Play Framework, pode instalar dependências do NPM.
  • O SBT suporta a execução de JavaScript. Você pode executar ferramentas Node sem instalar o próprio Node, graças ao plugin sbt-jsengine .
  • Scala.js pode usar um pacote gerado. Em Scala.js, há uma função de concatenação para incluir em seu aplicativo bibliotecas JavaScript arbitrárias.

Usando esses recursos, criei uma tarefa SBT que pode baixar as dependências do NPM e invocar o Browserify, produzindo um arquivo bundle.js . Tentei integrar o procedimento na cadeia de compilação e posso executar tudo automaticamente, mas ter que processar o pacote em cada compilação é simplesmente muito lento. Além disso, você não altera as dependências o tempo todo; portanto, é razoável que você tenha que criar manualmente um bundle de vez em quando ao alterar as dependências.

Então, minha solução foi construir um subprojeto. Este subprojeto baixa e empacota bibliotecas JavaScript com NPM e Browserify. Em seguida, adicionei um comando de bundle para realizar a coleta de dependências. O pacote resultante é adicionado aos recursos a serem usados ​​no aplicativo Scala.js.

Você deve executar este “pacote” manualmente sempre que alterar suas dependências de JavaScript. Como mencionado, não é automatizado na cadeia de compilação.

Como usar o empacotador

Se você quiser usar meu exemplo, faça o seguinte: primeiro, faça o checkout do repositório com o comando Git usual.

 git clone https://github.com/sciabarra/scalajs-browserify/

Em seguida, copie a pasta do bundle em seu projeto Scala.js. É um subprojeto para empacotamento. Para se conectar ao projeto principal, você deve adicionar as seguintes linhas em seu arquivo build.sbt :

 val bundle = project.in(file("bundle")) jsDependencies += ProvidedJS / "bundle.js" addCommandAlias("bundle", "bundle/bundle")

Além disso, você deve adicionar as seguintes linhas ao seu arquivo project/plugins.sbt :

 addSbtPlugin("com.typesafe.sbt" % "sbt-web" % "1.1.1") addSbtPlugin("com.typesafe.sbt" % "sbt-js-engine" % "1.1.3")

Uma vez feito, você tem um novo comando, bundle , que pode ser usado para reunir suas dependências. Ele irá gerar um arquivo bundle.js em sua pasta src/main/resources .

Como o pacote é incluído em seu aplicativo Scala.js?

O comando bundle que acabamos de descrever reúne dependências com o NPM e, em seguida, cria um bundle.js . Quando você executa os comandos fastOptJS ou fullOptJS , o ScalaJS criará um myproject-jsdeps.js , incluindo todos os recursos que você especificou como uma dependência JavaScript, portanto também seu bundle.js . Para incluir dependências agrupadas em seu aplicativo, você deve usar as seguintes inclusões:

 <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>

Seu pacote agora está disponível como parte do myproject-jsdeps.js . O pacote está pronto e já terminamos nossa tarefa (importar dependências e exportá-las para o navegador). A próxima etapa é usar as bibliotecas JavaScript, que é um problema diferente, um problema de codificação Scala.js. Para completar, discutiremos agora como usar o pacote em Scala.js e criar fachadas para usar as bibliotecas que importamos.

Com Scala.js você pode compartilhar código entre servidor e cliente

Com Scala.js, você pode compartilhar código facilmente entre servidor e cliente
Tweet

Usando uma biblioteca JavaScript genérica em seu aplicativo Scala.js

Para recapitular, acabamos de ver como usar o NPM e o Browserify para criar um pacote e incluir esse pacote no Scala.js. Mas como podemos usar uma biblioteca JavaScript genérica?

O processo completo, que explicaremos em detalhes no restante do post, é:

  • Selecione suas bibliotecas do NPM e inclua-as no bundle/package.json .
  • Carregue-os com require em um arquivo de módulo de biblioteca, em bundle/lib.js .
  • Escreva fachadas Scala.js para interpretar o objeto Bundle em Scala.js.
  • Por fim, codifique seu aplicativo usando as bibliotecas recém-digitadas.

Adicionando uma dependência

Usando o NPM, você precisa incluir suas dependências no arquivo package.json , que é o padrão.

Então, vamos supor que você queira usar duas bibliotecas famosas como jQuery e Loadash. Este exemplo é apenas para fins de demonstração, pois já existe um excelente wrapper para jQuery disponível como dependência para Scala.js com um módulo adequado, e Lodash é inútil no mundo Scala. No entanto, acho que é um bom exemplo, mas tome-o apenas como exemplo.

Portanto, acesse o site npmjs.com e localize a biblioteca que deseja usar e selecione uma versão também. Vamos supor que você escolha jquery-browserify versão 13.0.0 e lodash versão 4.3.0. Em seguida, atualize o bloco de dependencies do seu packages.json da seguinte forma:

 "dependencies": { "browserify": "13.0.0", "jquery-browserify": "1.8.1", "lodash": "4.3.0" }

Sempre mantenha browserify , pois é necessário para gerar o pacote. Você não é obrigado a incluí-lo no pacote, no entanto.

Observe que, se você tiver o NPM instalado, basta digitar no diretório do bundle :

 npm install --save jquery-browserify lodash

Ele também atualizará o package.json . Se você não tiver o NPM instalado, não se preocupe. O SBT instalará uma versão Java do Node.js e do NPM, fará o download dos JARs necessários e os executará. Tudo isso é gerenciado quando você executa o comando bundle do SBT.

Exportando a Biblioteca

Agora sabemos como baixar os pacotes. A próxima etapa é instruir o Browserify a reuni-los em um pacote e disponibilizá-los para o restante do aplicativo.

Browserify é um coletor de require , emulando o comportamento do Node.js para o navegador, o que significa que você precisa ter em algum lugar um require para importar sua biblioteca. Como precisamos exportar essas bibliotecas para Scala.js, o pacote também está gerando um objeto JavaScript de nível superior chamado Bundle . Então, o que você precisa fazer é editar o lib.js , que exporta um objeto JavaScript, e requer todas as suas bibliotecas como campos deste objeto.

Se quisermos exportar para bibliotecas Scala.js jQuery e Lodash, no código isso significa:

 module.exports = { "jquery": require("jquery-browserify"), "lodash": require("lodash") }

Agora, basta executar o bundle de comandos e a biblioteca será baixada, reunida e colocada no pacote, pronta para ser usada em sua aplicação Scala.js.

Acessando o pacote

Tão longe:

  • Você instalou o subprojeto de pacote configurável em seu projeto Scala.js e o configurou corretamente.
  • Para qualquer biblioteca desejada, você a adicionou no package.json .
  • Você os exigiu no lib.js .
  • Você executou o comando bundle .

Como resultado, agora você tem um objeto JavaScript de nível superior Bundle , fornecendo todos os pontos de entrada para as bibliotecas, disponíveis como campos desse objeto.

Agora você está pronto para usá-lo com Scala.js. No caso mais simples, você pode fazer algo assim para acessar as bibliotecas:

 @js.native object Bundle extends js.Object { def jquery : js.Any = js.native def lodash: js.Any = js.native }

Esse código permite acessar as bibliotecas de Scala.js. No entanto, não é assim que você deve fazer com Scala.js, porque as bibliotecas ainda não são tipadas. Em vez disso, você deve escrever “fachadas” ou wrappers digitados, para que possa usar as bibliotecas JavaScript originalmente não tipadas de uma maneira tipada em escala .

Não posso dizer aqui como escrever as fachadas, pois depende da biblioteca JavaScript específica que você deseja envolver em Scala.js. Vou apenas mostrar um exemplo, para completar a discussão. Verifique a documentação oficial do Scala.js para obter mais detalhes. Além disso, você pode consultar a lista de fachadas disponíveis e ler o código-fonte para se inspirar.

Até agora, cobrimos o processo para bibliotecas arbitrárias e ainda não mapeadas. No restante do artigo estou me referindo à biblioteca com uma fachada já disponível, então a discussão é apenas um exemplo.

Envolvendo uma API JavaScript em Scala.js

Quando você usa require em JavaScript, você obtém um objeto que pode ser muitas coisas diferentes. Pode ser uma função ou um objeto. Pode até ser apenas uma string ou um booleano, nesse caso o require é invocado apenas para os efeitos colaterais.

No exemplo que estou fazendo, jquery é uma função, retornando um objeto que fornece métodos adicionais. O uso típico é jquery(<selector>).<method> . Isso é uma simplificação, porque jquery também permite o uso de $.<method> , mas neste exemplo simplificado não vou cobrir todos esses casos. Observe que, em geral, para bibliotecas JavaScript complexas, nem toda a API pode ser facilmente mapeada para tipos Scala estáticos. Você pode precisar recorrer ao js.Dynamic fornecendo uma interface dinâmica (não tipada) para o objeto JavaScript.

Então, para capturar apenas o caso de uso mais comum, defini no objeto Bundle , jquery :

 def jquery : js.Function1[js.Any, Jquery] = js.native

Esta função retornará um objeto jQuery. Uma instância de um traço é no meu caso definida com um único método (uma simplificação, você pode adicionar o seu próprio):

 @js.native trait Jquery extends js.Object { def text(arg: js.Any): Jquery = js.native }

Para a biblioteca Lodash, modelamos toda a biblioteca como um objeto JavaScript, pois é uma coleção de funções que você pode chamar diretamente:

 def lodash: Lodash = js.native

Onde o traço lodash é o seguinte (também uma simplificação, você pode adicionar seus métodos aqui):

 @js.native trait Lodash extends js.Object { def camelCase(arg: js.Any): String = js.native }

Usando essas definições, agora podemos finalmente escrever código Scala usando as bibliotecas jQuery e Lodash subjacentes, ambas carregadas do NPM e depois navegadas :

 object Main extends JSApp { def main(): Unit = { import Bundle._ jquery("#title").text(lodash.camelCase("This is a test")) } }

Você pode conferir o exemplo completo aqui.

Conclusão

Sou um desenvolvedor Scala e fiquei empolgado quando descobri o Scala.js porque podia usar a mesma linguagem tanto para o servidor quanto para o cliente. Como Scala é mais parecido com JavaScript do que com Java, Scala.js é bem fácil e natural no navegador. Além disso, você também pode usar recursos poderosos do Scala como uma rica coleção de bibliotecas, macros e IDEs poderosos e ferramentas de construção. Outras vantagens importantes são que você pode compartilhar código entre servidor e cliente. Existem muitos casos em que esse recurso é útil. Se você usa um transpilador para Javascript como Coffeescript, Babel ou Typescript, você não notará muitas diferenças ao usar Scala.js, mas ainda assim há muitas vantagens. O segredo é pegar o melhor de cada mundo e garantir que eles funcionem bem juntos.

Relacionado: Por que devo aprender Scala?