|
|
作者:Philip M… 文章来源:IBM
GWT(请参阅 参考资料)采用了一种不寻常的方式进行 Web 应用程序开发。它没有采用客户端和服务器端代码库的普通隔离,而是提供了一个 Java API,该 API 允许创建基于组件的 GUI,然后编译它们,从而在用户的 Web 浏览器上显示它们。与一般的 Web 应用程序开发体验相比,使用 GWT 更接近于使用 Swing 或 SWT 进行开发,它还试图将 HTTP 协议和 HTML DOM 模型抽象出去。实际上,应用程序最终几乎总是会呈现在 Web 浏览器中。
GWT 是通过代码生成来实现这些功能的,它利用其编译器从客户端 Java 代码生成 JavaScript。GWT 支持 java.lang 和 java.util 包的子集,还支持 GWT 自身提供的 API。编译后的 GWT 应用程序由 HTML、XML 和 JavaScript 片段组成。但是,这些片段很难区分,所以最好把编译后的应用程序当成是黑盒子 —— Java 字节码的 GWT 等价物。
在这篇文章中,我将创建一个简单的 GWT 应用程序,用该程序从远程 Web API 获得天气报告,并在浏览器中显示它。在整个过程中,我将简要介绍尽可能多的 GWT 功能,还将提到一些可能遇到的潜在问题。
从简单的开始
清单 1 显示了可以用 GWT 制作的最简单的应用程序的 Java 源代码:
清单 1. 最简单的 GWT 示例
public class Simple implements EntryPoint {
public void onModuleLoad() {
final Button button = new Button("Say 'Hello'");
button.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
Window.alert("Hello World!");
}
});
RootPanel.get().add(button);
}
}
这个代码看起来非常像使用 Swing、AWT 或 SWT 编写的 GUI 代码。不出所料,清单 1 创建了一个按钮,在单击此按钮时会显示消息 “Hello World!”。该按钮被添加到 RootPanel,这是一个环绕 HTML 页面主体的 GWT 包装对象。图 1 显示了应用程序在 GWT Shell 中运行时的情况。GWT Shell 是一个包含在 GWT SDK 中的调试宿主环境(debugging hosting environment),与一个简单的浏览器组合在一起。
图 1. 运行最简单的 GWT 示例
回页首
构建 Weather Reporter 应用程序
我将用 GWT 创建一个简单的 Weather Reporter 应用程序。该应用程序的 GUI 向用户显示了一个用于输入 ZIP 代码的输入框,还显示了一个使用摄氏温度还是华氏温度来表示温度的选项。当用户单击 Submit 按钮时,该应用程序用 Yahoo! 的免费天气 API 获得所选定地区的 RSS 格式的报告。然后提取这个文档的 HTML 部分,并将它显示给用户。
GWT 应用程序被打包成模块,并且必须符合特定的结构。名为 module-name.gwt.xml 的配置文件定义了充当应用程序入口点的类,并指明是否要从其他 GWT 模块继承资源。在应用程序的源包结构中,必须将配置文件放在与 client 包和 public 目录相同的级别上,所有客户端 Java 代码都在 client 包中,而 public 目录包含项目的 Web 资源,比如图片、CSS 和 HTML。最后,public 目录中必须包含一个 HTML 文件,该文件中必须有一个包含模块的限定名称的 meta 标记。GWT 的运行时 JavaScript 库使用这个文件来初始化应用程序。
在指定了入口点类的情况下,GWT 的 applicationCreator 会替您生成这个基本结构。所以可以将调用
applicationCreator developerworks.gwt.weather.client.Weather 生成一个项目框架作为创建 Weather Reporter 应用程序的起点。在该应用程序的源代码下载中包含的 Ant 构建文件中,有一些有用的目标(target),可使用它们让 GWT 项目符合这个结构。(请参阅 下载)。
开发基本的 GUI
首先,我将开发应用程序的用户界面小部件(widget)的基本布局,且不添加其他任何行为。Widget 类是可以呈现在 GWT UI 中的几乎所有类的超类。Widget 总是包含在 Panel中,Panel 本身也是 Widget,所以可以被嵌套。不同类型的面板提供了不同的布局行为。所以,GWT Panel 扮演的角色与 AWT/Swing 中的 Layout 或 XUL 中的 Box 类似。
所有小部件和面板最终都要附加到包含它们的 Web 页面上。如 清单 1 所示,可以直接把它们附加到 RootPanel 上。或者,可以用 RootPanel 获得对使用 ID 或类名标识的 HTML 元素的引用。在这个示例中,我将使用两个独立的 HTML DIV 元素,它们的名称分别是 input-container 和 output-container。第一个元素包含 Weather Reporter 应用程序的 UI 控件,第二个元素显示天气报告本身。
清单 2 显示了设置基本布局所需的代码;它应当是自解释的。HTML 小部件只是 HTML 标记的容器,来自 Yahoo! 天气种子(weather feed)的 HTML 输出将显示在这里。这些代码都位于 Weather 类的 onModuleLoad() 方法中,这个方法由 EntryPoint 接口提供。在将包含天气模块的 Web 页面装入客户机的 Web 浏览器时,将调用这个方法。
清单 2. Weather Reporter 应用程序的布局代码
public void onModuleLoad() {
HorizontalPanel inputPanel = new HorizontalPanel();
// Align child widgets along middle of panel
inputPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
Label lbl = new Label("5-digit zipcode: ");
inputPanel.add(lbl);
TextBox txBox = new TextBox();
txBox.setVisibleLength(20);
inputPanel.add(txBox);
// Create radio button group to select units in C or F
Panel radioPanel = new VerticalPanel();
RadioButton ucRadio = new RadioButton("units", "Celsius");
RadioButton ufRadio = new RadioButton("units", "Fahrenheit");
// Default to Celsius
ucRadio.setChecked(true);
radioPanel.add(ucRadio);
radioPanel.add(ufRadio);
// Add radio buttons panel to inputs
inputPanel.add(radioPanel);
// Create Submit button
Button btn = new Button("Submit");
// Add button to inputs, aligned to bottom
inputPanel.add(btn);
inputPanel.setCellVerticalAlignment(btn,
HasVerticalAlignment.ALIGN_BOTTOM);
RootPanel.get("input-container").add(inputPanel);
// Create widget for HTML output
HTML weatherHtml = new HTML();
RootPanel.get("output-container").add(weatherHtml);
}
图 2 显示了在 GWT Shell 中呈现的布局:
图 2. 基本 GUI 布局
用 CSS 添加样式
呈现的 Web 页面看起来很傻,所以它将从 CSS 样式规则中汲取一些优点。可以用两种方式为 GWT 应用程序添加样式。首先,默认情况下,每个小部件都有一个 CSS 类名,其形式为 project-widget。例如,gwt-Button 和 gwt-RadioButton 是两个核心 GWT 小部件类名。面板通常被实现为一堆嵌套式表格,所以没有默认的类名。
每个小部件类型一个类名(classname-per-widget-type)的默认方法使得在整个应用程序中一致地设置小部件样式变得非常容易。当然,普通的 CSS 选择器规则也可以应用,所以可以根据小部件的上下文,用选择器规则在同一小部件上应用不同的样式。要得到更多的灵活性,则可以调用小部件的 setStyleName() 和 addStyleName() 方法,临时替换和增加小部件的默认类名。
清单 3 组合了这些方法,把样式应用到 Weather Reporter 应用程序的输入面板上。通过对 inputPanel.setStyleName("weather-input-panel"); 的调用,在 Weather.java 中创建了 weather-input-panel 类名。
清单 3. 将 CSS 样式应用到 Weather Reporter 应用程序的输入面板
/* Style the input panel itself */
.weather-input-panel {
background-color: #AACCFF;
border: 2px solid #3366CC;
font-weight: bold;
}
/* Apply padding to every element within the input panel */
.weather-input-panel * {
padding: 3px;
}
/* Override the default button style */
.gwt-Button {
background-color: #3366CC;
color: white;
font-weight: bold;
border: 1px solid #AACCFF;
}
/* Apply a hover effect to the button */
.gwt-Button:hover {
background-color: #FF0084;
}
图 3 显示了应用程序被替换成这些样式之后的情况:
图 3. 应用了这些样式之后的输入面板
添加客户端行为
现在应用程序的基本布局和样式已经就绪,我将开始实现一些客户端行为。可以用熟悉的侦听器模式在 GWT 中执行事件处理。GWT 为鼠标事件、键盘事件、修改事件等提供了 Listener 接口,还提供了几个适配器和助手类,以获得更多方便。
一般情况下使用 Swing 程序员熟悉的内部类形式来添加事件侦听器。但是,所有 GWT Listener 方法的第一个参数都是事件的发送者,通常是用户刚刚与之交互的小部件。这意味着可以把同一个 Listener 实例附加到所需的多个小部件上;可以用 sender 参数确定是哪个小部件触发了事件。
清单 4 显示了 Weather Reporter 应用程序中实现的两个事件侦听器。click 句柄被添加到了 Submit 按钮上,keyhandler 被添加到了 TextBox 上。不管是单击 Submit 按钮,还是在 TextBox 拥有焦点时按下回车键,都会导致相关的句柄调用私有的 validateAndSubmit() 方法。在添加到清单 4 的代码中之后,txBox 和 ucRadio 已经成为 Weather 类的实例变量,所以可以从验证方法访问它们。
清单 4. 添加客户端行为
// Create Submit button, with click listener inner class attached
Button btn = new Button("Submit", new ClickListener() {
public void onClick(Widget sender) {
validateAndSubmit();
}
});
// For usability, also submit data when the user hits Enter
// when the textbox has focus
txBox.addKeyboardListener(new KeyboardListenerAdapter(){
public void onKeyPress(Widget sender, char keyCode, int modifiers) {
// Check for Enter key
if ((keyCode == 13) && (modifiers == 0)) {
validateAndSubmit();
}
}
});
清单 5 显示了 validateAndSubmit() 方法的实现。该实现非常简单,由封装验证逻辑的 ZipCodeValidator 类完成。如果用户没有输入正确的 5 位数字的 ZIP 代码,那么 validateAndSubmit() 将在警告框中显示错误消息,如果这种情况出现在 GWT 中,则会调用 Window.alert()。如果 ZIP 代码正确,那么它将与用户对摄氏或华氏温度单位的选择一起被传递给 fetchWeatherHtml() 方法,这个方法稍后再介绍。
清单 5. validateAndSubmit 逻辑
private void validateAndSubmit() {
// Trim whitespace from input
String zip = txBox.getText().trim();
if (!zipValidator.isValid(zip)) {
Window.alert("Zip-code must have 5 digits");
return;
}
// Disable the TextBox
txBox.setEnabled(false);
// Get choice of celsius/fahrenheit
boolean celsius = ucRadio.isChecked();
fetchWeatherHtml(zip, celsius);
}
回页首
用 GWT Shell 进行客户端调试
在这里我要岔开一会,提一下 GWT Shell,它拥有允许在 Java IDE 中调试客户端代码的 JVM 挂钩。您可以与 Web UI 进行交互,分步调试表示客户端执行的相应 JavaScript 代码的 Java 代码。这是一项很重要的功能,因为在客户端上调试所生成的 JavaScript 基本上是不可能的。
可以很容易地配置一个 Eclipse 调试任务,从而通过 com.google.gwt.dev.GWTShell 类启动 GWT Shell。图 4 显示了按下 Submit 按钮后,在 validateAndSubmit() 方法的断点处暂停的 Eclipse:
图 4. 调试客户端 GWT 代码的 Eclipse
回页首
与服务器端组件进行通信
现在 Weather Reporter 应用程序就可以搜集和验证用户输入了。下一步是从服务器中检索数据。在正常的 Ajax 开发中,需要直接从 JavaScript 调用服务器端资源,并接收编码成 JavaScript Object Notation(JSON)或 XML 的数据。GWT 在自己的远程过程调用(remote procedure call,RPC)机制背后抽象这个通信过程。
在 GWT 的术语中,客户机代码与运行在 Web 服务器上的服务 进行通信。用来公开这些服务的 RPC 机制与 Java RMI 使用的方法类似。这意味着只需要编写服务的服务器端实现和两个接口即可。代码生成和反射将负责处理客户机存根和服务器端主干代理(server-side skeleton proxies)。
相应地,要做的第一步是定义 Weather Reporter 服务的接口。这个接口必须扩展 GWT RemoteService 接口,它包含应该公开给 GWT 客户机代码的服务方法的签名。因为 GWT 中的 RPC 调用是在 JavaScript 代码和 Java 代码之间进行的,所以 GWT 集成了对象序列化机制,用它来协调跨语言分界(language divide)的参数和返回值(请参阅 可序列化类型 侧栏,了解您可以使用哪些可序列化类型)。
可序列化类型
GWT 下可序列化类型的简要概括如下:
基本类(例如 int)和基本包装对象类(例如 Integer)是可序列化的。
String 和 Date 是可序列化的。
可序列化类型的数组本身是可序列化的。
如果用户自定义类的所有持久性成员是可序列化的,而且用户自定义类实现了 GWT 的 IsSerializable 接口,那么自定义类是可序列化的。
Collection 类可以与 Javadoc 注释结合使用,通过注释声明它们包含的可序列化类型。
因为客户机代码被限制在 GWT 实现的 Java 类的一个很小的子集上,所以这些可序列化类型的覆盖面相当广泛。
定义了服务接口之后,下一步就是在扩展 GWT 的 RemoteServiceServlet 类的类中实现该接口。顾名思义,这是 Java 语言的 HttpServlet 的一个具体类,所以可以将它放在任何 servlet 容器中。
这里值得一提的一个 GWT 特性是:服务的远程接口必须位于应用程序的 client 包中,因为需要将它集成到 JavaScript 的生成过程中。但是,因为服务器端实现类引用了远程接口,所以现在在服务器端和客户机代码之间存在一个 Java 编译时依赖项。对于这个问题,我的解决方案是将远程接口放在 client 的 common 子包中。然后在 Java 构建中包含 common 包,但不包含 client 包中的剩余部分。这可以确保客户机代码生成的类文件只是那些需要转换成 JavaScript 的文件。更好的解决方案是将包结构分解成两个源目录,一个负责客户端代码,一个负责服务器端代码,然后将公共类复制到两个目录中。
清单 6 显示了 Weather Reporter 应用程序使用的远程服务接口 WeatherService。它接受 ZIP 代码和摄氏/华氏标记作为输入,返回包含 HTML 天气描述的 String。清单 6 显示了 YahooWeatherServiceImpl 的框架,它使用 Yahoo! 的天气 API 获得给定 ZIP 代码的 RSS 天气种子,并从中获得 HTML 描述。
清单 6. 远程 WeatherService 接口和部分实现
public interface WeatherService extends RemoteService {
/**
* Return HTML description of weather
* @param zip zipcode to fetch weather for
* @param isCelsius true to fetch temperatures in celsius,
* false for fahrenheit
* @return HTML description of weather for zipcode area
*/
public String getWeatherHtml(String zip, boolean isCelsius)
throws WeatherException;
}
|
|