使用帶有 NPM 和 Browserify 的 Scala.js

已發表: 2022-03-11

如果您使用 Scala.js,Scala 語言到 JavaScript 的編譯器,您可能會發現 Scala.js 的標準依賴管理在現代 JavaScript 世界中過於局限。 Scala.js 使用 WebJars 管理依賴項,而 JavaScript 開發人員使用 NPM 管理依賴項。 由於 NPM 生成的依賴項是服務器端的,因此通常需要使用 Browserify 或 Webpack 生成瀏覽器代碼的額外步驟。

在這篇文章中,我將描述如何將 Scala.js 與 NPM 上可用的大量 JavaScript 模塊集成。 您可以查看此 GitHub 存儲庫以獲取此處描述的技術的工作示例。 使用這個例子和閱讀這篇文章,你將能夠使用 NPM 收集你的 JavaScript 庫,使用 Browserify 創建一個包,並在你自己的 Scala.js 項目中使用結果。 所有這一切都無需安裝 Node.js,因為一切都由 SBT 管理。

Browserify 和 Scala.js

Browserify 的魔法也可以在 Java 世界中發揮出色!
鳴叫

管理 Scala.js 的依賴關係

今天,用可編譯為 JavaScript 的語言編寫應用程序已成為一種非常普遍的做法。 越來越多的人轉向擴展的 JavaScript 語言,比如 CoffeeScript 或 TypeScript,或者像 Babel 這樣的轉譯器,它們讓你現在可以使用 ES6。 同時,Google Web Toolkit(從 Java 到 JavaScript 的編譯器)主要用於企業應用程序。 由於這些原因,作為一名 Scala 開發人員,我不再認為使用 Scala.js 是一個奇怪的選擇。 編譯器速度快,生成的代碼高效,總體而言,它只是在前端和後端使用相同語言的一種方式。

也就是說,在 JavaScript 世界中使用 Scala 工具還不是 100% 自然的。 有時您必須填補從 JavaScript 生態系統到 Scala 生態系統的空白。 Scala.js 作為一種語言與 JavaScript 具有出色的互操作性。 因為 Scala.js 是 Scala 語言到 JavaScript 的編譯器,所以很容易將 Scala 代碼連接到現有的 JavaScript 代碼。 最重要的是,Scala.js 使您能夠創建類型化的接口(或外觀)來訪問無類型的 JavaScript 庫(類似於您使用 TypeScript 所做的事情)。 對於習慣於 Java、Scala 甚至 Haskell 等強類型語言的開發人員來說,JavaScript 的類型過於鬆散。 如果您是這樣的開發人員,可能主要原因是因為您可能想要使用 Scala.js 是為了在無類型語言之上獲得(強)類型語言。

標準 Scala.js 工具鏈中的一個問題是:如何在您的項目中包含依賴項,如附加的 JavaScript 庫? SBT 對 WebJars 進行了標準化,因此您應該使用 WebJars 來管理您的依賴項。 不幸的是,根據我的經驗,它被證明是不夠的。

WebJars 的問題

如前所述,檢索 JavaScript 依賴項的標準 Scala.js 方法是基於 WebJars。 畢竟,Scala 是一種 JVM 語言。 Scala.js 主要使用 SBT 來構建程序,最後 SBT 在管理 JAR 依賴項方面表現出色。

出於這個原因, WebJar格式被定義為在 JVM 世界中導入 JavaScript 依賴項。 WebJar 是包含 Web 資產的 JAR 文件,其中簡單的 JAR 文件僅包含已編譯的 Java 類。 因此,Scala.js 的想法是您應該通過簡單地添加 WebJar 依賴項來導入您的 JavaScript 依賴項,就像 Scala 添加 JAR 依賴項一樣。

好主意,但它不起作用

