Skip to content

Latest commit

 

History

History
1045 lines (789 loc) · 44.7 KB

21.Scala.js、GraalVM和jpackage.md

File metadata and controls

1045 lines (789 loc) · 44.7 KB

21. Scala.js、GraalVM和jpackage

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应用程序。

21.1 Scala.js入门

问题

你想开始使用Scala.js,需要知道如何安装它并创建一个“Hello, world”的例子。

解决方案

本小节展示了如何开始使用Scala.js,并假定你熟悉JavaScript、HTML和文档对象模型(DOM)。

开始使用Scala.js是一个多步骤的过程:

  1. 处理好先决条件(开发运行环境等)
  2. 创建一个使用Scala.js插件的新sbt项目
  3. 创建一个Scala/Scala.js文件
  4. 编译并运行Scala代码

1. 先决条件

要开始使用Scala.js,你需要在系统上安装这些工具:

在macOS系统上,我用 brew install node 命令安装了Node.js,但你也可以按照该链接,用它所提供的安装程序来安装它。

2. 创建一个新sbt项目

创建一个新的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库。

3. 创建一个Scala/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插件。

4. 编译并运行Scala代码

为了运行 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.jsmain.js.map
  • 如果你用 catmore 或其他工具看一下产生的 target/scala-3.0.0/scalajs-hello-world-fastopt/main.js 文件,你会发现它包含两千多行一些难以阅读的JavaScript源代码。

在该文件的最后,你会看到一行代码,看起来像这样:

    $s_Lhello_hello__main__AT__V(new ($d_T.getArrayOf().constr)([]));

虽然很难读懂,但是 hellomain 的引用表明这段代码是由你的Scala @main方法产生的。

恭喜你,你刚刚把你的第一段Scala代码编译成了JavaScript。

讨论

这是开始使用Scala.js的第一步。当然,如果你希望看到代码在浏览器中运行,只需再做几步就行: 5. 更新 build.sbt。 6. 创建一个HTML文件。 7. 更新你的Scala代码。 8. 用 fastLinkJS 运行该应用程序。 9. 在浏览器中打开HTML文件。

5. 更新build.sbt

下一步,在 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

6. 创建 hello1.html

接下来,在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文件。还要注意的是,这个文件名与解决方案中的代码所生成的名字不同。

7. 更新Hello1.scala

现在更新 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代码。这段代码创建了一个段落标签,在其中放入了一些文本,然后将段落节点添加到文档的主体中。

8. 使用fastLinkJS编译代码

接下来,从你的Scala源代码文件中生成JavaScript文件。可以用sbt fastLinkJS 命令来做这件事:

    sbt> fastLinkJS

这条命令生成了你刚才在HTML文件中包含的 target/scala-3.0.0/scalajs-hello-world-fastopt.js 文件。

显示所生成的文件名 -- TODO 耗子栏

    要查看 fastLinkJS 生成的文件名,请在 fastLinkJS 命令前加上 show

    sbt> show fastLinkJS
    [info] target/scala-3.0.0/scalajs-hello-world-fastopt.js

9. 在浏览器中打开hello1.html

现在在浏览器中打开该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应用程序的简单方法。

另见

21.2 使用Scala.js响应事件

问题

你想知道如何使用Scala.js来响应事件,比如处理一个按钮点击事件。

解决方案

我在之前的示例中,展示了如何设置Scala.js的工作环境。本解决方案以该示例为基础,展示了如何使用Scala/Scala.js代码来响应HTML按钮的点击事件。

这是一个多步骤的解决方案,它建立在上一个示例中创建的sbt项目之上:

  1. 创建一个新的HTML页面。
  2. 更新sbt以支持使用jQuery。
  3. 编写新的Scala/Scala.js代码。
  4. 在sbt中设置 main class。
  5. 运行代码。

这些步骤将在下面的小节中展示。

1. 创建一个新的HTML页面

假设你正在使用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 中所指定的项目名称。

2. 更新sbt以支持使用jQuery

要更新我们的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。

3. 编写新的Scala/Scala.js代码

现在你已经有了新的HTML文件,并且jQuery已经准备好在项目中使用,剩下的主要事情就是写一些Scala/Scala.js代码来响应 <button> 的点击。在这样做之前,需要注意的是 hello2.html 中的 <button>idhello-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是一致的。

4. 在sbt中设置main class

