Scala.js项目( https://www.scala-js.org )作为一个强大的、类型安全的JavaScript的开发替代品。有了它,当你的项目需要编写JavaScript时,可以用Scala.js来代替,并使用sbt和Scala.js插件将代码编译成JavaScript。就像 scalac 把Scala代码编译成能在 JVM 上运行的 .class 文件一样,Scala.js插件把Scala代码编译成能在浏览器上运行的JavaScript代码。
Scala.js网站将其描述为“以更安全的方式构建强大的前端web应用”。Scala.js有很多优点,比如可以使用类、模块、强大的类型系统、大量的类库,以及IDE对简单重构、代码补全等功能的支持,Scala.js是JavaScript生态中强力备选之一,正如原生Javascript、CoffeeScript、Dart和TypeScript等。Scala.js可以使用相同工具来编写服务器端和客户端代码。
图21-1总结了Scala.js与其他浏览器技术相比的优势,该图由Scala.js网站提供,转载于此。
本章包括三个Scala.js示例,以帮助你入门:
- 21.1小节展示了如何搭建Scala.js环境和运行Scala.js程序。
- 在设置好环境后,21.2小节展示了如何编写Scala/Scala.js代码以响应按钮点击之类的事件。
- 然后,21.3小节展示了如何开始编写单页面的Web应用程序。
图21-1. Scala.js的优点(由scala-js.org提供)
接下来,21.4小节展示了GraalVM( https://www.graalvm.org )和它的 native-image 命令。顾名思义,这个命令可以把Scala应用程序变成一个native镜像。通过创建一个本地可执行文件 —— 例如Microsoft Windows上的 .exe 文件 —— 应用程序将立即启动,而没有JVM应用程序最初启动时的滞后感。
最后,jpackage 命令是Java 14 JDK附带的一个优秀工具。它可以把JVM应用程序打包成macOS、Windows和Linux平台上的native(原生)应用程序。jpackage 适用于任何为JVM生成 class 文件的语言,所以21.5小节展示了如何使用它把Scala应用程序打包成native应用程序。
你想开始使用Scala.js,需要知道如何安装它并创建一个“Hello, world”的例子。
本小节展示了如何开始使用Scala.js,并假定你熟悉JavaScript、HTML和文档对象模型(DOM)。
开始使用Scala.js是一个多步骤的过程:
- 处理好先决条件(开发运行环境等)
- 创建一个使用Scala.js插件的新sbt项目
- 创建一个Scala/Scala.js文件
- 编译并运行Scala代码
要开始使用Scala.js,你需要在系统上安装这些工具:
- Scala 3 ( https://www.scala-lang.org )
- sbt (1.5.0更高版本) ( https://www.scala-sbt.org )
- Node.js ( https://nodejs.org/en/download )
在macOS系统上,我用 brew install node 命令安装了Node.js,但你也可以按照该链接,用它所提供的安装程序来安装它。
创建一个新的sbt项目目录结构,如17.1小节“为sbt创建一个项目目录结构”所示。然后编辑 build.sbt 文件,改成下面这些内容:
ThisBuild / scalaVersion := "3.0.0"
// 启用`project/plugins.sbt`中的插件
enablePlugins(ScalaJSPlugin)
// 这表明这是一个具有main方法的应用程序
scalaJSUseMainModuleInitializer := true
lazy val root = project
.in(file("."))
.settings(
name := "Scala.js Hello World",
version := "0.1.0"
)
如果你在电脑上跟着做练习,我强烈建议你完全复刻这上述例子的具体代码,因为它们会影响Scala代码被编译成JavaScript文件时的文件名。
最后,在 project/plugins.sbt 文件中添加这行:
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.5.1")
这句话告诉sbt如何下载Scala.js库。
设置好sbt后,先创建一个简单的Scala “Hello, world”应用程序。然后,创建一个名为 src/main/scala/hello/Hello1.scala 的Scala源代码文件,内容如下:
package hello
@main def hello() = println("Hello, world")
这段代码并没有对Scala.js做任何特定的处理,但当你编译并运行它时,你会看到编译过程是如何使用sbt和Scala.js插件。
为了运行 Hello1.scala 的代码,首先启动sbt shell:
$ sbt
现在在sbt shell里面输入 run 命令,你可能会看到大量的初始输出,并最终以你程序的输出结束:
sbt> run
// possibly more output here ...
[info] compiling 1 Scala source to target/scala-3.0.0/classes ...
[info] Fast optimizing target/scala-3.0.0/scalajs-hello-world-fastopt
[info] Running hello.hello. Hit any key to interrupt.
Hello, world
示例所示,Hello, world 是在Scala代码被编译成JavaScript后打印出来的。之后,正如scala-js.org基本教程( https://oreil.ly/gMn7L )所指出的,“这段代码实际上是由一个JavaScript解释器,即Node运行的”。
一个需要注意的重要问题是,sbt run 命令会创建这个目录:
target/scala-3.0.0/scalajs-hello-world-fastopt
关于这个目录的一些说明:
- 目录名称是基于sbt项目名称的(“Scala.js Hello World”)。这就是为什么我之前建议你在 build.sbt 文件中使用我的项目名称 —— 这样我们产生的目录名称就会是相同的。
- Scala.js的文件命名过程会在该目录名的末尾加上 -fastopt。
- 在该目录中,你会发现有两个文件,main.js 和 main.js.map。
- 如果你用 cat、more 或其他工具看一下产生的 target/scala-3.0.0/scalajs-hello-world-fastopt/main.js 文件,你会发现它包含两千多行一些难以阅读的JavaScript源代码。
在该文件的最后,你会看到一行代码,看起来像这样:
$s_Lhello_hello__main__AT__V(new ($d_T.getArrayOf().constr)([]));
虽然很难读懂,但是 hello 和 main 的引用表明这段代码是由你的Scala @main方法产生的。
恭喜你,你刚刚把你的第一段Scala代码编译成了JavaScript。
这是开始使用Scala.js的第一步。当然,如果你希望看到代码在浏览器中运行,只需再做几步就行: 5. 更新 build.sbt。 6. 创建一个HTML文件。 7. 更新你的Scala代码。 8. 用 fastLinkJS 运行该应用程序。 9. 在浏览器中打开HTML文件。
下一步,在 build.sbt 文件的末尾添加这一行:
libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "1.1.0"
这让我们可以在Scala代码中使用Scala.js的DOM库( https://oreil.ly/Me2OZ ),我们一会儿就会这么做。请注意,这个特定的DOM库只是Scala.js( https://oreil.ly/RgfqJ )的众多JavaScript facade类库中的一个。
如果你还在sbt shell中,重新加载配置文件:
sbt> reload
接下来,在sbt项目的根目录下创建一个名为 hello1.html 的HTML文件,内容如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Scala.js Hello, World</title>
</head>
<body>
<!-- include the Scala.js compiled code -->
<script type="text/javascript"
src="./target/scala-3.0.0/scalajs-hello-world-fastopt.js">
</script>
</body>
</html>
注意,script 标签中包括了你一会儿要从Scala代码中生成的 target/scala-3.0.0/scalajs-hello-world-fastopt.js JavaScript文件。还要注意的是,这个文件名与解决方案中的代码所生成的名字不同。
现在更新 Hello1.scala 文件,使其包含这些内容:
package hello
import org.scalajs.dom
import dom.document
@main def hello1() =
val parNode = document.createElement("p")
val textNode = document.createTextNode("Hello, world")
parNode.appendChild(textNode)
document.body.appendChild(parNode)
在解决方案展示的例子中,这个文件包含普通的Scala代码,但现在它使用DOM库来创建代码,这看起来很像JavaScript代码。这段代码创建了一个段落标签,在其中放入了一些文本,然后将段落节点添加到文档的主体中。
接下来,从你的Scala源代码文件中生成JavaScript文件。可以用sbt fastLinkJS 命令来做这件事:
sbt> fastLinkJS
这条命令生成了你刚才在HTML文件中包含的 target/scala-3.0.0/scalajs-hello-world-fastopt.js 文件。
要查看 fastLinkJS 生成的文件名,请在 fastLinkJS 命令前加上 show:
sbt> show fastLinkJS
[info] target/scala-3.0.0/scalajs-hello-world-fastopt.js
现在在浏览器中打开该HTML文件。在macOS上,你可以用这个命令从命令行打开它:
$ open hello1.html
你也可以用一个文件打开该文件。这个URL取决于你在文件系统中的项目路径。它看起来会像 file:///Users/al/ScalaJSHelloWorld/hello1.html。
假设你能成功执行以上操作,应该可以在浏览器中看到文本“Hello, world”。
作为加分项,特别是你想再添加点乐趣,可以找机会用下面段代码做做实验。试试把这两行代码放在 Hello1.scala 文件中 @main 方法的任何地方:
println("foo")
System.err.println("bar")
添加完这几行后,再次运行 fastLinkJS 命令,重新加载你的网页,并查看你的浏览器控制台,比如说以下步骤:
- Chrome和Firefox:右键单击 → 检查 → 控制台
在浏览器控制台中,你应该看到 foo 以正常颜色打印,而 bar 以红色打印。这是一个帮助调试Scala.js应用程序的简单方法。
- Scala.js主页( https://www.scala-js.org )。
- 本小节开头部分很大程度上参考了Scala.js的基础教程( https://oreil.ly/LFT0E )。
你想知道如何使用Scala.js来响应事件,比如处理一个按钮点击事件。
我在之前的示例中,展示了如何设置Scala.js的工作环境。本解决方案以该示例为基础,展示了如何使用Scala/Scala.js代码来响应HTML按钮的点击事件。
这是一个多步骤的解决方案,它建立在上一个示例中创建的sbt项目之上:
- 创建一个新的HTML页面。
- 更新sbt以支持使用jQuery。
- 编写新的Scala/Scala.js代码。
- 在sbt中设置 main class。
- 运行代码。
这些步骤将在下面的小节中展示。
假设你正在使用21.1小节中创建的sbt项目,第一步是创建一个新的HTML网页。把这个文件命名为 hello2.html,并把它放在项目的根目录下,其内容如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Scala.js, Hello World 2</title>
</head>
<body>
<button type="button" id="hello-button">
Click me!
</button>
<!-- the 'jsdeps' file must be first -->
<script type="text/javascript"
src="./target/scala-3.0.0/scalajs2-jsdeps.js"></script>
<script type="text/javascript"
src="./target/scala-3.0.0/scalajs2-fastopt.js"></script>
</body>
</html>
与前面示例中的HTML文件相比,这个文件的重要变化是:
- 本页有一个 <button> 元素。
- 这个页面包括一个名为 scalajs2-jsdeps.js 的新文件。正如jsdeps这个名字所暗示的,这个文件包含了我们代码的依赖项,特别是 scalajs2-fastopt.js 所依赖的依赖项。稍后你会了解关于这些依赖的更多信息。
- 我们的Scala/Scala.js代码生成的新文件被命名为 scalajs2-fastopt.js。正如前面示例中所提到的,这个名字是基于 build.sbt 中所指定的项目名称。
要更新我们的sbt配置,首先要更新 project/plugins.sbt 文件,使其具有这些内容:
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.5.1")
// for adding webjars
addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.1")
第一行是在上一个示例中使用的,它将Scala.js sbt插件添加到项目中。最后一行是这个示例的新配置,它是能让我们在代码中使用WebJars库的第一步。(稍后会有更多关于这个的内容)。
接下来,从Scala.js代码中处理HTML按钮点击的最简单方法是与Scala.js一起使用Scala.js jQuery facade库。有多个facade库可选用,如果要在这个项目中使用名为jquery-facade( https://oreil.ly/PH6ty )的库,请在 build.sbt 文件的 libraryDependencies 中添加这一行:
("org.querki" %%% "jquery-facade" % "2.0").cross(CrossVersion.for3Use2_13)
(稍后你将看到完整的 build.sbt 文件)。
jquery-facade库是用Scala编写的,在 build.sbt 文件中加入这一行,就可以把它作为这个项目的依赖项。关于这行的重点是,cross(CrossVersion.for3Use2_13) 设置是让你在Scala 3项目中使用Scala 2.13库的神奇魔法。
接下来,还需要在HTML文件中加入实际的jQuery库 —— 用JavaScript编写的。你可以在HTML文件中加入一行这样的代码:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.1/jquery.min.js">
</script>
但是,sbt给你提供了另一种方式来将jQuery的JavaScript库引入到项目。在 build.sbt 文件中添加下面一行代码以使用sbt的方式:
jsDependencies += "org.webjars" % "jquery" % "2.2.1" / "jquery.js" minified ↵
"jquery.min.js"
这一行让你在项目中使用WebJars( https://www.webjars.org )的“被打包为JAR文件的客户端web库”。它特意引入了2.2.1版本的jQuery JavaScript库( https://jquery.com )—— 真正的JavaScript库,而不是Scala facade —— 然后当你在sbt中运行 fastLinkJS 命令时,这些JavaScript代码就被写入本地的 target/scala-3.0.0/scalajs2-jsdeps.js 文件。这一步不是必须的 —— 如前所示,你可以使用 <script> 标签引入jQuery —— 但这展示了一种将JavaScript库/依赖项添加到Scala.js/sbt项目的可能方式。
此时,hello2.html 文件中的这两行便容易理解了:
<!-- the 'jsdeps' file must be first -->
<script type="text/javascript"
src="./target/scala-3.0.0/scalajs2-jsdeps.js"></script>
<script type="text/javascript"
src="./target/scala-3.0.0/scalajs2-fastopt.js"></script>
第一行将我们的JavaScript依赖关系导入到HTML文件(这里是指jQuery),第二行包含你在 src/main/scala/hello/Hello2.scala 中编写的自定义Scala/Scala.js代码所生成的JavaScript。
最后,因为你要写的Scala代码依赖于jQuery,HTML文件中首先需要导入jsdeps。
现在你已经有了新的HTML文件,并且jQuery已经准备好在项目中使用,剩下的主要事情就是写一些Scala/Scala.js代码来响应 <button> 的点击。在这样做之前,需要注意的是 hello2.html 中的 <button> 的 id 是 hello-button:
<button type="button" id="hello-button">
------------
你会在接下来的Scala代码中使用到这个 id 。
用jQuery facade库响应按钮点击并显示一个JavaScript窗口的Scala代码出奇的简单。将这段代码保存在一个名为 src/main/scala/hello/Hello2.scala 的文件中:
import org.scalajs.dom
import org.querki.jquery.*
@main def hello2 =
// handle the login button click
$("#hello-button").click{ () =>
dom.window.alert("Hello, world")
}
如果你以前使用过jQuery,这段代码看起来会很熟悉。它可以被解读为:“找到 id 值为 hello-button 的HTML元素,当它被点击时,运行这个小算法,显示一个JavaScript提示窗口,内容为 Hello, world。” 这太好了,因为你可以在Scala代码中使用 $ 符号,这与在JavaScript中使用jQuery是一致的。
在运行这个例子之前,你还需要做一件事:更新 build.sbt 文件,以考虑到项目中现在有两个 main 方法 —— 21.1小节中的 Hello1.scala 和 Hello2.scala 中的另一个。要做到这一点,在 build.sbt 文件中添加这行,就在 scalaJSUseMainModuleInitializer 设置的下面:
Compile/mainClass := Some("hello.Hello2")
有了这个示例中所展示的变化,完整的 build.sbt 文件现在应该有这些内容:
ThisBuild / scalaVersion := "3.0.0"
// 启用`project/plugins.sbt`中的插件
enablePlugins(ScalaJSPlugin)
// 这说明这是一个有main方法的应用程序
scalaJSUseMainModuleInitializer := true
Compile/mainClass := Some("hello.Hello2")
lazy val root = project
.in(file("."))
.settings(
name := "ScalaJs2",
version := "0.1",
libraryDependencies ++= Seq(
("org.scala-js" %%% "scalajs-dom" % "1.1.0")
.cross(CrossVersion.for3Use2_13),
("org.querki" %%% "jquery-facade" % "2.0")
.cross(CrossVersion.for3Use2_13)
),
)
// this includes jquery with webjars.
// see: https://github.com/scala-js/jsdependencies
enablePlugins(JSDependenciesPlugin)
jsDependencies += "org.webjars" % "jquery" % "2.2.1" / "jquery.js" ↵
minified "jquery.min.js"
关于这个文件的其他说明:
- enablePlugins(ScalaJSPlugin) 一行是在一个sbt项目中使用Scala.js的示例的一部分。
- scalaJSUseMainModuleInitializer 表明这是一个有 main 方法的应用程序。
- Compile/mainClass 设置用来声明构建的 main 方法是 hello.Hello2( hello 包中 Hello2 类的main方法)。
- 把项目名称改为“ScalaJs2”。
- 使用 %%% 来引用那些为Scala.js编译的依赖项(而不是普通Scala依赖项使用的 %% )。
最后,cross(CrossVersion.for3Use2_13) 语法让你在Scala 3项目中使用Scala 2.13依赖项。如果这两个 libraryDependencies 条目是Scala 3的库,你可以这样来引用它们:
// 如果这是Scala 3的库
libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "1.1.0"
但是当你需要在Scala 3项目中使用Scala 2.13的库时,应该用这个语法来代替:
// 在Scala 3的构建中使用Scala 2.13的库
("org.scala-js" %%% "scalajs-dom" % "1.1.0").cross(CrossVersion.for3Use2_13)
现在你已经准备好运行这个例子了。假设你还在sbt控制台中,运行 reload 命令来更新配置文件的变更:
sbt> reload
之后,运行 fastLinkJS 命令,将 Hello2.scala 代码编译为JavaScript:
sbt> fastLinkJS
现在,在你的浏览器中打开 hello2.html 文件。在macOS上,你可以用终端命令行中的 open 命令来做这件事:
$ open hello2.html
你也可以用一个在文件系统中的项目路径的URL来打开该文件。它看起来类似这样 file:///Users/al/ScalaJSHelloWorld/hello2.html。
当该文件在浏览器中打开时,你应该看到一个“Click me”的按钮,如图21-2所示。
图21-2. HTML页面中的“Hello, world”按钮
当你点击该按钮时,你应该看到一个像图21-3所示的警告对话框。
图21-3. 当按钮被点击时显示的JavaScript警告窗口
如果一切顺利,那么恭喜你,你刚刚写了一些Scala.js代码来响应HTML按钮的点击,这要感谢Scala、sbt、Scala.js、jQuery和jQuery facade库(以及Scala.js DOM库、Node.js等)。
再次需要注意的是,当你在 build.sbt 文件中添加为Scala.js而构建的依赖项时,需要使用三个百分符号:
("org.querki" %%% "jquery-facade" % "2.0").cross(CrossVersion.for3Use2_13)
---
它的工作方式是,如果是引入为Scala编译的库,应当使用 %%,但如果一个库是为Scala.js编译的,则应使用 %%%。正如“Simple Command Line Tools with Scala Native”( https://oreil.ly/F8Gau )中解释的那样,两个百分号的符号告诉sbt使用正确的依赖版本,而三个百分号的符号告诉sbt使用正确的目标环境,目前,它要么是Scala Native,要么就是本例中的Scala.js。
在解决方案中,我展示了sbt shell中的 fastLinkJS 命令,但当你在实际开发代码时,更好的解决方案是像这样运行它:
sbt> ~ fastLinkJS
就像用 ~compile 持续编译代码或用 ~test 测试它一样,这个命令告诉sbt监听项目中的文件,并在任何一个源代码文件改变时重新运行 fastLinkJS 命令。当你第一次发出这个命令时,应该看到一些像这样的输出:
sbt:ScalaJs2> ~ fastLinkJS
[success] Total time: 0 s, completed Apr 30, 2021, 8:45:03 AM
[info] 1. Monitoring source files
[info] Press <enter> to interrupt or ? for more options.
然后当你对代码进行修改时,你会看到这个输出被更新了。对一个开发者来说,这很好,因为现在你需要做的就是在代码修改后,在浏览器中刷新下页面即可。
- jquery-facade库( https://oreil.ly/PH6ty )。
- 你可以在Scala.js JavaScript library facades页面上找到其他facades库的列表( https://oreil.ly/RgfqJ )。
- WebJars项目( https://www.webjars.org )。
你想学习如何用Scala.js来构建单页面的web应用。
本方案展示了如何使用Scala.js开始构建单页面的应用程序(SPA)。在之前的两个方案中,我展示了如何设置Scala.js工作环境,本方案建立在这两个方案中创建的sbt项目之上。与之前的项目一样,本示例假定你熟悉HTML、JavaScript和DOM。
这个示例的步骤是:
- 更新 build.sbt。
- 创建一个新的HTML文件。
- 创建一个新的Scala/Scala.js文件。
- 运行代码。
这些步骤的具体介绍在下面的章节中。
这个示例使用了Scalatags库( https://oreil.ly/kOthX ),所以第一步是把它作为一个依赖项添加到 build.sbt 文件。如果这是一个Scala 3库,则可以添加这行:
libraryDependencies += "com.lihaoyi" %%% "scalatags" % "0.9.4"
但是因为它是一个Scala 2.13库,而你在Scala 3项目中使用它,所以应该用这个语法:
("com.lihaoyi" %%% "scalatags" % "0.9.4").cross(CrossVersion.for3Use2_13)
另外,因为你将为这个示例创建一个新的 Hello3.scala 文件,所以修改 build.sbt 中的 mainClass 设置以使用该文件(你很快就会创建):
Compile/mainClass := Some("hello.Hello3")
build.sbt 文件在这个示例和前两个示例中被修改之后,你的 build.sbt 文件应该变成这样的:
ThisBuild / scalaVersion := "3.0.0"
// enable the plugin that’s in 'project/plugins.sbt'
enablePlugins(ScalaJSPlugin)
// this states that this is an application with a main method
scalaJSUseMainModuleInitializer := true
Compile/mainClass := Some("hello.Hello3")
lazy val root = project
.in(file("."))
.settings(
name := "ScalaJs3",
version := "0.1",
libraryDependencies ++= Seq(
("org.scala-js" %%% "scalajs-dom" % "1.1.0") ↵
.cross(CrossVersion.for3Use2_13),
("org.querki" %%% "jquery-facade" % "2.0") ↵
.cross(CrossVersion.for3Use2_13),
("com.lihaoyi" %%% "scalatags" % "0.9.4") ↵
.cross(CrossVersion.for3Use2_13)
),
)
enablePlugins(JSDependenciesPlugin)
jsDependencies += "org.webjars" % "jquery" % "2.2.1" / "jquery.js" minified ↵
"jquery.min.js"
jQuery的依赖项在本教程中不是必须的,但我曾在上个教程中加入了它们,如果你决定使用Scala.js构建自己的SPAs,一般会需要它们。(当然,如果你不打算使用jQuery,那就没有必要将jsdeps的JavaScript文件导入到HTML页面中。)
接下来,在项目的根目录下创建一个 hello3.html 文件,其中包含这些内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Scala.js—Hello, world, Part 3</title>
</head>
<body>
<div id="root"></div>
<!-- "jsdeps" must be listed first -->
<script type="text/javascript"
src="./target/scala-3.0.0/scala-js-hello-world-jsdeps.js"></script>
<script type="text/javascript"
src="./target/scala-3.0.0/scalajs3-fastopt.js"></script>
</body>
</html>
从上个示例中的 hello2.html 到 hello3.html,最大的变化是删除了HTML按钮,增加了这块代码:
<div id="root"></div>
你将在我们接下来要创建的Scala代码中使用这个 root id。另外,如果你是SPAs新手,请注意这个“HTML”文件中的HTML是多么的少(!)。
接下来,在 src/main/scala/hello 目录下创建一个名为 Hello3.scala 的Scala源代码文件,内容如下:
package hello
import org.scalajs.dom
import dom.document
import scalatags.JsDom.all.*
@main def hello3 =
// create an html button with scalatags
val btn = button(
"Click me",
onclick := { () =>
dom.window.alert("Hello, world")
}
)
// this is intentional overkill to demonstrate scalatags.
// the most important thing is that the button is added here.
val content =
div(id := "foo",
div(id := "bar",
h2("Hello"),
btn
)
)
val root = dom.document.getElementById("root")
root.innerHTML = ""
root.appendChild(content.render)
下面是这个代码的简要描述。首先,我创建了一个Scalatags button。这个按钮的标签是 Click me ,当它被点击时,便显示一个JavaScript提示窗口,类似于我在前面的示例中所做的:
val btn = button(
"Click me",
onclick := { () =>
dom.window.alert("Hello, world")
}
)
接下来,只是为了演示Scalatags是如何工作的,我创建了一个类名为 foo 的 div,然后在它里面再放一个类名为 bar 的 div,然后在这两个 div 里面放一个 h2 元素和按钮。最重要的是要注意,这便是按钮对象 btn 被添加到DOM中的地方:
val content =
div(id := "foo",
div(id := "bar",
h2("Hello"),
btn
)
)
额外的 div 标签并不是必须的,我只是把它们加进去,以演示Scalatags的工作原理。我喜欢Scalatags的一点是,这段Scala代码与它要发出的HTML代码的对应性非常好。另一件好事是,Scalatags得到了IDE的支持,所以IDE的代码补全功能对弄清如何使用Scalatags功能有很大帮助。
最后,我用这段代码将Scala代码与HTML网页的 root 元素连接起来:
val root = dom.document.getElementById("root")
root.innerHTML = ""
root.appendChild(content.render)
如果你熟悉编写JavaScript时与DOM打交道,这段代码看起来应该很熟悉。一个重要的注意事项是,你需要记住在Scalatags代码上调用 render 方法,使程序能正常运行:
root.appendChild(content.render)
------
值得一提的是,在我的代码中,我往往会忘记这一部分。
一切就绪后,你现在可以将这段Scala代码编译成JavaScript。回到 sbt 控制台,假设它还在运行上次示例中的 ~fastLinkJS 命令,按回车键停止它。然后运行 reload 命令,更新 build.sbt 的修改:
sbt> reload
然后重新运行 ~fastLinkJS 命令:
sbt> ~fastLinkJS
如果你需要知道当你运行 fastLinkJS 时产生了哪些文件,可以用 show fastLinkJS 这个命令来代替。它显示了生成的任何文件的名称:
sbt:ScalaJs3> show fastLinkJS
[info] Attributed(target/scala-3.0.0/scalajs3-fastopt.js)
现在,在你的浏览器中打开 hello3.html 文件。在macOS上,你可以用 open 命令:
$ open hello3.html
否则,你的文件的URL可能是像这样的:file:///Users/al/ScalaJSHello-World/hello3.html。
当你打开这个文件时,应该在浏览器中看到图21-4所示的结果。
图21-4. HTML页面中的“Click me”按钮
现在点击“Click me”按钮,你应该看到图21-5中所示的结果。
图21-5. 当按钮被点击时显示的JavaScript警告窗口
如果你跟随本示例操作了,那么恭喜你,你刚刚用Scala.js创建了一个单页面的web应用程序。
如果你以前创建过SPAs,希望你能看到这种方法的潜力:它可以让你利用Scala编程语言的力量创建单页面的web应用。
在编写SPAs的过程中,下一步便是处理HTTP请求。这个scalajs.org页面( https://oreil.ly/ZaT4w )展示了在JavaScript ES6和Scala.js中,创建和使用 XMLHttpRequest 几乎是一样的。
scala-js-dom 网站( https://oreil.ly/zZDav )展示了在Scala.js中使用 XMLHttpRequest 和JavaScript中是多么的相似(并且也很容易):
def main(pre: html.Pre) = {
val xhr = new dom.XMLHttpRequest()
xhr.open(
"GET",
"http://api.openweathermap.org/data/2.5/weather?q=Singapore"
)
xhr.onload = { (e: dom.Event) =>
if (xhr.status == 200) {
pre.textContent = xhr.responseText
}
}
xhr.send()
}
该页面还展示了如何使用WebSocket,以及如何使用 dom.ext.Ajax 对象的 get 和 post 方法作为 XmlHttpRequests 的一个更简单的替代品。
关于用Scala.js创建SPAs的更多信息,请参阅这些资源:
- The Scala.js网站( https://www.scala-js.org )。
- Scalatags库( https://oreil.ly/kOthX )。
- 这个Scala.js for JavaScript开发者的页面( https://oreil.ly/ZaT4w )提供了一个关于使用 XMLHttpRequest 的介绍。
- 如果你想尝试用Scala.js编写一个WebSocket应用程序,这有我创建的Play Framework和WebSocket示例( https://oreil.ly/tcClM ),其中还包括一些可以转换为Scala.js的JavaScript代码。
你想从Scala代码中构建一个native的可执行文件,这样你的应用程序就会启动得更快,并有可能运行得更快,消耗的内存更少。
首先,照常构建一个独立的JAR文件,比如用 sbt package 或 sbt assembly,然后使用GraalVM的sbt-native-image插件( https://oreil.ly/v8srq )来构建native镜像 —— 一个针对操作系统平台的native的二进制可执行文件。
例如,我创建了一个名为sbtmkdirs( https://oreil.ly/o5uYO )的小命令行Scala应用程序,以生成新的sbt项目目录结构。为了使 sbtmkdirs 几乎立即启动,我用sbt-native-image插件将其编译为一个native的可执行文件。其步骤是:
- 配置sbt项目以使用sbt-native-image插件。
- 用 sbt nativeImage 命令创建native的可执行文件(可通过该插件获得)。
在撰写本文时,sbt-native-image插件的版本为0.3.0,配置步骤如下。首先,按照17.1小节“为sbt创建一个项目目录结构”中所述,创建一个sbt项目。然后在你的 project/plugins.sbt 文件中添加这一行:
addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.0")
接下来,更新你的build.sbt文件,使之看起来像这样:
lazy val root = (project in file("."))
.enablePlugins(NativeImagePlugin)
.settings(
name := "Sbtmkdirs",
version := "0.2",
scalaVersion := "3.0.0",
Compile / mainClass := Some("sbtmkdirs.Sbtmkdirs")
)
然后根据需要修改项目的 name、version、scalaVersion 和 mainClass 的值:
对于这个示例,该文件中的关键行是:
- enablePlugins 一行告诉sbt要使用这个插件。
- mainClass 一行告诉sbt,我的 @main 被命名为 Sbtmkdirs,并且它是在 sbtmkdirs包中。
其他行则是标准的sbt build.sbt 文件中常用的。
有了这些设置,你只需在sbt shell中运行 nativeImage 命令,或在操作系统的命令行中运行:
$ sbt nativeImage
这个命令会编译代码并构建native镜像。第一次运行时,它可能需要下载许多工件,包括GraalVM和它的 native-image 命令。一旦它完成,你应该看到一个类似这样的结果:
[info] Native image ready!
[info] target/native-image/Sbtmkdirs
[success] Total time: 42 s
现在你便可以进入到 target/native-image 目录并运行新命令来测试native镜像:
$ cd target/native-image
$ ./Sbtmkdirs
你也可以使用这个插件命令来生成native镜像,并在建立后立即运行:
$ sbt nativeImageRun
根据native-image文档( https://oreil.ly/vvSWF ):
GraalVM Native Image允许你提前将Java代码编译成一个独立的可执行文件,称为 native image。这个可执行文件包括应用程序类、其依赖的类、JDK的运行时库类和JDK静态链接的native代码。它不在Java虚拟机上运行,而是包括来自不同的虚拟机的必要的组件,如内存管理和线程调度,称为“Substrate 虚拟机”。
注意,如果你想使用其他一些功能,如Java的 java.net 网络库,则需要使用命令行标志才能如愿:
--enable-http enable http support in the generated image
--enable-https enable https support in the generated image
你也可以自己运行 native-image 命令,但这需要相当多的工作。如果你有兴趣尝试,第一步是下载并安装GraalVM,并将其作为你的Java库。在 Unix 系统上,要这样设置 JAVA_HOME 和 PATH:
$ export JAVA_HOME=~/bin/graalvm-ce-java11-21.1.0/Contents/Home/
$ export PATH=~/bin/graalvm-ce-java11-21.1.0/Contents/Home/bin:$PATH
然后,如GraalVM这个页面( https://oreil.ly/vvSWF )所述,你需要单独安装GraalVM的 native-image 命令。
你还需要像往常一样,安装Scala。之后,像我在Scala 2.13中那样,创建一个这样的shell脚本:
# SCALA_HOME needs to be set
export SCALA_HOME=~/bin/scala-2.13.3
# this script assumes that this file was already created
# in this directory with `sbt package` or `sbt assembly`
JAR_FILE=sbtmkdirs_2.13-0.2.jar
JAR_DIR=../target/scala-2.13
echo "deleting old JAR file ..."
rm $JAR_FILE 2> /dev/null
echo "copying JAR file to current dir ..."
cp ${JAR_DIR}/${JAR_FILE} .
echo "running 'native-image' command on ${JAR_FILE} ..."
# create a native image from the jar file and name
# the resulting executable 'sbtmkdirs'
native-image -cp .:${SCALA_HOME}/lib/scala-library.jar:${JAR_FILE} \
--no-server \
--no-fallback \
--initialize-at-build-time \
-jar ${JAR_FILE} sbtmkdirs
该脚本假定你的JAR文件具有本例所示的名称,并且在本例所示的目标目录中。它使用该JAR文件作为输入来创建native镜像。
- 在写这篇文章的时候,GraalVM的native-image功能仍在快速变化。请参阅GraalVM Native Image的文档( https://oreil.ly/vvSWF ),以了解最新的信息。
你想对Scala应用程序进行打包,使其看起来像一个native应用程序,例如,创建一个native macOS应用程序(“App”)的“.app”发行版。
你可以根据需要构建Scala应用程序,通常使用sbt-assembly或类似工具来创建一个JAR文件。然后使用JDK 14及更高版本附带的 jpackage 工具,为macOS、Linux或Windows平台打包应用程序。
例如,想象一下,你已经创建了这个Scala/Swing文本编辑器应用程序,并想把它捆绑成一个macOS应用程序:
package com.alvinalexander.myapp
import java.awt.{BorderLayout,Dimension}
import javax.swing.*
@main def mySwingApp =
val frame = JFrame("My App")
val textArea = JTextArea("Hello, Scala 3 world")
val scrollPane = JScrollPane(textArea)
SwingUtilities.invokeLater(new Runnable {
def run =
frame.getContentPane.add(scrollPane, BorderLayout.CENTER)
frame.setSize(Dimension(400,300))
frame.setLocationRelativeTo(null)
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
frame.setVisible(true)
})
你要做的第一件事是把应用程序打包成一个JAR文件,使用17.11小节“部署一个可执行的JAR文件”中的sbt-assembly技术。在写这篇文章时,配置sbt-assembly只需要在sbt构建中的 project/plugins.sbt 配置文件中添加这一行:
// note: the version number changes several times a year
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")
之后,在sbt shell提示下使用 assembly 命令创建输出JAR文件:
sbt:MySwingApp> assembly
因为我总是记不住 assembly 把它的输出文件写在哪里,所以我通常用这个命令来代替:
sbt:MySwingApp> show assembly
[info] target/scala-3.0.0-RC1/MySwingApp-assembly-0.1.0.jar
如上所示,该命令的输出打印了输出JAR文件的写入位置。
现在我有了一个单一的JAR文件,下一步是创建一个构建App的子目录,所以我创建了一个名为 jpackage 的子目录,并进入到该目录:
$ mkdir jpackage
$ cd jpackage
在该目录中,我创建了两个子目录,命名为 Input 和 Output:
$ mkdir Input
$ mkdir Output
Input 目录是我放置用于构建应用程序的资源的地方,而 Output 目录则是生成的应用程序的位置。
现在我创建一个shell脚本来构建应用程序。我将这个文件命名为 BuildApp.sh:
# this requires JDK 14+
# the jar file must be built with sbt-assembly or similar
JAR_FILE=MySwingApp-assembly-0.1.0.jar
MAIN_CLASS=com.alvinalexander.myapp.mySwingApp
APP_NAME=MyApp
TARGET_DIR=../target/scala-3.0.0-RC1
rm Input/${JAR_FILE} 2> /dev/null
rm -rf Output/${APP_NAME}.app 2> /dev/null
# get the latest sbt-assembly jar file
cp ${TARGET_DIR}/${JAR_FILE} Input
# creates Output/MyApp.app (a MacOS app)
echo "Creating a macOS app with jpackage ..."
jpackage \
--name $APP_NAME \
--type app-image \
--input Input \
--dest Output \
--main-jar $JAR_FILE \
--main-class $MAIN_CLASS
# optional: specify an app icon
# --icon Input/MyApp.icns \
echo "Created Output/MyApp.app (hopefully)"
关于脚本的一些说明:
- 删除 Input 和 Output 目录中的旧工件
- 将最新的sbt-assembly JAR文件复制到 Input 目录中
- 运行 jpackage 命令来创建一个macOS应用程序
如果你不熟悉什么是macOS应用程序,只需记住它只是一个目录下的文件和目录的集合,其名称以 .app 扩展名结尾。.app 下的文件和目录必须有适当的格式,并包括某些配置文件,而 jpackage 帮助你构建macOS应用程序。
接下来,我使该脚本可执行,然后运行它:
$ chmod +x BuildApp.sh
$ ./BuildApp.sh
Creating a macOS app with jpackage ...
Created Output/MyApp.app (hopefully)
虽然没有考虑到脚本会错误,但是先假设一切正常,于是它在 Output 目录下创建了一个名为 MyApp.app 的macOS应用程序。在macOS系统上,你可以用 open 命令运行该应用程序:
$ open Output/MyApp.app
假设一切正常的话,你便可以打开Scala/Swing文本编辑器应用程序,如图21-6所示。
图21-6. Scala/Swing应用程序,显示在其macOS菜单旁边
在这个例子中,我使用这个 jpackage 参数来构建一个macOS应用程序:
--type app-image
除了指定类型为 app-image 外,jpackage 的帮助文本显示,你还可以建立其他类型的包:
--type -t <type>
The type of package to create
Valid values are: {"app-image", "dmg", "pkg"}
If this option is not specified a platform dependent
default type will be created.
“dmg”和“pkg”代表了macOS平台的两种安装程序。jpackage 也可以在Linux和Windows系统上创建软件包。
解决方案只展示了一组最小的 jpackage 选项。下面这个例子展示了我用来为macOS构建一个真实的Scala/JavaFX应用程序的选项:
APP_DIR_NAME=CliffsNotesFx.app
APP_NAME=CliffsNotesFx
APP_MAIN=com.alvinalexander.cliffsnotes.CliffsNotesGui
INPUT_JAR_FILE=${ASSEMBLY_JAR_FILENAME}
ICON_FILE=AlsNotes.icns
jpackage \
--type app-image \
--verbose \
--input input \
--dest release \
--name $APP_NAME \
--main-jar $INPUT_JAR_FILE \
--main-class $APP_MAIN \
--icon $ICON_FILE \
--module-path ~/bin/[email protected]/Contents/Home/jmods \
--add-modules java.base,javafx.controls,javafx.web (many more modules) ... \
--mac-package-name $APP_NAME \
--mac-package-identifier $APP_MAIN \
--app-version 1.1 \
--description "A CliffsNotes browser" \
--vendor "Alvin J. Alexander" \
--java-options -Dapple.laf.useScreenMenuBar=true \
--java-options '--add-opens javafx.base/com.sun.javafx.reflect=ALL-UNNAMED' \
--java-options -Xmx2048m
如果你想在苹果应用商店发布应用程序,还需要签署它。jpackage 命令包括这些用于该过程的选项:
--mac-sign
--mac-signing-keychain <file path>
--mac-signing-key-user-name <team name>
如前所述,jpackage 命令在创建macOS应用程序时为你做了很多工作。为了证明它的作用,这里是 MyApp.app 目录下的 tree 命令的修剪输出:
$ tree MyApp.app MyApp.app
└── Contents
├── Info.plist
├── MacOS
│ ├── MyApp
│ └── libapplauncher.dylib
├── PkgInfo
├── Resources
│ └── MyApp.icns
├── app
│ ├── MyApp.cfg
│ ├── MyApp.icns
│ └── MySwingApp-assembly-0.1.0.jar
└── runtime
└── Contents
│ a copy of the JVM is under here ...
│ ...
82 directories
值得注意的是,JAR文件被复制到了这个目录结构中。我的构建还使用了一个名为 MyApp.icns 的图标文件,它也被复制到了 MyApp.app 下。JVM的副本也被添加到这里,所以应用程序的用户不需要在他们的系统上安装Java。JVM是用 jpackage 提供的启动器(launcher)启动的。
- “Packaging Tool User’s Guide”( https://oreil.ly/swLD7 )是Oracle的关于打包应用程序的文档。
- jpackage 命令帮助页( https://oreil.ly/X1pGN )显示了所有可用的选项。