Webjar 的最大問題是隨機 JavaScript 庫上的任意版本很少可用作 WebJar。 同時,絕大多數 JavaScript 庫都可以作為 NPM 模塊使用。 但是,在 NPM 和 WebJars 之間存在一個假定的橋樑,帶有一個自動化的npm-to-webjar打包器。 所以我嘗試導入一個庫,可作為 NPM 模塊使用。 在我的例子中,它是 VoxelJS,一個用於在網頁中構建類似 Minecraft 的世界的庫。 我嘗試將庫請求為 WebJar,但橋失敗只是因為描述符中沒有許可證字段。

WebJar 庫不工作

由於其他原因,您在使用其他庫時也可能會遇到這種令人沮喪的經歷。 簡而言之,您似乎無法以 WebJar 的形式訪問野外的每個庫。 您必須使用 WebJars 來訪問 JavaScript 庫的要求似乎太有限了。

進入 NPM 和 Browserify

正如我已經指出的,大多數 JavaScript 庫的標準打包格式是 Node 包管理器或 NPM,它包含在任何版本的 Node.js 中。 通過使用 NPM,您可以輕鬆訪問幾乎所有可用的 JavaScript 庫。

請注意,NPM 是Node包管理器。 Node 是 V8 JavaScript 引擎的服務器端實現,它安裝包以供 Node.js 在服務器端使用。 事實上,NPM 對瀏覽器毫無用處。 然而,由於無處不在的 Browserify 工具,NPM 的實用性已經擴展了一段時間,可以與瀏覽器應用程序一起使用,它當然也作為 NPM 包分發。

Browserify 簡單來說就是一個瀏覽器的打包器。 它將收集 NPM 模塊,生成可在瀏覽器應用程序中使用的“包”。 許多 JavaScript 開發人員以這種方式工作 - 他們使用 NPM 管理包,然後將它們瀏覽器化以在 Web 應用程序中使用。 請注意,還有其他工具以相同的方式工作,例如 Webpack。

填補從 SBT 到 NPM 的空白

由於我剛才描述的原因,我想要的是一種使用 NPM 從 Web 安裝依賴項的方法,調用 Browserify 來收集瀏覽器的依賴項,然後將它們與 Scala.js 一起使用。 事實證明,這項任務比我預期的要復雜一些,但仍然是可能的。 確實,我做了這項工作,我在這裡描述它。

為簡單起見,我選擇 Browserify 也是因為我發現它可以在 SBT 中運行。 我還沒有嘗試過使用 Webpack,儘管我想這也是可能的。 幸運的是,我不必從真空中開始。 有一些已經到位:

  • SBT 已經支持 NPM。 為 Play Framework 開發的sbt-web插件可以安裝 NPM 依賴項。
  • SBT 支持 JavaScript 的執行。 sbt-jsengine插件,您可以在不安裝 Node 本身的情況下執行 Node 工具。
  • Scala.js 可以使用生成的包。 在 Scala.js 中,有一個連接函數可以在您的應用程序中包含任意 JavaScript 庫。

使用這些特性,我創建了一個 SBT 任務,它可以下載 NPM 依賴項,然後調用 Browserify,生成一個bundle.js文件。 我試圖將程序集成到編譯鏈中,我可以自動運行整個過程,但是每次編譯都必須處理捆綁太慢了。 此外,您不會一直更改依賴項; 因此,當您更改依賴項時,您必須偶爾手動創建一個包是合理的。

所以,我的解決方案是建立一個子項目。 這個子項目使用 NPM 和 Browserify 下載和打包 JavaScript 庫。 然後,我添加了一個bundle命令來執行依賴項的收集。 生成的包被添加到要在 Scala.js 應用程序中使用的資源中。

每當您更改 JavaScript 依賴項時,您都應該手動執行此“捆綁包”。 如前所述,它在編譯鏈中不是自動化的。

如何使用捆綁器

如果您想使用我的示例,請執行以下操作:首先,使用通常的 Git 命令簽出存儲庫。

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

