覆盖

假设你正在愉快地使用 JSNI 从你的 GWT 模块中调用一些手写的 JavaScript 代码。它运行良好,但 JSNI 只在单个方法级别起作用。一些集成场景要求你更深入地交织 JavaScript 和 Java 对象——DOM 和 JSON 编程是两个很好的例子——所以我们真正想要的是一种方法,让我们能够从 Java 源代码中直接与 JavaScript 对象进行交互。换句话说,我们想要在编码时 _看起来像_ Java 对象的 JavaScript 对象。

GWT 1.5 引入了 **JavaScript 覆盖类型**,使将整个 JavaScript 对象族轻松集成到 GWT 项目中变得轻而易举。这种技术有很多好处,包括即使你在处理无类型的 JavaScript 对象时,也能使用 Java IDE 的代码完成功能和重构功能。

  1. 示例:简单高效的 JSON
  2. JavaScript 对象和接口
  3. 示例:轻量级集合
  4. 整合在一起

示例:简单高效的 JSON

覆盖类型最容易通过示例来理解。假设我们要访问一个 JSON 对象数组,它们表示一组“客户”实体。JavaScript 结构可能如下所示

var jsonData = [
  { "FirstName" : "Jimmy", "LastName" : "Webber" },
  { "FirstName" : "Alan",  "LastName" : "Dayal" },
  { "FirstName" : "Keanu", "LastName" : "Spoon" },
  { "FirstName" : "Emily", "LastName" : "Rudnick" }
];

要在上述结构上叠加一个 Java 类型,你需要从子类化 JavaScriptObject 开始,这是一个标记类型,GWT 用它来表示 JavaScript 对象。让我们也添加一些 getter。

// An overlay type
class Customer extends JavaScriptObject {

  // Overlay types always have protected, zero-arg ctors
  protected Customer() { }

  // Typically, methods on overlay types are JSNI
  public final native String getFirstName() /*-{ return this.FirstName; }-*/;
  public final native String getLastName()  /*-{ return this.LastName;  }-*/;

  // Note, though, that methods aren't required to be JSNI
  public final String getFullName() {
    return getFirstName() + " " + getLastName();
  }
}

现在,GWT 会理解任何 Customer 的实例实际上都是来自 GWT 模块外部的真实 JavaScript 对象。这具有有益的影响。例如,请注意 getFirstName()getLastName() 中的 this 引用。那个 this 真正代表 JavaScript 对象的身份,所以你与它交互的方式与它在 JavaScript 中存在的完全一样。在本例中,我们可以直接访问我们知道存在的 JSON 字段,this.FirstNamethis.LastName

那么,你如何实际获得一个要对其进行 Java 类型叠加的 JavaScript 对象呢?你不能通过编写 new Customer() 来构造它,因为重点是将一个 Java 类型_叠加_到一个_已经存在的_ JavaScript 对象上。因此,我们必须使用 JSNI 从外部获取这样的对象。

class MyModuleEntryPoint implements EntryPoint {
  public void onModuleLoad() {
    Customer c = getFirstCustomer();
    // Yay! Now I have a JS object that appears to be a Customer
    Window.alert("Hello, " + c.getFirstName());
  }

  // Use JSNI to grab the JSON object we care about
  // The JSON object gets its Java type implicitly
  // based on the method's return type
  private native Customer getFirstCustomer() /*-{
    // Get a reference to the first customer in the JSON array from earlier
    return $wnd.jsonData[0];
  }-*/;
}

让我们澄清一下我们在这里做了什么。我们获取了一个普通的 JSON 对象(POJSONO,有人吗?没有?),并创建了一个看起来很正常的 Java 类型,可以用来在 GWT 代码中与它交互。你可以获得代码完成功能、重构功能和编译时检查,就像你使用任何 Java 代码一样。然而,你仍然可以灵活地与任意 JavaScript 对象进行交互,这使得像通过 RequestBuilder 访问 JSON 服务这样的事情变得轻而易举。

对于编译器极客来说,这是一个简短的题外话。覆盖类型的另一个巧妙之处在于,你可以扩展 Java 类型,而不会影响底层 JavaScript 对象。在上面的示例中,请注意我们添加了 getFullName() 方法。它纯粹是 Java 代码——它不存在于底层 JavaScript 对象上——然而,该方法是根据底层 JavaScript 对象编写的。换句话说,对 JavaScript 对象的 Java 视图可以在功能上比对同一对象的 JavaScript 视图更丰富,而无需修改底层 JS 对象,无论是实例还是它的 prototype

(这仍然是题外话的一部分。)对覆盖类型添加新方法这种酷炫的怪异之处之所以成为可能,是因为覆盖类型的规则在设计上禁止多态调用;所有方法必须是 final 和/或 private。因此,覆盖类型上的每个方法都可以在编译时通过静态解析,因此在运行时永远不需要动态调度。这就是为什么我们不必对对象的函数指针进行处理;编译器可以生成对该方法的直接调用,就像它是全局函数一样,外部于对象本身。很容易看出,直接函数调用比间接函数调用更快。更好的是,由于对覆盖类型上的方法的调用可以进行静态解析,因此它们都是自动内联的候选者,当你在一个脚本语言中为性能而奋斗时,这是一件非常好的事情。在下面,我们将重新讨论这一点,向你展示这种方案到底带来了多少收益。

JavaScript 对象和接口

