Scala JS相关更新
前言
距离上次写ScalaJS那篇博客已经过去一年了。这段时间内还是有不少更新的。
首先对于ScalaJS来说,更新到0.12后fastOptJS/fullOptJS
命令改为了fastLinkJS/fullLinkJS
命令,生成的也不再是单一文件,而是一组文件,以方便分割模块。
对于ScalaJSBunlder来说,相应的要更新到0.20.0。命令还是老样子,依然是fastOptJS::webpack
和fullOptJS::webpack
,不过这是旧版的sbt命令行写法。新版的应该是fastOptJS/webpack
和fullOptJS/webpack
。
最重要的就是Scala 3发布了。ScalaJS更新了支持,scalajs-dom也发布了2.0版本。对语言本身而言,implicit的用法都要发生改变。这次想到写这篇博客也是因为看到了Scala 3官方文档的时候的一个例子,想要记录一下。
最初是看到了Kotlin的React写法,觉得非常的JSX,但是不知道在Scala中如何实现。比如:
div {
img(src = "something"){}
}
然后就看到了Scala 3官方文档的例子,几乎一模一样的实现。不妨来试试。
最简单的基础
首先我们从最简单的React App开始。内容和之前一样,一个计数器,一个加,一个减,用antd美化。
生成项目
用最简单的Scala 3模板生成我们的基础项目。
添加依赖
基本上全部更新到最新版本。
build.sbt
:
val scala3Version = "3.1.0"
enablePlugins(ScalaJSPlugin)
enablePlugins(ScalaJSBundlerPlugin)
lazy val root = project
.in(file("."))
.settings(
name := "js2",
version := "0.1.0-SNAPSHOT",
scalaVersion := scala3Version,
libraryDependencies += ("org.scala-js" %%% "scalajs-dom" % "2.0.0"),
scalaJSUseMainModuleInitializer := true,
Compile / npmDependencies ++= Seq(
"react" -> "17.0.2",
"react-dom" -> "17.0.2",
"antd" -> "4.16.13"
)
)
project/build.properties
:
sbt.version=1.5.5
project/plugins.sbt
:
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.1")
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.20.0")
源代码
源代码就不赘述了,反正和之前那篇最后的完成结果相同。我把它打包以供下载。
使用Builder Pattern
基本的想法是这样的:在Scala中要达成多行之间无符号分割的,也就是说形如
{
a
b
}
而不是
(
a,
b
)
的话,只能通过语句来实现,否则如果用参数的形式,会有多余的分隔符。而为了传递上下文,就要用到Scala的抽象上下文功能,也就是以前的implicit
,现如今的given
和using
。需要做的就是在语句的环境中给一个可变变量,然后每个语句都修改这个可变变量即可。最外层只需要提供,中间层则需要同时接受和提供。
举例来说,原本React创建一个控件的定义如下:
def createElement(
typeName: String | js.Function1[js.Object, Component],
props: js.Object,
children: js.Any*
): Component = js.native
而对应的代码则是:
React.createElement("div", null,
React.createElement(Button, new ButtonProps{val onClick = () => setState(state - 1)}, "-1"),
React.createElement("div", null, s"${state}"),
React.createElement(Button, new ButtonProps{val onClick = () => setState(state + 1)}, "+1")
)
这里的children都需要通过逗号隔开。我们可以做的是改为如下定义:
def v(children: mutable.ListBuffer[js.Any] ?=> Unit): Component = {
given seq: mutable.ListBuffer[js.Any] = ListBuffer()
children
React.createElement("div", null, (seq.toSeq)*)
}
def e(
typeName: String | js.Function1[js.Object, Component],
props: js.Object
)(
children: mutable.ListBuffer[js.Any] ?=> Unit
)(using seq: mutable.ListBuffer[js.Any]): Unit = {
given sequence: mutable.ListBuffer[js.Any] = ListBuffer()
children
seq.addOne(React.createElement(typeName, props, (sequence.toSeq)*))
}
然后我们就可以这么做:
v {
e("div", null) {
e(Button, new ButtonProps { val onClick = () => setState(state - 1) }) {
summon[ListBuffer[js.Any]].addOne("-1")
}
e("div", null) {
summon[ListBuffer[js.Any]].addOne(s"${state}")
}
e(Button, new ButtonProps { val onClick = () => setState(state + 1) }) {
summon[ListBuffer[js.Any]].addOne("+1")
}
}
}
可见中间的逗号被省略了。以此类推我们还可以得出如下定义:
def table(
props: js.Object = null
)(
children: mutable.ListBuffer[js.Any] ?=> Unit
)(using seq: mutable.ListBuffer[js.Any]): Unit = {
given sequence: mutable.ListBuffer[js.Any] = ListBuffer()
children
seq.addOne(React.createElement("table", props, (sequence.toSeq)*))
}
从而可以写出这样的定义:
v {
table() {
tr() {
th() {
summon[ListBuffer[js.Any]].addOne("Alphabet")
}
th() { summon[ListBuffer[js.Any]].addOne("Number") }
}
tr() {
td() { summon[ListBuffer[js.Any]].addOne('a') }
td() { summon[ListBuffer[js.Any]].addOne(61) }
}
tr() {
td() { summon[ListBuffer[js.Any]].addOne('b') }
td() { summon[ListBuffer[js.Any]].addOne(62) }
}
}
}
如果觉得当中的summon
太丑了,也可以另外写方法包一下。
扩展方法
Scala 3对于给已有对象添加方法的方法是扩展方法,不妨来试试:
extension (s: StringContext) {
def e(args: Any*)(props: js.Object = null)(
children: mutable.ListBuffer[js.Any] ?=> Unit = ()
)(using seq: mutable.ListBuffer[js.Any]): Unit = {
val tag = s.s(args)
given sequence: mutable.ListBuffer[js.Any] = ListBuffer()
children
seq.addOne(React.createElement(tag, props, (sequence.toSeq)*))
}
}
这里用到了一个机制,那就是在Scala中,对于像s"${1+1}"
生成"2"
的,其实都是通过一个StringContext
调用了它的s(arg: Any*)
方法。我们这里就借用了这个机制,添加了一个方法e,这个e会把字符串当成标签名用来创建这个对象。于是之前的代码也可以是这样:
v {
e"table" () {
e"tr" () {
e"th" () {
summon[ListBuffer[React.Component | js.Any]].addOne("Alphabet")
}
e"th" () {
summon[ListBuffer[React.Component | js.Any]].addOne("Number")
}
}
e"tr" () {
e"td" () { summon[ListBuffer[React.Component | js.Any]].addOne('a') }
e"td" () { summon[ListBuffer[React.Component | js.Any]].addOne(61) }
}
e"tr" () {
e"td" () { summon[ListBuffer[React.Component | js.Any]].addOne('b') }
e"td" () { summon[ListBuffer[React.Component | js.Any]].addOne(62) }
}
}
}
虽然在这里似乎没什么太大用处。
总结
感觉Scala 3以后Scala的语言表达能力应该是越来越强了,如同Kotlin一样都是适合搞DSL的语言。这么玩玩挺有意思的,不过工程上使用还请用scala-react(话说真的会有人用Scala写React吗?)。