然後,複製 Scala.js 項目中的bundle文件夾。 它是捆綁的子項目。 要連接到主項目,您應該在build.sbt文件中添加以下行:

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

此外,您必須將以下行添加到您的project/plugins.sbt文件中:

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

完成後,您有一個新命令bundle ,您可以使用它來收集您的依賴項。 它將在您的src/main/resources文件夾下生成一個文件bundle.js

捆綁包是如何包含在您的 Scala.js 應用程序中的?

剛剛描述的bundle命令使用 NPM 收集依賴項,然後創建一個bundle.js 。 當您運行fastOptJSfullOptJS命令時,ScalaJS 將創建一個myproject-jsdeps.js ,包括您指定為 JavaScript 依賴項的所有資源,因此也包括您的bundle.js 。 要在您的應用程序中包含捆綁的依賴項,您應該使用以下包含項:

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

您的包現在可以作為myproject-jsdeps.js的一部分使用。 包已經準備好了,我們已經完成了一些任務(導入依賴項並將它們導出到瀏覽器)。 下一步是使用 JavaScript 庫,這是一個不同的問題,一個 Scala.js 編碼問題。 為了完整起見,我們現在將討論如何在 Scala.js 中使用 bundle 並創建外觀以使用我們導入的庫。

使用 Scala.js,您可以在服務器和客戶端之間共享代碼

使用 Scala.js,您可以輕鬆地在服務器和客戶端之間共享代碼
鳴叫

在 Scala.js 應用程序中使用通用 JavaScript 庫

回顧一下,我們剛剛看到瞭如何使用 NPM 和 Browserify 創建一個包,並將該包包含在 Scala.js 中。 但是我們如何使用通用的 JavaScript 庫呢?

我們將在本文的其餘部分詳細解釋的完整過程是:

  • 從 NPM 中選擇您的庫並將它們包含在bundle/package.json中。
  • bundle/lib.js中的庫模塊文件中使用require加載它們。
  • 編寫 Scala.js 外觀來解釋 Scala.js 中的Bundle對象。
  • 最後,使用新類型的庫對您的應用程序進行編碼。

添加依賴項

使用 NPM,您必須在package.json文件中包含您的依賴項,這是標準的。

因此,假設您想使用兩個著名的庫,如 jQuery 和 Loadash。 此示例僅用於演示目的,因為已經有一個出色的 jQuery 包裝器可用作具有適當模塊的 Scala.js 的依賴項,而 Lodash 在 Scala 世界中毫無用處。 儘管如此,我認為這是一個很好的例子,但僅作為一個例子。

因此,請訪問npmjs.com網站並找到您要使用的庫並選擇一個版本。 假設您選擇jquery-browserify browserify 版本 13.0.0 和lodash版本 4.3.0。 然後,更新您的packages.jsondependencies塊,如下所示:

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

始終保留browserify ,因為它是生成捆綁包所必需的。 不過,您不需要將其包含在捆綁包中。

請注意,如果您安裝了 NPM,則只需從bundle目錄中鍵入:

 npm install --save jquery-browserify lodash

它還將更新package.json 。 如果您沒有安裝 NPM,請不要擔心。 SBT 將安裝 Java 版本的 Node.js 和 NPM,下載所需的 JAR 並運行它們。 當您從 SBT 運行bundle命令時,所有這些都是管理的。

導出庫

現在我們知道如何下載軟件包了。 下一步是指示 Browserify 將它們收集到一個包中,並讓它們對應用程序的其餘部分可用。

Browserify 是require的收集器,模擬瀏覽器的 Node.js 行為,這意味著您需要在某個地方require導入您的庫。 因為我們需要將這些庫導出到 Scala.js,所以 bundle 也會生成一個名為Bundle的頂級 JavaScript 對象。 因此,您需要做的是編輯lib.js ,它導出一個 JavaScript 對象,並要求您的所有庫作為該對象的字段。

