7.3 Scala.js与前端互操作


7.3 Scala.js 与前端互操作

Scala.js 把 Scala 编译成 JavaScript 而非字节码,让前后端共享同一门语言、同一套领域模型。代价是要面对两套类型系统之间的"翻译损耗"。本节讲它的编译路线、与 JS 的互操作面、以及何时值得选它。

编译路线的改道

Scala 编译器前端把源码产出为中间表示(SJSIR),Scala.js 后端再发射 JavaScript。源码不变,后端换轨:

JVM 路线 Scala.js 路线
产物 JVM 字节码 优化的 JS(支持死码消除)
库生态 Maven 中央仓库 兼容库 + JS 生态 facade
运行 JVM 浏览器与 Node
典型用途 后端、大数据 前端、全栈共享模型

真正的高价值场景是领域模型与校验逻辑前后端共享:订单表单的校验规则写一遍,浏览器里即时提示,服务端再跑同一份代码兜底——校验漂移(前后端规则不一致)这类 bug 从结构上消失。

与 JavaScript 的桥

JS 是动态类型,Scala 是静态的,桥靠 facade(外观类型):

// 为 JS 库写类型外观(示意) @js.native @JSGlobal("localStorage") object LocalStorage extends js.Object: def getItem(key: String): String = js.native def setItem(key: String, value: String): Unit = js.native // Scala 侧静态调用,编译器检查键值类型 LocalStorage.setItem("token", jwt)

动态类型侧还有逃生门 js.Dynamic.global,但每个绕过类型系统的调用都在积累债务。Laminar、Tyrian 等纯 Scala 前端框架则让你完全写 Scala、零 facade 需求,代价是离开 React/Vue 生态。

代价清单(诚实版)

  • 生态错位:npm 里的组件需要 facade 才能用,长尾组件没人写。
  • 类型鸿沟:undefined/null、数字精度、可变对象等语义差异需要纪律掩盖。
  • 团队成本:前端团队学 Scala 的曲线普遍陡于学 TypeScript。
  • 构建复杂度:两条编译路线(JVM + JS)的全栈项目,sbt 配置工作量翻倍。

所以我的判断线:已有 Scala 后端且领域逻辑密集的团队,Scala.js 收益明确(共享模型是硬收益);以前端为主、招 TS 工程师的项目,没有理由翻越这道山脊。

图 7-2 Scala.js 编译路线与桥接点

图 7-2 Scala.js 编译路线与桥接点

完整案例:把温度转换器编译进浏览器

背景:想在网页里复用 Scala 写的温度逻辑,而不是用 JavaScript 重写一遍。三步走:

// build.sbt 里启用插件后 import org.scalajs.dom.document @main def front(): Unit = val input = document.getElementById("c").asInstanceOf[org.scalajs.dom.HTMLInputElement] val out = document.getElementById("out") document.getElementById("btn") .asInstanceOf[org.scalajs.dom.HTMLButtonElement] .onclick = _ => val c = input.value.toDoubleOption.getOrElse(0.0) out.textContent = s"${c}C = ${c * 9 / 5 + 32}F" // 与后端同一份核心逻辑

操作:核心转换函数不动,前端只加一层 DOM 绑定;结果:sbt fastLinkJS 产出 JS 包,页面直接运行。解读:Scala.js 把 Scala 编译成等价 JS 而非跑在虚拟机里,产物体积可控、可与 npm 生态互操作;业务规则一份代码两端复用,是它最大的卖点。变式:需要强类型前端选 Laminar 这类纯 Scala UI 框架;需要复用 React 生态则走Slinky。

与 Scala Native 的分界

Scala.js 目标是浏览器与 Node,产物是 JavaScript;Scala Native 目标是免 JVM 的原生可执行文件,产物经 LLVM 编译。两者共享"同一份业务代码、多个编译目标"的理念,选型只看部署环境:网页选前者,边缘设备与命令行小工具选后者。

与 JavaScript 的类型边界:JS 特质与动态类型

import scala.scalajs.js @js.native @JSGlobal("Math") object JSMath extends js.Object: def max(a: Double, b: Double): Double = js.native @js.native trait ConsoleLike extends js.Object: def log(msg: String): Unit = js.native val m2: Double = JSMath.max(3.0, 7.0) // 直接吃 JS 全局对象 val dyn = scala.scalajs.js.Dynamic.global dyn.console.log("dynamic 通道") // 动态通道:无检查,慎用

@js.native 声明"这是对岸已有的东西",编译器不做实现只做类型对接;Dynamic 则完全绕开类型检查。纪律与 5.2 节的结构类型一致:能用静态门面就用静态,动态通道只留给无法预知形状的场景(如读任意 JSON),并且一出边界立刻转成强类型模型,别让 Any 长驱直入业务层。

体积控制是上生产前的必修课:fullLinkJS 会做死代码消除,只留真正用到的标准库分支,配合模块拆分与 sourcemap 管理,一个中型前端的 JS 产物可以压到几百 KB——先量后砍,别凭印象否决。

本节要点回顾

  • 后端换轨:同一门语言编译到 JS,死码消除产物可接受。
  • 核心收益是前后端共享领域模型,消灭校验漂移。
  • facade 是跨类型系统的桥,js.Dynamic 是要还债的逃生门。
  • 选型判断:Scala 后端 + 领域密集才上;否则 TypeScript 是更平的路。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U