【问题标题】:scala.js — getting complex objects from JavaScriptscala.js — 从 JavaScript 获取复杂对象
【发布时间】:2016-03-15 06:08:41
【问题描述】:

我正在尝试 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)
  }

显然,它失败了:

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 注释来创建类似于“外观”类的内容,如文档中所述:

  @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“外观”添加一个方法,该方法会将其转换为正确的 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

...哪一种真的让我想起了尝试#1。

显然,仍然可以在 main 方法中进行所有类型转换:

  @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)
  }

然而,它很快变得和尝试 #2 一样丑陋。

到目前为止的结论

所以,我有点沮丧。尝试 #2 和 #3 确实有效,但我真的觉得我错过了一些东西,它不应该那么丑陋、不舒服,并且需要编写大量的 JS 到 Scala 类型转换器代码来访问字段传入的 JS 对象。有什么更好的方法?

【问题讨论】:

  • 您可能希望使用酸洗框架(如 uPickle)将 JSON 反序列化为案例类。它基本上会为您编写样板文件。见:scala-js.org/libraries/libs.html
  • 一个好主意,但实际上,它并不是真正的 JSON,我宁愿跳过额外的 JSON 转换步骤。

标签: javascript scala scala.js


【解决方案1】:

您的尝试 #4 很接近,但还不够。你想要的不是 Scala.js 定义的 JS 类。你想要一个真正的外观trait。然后,您可以在其伴生对象中“拉皮条”将其转换为您的 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)
    }
  }
}

【讨论】:

  • 感谢您提供示例的扩展答案。不幸的是,我无法让它工作。当在 trait 中使用任何“非 JS”类型时,它会以相同的“未定义不是...的实例”结束。如果我用js.UndefOr[js.Array[JSItem]] 替换js.UndefOr[js.Array[Item]],它可以工作,但是它需要额外的.toItem 调用伴随对象。无论如何,这也是一个巨大的矫枉过正。您基本上必须至少定义三次对象结构:常规 Scala 案例类 + 特征 + 隐含在对象中。
  • 是的,你没看错,应该是JSItem。我修正了我的答案。
【解决方案2】:

将这些对象从 JavaScript 获取到 Scala 实际上相当容易。你走在正确的轨道上,但还需要更多——诀窍是,对于这种情况,你需要使用js.UndefOr[T] 而不是Option[T],并将其定义为外观。 UndefOr 是一个 Scala.js 类型,意思是“这是一个 T 或未定义”,主要用于这样的交互情况。它包含一个.toOption 方法,因此很容易与Scala 代码交互。然后,您可以简单地将您从 JavaScript 获得的对象转换为这种外观类型,一切都应该正常工作。

从 Scala 创建其中一个 JSMegaObjects 需要更多的工作。对于这种情况,您尝试创建一个包含许多可能存在或可能不存在的字段的复杂结构,我们有JSOptionBuilder。之所以这样命名,是因为它是为 jQuery 中常见的大“选项”对象编写的,但它不是 jQuery 特定的。您可以在jsext library 中找到它,并且可以在首页上找到文档。

您还可以在 jquery-facade 的 the JQueryAjaxSettings class 中看到一个中等复杂的完整示例。这显示了 JQueryAjaxSettings trait(JavaScript 对象的外观)和 JQueryAjaxSettingsBuilder(它允许您在 Scala 中从头开始构建一个)。

【讨论】:

    猜你喜欢
    • 2017-12-27
    • 2020-08-16
    • 2016-04-30
    • 2020-05-17
    • 2020-12-14
    • 1970-01-01
    • 1970-01-01
    • 2014-08-22
    • 2013-10-09
    相关资源
    最近更新 更多