如果我們想導出到 Scala.js 的 jQuery 和 Lodash 庫,在代碼中這意味著:

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

現在,只需執行命令bundle ,該庫將被下載、收集並放置在 bundle 中,準備好在您的 Scala.js 應用程序中使用。

訪問捆綁包

迄今為止:

  • 您在 Scala.js 項目中安裝了 bundle 子項目並正確配置了它。
  • 對於您想要的任何庫,您將其添加到package.json中。
  • 您在lib.js中需要它們。
  • 您執行了bundle命令。

結果,您現在有一個Bundle頂級 JavaScript 對象,提供庫的所有入口點,可用作該對象的字段。

現在您已準備好將它與 Scala.js 一起使用。 在最簡單的情況下,您可以執行以下操作來訪問庫:

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

此代碼允許您從 Scala.js 訪問庫。 但是,這不是您應該使用 Scala.js 的方式,因為這些庫仍然是無類型的。 相反,您應該編寫類型化的“外觀”或包裝器,以便您可以以類型化的方式使用最初無類型的 JavaScript 庫。

我不能在這裡告訴您如何編寫外觀,因為它取決於您要包裝在 Scala.js 中的特定 JavaScript 庫。 我將僅展示一個示例,以完成討論。 請查看官方 Scala.js 文檔以獲取更多詳細信息。 此外,您可以查閱可用外觀列表並閱讀源代碼以獲取靈感。

到目前為止,我們已經介紹了任意、仍然未映射的庫的過程。 在本文的其餘部分中,我指的是具有已經可用外觀的庫,因此討論只是一個示例。

在 Scala.js 中包裝 JavaScript API

當你在 JavaScript 中使用require時,你會得到一個可以是許多不同事物的對象。 它可以是函數或對象。 它甚至可以只是一個字符串或布爾值,在這種情況下,僅針對副作用調用require

在我正在做的示例中, jquery是一個函數,返回一個提供附加方法的對象。 典型用法是jquery(<selector>).<method> 。 這是一個簡化,因為jquery還允許使用$.<method> ,但在這個簡化的示例中,我不會涵蓋所有這些情況。 請注意,一般來說,對於復雜的 JavaScript 庫,並非所有 API 都可以輕鬆映射到靜態 Scala 類型。 您可能需要js.Dynamic為 JavaScript 對象提供動態(無類型)接口。

因此,為了捕捉更常見的用例,我在Bundle對象jquery中定義:

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

此函數將返回一個 jQuery 對象。 在我的例子中,一個特徵的實例是用一個方法定義的(一種簡化,你可以添加你自己的):

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

對於 Lodash 庫,我們將整個庫建模為 JavaScript 對象,因為它是您可以直接調用的函數的集合:

 def lodash: Lodash = js.native

lodash特徵如下(也是一種簡化,您可以在此處添加您的方法):

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

使用這些定義,我們現在終於可以使用底層的 jQuery 和Lodash庫來編寫 Scala 代碼,這兩個庫都是從 NPM 加載然後瀏覽器化的:

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

您可以在此處查看完整示例。

結論

我是一名 Scala 開發人員,當我發現 Scala.js 時我很興奮,因為我可以為服務器和客戶端使用相同的語言。 由於 Scala 更類似於 JavaScript 而不是 Java,因此 Scala.js 在瀏覽器中非常簡單自然。 此外,您還可以將 Scala 的強大功能用作豐富的庫、宏以及強大的 IDE 和構建工具的集合。 其他關鍵優勢是您可以在服務器和客戶端之間共享代碼。 在很多情況下,此功能很有用。 如果您使用 Coffeescript、Babel 或 Typescript 之類的 Javascript 轉譯器,您在使用 Scala.js 時不會注意到太多差異,但仍然有很多優點。 秘訣是充分利用每個世界的精華,並確保它們很好地協同工作。

相關:我為什麼要學習 Scala?