在运行这个例子之前,你还需要做一件事:更新 build.sbt 文件,以考虑到项目中现在有两个 main 方法 —— 21.1小节中的 Hello1.scalaHello2.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.Hello2hello 包中 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)

5. 运行代码

现在你已经准备好运行这个例子了。假设你还在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。

使用fastLinkJS持续的编译

在解决方案中,我展示了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.

然后当你对代码进行修改时,你会看到这个输出被更新了。对一个开发者来说,这很好,因为现在你需要做的就是在代码修改后,在浏览器中刷新下页面即可。

另见

21.3 使用Scala.js构建单页面的应用程序

问题

你想学习如何用Scala.js来构建单页面的web应用。

解决方案

本方案展示了如何使用Scala.js开始构建单页面的应用程序(SPA)。在之前的两个方案中,我展示了如何设置Scala.js工作环境,本方案建立在这两个方案中创建的sbt项目之上。与之前的项目一样,本示例假定你熟悉HTML、JavaScript和DOM。

这个示例的步骤是:

  1. 更新 build.sbt
  2. 创建一个新的HTML文件。
  3. 创建一个新的Scala/Scala.js文件。
  4. 运行代码。

这些步骤的具体介绍在下面的章节中。

1. 更新build.sbt

这个示例使用了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页面中。)

2. 创建一个新的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.htmlhello3.html,最大的变化是删除了HTML按钮,增加了这块代码:

    <div id="root"></div>

你将在我们接下来要创建的Scala代码中使用这个 root id。另外,如果你是SPAs新手,请注意这个“HTML”文件中的HTML是多么的少(!)。

3. 创建Hello3.scala

接下来,在 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是如何工作的,我创建了一个类名为 foodiv,然后在它里面再放一个类名为 bardiv,然后在这两个 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)
                             ------

值得一提的是,在我的代码中,我往往会忘记这一部分。

4. 运行代码

一切就绪后,你现在可以将这段Scala代码编译成JavaScript。回到 sbt 控制台,假设它还在运行上次示例中的 ~fastLinkJS 命令,按回车键停止它。然后运行 reload 命令,更新 build.sbt 的修改:

    sbt> reload

然后重新运行 ~fastLinkJS 命令:

    sbt> ~fastLinkJS

显示由fastLinkJS生成的文件 -- TODO 耗子栏

如果你需要知道当你运行 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 对象的 getpost 方法作为 XmlHttpRequests 的一个更简单的替代品。

另见

关于用Scala.js创建SPAs的更多信息,请参阅这些资源:

21.4 使用GraalVM构建本地可执行文件

问题

你想从Scala代码中构建一个native的可执行文件,这样你的应用程序就会启动得更快,并有可能运行得更快,消耗的内存更少。

解决方案

首先,照常构建一个独立的JAR文件,比如用 sbt packagesbt assembly,然后使用GraalVM的sbt-native-image插件( https://oreil.ly/v8srq )来构建native镜像 —— 一个针对操作系统平台的native的二进制可执行文件。

例如,我创建了一个名为sbtmkdirs( https://oreil.ly/o5uYO )的小命令行Scala应用程序,以生成新的sbt项目目录结构。为了使 sbtmkdirs 几乎立即启动,我用sbt-native-image插件将其编译为一个native的可执行文件。其步骤是:

  1. 配置sbt项目以使用sbt-native-image插件。
  2. sbt nativeImage 命令创建native的可执行文件(可通过该插件获得)。

1. 配置sbt项目

在撰写本文时,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")
        )

然后根据需要修改项目的 nameversionscalaVersionmainClass 的值:

对于这个示例,该文件中的关键行是:

  • enablePlugins 一行告诉sbt要使用这个插件。
  • mainClass 一行告诉sbt,我的 @main 被命名为 Sbtmkdirs,并且它是在 sbtmkdirs包中。

其他行则是标准的sbt build.sbt 文件中常用的。

2. 创建native的可执行文件

有了这些设置,你只需在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

你也可以自己运行 native-image 命令,但这需要相当多的工作。如果你有兴趣尝试,第一步是下载并安装GraalVM,并将其作为你的Java库。在 Unix 系统上,要这样设置 JAVA_HOMEPATH

    $ 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 ),以了解最新的信息。

21.5 使用jpackage打包应用程序

问题

你想对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

在该目录中,我创建了两个子目录,命名为 InputOutput

    $ 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)"

关于脚本的一些说明:

  • 删除 InputOutput 目录中的旧工件
  • 将最新的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创造了什么

如前所述,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)启动的。

另见