使用Google Web Toolkit和Eclipse Galileo进行高性能Web开发

使用Google Web Toolkit和Eclipse Galileo进行高性能Web开发

您以前可能听说过 Google Web Toolkit (GWT),它支持以 Java™ 编程语言编写 Web 应用程序,然后将 Web 应用程序编译为 JavaScript 以便在 Web 浏览器中运行。这允许通过利用 Java 的静态类型和 Eclipse 之类的出色工具提高生产力。您也许看到过一些构建在 GWT 上的有用的、流行的小部件。有一点您也许不知道,GWT 支持创建高性能 Web 应用程序。

先决条件

本文将查看几个 GWT 特性,以及这些特性如何帮助您构建高性能 Web 应用程序。本文不是一个 GWT 简介,我们假定您拥有 GWT 经验。本文还假定您了解 Java 技术并熟悉 JavaScript、CSS 和 HTML。本文使用 Google Plug-in for Eclipse,使用的版本为 Google Plug-in V1.1 和 Eclipse V3.5 (Galileo)。本文还使用 Firebug plug-in for Mozilla Firefox,使用的版本为 Firebug V1.4.2 和 Firefox V3.5.2。

更快的 JavaScript

GWT 以将 Java 编译为 JavaScript 的能力而著名,支持开发人员开发动态 Web 应用程序。所有代码都编译为 JavaScript,GWT 的许多性能特性的设计目标都是为了使 JavaScript 在浏览器中运行得更快。GWT 中有几个这样的特性,包括特定于浏览器的优化和简化的 Ajax,但多数特性都来自 GWT 的 Java-to-JavaScript 编译器。因此,我们将从这个编译器开始探讨 GWT 的与性能有关的特性。

编译器优化

GWT 编译器将把您的 Java 代码转换为在浏览器中运行的 JavaScript,但它的功能不仅仅是这一点,它还进行大量优化以使您的代码运行更快。然而,要准确理解这些优化的详细过程很困难,因为 GWT 发出的 JavaScript 经过了模糊化,难以阅读。首先,我想指示 GWT 编译器生成一些可以阅读的 JavaScript,这样我们就能更好地理解编译器实施的优化。

GWT 编译器有 3 种运行模式。默认模式是 obfuscated,原因是 GWT 编译器将发出模糊的 JavaScript。这种 JavaScript 不仅难以阅读,它还经过压缩。这使其体积更小,从而能在 Internet 上快速加载。更小的体积也有助于浏览器更快地解析 JavaScript。

您也许会认为在网络上发送经过压缩的 JavaScript 没有什么特别的,因为多数 Web 服务器都使用 gzip 压缩格式发送 JavaScript 且所有现代 Web 浏览器都支持 gzip 格式。但是,GWT 编译器不仅压缩 JavaScript,而且它的压缩方式本质上还支持 gzip 压缩。换句话说,尽管已经经过压缩,GWT 的模糊化的 JavaScript 仍是高度可 gzip 压缩的。这样,如果您的 Web 服务器不使用 gzip,您可以通过使用 GWT 模糊特性获得一个很大的速度提升。但是,即使您的 Web 服务器使用 gzip,您也能从 GWT 模糊特性获得明显的速度提升。

因此,对于生产代码,我们当然希望 GWT 编译器设置为发出模糊 JavaScript。但是,这显然会使发出的 JavaScript 几乎不能被阅读。为了说明这个问题,请查看清单 1 中的一些 GWT 模糊 JavaScript。

清单 1. 模糊化的 JavaScript

