Scala.js 与 Scala Native:跨平台编译、互操作与工程实践

Scala 三大目标平台的工程实战:Scala.js(前端/Node 的 JS 编译)、Scala Native(原生二进制)、crossProject 共享代码,覆盖互操作(JS Facade / C interop)、性能对比与部署集成。

引言

Scala 不止活在 JVM 上:Scala.js 把 Scala 编译成 JavaScript(浏览器 + Node),Scala Native 编译成原生二进制(无 JVM 运行时)。这意味着同一套领域逻辑可以三平台共享——后端跑 JVM、前端跑 JS、CLI 工具跑原生。本文讲三平台的定位与选择、Scala.js 的前端工程(DOM/框架/JS 互操作)、Scala Native 的原生工程(C 互操作/资源)、crossProject 的共享代码组织,最后对比性能与部署方式。

前置:/scala-build-tooling/(sbt 多模块)、/scala-functional-programming/(纯函数共享代码基础)、/scala-collections/(跨平台集合)。


目录


1. 三平台定位:JVM、JS 与 Native 怎么选

Scala 的三个目标平台:

JVM 平台    → 编译为字节码,跑在 JVM(后端服务/大数据)
JS 平台     → 编译为 JavaScript(浏览器/Node)
Native 平台 → 编译为机器码,无运行时(CLI/嵌入式/低延迟)

选择标准:

需求首选原因
后端服务/大数据JVM生态最全(Akka/Spark/库)
前端交互/DOMJS直接进浏览器
命令行工具/低延迟Native秒启、无 JVM 开销
共享业务逻辑crossProject三平台一套代码

三平台能力差异:

- 标准库子集:js/native 平台比 JVM 平台库少(无 java.* 全量)
- 反射:Native 无完整反射,JS 有限
- 并发:JVM 线程、JS 事件循环、Native 原生线程

关键心态:先写纯领域逻辑(不依赖平台库),再按平台接 IO——共享代码最大化。

记忆:JVM 跑服务、JS 进浏览器、Native 做 CLI/低延迟;纯逻辑放共享、平台 IO 分头接;反射和标准库在三平台有裁剪。


2. Scala.js 入门:sbt 配置、DOM 与构建流程

build.sbt 配置:

// project/plugins.sbt
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0")

// build.sbt
enablePlugins(ScalaJSPlugin)
scalaJSUseMainModuleInitializer := true

最小 DOM 应用:

import org.scalajs.dom
import org.scalajs.dom.document

object Main {
  def main(args: Array[String]): Unit = {
    val p = document.createElement("p")
    p.textContent = "Hello from Scala.js"
    document.body.appendChild(p)
  }
}

开发/构建流程:

sbt ~fastLinkJS    # 开发:快速链接(.js 增量)
sbt fullLinkJS     # 生产:全量优化(.js 压缩)
sbt run            # 或直接跑(配合 scalajs-bundler)

与前端构建链结合:scalajs-bundler/sbt-scalajs-vite 输出与 Vite/webpack 衔接。

浏览器交互核心:

import org.scalajs.dom.window
window.addEventListener("click", e => dom.console.log("clicked"))

记忆:Scala.js 用 fastLinkJS 开发、fullLinkJS 生产;DOM API 通过 scalajs-dom 访问,浏览器事件监听与 JS 一致;与 Vite/webpack 用 scalajs-bundler 衔接。


3. Scala.js 前端框架:scalajs-react、Laminar 与 UI

三个前端路线:

① scalajs-react:React 的 Scala 类型安全封装
② Laminar:响应式声明式 UI(fp 风格)
③ 直接 DOM:轻量场景手写

Laminar 示例(响应式声明式):

import com.raquo.laminar.api.L._

val count = Var(0)
val app = div(
  span(child.text <-- count.signal),
  button("+1", onClick --> (_ => count.update(_ + 1)))
)

scalajs-react 思路:组件用 Scala 类型化 props/state,编译期检查 props 类型。

选型建议:

- 团队熟悉 React → scalajs-react
- 想要纯函数式响应式 → Laminar
- 需要与既有 JS 组件大量互操作 → 直接 JS 封装(见第 4 节)

与前端生态集成:CSS 用全局类名、UI 库用 JS 组件封装。

记忆:前端三路线——scalajs-react 类型安全 React、Laminar 响应式声明式、直接 DOM 轻量;已有 JS 组件多就走 JS 封装互操作。


