常见问题解答 - 客户端

  1. 项目架构
    1. 什么是 GWT 模块?
  2. 编写 Java 代码
    1. 如何启用断言?
  3. JavaScript 本机接口
    1. 如何从手写 JavaScript 或第三方库中调用 Java 方法?
    2. 救命!我的 JSNI 方法中的 eval() 出现问题!
    3. 为什么我的 JSNI 方法的桥接调用无法在 <some_obj>.onclick 中工作?
    4. 如何将 Java 方法作为回调函数传递?
  4. 延迟绑定
    1. 什么是延迟绑定?

项目架构

什么是 GWT 模块?

GWT 模块只是对功能的封装。它与 Java 包有一些相似之处,但并非完全相同。

GWT 模块的命名方式与 Java 包类似,遵循通常的点分路径命名约定。例如,大多数标准 GWT 模块位于“com.google.gwt”之下。但是,GWT 模块与 Java 包之间的相似之处仅限于此命名约定。

模块由一个以“.gwt.xml”为扩展名的 XML 描述符文件定义,该文件的文件名决定了模块的名称。例如,如果您有一个名为 src/com/mycompany/apps/MyApplication.gwt.xml 的文件,那么它将创建一个名为 com.mycompany.apps.MyApplication 的 GWT 模块。.gwt.xml 文件的内容指定了 GWT 模块中包含的 Java 类和其他资源的精确列表。

编写 Java 代码

如何启用断言?

为了在 Java 中使用 assert() 功能,您必须使用 -enableassertions-ea 命令行参数启用它们。有关更多详细信息,请参阅 Java 网站上的文章使用断言进行编程。如果您使用的是 IDE,请将此参数添加到 VM 参数列表中。

GWT 编译器还识别 -ea 标志以在编译的 JavaScript 中生成断言代码。

仅将断言用于调试目的,而不是生产逻辑,因为断言仅在 GWT 的开发模式下有效。默认情况下,它们由 GWT 编译器编译掉,因此在生产模式下没有任何效果,除非您显式启用它们。

JavaScript 本机接口

如何从手写 JavaScript 或第三方库中调用 Java 方法?

您可以通过 JSNI 将该方法分配给一个外部的全局可见的 JavaScript 名称来实现这一点,该名称可以被您手工编写的 JavaScript 代码引用。例如,

package mypackage;
public MyUtilityClass
{
    public static int computeLoanInterest(int amt, float interestRate, int term) { ... }
    public static native void exportStaticMethod() /*-{
       $wnd.computeLoanInterest = 
         $entry(@mypackage.MyUtilityClass::computeLoanInterest(IFI));
    }-*/;
}

请注意,对导出方法的引用已包装在对 $entry 函数的调用中。这个隐式定义的函数确保 Java 派生方法在安装了未捕获异常处理程序的情况下执行,并提供其他一些实用服务。$entry 函数是可重入安全的,应在从非 GWT 上下文调用 GWT 派生的 JavaScript 的任何地方使用。

在应用程序初始化时,调用 MyUtilityClass.exportStaticMethod()(例如,从您的 GWT 入口点)。这将在窗口对象上创建一个名为 computeLoanInterest() 的函数,该函数将通过 JSNI 调用具有相同名称的编译的 Java 方法。桥接方法是必要的,因为 GWT 编译器会在将 Java 方法转换为 JavaScript 时混淆/压缩/重命名 Java 方法的名称。

救命!我的 JSNI 方法中的 eval() 出现问题!

有时您可能希望对 GWT 类方法使用此习惯用法

public static native String myMethod(String arg) /*-{
    eval("var myVar = 'arg is ' + arg;");
    return myVar;
}-*/;

上面的代码在开发模式下有效,但在生产模式下无效。原因是当 GWT 将 Java 源代码编译为 JavaScript 时,它会混淆变量名称。在本例中,它将更改 arg 变量的名称。但是,GWT 无法看到 JSNI 方法中的 JavaScript 字符串文字,因此无法更新 arg 的相应嵌入引用以也使用新的 varname。

解决方法是调整 eval() 语句,以便 GWT 编译器可以看到变量名称

public static native String myMethod(String arg) /*-{
    eval("var myVar = 'arg is " + arg + "';");
    return myVar;
}-*/;

为什么我的 JSNI 方法的桥接调用无法在 <some_obj>.onclick 中工作?

虽然桥接调用似乎没有在 onclick 中调用有很多原因,但最常见的原因之一与函数闭包以及 JavaScript 变量的存储方式有关。

下面的代码说明了这一点

public native void doSomething() /*-{
    [email protected]::doSomethingElse(Ljava/lang/String;)("immediate");
    someObj.onclick = function() {
        [email protected]::doSomethingElse(Ljava/lang/String;)("on click");
    }
}-*/;

public void doSomethingElse(String foo) {
    Window.alert(foo);
}