function qH(){return np}
function mH(){}
_=mH.prototype=new mu;_.gC=qH;_.tI=0;function 
uH(){uH=ZH;sH={};tH=[];sH[LM]=[Is,Hs,Js];sH[JM]=[rt,qt,st];Xv(tH,yn,LM);Xv(tH,To,JM)}
var sH,tH;function AH(a){a.b=oH(new mH);return a}
function BH(a){var b,c,d,e,f,g,h,i,j,k;g=ox(new cx,MM);f=OA(new FA);j=XH(new 
 VH,NM,OM);KA(f,j.b+sJ+j.c);pw(g.B,PM,true);Zw(gA(QM),f);Zw(gA(RM),g);f.B.focus()
;k=Jg(f.B,NJ).length;k>0&&JA(f,0,k);c=py(new my);Lf((tf(),c.b.B),SM);c.o=true;
b=ox(new cx,TM);b.B.id=UM;i=Py(new Ny);h=Ty(new My);d=UA(new RA);pw(d.B,VM,true);
VA(d,Uy(new My,WM));VA(d,i);VA(d,Uy(new My,XM));VA(d,h);d.b=(kz(),jz);VA(d,b);
Ax(c.j,d);Mx(c);vw(b,FH(new DH,c,g),(sh(),rh));e=KH(new IH,a,g,f,i,h,c,b);
vw(g,e,rh);vw(f,e,(hi(),gi))}
function CH(){return rp}
function xH(){}

幸运的是,可以很容易地 “哄骗” GWT 编译器创建一些人类可读的 JavaScript。这只需向编译器传递一个 -style=PRETTY 参数,使用 Google Plug-in for Eclipse 可以轻松地做到这一点。当您触发一个 GWT 编译时,将显示一个如图 1 所示的对话框。

图 1. GWT 编译器选项
使用Google Web Toolkit和Eclipse Galileo进行高性能Web开发

要查看编译器发出的 JavaScript,只需选择图 1 中显示的 Pretty 设置。现在,上述代码看起来将如清单 2 所示。

图 2. Pretty JavaScript

var $wnd = parent;
var $doc = $wnd.document;
var $moduleName, $moduleBase;
var $strongName = '21B409FCD39529C5A9DB925F7D8D9A95';
var $stats = $wnd.__gwtStatsEvent ? function(a) {return 
  $wnd.__gwtStatsEvent(a);} : null;
$stats && $stats({moduleName:'gwtperf',subSystem:'startup',evtGroup:
'moduleStartup',millis:(new Date()).getTime(),type:'moduleEvalStart'});
var _;
function nullMethod(){
}

function equals(other){
 return this === (other == null?null:other);
}

function getClass_0(){
 return Ljava_lang_Object_2_classLit;
}

function hashCode_0(){
 return this.$H || (this.$H = ++sNextHashId);
}

function toString_0(){
 return (this.typeMarker$ == nullMethod || this.typeId$ ==
2?this.getClass$():Lcom_google_gwt_core_client_JavaScriptObject_2_classLit)
.typeName + '@' + toPowerOfTwoString(this.typeMarker$ == nullMethod || this.typeId$ 
== 2?this.hashCode$():this.$H || (this.$H = ++sNextHashId), 4);
}

function Object_0(){
}

_ = Object_0.prototype = {};
_.equals$ = equals;
_.getClass$ = getClass_0;
_.hashCode$ = hashCode_0;
_.toString$ = toString_0;
_.toString = function(){
 return this.toString$();
}
;

这段代码仍是经过高度优化的 JavaScript,但更容易理解了。当然,这样创建的 JavaScript 的大小会有明显的不同。可以使用 Firebug plug-in for Firefox 来检查这种区别,如图 2 所示。

图 2. 比较 JavaScript 文件大小:Obfuscation 和 Pretty
使用Google Web Toolkit和Eclipse Galileo进行高性能Web开发

图 2 显示使用 obfuscated JavaScript(顶部)和使用 pretty JavaScript(底部)编译的同一个 GWT 应用程序(由 Google Plug-in 创建的 starter 项目)。如图所示,当 JavaScript 从 obfuscated 转换为 pretty 时,其大小也从 58 KB 增加到 146 KB。

现在,我们可以通过检查一些代码来查看 GWT 编译器是如何优化代码的。GWT 背后的一个理念是支持使用软件工程最佳实践来编写代码。可以使用适当的抽象来使代码更健壮、更易于维护。同时,GWT 将编译出非常快的代码。让我们用一个普通样例类(如清单 3 所示)来建模用户。