从 GWT 2.0 开始,允许 JavaScriptObject 子类型实现接口。接口中定义的每个方法最多可以映射到 JavaScriptObject 子类型中声明的一个方法。从实际意义上讲,这意味着只有一个 JavaScriptObject 类型可以实现任何给定的接口,但任何数量的非 JavaScriptObject 类型也可以实现该接口。

interface Person {
  String getName();
}

/** The JSO implementation of Person. */
class PersonJso extends JavaScriptObject implements Person {
  protected PersonJso() {}

  public static native PersonJso create(String name) /*-{
    return {name: name};
  }-*/;

  public final native String getName() /*-{
    return this.name;
  }-*/;
}

/** Any number of non-JSO types may implement the Person interface. */
class PersonImpl implements Person {
  private final String name;

  public PersonImpl(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }
}

// Elsewhere
class Troll {
  /** This method doesn't care about whether p is a JSO or not, this makes testing easier. */
  public void grindBones(Person p) {
    String name = p.getName();
    ...
  }
}

在上面的示例中,Person.getName() 将映射到 PersonJso.getName()。由于 JavaScriptObject 方法必须是最终的,所以允许 PersonJso 的子类,因为它们不能覆盖 getName()。如果声明 class SomeOtherJso extends JavaScriptObject implements Person{},则会出错,因为 JavaScriptObject 在运行时没有类型信息,所以 Person.getName() 无法明确地调度。

示例:轻量级集合

我们在上面的示例中忽略了一些内容。getFirstCustomer() 方法非常不现实。你当然希望能够访问客户的整个数组。因此,我们需要一个表示 JavaScript 数组本身的覆盖类型。幸运的是,这很简单。

// w00t! Generics work just fine with overlay types
class JsArray<E extends JavaScriptObject> extends JavaScriptObject {
  protected JsArray() { }
  public final native int length() /*-{ return this.length; }-*/;
  public final native E get(int i) /*-{ return this[i];     }-*/;
}

现在我们可以编写更有意思的代码

class MyModuleEntryPoint implements EntryPoint {
  public void onModuleLoad() {
    JsArray<Customer> cs = getCustomers();
    for (int i = 0, n = cs.length(); i < n; ++i) {
      Window.alert("Hello, " + cs.get(i).getFullName());
    }
  }

  // Return the whole JSON array, as is
  private final native JsArray<Customer> getCustomers() /*-{
    return $wnd.jsonData;
  }-*/;
}

这是非常干净的代码,特别是考虑到它构建在其上的管道灵活性的原因。正如之前所暗示的那样,编译器可以执行一些非常巧妙的操作,使它非常高效。看一下 onModuleLoad() 方法的未混淆的编译输出

function $onModuleLoad(){
  var cs, i, n;
  cs = $wnd.jsonData;
  for (i = 0, n = cs.length; i < n; ++i) {
    $wnd.alert('Hello, ' + (cs[i].FirstName + ' ' + cs[i].LastName));
  }
}

这是非常优化的。甚至 getFullName() 方法的开销也消失了。事实上,_所有_ Java 方法调用都消失了。当我们说“GWT 为你提供了负担得起的抽象”时,这就是我们所说的。内联代码不仅运行速度明显更快,我们也不必再包含函数定义本身,从而使脚本大小也略微缩小。(公平地说,内联也可能很容易增加脚本大小,所以我们谨慎地平衡大小和速度。)回过头来看上面的原始 Java 源代码,并试图推断编译器必须执行哪些优化序列才能得到这里,这很有趣。

当然,我们忍不住向你展示相应的混淆代码

function B(){var a,b,c;a=$wnd.jsonData;for(b=0,c=a.length;b<c;++b){
  $wnd.alert(l+(a[b].FirstName+m+a[b].LastName))}}

请注意,在这个版本中,唯一 _没有_ 被混淆的部分是源自 JavaScript 的标识符,例如 FirstNameLastNamejsonData 等。这就是为什么,尽管 GWT 努力使进行大量 JavaScript 交互变得容易,但我们尽力说服人们尽可能多地编写纯 Java 源代码,而不是与 JavaScript 混合。希望现在你听到我们说这句话时,你会明白我们不是在贬低 JavaScript——只是我们无法对其进行优化,这让我们很难过。

整合在一起

覆盖类型是 GWT 1.6 中提供的一项关键功能。最简单地说,这种技术使与 JavaScript 库的直接交互变得容易得多。希望在阅读完这篇文章后,你能想象如何将几乎任何 JavaScript 库直接移植到 GWT 中,作为一组 Java 类型,从而允许使用 Java IDE 进行高效的开发和调试,而不会因任何 GWT 开销而影响大小或速度。同时,覆盖类型可以用作一种强大的抽象工具,用于提供更优雅的低级 API,例如 新的 GWT DOM 包

有关更多信息…

  • 令人惊讶的摇滚 JavaScript 和 DOM 编程 - 来自 Google I/O 的这段视频(或相关的幻灯片)是获取覆盖类型的端到端解释的最佳场所。该演示展示了新的 GWT DOM 类,并解释了我们如何使用覆盖类型来实现所有内容。它还详细说明了构建你自己的覆盖类型。
  • GWT 和客户端服务器通信 - 同样来自 Google I/O,Miguel Mendez 解释了访问浏览器数据的各种方法,包括如何将 RequestBuilder 和覆盖类型结合起来,以实现真正便捷的 JSON 访问。
  • 设计:覆盖类型 - 自行阅读风险自负 :-) 这些是极其技术性的细节。它相当有趣,但不一定有指导意义。