Scala React
背景介绍
这年头哪个语言不能用来写Javascript都要不好意思了。Scala也有了Scala.js。用Scala写React并不是没有人做过,像ScalaJS-React应该说就已经很成熟了,但是我依然希望可以自己尝试整个过程。
Scala.js可以单独写Javascript的程序,也可以和Javascript现有库结合在一起。但是若是要结合在一起,需要自己手写接口,并且还会遇到在某些场合
不能使用纯javascript,即标记为js.native
的类的情况。由于具体规则比较复杂,而且React的类组件都需要继承React.Component
,且这个类是js.native
,
因此我准备仅仅使用函数组件。具体遇到问题再一一解决。
熟悉Scala.js和React
初步准备
这一次先做一个最简单的Hello World的显示,同时不使用npm,直接用<script>
加入React文件。
软件包管理系统使用的是sbt。在国内sbt镜像源好像只有华为。设置参考Scala.js的基础教程。
在project/plugins.sbt
中添加sbt的插件:
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.2.0")
在build.sbt
中激活插件,并取消主程序入口:
enablePlugins(ScalaJSPlugin)
name := "Scala.js Tutorial"
scalaVersion := "2.13.3"
scalaJSUseMainModuleInitializer := false
在project/build.properties
中添加sbt版本限制:
sbt.version=1.3.13
之后我们先添加一个简单的React网页。在index.html
中添加:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>The Scala.js Tutorial</title>
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
</head>
<body>
<div id="root"></div>
<script>
function Hello() {
return React.createElement('div', null, `Hello World`);
}
ReactDOM.render(
React.createElement(Hello, null, null),
document.getElementById('root')
);
</script>
</body>
</html>
可以看到我们用的是普通的Javascript语法,而并未使用JSX。
之后我们将Hello这个函数组件替换成Scala.js。
在src/main/scala/tutorial/react/React.scala
中定义React和一些方法:
package tutorial.react
import scalajs.js
import scalajs.js.annotation.JSGlobal
@js.native
@JSGlobal
object React extends js.Object {
def createElement(
typeName: String,
props: js.Object,
children: js.Any*
): Component = js.native
@js.native
trait Component extends js.Object{
def render(): Unit
}
}
之后在src/main/scala/tutorial/webapp/TutorialApp.scala
中定义我们的Hello函数组件:
package tutorial.webapp
import scala.scalajs.js.annotation._
import scala.scalajs.js
import tutorial.react.React
object TutorialApp {
@JSExportTopLevel("Hello")
def hello() = {
React.createElement("div", null, "Hello World")
}
}
之后运行sbt fastOptJS
即可编译得到target/scala-2.13/scala-js-test-fastopt.js
。之后将其加入html
文件,index.html
的body部分就变成了:
<body>
<div id="root"></div>
<!-- Include Scala.js compiled code -->
<script type="text/javascript" src="./target/scala-2.13/scala-js-test-fastopt.js"></script>
<script>
ReactDOM.render(
React.createElement(Hello, null, null),
document.getElementById('root')
);
</script>
</body>
之后打开index.html,即可看到Hello World。
修改字符串
当我们想要接受字符串的时候,index.html
的body部分是:
<body>
<div id="root"></div>
<script>
function Hello(props) {
return React.createElement('div', null, `Hello ${props.str}`);
}
ReactDOM.render(
React.createElement(Hello, {str: "Me"}, null),
document.getElementById('root')
);
</script>
</body>
为此我们需要定义一个trait
对应Hello
接收的props
。在src/main/scala/tutorial/webapp/TutorialApp.scala
中添加:
trait HelloProps extends js.Object {
val str: String
}
并将Hello
的定义改为:
object TutorialApp {
@JSExportTopLevel("Hello")
def hello(props: HelloProps) = {
React.createElement("div", null, s"Hello ${props.str}")
}
}
这样我们就能够在index.html
上看到"Hello Me"了。
修改状态
这一步尝试引进useState。
首先先写出普通Javascript的实现,看看我们需要什么。在index.html
中:
<body>
<div id="root"></div>
<script>
function Hello(props) {
let [count, setCount] = React.useState(0);
return React.createElement('div', null,
React.createElement('button', { onClick: () => { setCount(count - 1); } }, "-1"),
React.createElement('div', null, `${count}`),
React.createElement('button', { onClick: () => { setCount(count + 1); } }, "+1")
);
}
ReactDOM.render(
React.createElement(Hello, { str: "Me" }, null),
document.getElementById('root')
);
</script>
</body>
然后在React中写出API大致定义。需要注意的是,useState
返回的setState
可以接受新的state作为参数,也可以接受一个修改状态的函数。这种类型定义本身比较麻烦,需要引入Shapeless库来解决。这里为了简化,暂时默认只有接受新的state作为参数。另外setState返回的是一个数组,而不是Tuple2
,因此强制类型转换也不可避免。在React.scala
中:
package tutorial.react
import scalajs.js
import scalajs.js.annotation.JSGlobal
@js.native
@JSGlobal
object React extends js.Object {
def createElement(
typeName: String,
props: js.Object,
children: js.Any*
): Component = js.native
def useState[T](state: T): js.Array[js.Object] = js.native
@js.native
trait Component extends js.Object{
def render(): Unit
}
}
在TutorialApp.scala
中:
package tutorial.webapp
import scala.scalajs.js.annotation._
import scala.scalajs.js
import tutorial.react.React
trait ButtonProps extends js.Object {
val onClick: js.Function0[Unit]
}
object TutorialApp {
@JSExportTopLevel("Hello")
def hello() = {
val array = React.useState(0)
val (state, setState) = (array(0).asInstanceOf[Int], array(1).asInstanceOf[js.Function1[Int, Unit]])
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")
)
}
}
这样我们便得到了相同的效果。
引入npm包
在这一步我们将添加对React的依赖(而不是通过<script>
标签)以及对antd的依赖。为此,根据推荐,我们将使用scalajs-bundler。
首先根据scalajs-bundler的文档修改project/plugins.sbt
:
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.2.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.18.0")
和build.sbt
:
enablePlugins(ScalaJSPlugin)
enablePlugins(ScalaJSBundlerPlugin)
name := "Scala.js Test"
scalaVersion := "2.13.3"
version in webpack := "4.44.2"
scalaJSUseMainModuleInitializer := true
libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "1.1.0"
npmDependencies in Compile += "react" -> "16.13.1"
npmDependencies in Compile += "react-dom" -> "16.13.1"
来添加npm依赖。
之后添加ReactDOM的接口,src/main/scala/tutorial/react/ReactDOM.scala
:
package tutorial.react
import org.scalajs.dom
import scalajs.js
import scala.scalajs.js.annotation.JSImport
@js.native
@JSImport("react-dom", JSImport.Namespace)
object ReactDOM extends js.Object {
def render(component: React.Component, htmlDOM: dom.Node): Unit = js.native
}
再修改React的接口,添加使用函数式组件的方法:
package tutorial.react
import scalajs.js
import scalajs.js.annotation.JSGlobal
import scala.scalajs.js.annotation.JSImport
@js.native
@JSImport("react", JSImport.Namespace)
object React extends js.Object {
def createElement(
typeName: String,
props: js.Object,
children: js.Any*
): Component = js.native
def createElement(
typeName: js.Function1[js.Object, Component],
props: js.Object,
children: js.Any*
): Component = js.native
def useState[T](state: T): js.Array[js.Object] = js.native
@js.native
trait Component extends js.Object{
def render(): Unit
}
}
之后在TutorialApp.scala
中添加Main
函数并整理一下:
package tutorial.webapp
import org.scalajs.dom
import scala.scalajs.js.annotation._
import scala.scalajs.js
import tutorial.react.React
import tutorial.react.ReactDOM
object Hello {
def hello(nothing: js.Object) = {
val array = React.useState(0)
val (state, setState) = (array(0).asInstanceOf[Int], array(1).asInstanceOf[js.Function1[Int, Unit]])
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")
)
}
trait ButtonProps extends js.Object {
val onClick: js.Function0[Unit]
}
}
object TutorialApp {
def main(args: Array[String]) = {
ReactDOM.render(React.createElement(Hello.hello(_), null, null), dom.document.getElementById("root"))
}
}
最后运行sbt fastOptJS::webpack
,来生成组件。将对应文件位置加入index.html
,因此body部分变为:
<body>
<div id="root"></div>
<!-- Include Scala.js compiled code -->
<script type="text/javascript" src="./target/scala-2.13/scalajs-bundler/main/scala-js-test-fastopt-bundle.js"></script>
</body>
然后我们有了和之前完全一样的效果。
添加AntD
添加AntD很简单,只需要添加npm包依赖:
npmDependencies in Compile ++= Seq(
"react" -> "16.13.1",
"react-dom" -> "16.13.1",
"antd" -> "4.6.6"
)
增加一个AntD对应的接口,如src/main/scala/tutorial/antd/Button.scala
:
package tutorial.antd
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import tutorial.react.React
@js.native
@JSImport("antd", "Button")
object Button extends js.Function1[js.Object, React.Component] {
def apply(props: js.Object): React.Component = js.native
}
再将之前Tutorial.scala
中的按钮改成这个新添加的接口:
package tutorial.webapp
import org.scalajs.dom
import scala.scalajs.js.annotation._
import scala.scalajs.js
import tutorial.react.React
import tutorial.react.ReactDOM
import tutorial.antd.Button
object Hello {
def hello(nothing: js.Object) = {
val array = React.useState(0)
val (state, setState) = (array(0).asInstanceOf[Int], array(1).asInstanceOf[js.Function1[Int, Unit]])
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")
)
}
trait ButtonProps extends js.Object {
val onClick: js.Function0[Unit]
}
}
object TutorialApp {
def main(args: Array[String]) = {
ReactDOM.render(React.createElement(Hello.hello(_), null, null), dom.document.getElementById("root"))
}
}
并在index.html
开头加上antd
的css文件,即可完成。
小结
掌握了最基本的几个步骤,之后只需要给每个AntD的控件添加接口即可。由于是用TypeScript所写成,因此不会有很大难度。之后再添加React接口,最基本的使用就没有问题了。