4. JS 互操作:Facade 类型、动态调用与导出

与 JS 世界打交道的三个手段:

① JS 导入与动态调用:

import scala.scalajs.js
val lib = js.Dynamic.global.someLib
lib.doThing("arg")          // 动态调用,无类型检查

② Facade 类型(类型安全的 JS 接口):

@js.native
trait Window extends js.Object {
  def innerWidth: Int
  def fetch(url: String): js.Promise[js.Object]
}

③ Scala 导出给 JS(@JSExport):

object Api {
  @JSExportTopLevel("scalaApi")
  def greet(name: String): String = s"Hello $name"
}
// JS 里直接调用 window.scalaApi.greet("world")

互操作铁律:

□ 边界用原生类型(String/Int/Boolean/Array)传值
□ 复杂结构用 js.Object / js.Array,避免 Scala 类型直穿
□ 异步用 js.Promise / Future 互转
□ js.Dynamic 是逃生门,用了就失去类型安全

Promise ↔ Future:

import scala.concurrent.{Future, Promise}
import scala.scalajs.js.JSConverters._
val f: Future[Int] = jsPromise.toFuture   // js → scala
val jp: js.Promise[Int] = future.toJSPromise  // scala → js

记忆:互操作三件——js.Dynamic 动态逃生门、Facade 类型安全封装、@JSExportTopLevel 导出给 JS;边界走原生类型、异步走 Promise↔Future 互转。


5. Scala Native 入门:原生二进制、构建与资源

Scala Native 把 Scala 编译成 LLVM 机器码——启动毫秒级、无 JVM 内存开销。

build.sbt 配置:

// project/plugins.sbt
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.4")

// build.sbt
enablePlugins(ScalaNativePlugin)

最小程序:

object Main {
  def main(args: Array[String]): Unit =
    println("Hello, native world!")
}

构建产物:

sbt nativeLink    # 生成可执行二进制(target/scala-3.x/xxx-out)
sbt run           # 直接运行

内存与 GC:Native 用 Boehm/immix GC——普通 Scala 代码可写,但深度 JVM 依赖(反射/大堆)不适用。

资源与文件:标准库提供文件/网络/正则的 native 实现(少数依赖三方 C 库)。

使用场景:

- 命令行工具(秒启,无需 JVM)
- 边缘/嵌入式设备
- 需要低内存与快速启动的服务

记忆:Scala Native 用 LLVM 生成无运行时二进制——毫秒启动、低内存;nativeLink 出可执行文件;有 GC 但缺完整反射/JVM 库,适合 CLI 与低延迟工具。


6. C 互操作:extern、位置安全类型与内存管理

Scala Native 可以调用 C 库——这是它「原生」的真正价值。

extern 声明 C 函数:

import scala.scalanative.unsafe._

@extern
object libc {
  def malloc(size: CSize): Ptr[Byte] = extern
  def free(ptr: Ptr[Byte]): Unit     = extern
}

位置安全类型(Location-Safe Types):

// Ptr(指针)、CSize、CString、CInt 等映射 C 类型
def cstr(s: String): CString = toCString(s)
val p: Ptr[Int] = stackalloc[Int](4)   // 栈分配

内存管理铁律:

□ malloc 必配 free / Resource 作用域
□ 用 Zone/Resource 自动释放
□ 指针离开作用域小心悬垂
□ toCString 需要显式生命周期

安全封装模式:把 C 调用包进资源管理函数,内部用 Zone:

import scala.scalanative.unsafe.Zone
Zone.acquire { implicit zone =>
  val s = toCString("hello")   // 随 Zone 自动释放
  libc.printf(s)
}

记忆:C 互操作 = extern 声明 + Ptr/CString 位置安全类型 + 显式内存管理;malloc 配 free、优先用 Zone/Resource 自动释放,指针生命周期管好。


7. 共享代码与多平台工程:crossProject 组织

一套代码三平台跑——crossProject 是最佳实践。

build.sbt 多平台工程:

lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform)
  .in(file("core"))
  .settings(sharedSettings)      // 公共设置

lazy val coreJVM    = core.jvm
lazy val coreJS     = core.js
lazy val coreNative = core.native

目录结构:

core/
  shared/src/main/scala/   ← 三平台共享的纯逻辑
  jvm/src/main/scala/      ← JVM 特定实现
  js/src/main/scala/       ← JS 特定实现
  native/src/main/scala/   ← Native 特定实现

