我正在尝试使用scala.js,必须说它完全令人印象深刻!但是,我想逐渐将其引入我们的生产环境,并与现有的JavaScript代码并行工作。我遇到的一个问题是如何将复杂结构从JS传递到Scala。例如,我有一个准备好的JS对象,它来自另一个JS模块:
h = {
"someInt": 123,
"someStr": "hello",
"someArray": [
{"name": "a book", "price": 123},
{"name": "a newspaper", "price": 456}
],
"someMap": {
"Knuth": {
"name": "The Art of Computer Programming",
"price": 789
},
"Gang of Four": {
"name": "Design Patterns: Blah-blah",
"price": 1234
}
}
}
It
这个对象中包含一些整数、字符串(所有这些元素都有固定的键名!)、一些数组(其中又包含一些更多的对象)和一些映射(将任意字符串键映射到更多的对象)。所有内容均为可选,可能会缺失。显然,这只是一个虚构的例子,实际的对象要复杂得多,但所有基础知识都在这里了。我已经有了对应的Scala类层次结构,大致如下:
case class MegaObject(
someInt: Option[Int],
someStr: Option[String],
someArray: Option[Seq[Item]],
someMap: Option[Map[String, Item]]
)
case class Item(name: Option[String], price: Option[Int])
第一次尝试
我的第一次尝试是一个天真的想法,就是直接使用接收器类型:
@JSExport
def try1(src: MegaObject): Unit = {
Console.println(src)
Console.println(src.someInt)
Console.println(src.someStr)
}
显然,这个IT相关问题失败了:
An undefined behavior was detected: [object Object] is not an instance of my.package.MainJs$MegaObject
尝试二
我的第二个想法是将该对象接收为js.Dictionary[String]
,然后进行大量的类型检查和类型转换。首先,我们将定义一些帮助方法来从JS对象解析常规字符串和整数:
def getOptStr(obj: js.Dictionary[String], key: String): Option[String] = {
if (obj.contains(key)) {
Some(obj(key))
} else {
None
}
}
def getOptInt(obj: js.Dictionary[String], key: String): Option[Int] = {
if (obj.contains(key)) {
Some(obj(key).asInstanceOf[Int])
} else {
None
}
}
然后,我们将使用它们来解析来自相同源的
Item
对象: def parseItem(src: js.Dictionary[String]): Item = {
val name = getOptStr(src, "name")
val price = getOptInt(src, "price")
Item(name, price)
}
然后,一起解析整个MegaObject
:
@JSExport
def try2(src: js.Dictionary[String]): Unit = {
Console.println(src)
val someInt = getOptInt(src, "someInt")
val someStr = getOptStr(src, "someStr")
val someArray: Option[Seq[Item]] = if (src.contains("someArray")) {
Some(src("someArray").asInstanceOf[js.Array[js.Dictionary[String]]].map { item =>
parseItem(item)
})
} else {
None
}
val someMap: Option[Map[String, Item]] = if (src.contains("someMap")) {
val m = src("someMap").asInstanceOf[js.Dictionary[String]]
val r = m.keys.map { mapKey =>
val mapVal = m(mapKey).asInstanceOf[js.Dictionary[String]]
val item = parseItem(mapVal)
mapKey -> item
}.toMap
Some(r)
} else {
None
}
val result = MegaObject(someInt, someStr, someArray, someMap)
Console.println(result)
}
这段代码虽然可以工作,但是它非常丑陋。有很多重复的代码和重复的操作。可能可以通过重构将数组解析和映射解析提取出来,使其更加合理,但是仍然感觉不好 :(
第三次尝试
尝试使用@ScalaJSDefined
注解创建类似于文档中描述的“facade”类:
@ScalaJSDefined
class JSMegaObject(
val someInt: js.Object,
val someStr: js.Object,
val someArray: js.Object,
val someMap: js.Object
) extends js.Object
只是将其打印出来有点作用:
@JSExport
def try3(src: JSMegaObject): Unit = {
Console.println(src)
Console.println(src.someInt)
Console.println(src.someStr)
Console.println(src.someArray)
Console.println(src.someMap)
}
然而,一旦我尝试为JSMegaObject“facade”添加一个方法来将其转换为其正确的Scala对应项(即使是像这样的假对象):
@ScalaJSDefined
class JSMegaObject(
val someInt: js.Object,
val someStr: js.Object,
val someArray: js.Object,
val someMap: js.Object
) extends js.Object {
def toScala: MegaObject = {
MegaObject(None, None, None, None)
}
}
尝试调用它会失败,并显示以下错误信息:
An undefined behavior was detected: undefined is not an instance of my.package.MainJs$MegaObject
我想起了第一次尝试,这有点类似。
显然,在主方法中仍然可以进行所有的类型转换:
@JSExport
def try3real(src: JSMegaObject): Unit = {
val someInt = if (src.someInt == js.undefined) {
None
} else {
Some(src.someInt.asInstanceOf[Int])
}
val someStr = if (src.someStr == js.undefined) {
None
} else {
Some(src.someStr.asInstanceOf[String])
}
// Think of some way to access maps and arrays here
val r = MegaObject(someInt, someStr, None, None)
Console.println(r)
}
然而,它很快就变得和第二次尝试一样丑陋。
目前的结论
所以,我有点沮丧。第二次和第三次尝试是有效的,但真的感觉我漏了什么,不应该那么丑陋、不舒适,并需要编写大量的JS-to-Scala类型转换器代码才能访问传入的JS对象的字段。有什么更好的方法吗?