清单 3. 一个 Person 类

public class Person {
  final String firstName;
  final String lastName;
  public Person(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  public String getName(){
    return firstName + " " + lastName;
  }
}

现在,我们将更改来自 GWT start 项目的代码(如清单 4 所示)。

清单 4. 原始 GWT start 项目代码

final TextBox nameField = new TextBox();
nameField.setText("GWT User");

现在,我们将使用 Person 对象替代上面显示的硬编码字符串。这个更改如清单 5 所示。

清单 5. 修改为使用 Person

final TextBox nameField = new TextBox();
final Person user = new Person("GWT", "User");
nameField.setText(user.getName());

这个更改很简单,但很显然这是一个对应用程序有用的抽象。但是,这可能会以牺牲性能为代价,对象创建和方法分配都可能会存在开销。让我们看看 GWT 编译器是如何提供帮助的。清单 6 显示原始代码的编译版本。

清单 6. 原始代码,编译为 JavaScript

nameField = $TextBox(new TextBox());
nameField.element['value'] = 'GWT User' != null?'GWT User':'';

现在,让我们看看清单 5 编译为 JavaScript 将会怎样,如清单 7 所示。

清单 7. 修改后的代码,编译为 JavaScript

user = $Person(new Person(), 'GWT', 'User');
 $setText_1(nameField, user.firstName + ' ' + user.lastName);

第一行代码调用 Person 类的 JavaScript 构造器,这是对 Java 代码的直接转换。第二行代码有一些变化,没有对 Person 实例调用 getName() 方法,而是将其内联到代码中。也就是说,对方法的调用已被方法本身所代替。这是许多编译器 — 过去的 C/C++ 和 Java 编译器 — 经常实施的优化,但 GWT 编译器充分利用了这种技巧来生成快速 JavaScript。

在面向对象的开发中,有一种实践很常见:抽象出一个可能拥有多个实现的接口并编写代码以使用该接口,从而使其可以重复利用。这是另一种有用的工程抽象方法,但该方法增加了额外的查询和方法分配,损害了性能。让我们看看 GWT 编译器是如何提供帮助的。您可以使用 Eclipse 的重构工具从清单 3 中的 Person 类轻松提取一个接口。让我们调用一个 Thing,如清单 8 所示。

清单 8. 使用 Thing 接口重构

public interface Thing {
  String getName();
}
public class Person implements Thing {
  final String firstName;
  final String lastName;
  public Person(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  public String getName(){
    return firstName + " " + lastName;
  }
}

现在使用 Person 的客户端代码应该更改为使用 Thing 接口,如清单 9 所示。

清单 9. 重构的客户端代码

Thing user = new Person("GWT", "User");
nameField.setText(user.getName());

编译后的代码发生了哪些修改?我们需要付出性能代价吗?请查看清单 10。

清单 10. 重新编译了的重构代码

user = $Person(new Person(), 'GWT', 'User');
$setText_1(nameField, user.firstName + ' ' + user.lastName);

可以看出,根本没有任何修改。如果同时使用两个接口实现,情况又会怎样呢?这种情况如清单 11 所示。

Listing 11. Thing 的多个实现

public class Company implements Thing {
  private final String name;
  public Company(String name){
    this.name = name;
  }
  public String getName() {
    return this.name;
  }
}
// client code
final TextBox nameField = new TextBox();
Thing user = new Person("GWT", "User");
Thing userCompany = new Company("ACME");
nameField.setText(userCompany.getName() + " " + user.getName());

清单 12 显示由 GWT 编译器发出的 JavaScript。

清单 12. 编译后的多个实现

user = $Person(new Person(), 'GWT', 'User');
userCompany = $Company(new Company(), 'ACME');
$setText_2(nameField, userCompany.name_0 + ' ' + (user.firstName + ' ' + user.lastName));

可以看出,编译器仍然删除接口并内联 getName() 方法的每个实现,这仍然是非常优化的代码。但是,您可以做两件事来阻止优化,如清单 13 所示。

清单 13. 阻止编译器优化

private String mkString(Collection<Thing> things, char separator){
  StringBuilder sb = new StringBuilder();
  for (Thing thing : things){
    sb.append(thing.getName());
    sb.append(separator);
  }
  return sb.deleteCharAt(sb.length()).toString();
}
// client code
final TextBox nameField = new TextBox();
Thing user = new Person("GWT", "User");
Thing userCompany = new Company("ACME");
nameField.setText(mkString(Arrays.asList(user, userCompany), ' '));

在清单 13 中,我们引入了一个新的抽象:一个 helper 函数。该函数接收一个 Thing 对象的集合,连接对每个 Thing 对象调用 getName() 方法的结果。分隔符也抽象为这个函数的一个参数。现在,让我们看看清单 14 中显示的编译后的 JavaScript。

清单 14. 编译后的 mkString 代码

function $mkString(things, separator){
 var sb, thing, thing$iterator;
 sb = $StringBuilder(new StringBuilder());
 for (thing$iterator = $AbstractList$IteratorImpl(new AbstractList$IteratorImpl(),
things); thing$iterator.i < thing$iterator.this$0.size_0();) {
  thing = dynamicCast($next_1(thing$iterator), 16);
  $append_2(sb, thing.getName());
  sb.impl.string += String.fromCharCode(separator);
 }
 return $deleteCharAt(sb, sb.impl.string.length).impl.string;
}
// client code
user = $Person(new Person(), 'GWT', 'User');
userCompany = $Company(new Company(), 'ACME');
$setText_1(nameField, $mkString($Arrays$ArrayList(new Arrays$ArrayList(),
initValues(_3Lorg_developerworks_gwt_client_Thing_2_classLit, 0, 16,
 [user, userCompany])), 32));

清单 14 中的代码的复杂程度与 Java 源代码类似。注意,循环内部调用了一个名为 dynamicCast 的函数。这个 JavaScript 用于检查传递过来的对象是否可以转换为指定类型的对象。在本例中,它将检查这个对象是 Person 还是 Company,因为只有这两个对象实现 Thing。通过引入针对该接口编写的代码并拥有多个接口实现,GWT 编译器可以实施的优化将变得很少。

目前为止,我们检查的所有优化都是 GWT 编译器所做的语言级别的优化。GWT 还可以执行其他特定于浏览器的优化。这些类型的优化通常属于延迟绑定(deferred binding)的范畴。

延迟绑定

自从 Mosaic 不再是惟一的浏览器之后,Web 开发人员一直受到各种 Web 浏览器的变体的困扰。众多 JavaScript 框架的一大吸引力在于能够减小不同浏览器之间的差异,从而使您的工作更轻松。这个问题有两种解决方法。第一种方法是编写可以在不同浏览器之间移植的代码。这是一种最小公分母方法(least-common denominator),因为它在最好的情况下也不够理想,通常情况下,它远远不能达到最佳效果。另一种方法是先探测每个浏览器的特性,然后使用针对它们的优化代码。这导致了许多所谓的 “意大利面条式代码(spaghetti code)”,意味着许多从未在浏览器上执行过的代码被附带到浏览器。

GWT 的延迟绑定架构允许它针对各种浏览器编译多个版本的 JavaScript。一小段 JavaScript 被先下载到浏览器,对浏览器特性进行探测,然后再下载经过优化的 JavaScript。如果您回顾一下图 2,您将注意到下载了一个 .nocache.js 文件。这就是那个浏览器探测代码,在本例中,它的大小为 4KB。默认情况下,可以检查 4 种浏览器:Opera,Safari,Gecko(Firefox V2 或更低版本)和 Gecko V1.8 (Firefox V3+) 以及 Internet Explorer V6 和 Internet Explorer V8。

演示 API 在不同浏览器之间差别很大的一个经典例子就是设置一个元素的 innerText 属性。这是在服务器远程过程调用的回调处理器中的 starter 项目中完成。其 Java 代码非常简单,如清单 15 所示。

清单 15. 在 GWT 中设置文本

public void onSuccess(String result) {
  dialogBox.setText("Remote Procedure Call");
  serverResponseLabel.removeStyleName("serverResponseLabelError");
  serverResponseLabel.setHTML(result);
  dialogBox.center();
  closeButton.setFocus(true);
}

现在,让我们看看 GWT 将对各种浏览器发出什么代码。要弄清 GWT 编译器为每种浏览器编译出的是什么代码,请在 .nocache.js 文件中查找类似于清单 16 的代码。

清单 16. GWT 的浏览器探测代码

if (!strongName) {
 try {
  unflattenKeylistIntoAnswers(['opera'],
'D1B884746B9C511E12378A55F9FD97E2.cache.html');
  unflattenKeylistIntoAnswers(['safari'],
'12DC532DA52018F17FA7F84F7137102A.cache.html');
  unflattenKeylistIntoAnswers(['gecko1_8'],
'0986E60F243CC620FA7138AB06F221EB.cache.html');
  unflattenKeylistIntoAnswers(['gecko'],
'CF1F7CBAF43D18B03F82260D99CB1803.cache.html');
  unflattenKeylistIntoAnswers(['ie8'],
'1EE88964C0A866A7F2887C02F69F64D3.cache.html');
  unflattenKeylistIntoAnswers(['ie6'],
'5395DF4A8135D37430AAE1347158CE76.cache.html');
  strongName = answers[computePropValue('user.agent')];
 }
 catch (e) {
  return;
 }
}

现在您可以匹配每个键值(“opera”、“safari” 等)和生成的文件。这样,我们现在可以找到清单 15 中的 onSuccess 方法针对 Internet Explorer V6 编译后的版本,如清单 17 所示。

清单 17. 针对 Internet Explorer V6 编译后的版本

function $onSuccess(this$static, result){
 ($clinit_11() , this$static.val$dialogBox.caption.element)
.innerText = 'Remote Procedure Call';
 setStyleName(this$static.val$serverResponseLabel.element,
'serverResponseLabelError', false);
 this$static.val$serverResponseLabel.element.innerHTML = result || '';
 $center(this$static.val$dialogBox);
 $setFocus(this$static.val$closeButton, true);
}

对于 Internet Explorer V6,GWT 使用针对 Internet Explorer V6 的优化 API:使用元素的 innerText 属性。现在,让我们将它与清单 18 显示的针对 Gecko V1.8+ 的版本进行比较。

清单 18. 针对 Firefox V3+ 编译的版本

function $onSuccess(this$static, result){
 ($clinit_11() , this$static.val$dialogBox.caption.element)
.textContent = 'Remote Procedure Call';
 setStyleName(this$static.val$serverResponseLabel.element,
'serverResponseLabelError', false);
 this$static.val$serverResponseLabel.element.innerHTML = result || '';
 $center(this$static.val$dialogBox);
 $setFocus(this$static.val$closeButton, true);
}

对于 Firefox 的更新版本,GWT 将 Java 代码编译为使用元素的 textContent 属性的 JavaScript。这是一个简单的示例,但您很容易联想到在您的应用程序代码中多次使用 innerText 属性的情况。

如果您经常使用 GWT 进行开发,您很快就会了解延迟绑定 — 但并不总是由于好的方面。您已经看到,GWT 为您的应用程序面对的每个浏览器编译一个不同的 JavaScript 版本,这些版本也是 GWT 本地化您的代码的方式。默认有 6 个浏览器变体和一种语言。如果您需要支持 3 种语言,将有 18 种组合和 18 个 JavaScript 版本。这通常会花费很长的编译时间,导致您的生产力降低。克服这种问题的一种方法是在 Hosted Mode 模式下完成大部分工作,在该模式下,根本不需要编译为 JavaScript。这是目前为止最简便的代码调试方法。但是,如果您的确需要编译,可以告知 GWT 只针对一个浏览器(用于测试的任何浏览器)进行编译,从而极大地降低编译时间。您需要做的只是修改您的应用程序的模块 XML 配置文件,如清单 19 所示。

清单 19. 针对一个浏览器配置 GWT

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.7.0//EN"
"http://google-web-toolkit.googlecode.com/svn/tags/1.7.0/distro-source/core
/src/gwt-module.dtd">
<module rename-to='gwtperf'>
 <!-- Inherit the core Web Toolkit stuff.            -->
 <inherits name='com.google.gwt.user.User'/>

