Używanie Scala.js z NPM i Browserify
Opublikowany: 2022-03-11Jeśli używasz Scala.js, kompilatora języka Scala do JavaScript, standardowe zarządzanie zależnościami Scala.js może okazać się zbyt ograniczające we współczesnym świecie JavaScript. Scala.js zarządza zależnościami za pomocą WebJars, podczas gdy programiści JavaScript zarządzają zależnościami za pomocą NPM. Ponieważ zależności tworzone przez NPM są po stronie serwera, zwykle potrzebny jest dodatkowy krok za pomocą Browserify lub Webpack do wygenerowania kodu przeglądarki.
W tym poście opiszę jak zintegrować Scala.js z mnóstwem modułów JavaScript dostępnych na NPM. Możesz sprawdzić to repozytorium GitHub, aby uzyskać działający przykład opisanych tutaj technik. Korzystając z tego przykładu i czytając ten post, będziesz mógł zebrać swoje biblioteki JavaScript za pomocą NPM, utworzyć pakiet za pomocą Browserify i wykorzystać wynik we własnym projekcie Scala.js. Wszystko to bez konieczności instalowania Node.js, ponieważ wszystkim zarządza SBT.
Zarządzanie zależnościami dla Scala.js
Obecnie bardzo powszechną praktyką staje się pisanie aplikacji w językach kompilujących się do JavaScript. Coraz więcej osób przechodzi na rozszerzone języki JavaScript, takie jak CoffeeScript lub TypeScript, lub transpilatory, takie jak Babel, które pozwalają dziś korzystać z ES6. Jednocześnie Google Web Toolkit (kompilator Javy do JavaScriptu) jest najczęściej używany w aplikacjach korporacyjnych. Z tych powodów jako programista Scala nie uważam już, aby używanie Scala.js było dziwnym wyborem. Kompilator jest szybki, tworzony kod wydajny, a ogólnie jest to po prostu sposób na użycie tego samego języka zarówno na froncie, jak i zapleczu.
To powiedziawszy, używanie narzędzi Scali w świecie JavaScript nie jest jeszcze w 100 procentach naturalne. Czasami trzeba wypełnić lukę między ekosystemem JavaScript a Scala. Scala.js jako język ma doskonałą interoperacyjność z JavaScript. Ponieważ Scala.js jest kompilatorem języka Scala do JavaScript, bardzo łatwo jest połączyć kod Scala z istniejącym kodem JavaScript. Co najważniejsze, Scala.js daje możliwość tworzenia typowanych interfejsów (lub fasad) w celu uzyskania dostępu do niewpisanych bibliotek JavaScript (podobnie jak w przypadku TypeScript). Dla programistów przyzwyczajonych do silnie typowanych języków, takich jak Java, Scala, a nawet Haskell, JavaScript jest zbyt luźno typowany. Jeśli jesteś takim programistą, prawdopodobnie głównym powodem jest to, że możesz chcieć użyć Scala.js, aby uzyskać (mocno) typowany język na wierzchu niewpisanego języka.
Problem w standardowym toolchainie Scala.js, który jest oparty na SBT i nadal pozostaje nieco otwarty, to: Jak uwzględnić w projekcie zależności, takie jak dodatkowe biblioteki JavaScript? SBT standaryzuje WebJars, więc powinieneś używać WebJars do zarządzania zależnościami. Niestety z mojego doświadczenia wynika, że okazało się to niewystarczające.
Problem z WebJars
Jak wspomniano, standardowy sposób Scala.js do pobierania zależności JavaScript jest oparty na WebJars. Scala to przecież język JVM. Scala.js używa SBT głównie do budowania programów, a wreszcie SBT jest doskonały w zarządzaniu zależnościami JAR.
Z tego powodu format WebJar został zdefiniowany dokładnie do importowania zależności JavaScript w świecie JVM. WebJar to plik JAR zawierający zasoby internetowe, gdzie prosty plik JAR zawiera tylko skompilowane klasy Java. Tak więc idea Scala.js polega na tym, że powinieneś importować swoje zależności JavaScript, po prostu dodając zależności WebJar, w podobny sposób, w jaki Scala dodaje zależności JAR.
Fajny pomysł, ale to nie działa
Największym problemem z Webjarami jest to, że dowolna wersja w losowej bibliotece JavaScript jest rzadko dostępna jako WebJar. Jednocześnie zdecydowana większość bibliotek JavaScript jest dostępna jako moduły NPM. Istnieje jednak rzekomy pomost między NPM a WebJars za pomocą zautomatyzowanego programu pakującego npm-to-webjar
. Spróbowałem więc zaimportować bibliotekę, dostępną jako moduł NPM. W moim przypadku był to VoxelJS, biblioteka do budowania światów podobnych do Minecrafta na stronie internetowej. Próbowałem zażądać biblioteki jako WebJar, ale most nie powiódł się po prostu dlatego, że w deskryptorze nie ma pól licencji.
Możesz również spotkać się z tym frustrującym doświadczeniem z innych powodów w przypadku innych bibliotek. Mówiąc najprościej, wygląda na to, że nie możesz uzyskać dostępu do każdej biblioteki na wolności jako WebJar. Wymóg korzystania z WebJars w celu uzyskania dostępu do bibliotek JavaScript wydaje się zbyt ograniczający.
Wprowadź NPM i Browserify
Jak już wspomniałem, standardowym formatem pakowania większości bibliotek JavaScript jest Node Package Manager, czyli NPM, zawarty w każdej wersji Node.js. Korzystając z NPM, możesz łatwo uzyskać dostęp do prawie wszystkich dostępnych bibliotek JavaScript.
Zauważ, że NPM to menedżer pakietów węzła . Node jest implementacją silnika JavaScript V8 po stronie serwera i instaluje pakiety, które po stronie serwera mają być używane przez Node.js. Tak jak jest, NPM jest bezużyteczny dla przeglądarki. Jednak przydatność NPM została przedłużona do pracy z aplikacjami przeglądarkowymi, dzięki wszechobecnemu narzędziu Browserify, które oczywiście jest również dystrybuowane jako pakiet NPM.
Browserify to po prostu program pakujący dla przeglądarki. Będzie zbierać moduły NPM, tworząc „pakiet” użyteczny w aplikacji przeglądarkowej. Wielu programistów JavaScript działa w ten sposób - zarządzają pakietami za pomocą NPM, a następnie przeglądają je do wykorzystania w aplikacji internetowej. Pamiętaj, że istnieją inne narzędzia, które działają w ten sam sposób, takie jak Webpack.
Wypełnianie luki od SBT do NPM
Z powodów, które właśnie opisałem, chciałem, aby zainstalować zależności z sieci za pomocą NPM, wywołać Browserify, aby zebrać zależności dla przeglądarki, a następnie użyć ich za pomocą Scala.js. Zadanie okazało się nieco bardziej skomplikowane niż się spodziewałem, ale nadal możliwe. Rzeczywiście wykonałem pracę i opisuję ją tutaj.
Dla uproszczenia wybrałem Browserify również dlatego, że odkryłem, że można go uruchomić w SBT. Nie próbowałem z Webpackiem, chociaż myślę, że też jest to możliwe. Na szczęście nie musiałem zaczynać w próżni. Istnieje już kilka elementów:
- SBT już obsługuje NPM. Wtyczka
sbt-web
, opracowana dla Play Framework, może instalować zależności NPM. - SBT obsługuje wykonywanie JavaScript. Możesz uruchamiać narzędzia Node bez instalowania samego Node, dzięki wtyczce
sbt-jsengine
. - Scala.js może korzystać z wygenerowanego pakietu. W Scala.js istnieje funkcja konkatenacji, która umożliwia włączenie do aplikacji dowolnych bibliotek JavaScript.
Korzystając z tych funkcji, stworzyłem zadanie SBT, które może pobrać zależności NPM, a następnie wywołać Browserify, tworząc plik bundle.js
. Próbowałem zintegrować procedurę w łańcuchu kompilacji i mogę uruchomić całość automatycznie, ale konieczność przetwarzania pakietu przy każdej kompilacji jest po prostu zbyt wolna. Ponadto nie zmieniasz zależności przez cały czas; w związku z tym rozsądne jest ręczne tworzenie pakietu raz na jakiś czas, gdy zmieniasz zależności.
Więc moim rozwiązaniem było zbudowanie podprojektu. Ten podprojekt pobiera i pakuje biblioteki JavaScript z NPM i Browserify. Następnie dodałem polecenie bundle
do zbierania zależności. Powstały pakiet jest dodawany do zasobów do wykorzystania w aplikacji Scala.js.
Powinieneś wykonać ten „pakiet” ręcznie za każdym razem, gdy zmienisz swoje zależności JavaScript. Jak wspomniano, nie jest to zautomatyzowane w łańcuchu kompilacji.
Jak korzystać z pakietu?
Jeśli chcesz użyć mojego przykładu, wykonaj następujące czynności: najpierw pobierz repozytorium za pomocą zwykłego polecenia Git.
git clone https://github.com/sciabarra/scalajs-browserify/
Następnie skopiuj folder bundle
do swojego projektu Scala.js. Jest to podprojekt do wiązania. Aby połączyć się z głównym projektem, należy dodać następujące wiersze w pliku build.sbt
:
val bundle = project.in(file("bundle")) jsDependencies += ProvidedJS / "bundle.js" addCommandAlias("bundle", "bundle/bundle")
Ponadto musisz dodać następujące wiersze do pliku project/plugins.sbt
:
addSbtPlugin("com.typesafe.sbt" % "sbt-web" % "1.1.1") addSbtPlugin("com.typesafe.sbt" % "sbt-js-engine" % "1.1.3")
Po zakończeniu masz nowe polecenie, bundle
, którego możesz użyć do zebrania swoich zależności. Wygeneruje plik bundle.js
w folderze src/main/resources
.
W jaki sposób pakiet jest zawarty w Twojej aplikacji Scala.js?
Opisane właśnie polecenie bundle
zbiera zależności z NPM, a następnie tworzy plik bundle.js
. Po uruchomieniu poleceń fastOptJS
lub fullOptJS
ScalaJS utworzy myproject-jsdeps.js
, w tym wszystkie zasoby określone jako zależność JavaScript, a więc także bundle.js
. Aby dołączyć powiązane zależności do swojej aplikacji, powinieneś użyć następujących włączeń:

