Scala.js - 从JavaScript获取复杂对象

6

我正在尝试使用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对象的字段。有什么更好的方法吗?


你可能想要使用一个序列化框架(比如uPickle)将你的JSON反序列化成case类。它本质上会为你编写样板代码。请参见:http://www.scala-js.org/libraries/libs.html - gzm0
这是一个好主意,但实际上它并不是真正的JSON,我宁愿跳过使用JSON进行转换的额外步骤。 - Maguro
2个回答

8

你的第4次尝试已经接近成功,但还不够。你想要的不是一个Scala.js定义的JS类。你需要一个真正的门面trait。然后你可以在它的伴生对象中“pimp”它的转换为你的Scala类。你还必须小心地始终使用js.UndefOr作为可选字段。

@ScalaJSDefined
trait JSMegaObject extends js.Object {
  val someInt: js.UndefOr[Int]
  val someStr: js.UndefOr[String],
  val someArray: js.UndefOr[js.Array[JSItem]],
  val someMap: js.UndefOr[js.Dictionary[JSItem]]
}

object JSMegaObject {
  implicit class JSMegaObjectOps(val self: JSMegaObject) extends AnyVal {
    def toMegaObject: MegaObject = {
      MegaObject(
          self.someInt.toOption,
          self.someStr.toOption,
          self.someArray.toOption.map(_.map(_.toItem)),
          self.someMap.toOption.map(_.mapValues(_.toItem)))
    }
  }
}

@ScalaJSDefined
trait JSItem extends js.Object {
  val name: js.UndefOr[String]
  val price: js.UndefOr[Int]
}

object JSItem {
  implicit class JSItemOps(val self: JSItem) extends AnyVal {
    def toItem: Item = {
      Item(
          self.name.toOption,
          self.price.toOption)
    }
  }
}

感谢您提供了带有示例的详细答案。不幸的是,我无法使其正常工作。当在特质中使用任何“非JS”类型时,它最终会出现相同的“未定义不是...的实例”。如果我将js.UndefOr[js.Array[Item]]替换为js.UndefOr[js.Array[JSItem]],它就可以工作,但这需要在伴生对象中进行额外的.toItem调用。无论如何,这也是一个巨大的过度设计。您基本上必须至少三次定义对象结构:常规Scala case类+特质+对象中的隐式。 - Maguro
是的,你说得对,应该是 JSItem。我已经修正了我的回答。 - sjrd

2
从JavaScript到Scala获取这些对象实际上非常容易。你已经在正确的轨道上了,但需要更多的帮助 - 诀窍是,在这种情况下,你需要使用js.UndefOr[T]而不是Option[T],并将其定义为一个Facade。 UndefOr是一个Scala.js类型,意味着确切地“这可能是T或未定义”,主要用于交互案例。它包括一个.toOption方法,因此很容易与Scala代码进行交互。然后,您可以将从JavaScript获取的对象简单地转换为此facade类型,一切应该正常工作。
从Scala创建这些JSMegaObjects需要更多的工作。对于这种情况,您正在尝试创建具有许多可能存在或可能不存在的字段的复杂结构,我们有JSOptionBuilder。它被命名为这样,因为它是针对jQuery中常见的大型“选项”对象编写的,但它不是特定于jQuery的。您可以在jsext library中找到它,并且文档可以在那里的首页找到。
您还可以在jquery-facade的JQueryAjaxSettings类中看到一个适度复杂的完全工作示例。这显示了JQueryAjaxSettings特征(JavaScript对象的facade)和JQueryAjaxSettingsBuilder(它允许您从头开始在Scala中构建一个)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接