平台特定实现的模式(Platform-specific provider):

// shared:定义接口
trait Platform {
  def currentTimeMillis: Long
}

// 各平台各自实现
// jvm: object Platform extends Platform { def currentTimeMillis = System.currentTimeMillis() }
// js:  object Platform extends Platform { def currentTimeMillis = js.Date.now() }

依赖管理:公共依赖放 shared、平台库按平台加;测试同样共享。

铁律:共享模块只写纯逻辑(无平台库),平台差异用接口 + 各平台实现隔离。

记忆:crossProject 用 shared/jvm/js/native 四个 source 目录组织;共享代码只写纯逻辑、平台差异抽象成接口分头实现;依赖与测试也按平台分区。


8. 性能对比:JVM vs JS vs Native

三平台性能画像:

启动速度:Native(毫秒) >> JVM(秒级) > JS(依运行时)
峰值性能:JVM ≥ Native ≥ JS(高度优化后)
内存开销:Native < JVM(GC 差异)≈ JS
预热:Native 无预热、JVM 需 JIT 预热

实操对比表:

维度JVMJSNative
启动慢(JVM)快(浏览器里已加载)极快
峰值算力强(JIT)中强
内存大中小
反射/动态全有限无完整
并发模型线程事件循环原生线程

选型关键:吞吐敏感 & 生态在 JVM → JVM;交互/浏览器 → JS;秒启 & 低内存 & 无 JVM → Native。

注意:Native 的 GC 选择(Boehm vs immix)影响吞吐与延迟,需要按场景配。

记忆:Native 赢启动与内存、JVM 赢生态与反射、JS 赢浏览器直达;峰值算力 JVM/Native 相当;选型看场景——服务跑 JVM、交互跑 JS、秒启跑 Native。


9. 部署与集成:npm、CLI 与 Docker

三平台的交付方式:

JVM 平台   → fat jar / Docker / sbt-assembly
JS 平台    → npm 包 / 静态资源 / 配合 Vite/webpack
Native 平台 → 独立二进制 / 系统包 / Docker(scratch 镜像)

JS 交付 npm 包:

// build.sbt
Compile / npmPackage := ...   // scalajs-bundler 出 npm 包

Native 交付 CLI:

sbt nativeLink → 拷贝二进制到 /usr/local/bin
或用 sbt-native-packager 打 deb/rpm 包

Docker 集成:

# Native 二进制跑在精简镜像
FROM scratch
COPY mytool /usr/local/bin/mytool
ENTRYPOINT ["mytool"]

CI 集成:三平台在 CI 里分别测试(见 /scala-build-tooling/ 的 CI 章节)。

记忆:JVM 交付 fat jar/Docker、JS 交付 npm/静态资源、Native 交付独立二进制;Native 可跑 scratch 镜像最精简;CI 按平台分任务。


10. 速查表与一句话记忆

平台构建命令产物生态
JVMsbt compile.class / jar全量
JSfastLinkJS / fullLinkJS.js浏览器/Node
NativenativeLink可执行二进制C 互操作

一句话记忆:Scala 三平台各司其职——JVM 跑服务与大数据、Scala.js 用 fastLinkJS/fullLinkJS 进浏览器、Scala Native 用 nativeLink 出秒启二进制;互操作 JS 用 Facade/@JSExport/Promise↔Future、C 用 extern + Ptr + Zone 管理内存;crossProject 用 shared/jvm/js/native 组织共享代码,纯逻辑共享、平台 IO 分头实现;选型看场景——生态看 JVM、交互看 JS、低延迟秒启看 Native——一套领域逻辑,三平台自由落地。


延伸阅读

  • /scala-build-tooling/ — sbt 多平台工程与 CI
  • /scala-functional-programming/ — 可跨平台的纯函数风格
  • /scala-collections/ — 三平台标准集合的差异
  • /scala3-modern-features/ — Scala 3 语法在 JS/Native 的支持
  • /scala-typelevel-programming/ — 类型级约束跨平台复用
  • [[frontend]] — 前端工程与框架生态
  • [[tools]] — CLI 工具与原生开发

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「scala」更多文章

  1. 纯函数式效果系统实战:Cats Effect IO 与 ZIO
  2. Scala 领域建模实战:ADT、类型驱动设计与模块化架构
  3. Scala 类型级编程实战:Shapeless、Match Types 与编译期计算