使用带有 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?