 <!-- Inherit the default GWT style sheet. You can change    -->
 <!-- the theme of your GWT application by uncommenting     -->
 <!-- any one of the following lines.              -->
 <inherits name='com.google.gwt.user.theme.standard.Standard'/>
 <!-- <inherits name='com.google.gwt.user.theme.chrome.Chrome'/> -->
  <!-- <inherits name='com.google.gwt.user.theme.dark.Dark'/>   -->

 <!-- Other module inherits                   -->

 <!-- Specify the app entry point class.             -->
 <entry-point class='org.developerworks.gwt.client.GwtPerf'/>
 <set-property name="user.agent" value="gecko1_8"/>
</module>

这里的关键是倒数第二行:set-property 标记。我们只是将 user.agent 属性设置为 gecko1_8,这样 GWT 将只编译针对 Firefox V3+ 的 JavaScript。现在,我们已经看到 GWT 生成在用户的浏览器中快速运行的 JavaScript 的一些方法。

高速 Ajax

Ajax 在 Web 上无处不在,它是任何 Web 应用程序必不可少的组成部分。最初创造术语 Ajax 时,其中的 X 代表 XML,XML 被认为是在浏览器和服务器之间传递数据的格式。但是,许多 Ajax 应用程序实际上从与之通信的服务器端接收 HTML。这种情况很常见,因为这是最容易实现的任务。但是,这种方法很不理想。

有些 Ajax 应用服务器以 XML 格式返回数据,比起 HTML 来,这是一个巨大的进步,但还不够理想。XML 采用块的形式,使用 JavaScript 解析很不方便,因为这将导致大量字节在网络上传输,大量 JavaScript 需要被执行,用户必须耐心等待。许多 Web 应用程序已转而使用 JSON,这是一个进步。

GWT 包含针对 Ajax 的客户端和服务器端组件,因此您可能想知道 GWT 对 Ajax 使用哪种数据格式。如果 GWT 使用一种高度优化的特殊格式,您也许不会感到惊讶。要弄清这个问题,可以再次使用 Firebug 证明,如图 3 所示。

图 3. 使用 Firebug 监控 Ajax 流量
使用Google Web Toolkit和Eclipse Galileo进行高性能Web开发

Firebug 显示发送到服务器的数据和从服务器返回的数据。清单 20 提供了详细信息。

清单 20. GWT 请求和响应数据

Request:
5|0|6|http://localhost:8080/gwtperf/|29F4EA1240F157649C12466F01F46F60|
org.developerworks.gwt.client.GreetingService|greetServer|java.lang.String|
IBM developerWorks|1|2|3|4|1|5|6|
Response:
//OK[1,["Hello, IBM developerWorks!<br><br>I am running Google App Engine
Development/1.2.2.<br><br>It looks like you are using:<br>Mozilla/ 
5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.2) Gecko/20090729 Firefox/ 
3.5.2"],0,5]

当然,您要编写的 Java 代码非常简单。GWT 在设计时提供了一个简单的编程模型,但提供了一个高度优化的运行时实现。

结束语

本文展示了 GWT 为开发人员提供的许多性能优化以及如何利用 Google Plug-in for Eclipse Galileo。GWT 使编写动态的同时也是高性能的 Web 应用程序变得更轻松。GWT 将不断发展完善,最新的 GWT V2.0 版包含几个新特性,比如代码分隔和资源包,甚至还可以改善用 GWT 创建的 Web 应用程序的性能。您可以通过构建 GWT 主要源代码来提早熟悉这些特性。