引言
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 怎么选
- 2. Scala.js 入门:sbt 配置、DOM 与构建流程
- 3. Scala.js 前端框架:scalajs-react、Laminar 与 UI
- 4. JS 互操作:Facade 类型、动态调用与导出
- 5. Scala Native 入门:原生二进制、构建与资源
- 6. C 互操作:extern、位置安全类型与内存管理
- 7. 共享代码与多平台工程:crossProject 组织
- 8. 性能对比:JVM vs JS vs Native
- 9. 部署与集成:npm、CLI 与 Docker
- 10. 速查表与一句话记忆
- 延伸阅读
1. 三平台定位:JVM、JS 与 Native 怎么选
Scala 的三个目标平台:
JVM 平台 → 编译为字节码,跑在 JVM(后端服务/大数据)
JS 平台 → 编译为 JavaScript(浏览器/Node)
Native 平台 → 编译为机器码,无运行时(CLI/嵌入式/低延迟)
选择标准:
| 需求 | 首选 | 原因 |
|---|---|---|
| 后端服务/大数据 | JVM | 生态最全(Akka/Spark/库) |
| 前端交互/DOM | JS | 直接进浏览器 |
| 命令行工具/低延迟 | 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 预热
实操对比表:
| 维度 | JVM | JS | Native |
|---|---|---|---|
| 启动 | 慢(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. 速查表与一句话记忆
| 平台 | 构建命令 | 产物 | 生态 |
|---|---|---|---|
| JVM | sbt compile | .class / jar | 全量 |
| JS | fastLinkJS / fullLinkJS | .js | 浏览器/Node |
| Native | nativeLink | 可执行二进制 | 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 工具与原生开发
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。