<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>
Twój pakiet jest teraz dostępny jako część myproject-jsdeps.js
. Pakiet jest gotowy, a my w pewnym stopniu zakończyliśmy nasze zadanie (importowanie zależności i eksportowanie ich do przeglądarki). Następnym krokiem jest użycie bibliotek JavaScript, co jest innym problemem, problemem kodowania Scala.js. Dla kompletności omówimy teraz, jak korzystać z pakietu w Scala.js i tworzyć fasady, aby korzystać z zaimportowanych przez nas bibliotek.
Korzystanie z ogólnej biblioteki JavaScript w aplikacji Scala.js
Podsumowując, właśnie widzieliśmy, jak używać NPM i Browserify do tworzenia pakietu i dołączania tego pakietu do Scala.js. Ale jak możemy użyć ogólnej biblioteki JavaScript?
Cały proces, który szczegółowo wyjaśnimy w dalszej części postu, to:
- Wybierz swoje biblioteki z NPM i uwzględnij je w
bundle/package.json
. - Załaduj je z
require
w pliku modułu biblioteki, wbundle/lib.js
. - Napisz fasady Scala.js, aby zinterpretować obiekt
Bundle
w Scala.js. - Na koniec zakoduj swoją aplikację za pomocą nowo wpisanych bibliotek.
Dodawanie zależności
Korzystając z NPM, musisz uwzględnić swoje zależności w pliku package.json
, który jest standardem.
Załóżmy więc, że chcesz użyć dwóch znanych bibliotek, takich jak jQuery i Loadash. Ten przykład służy tylko do celów demonstracyjnych, ponieważ istnieje już doskonały wrapper dla jQuery dostępny jako zależność dla Scala.js z odpowiednim modułem, a Lodash jest bezużyteczny w świecie Scala. Mimo to uważam, że to dobry przykład, ale weźmy go tylko jako przykład.
Przejdź do witryny npmjs.com
i znajdź bibliotekę, której chcesz użyć, a także wybierz wersję. Załóżmy, że wybierasz jquery-browserify
wersji 13.0.0 i lodash
wersji 4.3.0. Następnie zaktualizuj blok dependencies
swojego packages.json
w następujący sposób:
"dependencies": { "browserify": "13.0.0", "jquery-browserify": "1.8.1", "lodash": "4.3.0" }
Zawsze trzymaj browserify
, ponieważ jest to potrzebne do wygenerowania pakietu. Nie musisz jednak dołączać go do pakietu.
Zwróć uwagę, że jeśli masz zainstalowany NPM, możesz po prostu wpisać z katalogu bundle
:
npm install --save jquery-browserify lodash
Zaktualizuje również package.json
. Jeśli nie masz zainstalowanego NPM, nie martw się. SBT zainstaluje wersję Java Node.js i NPM, pobierze wymagane pliki JAR i uruchomi je. Wszystkim tym zarządza się po uruchomieniu polecenia bundle
z SBT.
Eksportowanie Biblioteki
Teraz wiemy, jak pobrać pakiety. Następnym krokiem jest poinstruowanie Browserify, aby zebrał je w pakiet i udostępnił reszcie aplikacji.
Browserify zbiera require
, emulując zachowanie Node.js dla przeglądarki, co oznacza, że require
gdzieś zaimportować swoją bibliotekę. Ponieważ musimy wyeksportować te biblioteki do Scala.js, pakiet generuje również obiekt JavaScript najwyższego poziomu o nazwie Bundle
. Więc to, co musisz zrobić, to edytować lib.js
, który eksportuje obiekt JavaScript, i wymaga wszystkich twoich bibliotek jako pól tego obiektu.
Jeśli chcemy eksportować do bibliotek Scala.js jQuery i Lodash, w kodzie oznacza to:
module.exports = { "jquery": require("jquery-browserify"), "lodash": require("lodash") }
Teraz wystarczy uruchomić bundle
poleceń, a biblioteka zostanie pobrana, zebrana i umieszczona w pakiecie, gotowa do użycia w Twojej aplikacji Scala.js.
Dostęp do pakietu
Dotychczas:
- Zainstalowałeś podprojekt pakietu w swoim projekcie Scala.js i poprawnie go skonfigurowałeś.
- Dla dowolnej biblioteki, którą chciałeś, dodałeś ją w
package.json
. - Wymagałeś ich w
lib.js
. - Wykonałeś polecenie
bundle
.
W rezultacie masz teraz obiekt JavaScript najwyższego poziomu w Bundle
, zapewniający wszystkie punkty wejścia dla bibliotek, dostępne jako pola tego obiektu.
Teraz jesteś gotowy do użycia go ze Scala.js. W najprostszym przypadku możesz zrobić coś takiego, aby uzyskać dostęp do bibliotek:
@js.native object Bundle extends js.Object { def jquery : js.Any = js.native def lodash: js.Any = js.native }
Ten kod umożliwia dostęp do bibliotek ze Scala.js. Nie jest to jednak sposób, w jaki powinieneś to zrobić ze Scala.js, ponieważ biblioteki są nadal nieopisane. Zamiast tego powinieneś napisać typowane „fasady” lub wrappery , dzięki czemu możesz używać oryginalnie niewpisanych bibliotek JavaScript w sposób skalowalny.
Nie mogę ci tutaj powiedzieć, jak napisać fasady, ponieważ zależy to od konkretnej biblioteki JavaScript, którą chcesz zapakować w Scala.js. Pokażę tylko przykład, aby zakończyć dyskusję. Sprawdź oficjalną dokumentację Scala.js, aby uzyskać więcej informacji. Możesz również zapoznać się z listą dostępnych elewacji i przeczytać kod źródłowy inspiracji.
Do tej pory omówiliśmy proces dla dowolnych, wciąż niezmapowanych bibliotek. W dalszej części artykułu mam na myśli bibliotekę z już dostępną fasadą, więc dyskusja jest tylko przykładem.
Zawijanie API JavaScript w Scala.js
Kiedy używasz require
w JavaScript, otrzymasz obiekt, który może być wieloma różnymi rzeczami. Może to być funkcja lub obiekt. Może to być nawet tylko ciąg znaków lub wartość logiczna, w którym to przypadku require
jest wywoływane tylko w przypadku skutków ubocznych.
W przykładzie, który robię, jquery
jest funkcją zwracającą obiekt, który udostępnia dodatkowe metody. Typowym zastosowaniem jest jquery(<selector>).<method>
. Jest to uproszczenie, ponieważ jquery
pozwala również na użycie $.<method>
, ale w tym uproszczonym przykładzie nie będę omawiał wszystkich tych przypadków. Należy zauważyć, że w przypadku złożonych bibliotek JavaScript nie wszystkie API można łatwo odwzorować na statyczne typy Scala. Może zajść potrzeba skorzystania z js.Dynamic
zapewniającego dynamiczny (bez typu) interfejs do obiektu JavaScript.
Tak więc, aby uchwycić tylko bardziej powszechny przypadek użycia, zdefiniowałem w obiekcie Bundle
, jquery
:
def jquery : js.Function1[js.Any, Jquery] = js.native
Ta funkcja zwróci obiekt jQuery. Instancja cechy jest w moim przypadku definiowana jedną metodą (w uproszczeniu można dodać własną):
@js.native trait Jquery extends js.Object { def text(arg: js.Any): Jquery = js.native }
W przypadku biblioteki Lodash modelujemy całą bibliotekę jako obiekt JavaScript, ponieważ jest to zbiór funkcji, które można bezpośrednio wywołać:
def lodash: Lodash = js.native
Tam, gdzie cecha lodash
jest następująca (również uproszczenie, możesz tutaj dodać swoje metody):
@js.native trait Lodash extends js.Object { def camelCase(arg: js.Any): String = js.native }
Korzystając z tych definicji, możemy teraz w końcu napisać kod Scala, korzystając z bazowych bibliotek jQuery i Lodash , ładowanych z NPM, a następnie przeglądanych :
object Main extends JSApp { def main(): Unit = { import Bundle._ jquery("#title").text(lodash.camelCase("This is a test")) } }
Tutaj możesz sprawdzić pełny przykład.
Wniosek
Jestem programistą Scala i byłem podekscytowany, gdy odkryłem Scala.js, ponieważ mogłem używać tego samego języka zarówno dla serwera, jak i klienta. Ponieważ Scala jest bardziej podobna do JavaScript niż do Javy, Scala.js jest całkiem prosty i naturalny w przeglądarce. Co więcej, możesz również korzystać z zaawansowanych funkcji Scali jako bogatej kolekcji bibliotek, makr i potężnych środowisk IDE oraz narzędzi do budowania. Inne kluczowe zalety to możliwość współdzielenia kodu między serwerem a klientem. Jest wiele przypadków, w których ta funkcja jest przydatna. Jeśli używasz transpilera dla Javascript, takiego jak Coffeescript, Babel lub Typescript, nie zauważysz zbyt wielu różnic podczas korzystania ze Scala.js, ale wciąż jest wiele zalet. Sekret polega na tym, aby wykorzystać to, co najlepsze z każdego świata i zapewnić, że będą ze sobą dobrze współpracować.