一个 JavaScript 新手看到这段代码可能会认为“立即”警报会在代码运行时立即显示,而“单击”警报会在单击 someObj 时弹出。但是,实际上会发生的是,第一个警报会显示,但“单击”警报永远不会显示。这个问题的出现是因为“this.@com...”习惯用法会为 this 创建函数闭包。但是,在 JavaScript 函数闭包中,像 this 这样的变量是按引用存储的,而不是按值存储的。由于 this 的状态在 JSNI doSomething 函数超出范围后不能保证,因此当 onclick 匿名函数回调运行时,this 要么指向某个不同的对象(可能没有 doSomethingElse 方法),要么甚至是 null 或 undefined。解决方法是创建一个本地变量来存储 this 的副本,并在其上创建函数闭包。下面的代码片段展示了让桥接调用在单击 someObj 时运行的正确方法。

public native void doSomething() /*-{
    var foo = this;
    [email protected]::doSomethingElse(Ljava/lang/String;)("immediate");
    someObj.onclick = function() {
        [email protected]::doSomethingElse(Ljava/lang/String;)("on click");
    }
}-*/;

通过这种方式,我们创建了闭包的变量是按值存储的,因此即使 doSomething 函数超出范围后也会保留该值。

如何将 Java 方法作为回调函数传递?

JavaScript API 通常会通过回调函数异步返回一个值。您可以使用 JSNI 语法将 Java 函数视为一等公民。假设一个名为 externalJsFunction 的 JavaScript 函数,它接受一个数据值和一个回调函数,以下是如何编写代码的示例

package p;

class C {
  void doCallback(String callbackData) { ..... }
  native void invokeExternal(String data) /*-{
    $wnd.externalJsFunction(data, @p.C::doCallback(Ljava/lang/String;));
  }-*/;
}

根据回调的性质,有时在调用 API 方法时使用匿名 JavaScript 函数来创建包装回调非常有用。当回调被调用时,包装器会将参数值转发给 Java 方法

package p;

class D {
  void someCallback(int param1, int param2, String param3) { ..... }
  native void invokeExternal(String data) /*-{
    $wnd.externalJsFunction(data, function(int1, int2, string3) {
      @p.D::someCallback(IILjava/lang/String;)(int1, int2, string3);
    });
  }-*/
}

延迟绑定

什么是延迟绑定?

延迟绑定是 GWT 对 Java 反射的解决方案。

从一个用例开始解释延迟绑定最容易。每个网络浏览器都有自己的特性,通常很多。 (它们数量之多无法管理,这就是 GWT 最初创建的目的。)在 Java 中处理特性的标准方法是将自定义代码封装到子类中,每个支持的浏览器对应一个子类。在运行时,应用程序将使用反射和动态类加载来选择当前环境的适当子类,加载类,创建实例,然后在程序运行期间使用该实例作为服务提供者。

这确实是 GWT 的做法。但是,GWT 应用程序最终运行的 JavaScript 环境根本不支持动态类加载(也称为动态绑定)。您当然可以在生成的 JavaScript 代码中包含支持每个浏览器的代码,但是这样做就必须将对所有浏览器的支持包含在单个应用程序文件中。为什么 Opera 用户必须下载特定于 Firefox 的代码,而她根本不可能需要它?

由于 GWT 不支持**动态绑定**技术,因此它使用**延迟绑定**。 可以将延迟绑定理解为“在编译时而不是运行时发生的动态类加载”。当 GWT 编译器编译您的 Java 应用程序时,它会确定需要支持的所有“特性”,并为每个特定配置生成一个单独的、精简的应用程序版本。例如,它会为 Firefox 生成一个不同的应用程序文件,与为 Opera 生成的不同。

然而,它不仅仅是浏览器检测。延迟绑定是一种完全通用的机制,用于处理根据某些上下文而变化的功能。另一个典型的延迟绑定示例是国际化:GWT 编译器使用延迟绑定为每种语言生成一个完全独立的应用程序版本。为什么说英语的人需要下载您的应用程序的法国文本呢?

浏览器版本和语言代表了您应用程序的两个“变化轴”。如果您需要,您可以定义自己的轴,GWT 编译器会处理生成所有可能的排列的所有细节。例如,如果 GWT 支持 4 种浏览器,而您用 3 种语言编写您的应用程序,那么 GWT 将生成您应用程序的 12 种不同排列。在运行时的引导过程中,GWT 会选择合适的排列来向用户展示。

现在应该很清楚延迟绑定与标准动态绑定工作方式略有不同。从概念上讲,这两个概念非常相似,实际上您通常只需要用 GWT 方法替换 Java 反射方法即可。您使用 `GWT.create(MyClass)` 而不是 `Class.forName("MyClass")`。GWT 会处理管理所有排列的所有细节。

延迟绑定是 GWT 生成高质量、优化良好的 JavaScript 代码的关键功能之一。如果您只是使用 GWT,通常您不需要深入了解细节。如果您正在开发 GWT 的扩展(例如新的小部件),您需要进一步研究它。请参阅 GWT 开发人员指南,了解延迟绑定的具体示例。如果您有任何其他问题,请随时向 GWT 讨论组 寻求帮助!