《GWT揭秘》
书名:GWT揭秘
作者:徐 彬
ISBN:9787111294016
丛书名:揭秘系列丛书
出版社:机械工业出版社
出版日期:2010 年1月
开本:16
页码:320
版次:1-1
定价:49元
豆瓣网讨论地址:http://www.douban.com/subject/4223579/
China-pub预订地址:http://www.china-pub.com/196306
国内第一本基于GWT 2.0的经典著作,4大专业社区联袂推荐,权威性毋庸置疑!
本书内容全面,不仅详细介绍了GWT的主要模块和控件、GWT与JavaScript对象的交互、在GWT中使用XML、开发自定义GWT控件、GWT-RPC和Ext GWT等必备的基础知识,而且还深入讲解了GWT与Flex整合、图片缓存、本地化、GWT动画、历史管理、延时/等待/分片执行等高级知识;本书注重实战,所有知识点都配有精心设计的范例,尤为值得一提的是,还以迭代的方式重现了经典的俄罗斯方块游戏和一个完整的报销审批系统的开发全过程,既可以以它们为范例进行实战演练,又可以将它们直接应用到实际开发中去。
第二部分 基础篇
第3章 GWT模块
本章内容
* GWT模块详解
* 模块入口点
* 模块继承
* 附加CSS和JavaScript
* 路径配置
* 延迟绑定
* 发布JAR
* 深入研究
3.1 GWT模块详解
独立的GWT配置单元被称为GWT模块。GWT编译项目所需的设置信息都存放在GWT模块定义文件中。这些信息包括:模块入口点、模块继承信息、源代码路径设置、资源文件路径设置和延迟绑定规则。
GWT模块定义文件的后缀名为.gwt.xml,模块名由定义文件所在包的包名加上定义文件的文件名(不带后缀)组成。回忆第2章的例子,chapter2包内有个名为HelloGWT.gwt.xml的文件。这个文件就是HelloGWT项目里的GWT模块定义文件,模块名为chapter2.HelloGWT。编译和发布GWT项目时,GWT编译器要知道哪些GWT模块需要被编译。打开HelloGWT项目运行配置界面,选择GWT面板后可以看到HelloGWT - chapter2被加入到Available Modules中。在项目编译配置中也有同样的设置,如图3-1所示。
图3-1 GWT运行配置和编译配置界面
在第2章中最后给出的ANT模板文件中也有编译模块的设置。Compiler的最后一个参数变量${gwt.module.name}存放的就是要编译的模块名称。
现在打开第2章中的HelloGWT示例中的HelloGWT.gwt.xml文件,内容如代码清单3-1所示。
代码清单3-1 HelloGWT模块定义文件
1 <?xml version="1.0" encoding="utf-8"?> 2 <!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.6.4//EN" "http://google-web-toolkit.googlecode.com/svn/tags/1.6.4/distro-source/core/ src/gwt-module.dtd"> <module rename-to='hellogwt'> 3 <!-- Inherit the core Web Toolkit stuff. --> 4 <inherits name='com.google.gwt.user.User'/> 5 6 <!-- Inherit the default GWT style sheet. You can change --> 7 <!-- the theme of your GWT application by uncommenting --> 8 <!-- any one of the following lines. --> 9 <inherits name='com.google.gwt.user.theme.standard.Standard'/> 10 <!-- <inherits name='com.google.gwt.user.theme.chrome.Chrome'/> --> 11 <!-- <inherits name='com.google.gwt.user.theme.dark.Dark'/> --> 12 13 <!-- Other module inherits --> 14 15 <!-- Specify the app entry point class. --> 16 <entry-point class='chapter2.client.HelloGWT'/> 17 </module> 18
GWT模块定义文件是一个xml文件。定义文件的根元素是module,module元素的rename-to属性值可以为模块重命名输出目录。模块编译后,生成的内容存放在war项目输出文件夹下与模块名同名的目录中。比如,在这个例子中,模块编译结果存放在chapter2.HelloGWT中。如果模块名太长,就会变得很不方便,通过rename-to属性重命名模块输出目录就可以解决这个问题。在HelloGWT.gwt.xml的第3行,模块输出名被重命名为hellogwt。项目编译后,查看war文件夹,文件夹中多了一个hellogwt文件夹,而不是chapter2.HelloGWT文件夹。
GWT模块定义文件常用元素如表3-1所示。
表3-1 GWT模块定义文件常用元素介绍
3.2 模块入口点
使用JavaScript编写AJAX时,window onload事件是AJAX程序的入口点。GWT中模块也有入口点。实现EntryPoint接口的类可以作为GWT入口点,EntryPoint接口代码如下:
1 package com.google.gwt.core.client; 2 public interface EntryPoint { 3 void onModuleLoad(); 4 }
在EntryPoint接口中,只需实现一个函数——onModuleLoad。顾名思义,这个函数在模块装载完成后被调用。实现EntryPoint接口的类还需要在GWT模块定义文件中注册,模块定义文件的entry-point元素定义了模块的入口点,class属性值是入口点类的全名。代码清单3-1中第17行配置信息注册了chapter2.client.HelloGWT类作为入口点。HelloGWT类实现了EntryPoint接口,在onModuleLoad函数中为页面添加了一个按钮并注册了onClick事件。不要把这些代码写到EntryPoint的构造函数中。因为在构造函数被调用的时候,不能保证页面已经加载完成,代码中用到的页面元素此时可能还不存在。在一个GWT模块中可以配置多个入口点,当模块装载完成后,这些已注册的入口点的类都会被实例化,然后执行onModuleLoad函数。
注意 如果要实现EntryPoint接口的类,必须有一个无参数的构造函数。
3.3 模块继承
GWT模块定义文件中配置了大量的信息。通过继承可以简化和分割功能模块配置信息。在模块定义文件中使用inherits 元素继承其他模块,name属性值为被继承模块的模块名称。HelloGWT继承了两个模块:
5 <inherits name='com.google.gwt.user.User'/>
10 <inherits name='com.google.gwt.user.theme.standard.Standard'/>
当用到其他GWT库中的程序时,必须在模块定义文件中继承它。如果在GWT受控运行环境的控制台中看到“No source code is available…”的错误提示信息,那有可能就是缺少继承对应的模块,如图3-2所示。
警告 应当仅继承需要的模块,继承无用模块会增加编译时间。
通常来说,com.google.gwt.user.User是必须继承的。User模块包含所有GWT的核心功能,包括EntryPoint接口、界面控件和DOM访问等功能。
GWT包括的模块如表3-2所示。
图3-2 缺少继承的错误提示
表3-2 GWT包含的模块介绍
GWT程序的皮肤功能也是通过模块实现的。GWT预置了三个皮肤模块:Chrome、Dark、Standard,HelloGWT的例子使用了Standard皮肤。
10 <inherits name='com.google.gwt.user.theme.standard.Standard'/> 11 <!-- <inherits name='com.google.gwt.user.theme.chrome.Chrome'/> --> 12 <!-- <inherits name='com.google.gwt.user.theme.dark.Dark'/> -->
尝试不使用皮肤和依次使用三种不同的皮肤,查看效果如图3-3所示。
图3-3 从左到右依次是:不使用皮肤、Standard、Chrome、Dark
3.4 附加CSS和JavaScript
GWT模块有时需要引用外部的CSS和JavaScript文件。如果被引用的CSS和JavaScript写在HTML文件中,显然会破坏程序的高内聚、低耦合原则。另外,如果这个GWT模块被多个系统重用,那就需要在每个用到此模块的页面中添加CSS和JavaScript的引用,这明显又破坏了DRY(Don't Repeat Yourself)原则。
GWT的解决方法是把GWT模块要用到的CSS和JavaScript引用写在GWT模块定义文件中。GWT在模块初始化时会把CSS和JavaScript添加到对应的页面中。
HelloGWT的运行效果我们已经看到了,是一个白色背景的页面,我们希望通过CSS把它变成黑色背景。war输出文件夹中的HelloGWT.css文件我们还没用到,现在用这个CSS文件给body添加一个黑色的背景。修改HelloGWT.css,修改后的内容如下:
1 body { 2 background-color:black; 3 }
在HelloGWT.gwt.xml的第18行插入下面的定义语句:
17 <entry-point class='chapter2.client.HelloGWT'/> 18 <stylesheet src="../HelloGWT.css"/> 19 </module>
styelsheet元素用于指示GWT编译器,将src属性值所指的文件作为CSS文件添加到页面中。模块的相对路径是hellogwt,所以这里要用两点来指明位于上一级目录中。再次运行HelloGWT,页面背景已经变成黑色,表示CSS已经被应用在页面上了。
在3.3节中我们曾经提到,GWT的皮肤也是由模块实现的。现在让我们来看一下com.google.gwt.user.theme.standard.Standard皮肤模块定义文件的内容。Standard.gwt.xml的内容如下:
1 <module> 2 <stylesheet src="gwt/standard/standard.css"/> 3 </module>
原来GWT的皮肤就是用stylesheet来实现的。GWT控件会在初始化时设置特定的ClassName。比如Button控件在初始化时,为创建出来的Button元素设置gwt-Button风格,如代码清单3-2所示。
代码清单3-2 GWT源代码中的Button控件构造函数
public Button() { super(Document.get().createButtonElement()); adjustType(getElement()); setStyleName("gwt-Button"); }
在standard.css中定义了gwt-Button风格,如代码清单3-3所示。
代码清单3-3 standard.css中的gwt-Button风格
.gwt-Button { margin: 0; padding: 3px 5px; text-decoration: none; font-size: small; cursor: pointer; cursor: hand; background: url("images/hborder.png") repeat-x 0px -27px; border: 1px outset #ccc; }
参照standard.css的写法,我们现在可以编写自己的皮肤模块了。
接着我们尝试在页面中添加JavaScript文件。首先,在war输出文件夹中添加文件inject.js,文件就一行,内容如下:
1 alert("inject");
这个JavaScript的作用就是当它被添加到页面时,弹出对话框,证明脚本已被正确加载。同样打开HelloGWT.gwt.xml,在第19行插入下面的定义语句:
17 <entry-point class='chapter2.client.HelloGWT'/> 18 <stylesheet src="../HelloGWT.css"/> 19 <script src="../inject.js" /> 20 </module>
定义文件中的script元素用于指示编译器,将src属性值所指的文件作为JavaScript文件添加到页面中。同样,我们要用“..”来指明文件位于上一级目录。刷新页面,弹出“inject”对话框,证明GWT已经将inject.js添加到页面中。
3.5 路径配置
GWT将模块的client子包中的文件编译成JavaScript。在HelloGWT的例子中,HelloGWT.java位于chapter2.HelloGWT模块的client子包内,因此HelloGWT.java被编译成JavaScript。client子包是默认值,名称可以通过模块定义文件修改。
<source path="client-path" />
继续用HelloGWT作为例子,在chapter2包中增加client2子包。在包内添加MyAlert类,内容如代码清单3-4所示。
代码清单3-4 chapter2.client2.MyAlert类
1 package chapter2.client2; 2 3 import com.google.gwt.user.client.Window; 4 5 public class MyAlert { 6 public static void alert() { 7 Window.alert("alert"); 8 } 9 }
代码功能很简单,弹出一个“alert”对话框。我们在HelloGWT类的onModuleLoad函数里调用alert函数,不要忘记import相应的包名。
技巧 在Eclipse中可以按CTRL+SHIFT+O快速修复引用错误。
修改后的HelloGWT.java如代码清单3-5所示。
代码清单3-5 添加MyAlert.alert调用后的HelloGWT.java文件
1 package chapter2.client; 2 3 import chapter2.client2.MyAlert; 4 5 import com.google.gwt.core.client.EntryPoint; 6 import com.google.gwt.event.dom.client.ClickEvent; 7 import com.google.gwt.event.dom.client.ClickHandler; 8 import com.google.gwt.user.client.Window; 9 import com.google.gwt.user.client.ui.Button; 10 import com.google.gwt.user.client.ui.RootPanel; 11 12 public class HelloGWT implements EntryPoint { 13 public void onModuleLoad() { 14 MyAlert.alert(); 15 Button welcomeButton = new Button("欢迎"); ...
运行HelloGWT,结果出错。GWT受控运行环境控制台提示的错误消息与前面缺少模块继承提示的错误消息一样,如图3-4所示。
图3-4 缺少编译路径的错误提示
GWT提示找不到chapter2.client2.MyAlert的源代码。原因不是GWT编译器猜测的忘记继承所需的模块,而是我们没有把client2子包路径设置成需要GWT编译的包路径。在模块定义文件中添加source元素可以设置GWT编译源代码路径,path属性值为需要编译的源代码包路径。在定义文件中可以配置多个源代码路径。
打开HelloGWT.gwt.xml,在第20行后面插入以下内容:
21 22 <source path="client" /> 23 <source path="client2" />
提示 增加client2后不要忘记把client也加入GWT源代码路径中。添加source元素后,默认的client源代码路径就被取消。
在HelloGWT这个例子中,模块用到的HelloGWT.css放在war项目输出目录中。假如有多个程序要用到HelloGWT模块,那么除了把HelloGWT模块给它们外,还需要把HelloGWT.css和inject.js独立复制到它们的war目录中。这实在很不方便。幸好,GWT为我们想好了解决方法。
可以在HelloGWT模块中增加public目录,然后将HelloGWT.css和inject.js移动到public目录中,我们把Eclipse中的项目视图切换成树状显示,以便更清楚地看到目录层次结构。现在的目录结构如图3-5所示。
图3-5 添加public文件夹后的项目结构
提示 切换Project Explorer视图到树状显示模式,可以通过单击视图右上角向下箭头,选中“Package Presentation”→“Hierarchical”菜单项。
移动HelloGWT.css和inject.js这两个文件后,不要忘记修改HelloGWT.gwt.xml中stylesheet和script的路径配置。
19 <stylesheet src="HelloGWT.css"/> 20 <script src="inject.js" />
public文件夹是GWT编译器预定义的资源文件夹。模块编译后,GWT编译器将资源文件夹中的文件被复制到模块输出文件夹中。在模块定义文件中添加public元素可以重命名public路径,将path属性值设置为资源文件夹相对于模块的路径。同source元素一样,在模块定义文件中也可以有多个资源路径。下面代码是public的一个例子:
<public path="public-path" />
super-source是个比较难理解的元素,它的作用是对源代码中的类进行替换(supersede)。例如我们手上有一份第三方Java源代码,它是普通的Java程序。现在要直接利用这份源代码,但是代码中用到的某些类由于缺少源代码,GWT无法把它编译成JavaScript,这时候super-source就要登场了。
说明 GWT编译需要完整的源代码,无法编译只有class文件的库
假设,现在我们手上有一份开源项目ShowUUID的源代码,把代码添加到项目client文件夹下,让GWT能够编译ShowUUID,ShowUUID的源代码如代码清单3-6所示。
代码清单3-6 ShowUUID.java文件源代码
1 package chapter2.client; 2 3 import java.util.UUID; 4 5 import com.google.gwt.user.client.Window; 6 7 public class ShowUUID { 8 public static void show() { 9 UUID uuid = UUID.randomUUID(); 10 Window.alert(uuid.toString()); 11 } 12 }
ShowUUID的作用是弹出一个显示随机UUID值的对话框。在HelloGWT中使用ShowUUID,修改HelloGWT.java后代码如下所示:
12 public class HelloGWT implements EntryPoint { 13 public void onModuleLoad() { 14 ShowUUID.show(); 15 MyAlert.alert(); 16 Button welcomeButton = new Button("欢迎");
运行程序失败,显示与缺少模块继承相同的错误消息。GWT报告找不到java.util.UUID的源代码,因为java.util.UUID类在jre中。即使我们有UUID的源代码,UUID的算法需要读取系统底层数据,GWT也无法把这些代码转换成能够在浏览器端运行的JavaScript。可以通过直接修改ShowUUID的源代码,将生成伪UUID的算法加入到ShowUUID的代码中。但是,这样的做法很不妥当。如果ShowUUID的作者升级了ShowUUID的代码,我们不得不将我们对ShowUUID做的修改移植到新的ShowUUID中。对于像ShowUUID这样的简单代码还勉强能接受,如果有上千行的代码涉及几十处UUID的使用,那就很不方便了。
GWT的做法是,使用super-source元素重定位UUID.java,打开HelloGWT.gwt.xml,添加super-source元素,代码如下所示:
25 <public path="public" /> 26 <public path="mypublic" /> 27 28 <super-source path="jre" /> 29 </module>
super-source元素告诉GWT编译器,将jre包中的内容替换掉同名的类。接着我们在HelloGWT模块中添加jre子包,在jre子包内添加java.util包,创建UUID.java文件放到java.util包中。UUID.java的内容如代码清单3-7所示。
代码清单3-7 UUID.java文件代码
1 package java.util; 2 3 public class UUID { 4 public static UUID randomUUID() { 5 return new UUID(); 6 } 7 8 public String toString() { 9 return "pseudo uuid string"; 10 } 11 }
这个UUID类虽然在chapter2.jre.java.util包中,但是我们还是要把它的package写成java.util。不用管Eclipse中显示错误消息,这个类只参与GWT的编译。现在项目结构如图3-6所示:
图3-6 添加UUID.java后的项目结构
程序运行后弹出“UUID”对话框。好像不对!我们只写了“pseudo uuid string”,但是这里显示的是正确的“UUID”。这是因为在受控运行环境中,jre包中的类都会被GWT执行替换动作。把代码编译后运行,这次程序正确弹出“pseudo uuid string”对话框。
source、public、super-source都是与路径有关的模块设置,可以基于规则(pattern-base)包括或排除一些文件或文件夹。GWT使用和ANT FileSet元素一样的规则,但并不是支持FileSet所有的写法,只支持以下的属性或元素:includes属性、excludes属性、defaultexcludes属性、casesensitive属性、include元素、exclude元素。具体语法可以参考ANT文件,这里给出一个简单的例子:
<public path="public"> <exclude name="**/*.css"/> </public
这个例子中,public目录下所有后缀名为.css的文件都排除掉。
3.6 延迟绑定
不同的浏览器对HTML、CSS、JavaScript的解释存在着差异。使用JavaScript编写AJAX程序时,我们会用if (agent.indexOf("msie")) {} else if (agent.indexOf("mozilla")) {} else ...这样的语法来针对不同的浏览器平台使用不同的实现。GWT使用延迟绑定技术来为不同的浏览器类型加载不同的JavaScript代码。使用GWT延迟绑定有以下好处:
* 减少浏览器需要下载的JavaScript文件大小,GWT只返回针对特定浏览器需要的JavaScript代码。
* 节约开发时间,GWT自动生成接口代码以及代理类。
* 在编译时进行预绑定,因此没有类似于动态绑定或虚函数这样的运行时开销。
延迟绑定是GWT的重要基础,控件、DOM、GWT-RPC、国际化等功能的实现都是基于延迟绑定来实现的。
继续以HelloGWT作为例子,让我们来增强HelloGWT打招呼的能力。当用户用Firefox浏览器浏览HelloGWT时,单击欢迎按钮弹出“Hello GWT! @FireFox”。当用户用IE浏览器时,弹出“Hello GWT! @IE”。
首先添加类WelcomeImpl,内容如代码清单3-8所示。
代码清单3-8 WelcomeImpl源代码
1 package chapter2.client; 2 3 import com.google.gwt.user.client.Window; 4 5 public class WelcomeImpl { 6 void greeting(){ 7 Window.alert("Hello GWT! @Unknown"); 8 } 9 }
WelcomeImpl有一个打招呼的函数greeting()。接着我们做两个实现版本:WelcomeImplIE和WelcomeImplFirefox,分别作为IE和Firefox的实现版本,内容如代码清单3-9和代码清单3-10所示。
代码清单3-9 WelcomeImplIE源代码
1 package chapter2.client; 2 3 import com.google.gwt.user.client.Window; 4 5 public class WelcomeImplIE implements WelcomeImpl { 6 7 @Override 8 public void greeting() { 9 Window.alert("Hello GWT! @IE"); 10 } 11 12 }
代码清单3-10 WelcomeImplFirefox源代码
1 package chapter2.client; 2 3 import com.google.gwt.user.client.Window; 4 5 public class WelcomeImplFirefox implements WelcomeImpl { 6 7 @Override 8 public void greeting() { 9 Window.alert("Hello GWT! @Firefox"); 10 } 11 12 }
打开HelloGWT.gwt.xml配置延时绑定。我们需要配置三个延迟绑定项,分别用于IE、Firefox和其他浏览器。HelloGWT.gwt.xml修改后如代码清单3-11所示。
代码清单3-11 在HelloGWT.gwt.xml中配置延迟绑定项
25 <public path="public"/> 26 27 <super-source path="jre" /> 28 29 <replace-with class="chapter2.client.WelcomeImpl"> 30 <when-type-is class="chapter2.client.WelcomeImpl" /> 31 </replace-with> 32 33 <replace-with class="chapter2.client.WelcomeImplIE"> 34 <when-type-is class="chapter2.client.WelcomeImpl"/> 35 <when-property-is name="user.agent" value="ie6" /> 36 </replace-with> 37 38 <replace-with class="chapter2.client.WelcomeImplFirefox"> 39 <when-type-is class="chapter2.client.WelcomeImpl" /> 40 <any> 41 <when-property-is name="user.agent" value="gecko"/> 42 <when-property-is name="user.agent" value="gecko1_8" /> 43 </any> 44 </replace-with> 45 46 </module>
第29~31行配置的延迟绑定项在其他所有延迟绑定项都没有匹配时使用,类似于编程语言中的else。
第33~36行配置的延迟绑定项是,当用户的user.agent属性值是ie6时,用chapter2.client.
WelcomeImplIE替换chapter2.client.WelcomeImpl。
第38~44行配置的延迟绑定项是,当用户的user.agent等于gecko或gecko1_8时,用chapter2.
client.WelcomeImplFirefox替换chapter2.client.WelcomeImpl。
提示 GWT支持的user.agent有gecko1_8(Firefox 1.5以上版本)、opera、safari、gecko(Firefox1.0)、ie6(IE6以上版本)。
我们打开HelloGWT.java,将按钮onClieck事件里的代码替换成延迟绑定写法,修改后代码如代码清单3-12所示。
代码清单3-12 调用延迟绑定WelcomeImpl类
17 welcomeButton.addClickHandler(new ClickHandler() { 18 @Override 19 public void onClick(ClickEvent event) { 20 WelcomeImpl welcome = GWT.create(WelcomeImpl.class); 21 welcome.greeting(); 22 } 23 });
GWT.create是延迟绑定的类工厂方法。函数会返回符合条件的对象实例。GWT.create函数接收一个class参数,这个参数就是延迟绑定项中when-type-is的判别条件。第20行的作用是,通过GWT的延迟绑定技术获取和用户浏览器对应的WelcomeImpl类。程序运行结果如图3-7所示。
图3-7 使用延迟绑定在不同浏览器上运行的结果
除了通过模块定义文件配置外,还可以编写Generator派生类来根据更复杂的规则进行延迟绑定。Generator有一个抽象方法generate,根据调用的条件,返回要生成延迟绑定类的完整类名。
现在我们把前面的例子用Generator来实现,首先添加一个Generator的派生类chapter2.WelcomeGenerator,Generator只在编译时使用,所以不需要放在client包中被编译成JavaScript。WelcomeGenerator类如代码清单3-13所示。
代码清单3-13 WelcomeGenerator源代码
1 package chapter2; 2 3 import com.google.gwt.core.ext.BadPropertyValueException; 4 import com.google.gwt.core.ext.Generator; 5 import com.google.gwt.core.ext.GeneratorContext; 6 import com.google.gwt.core.ext.PropertyOracle; 7 import com.google.gwt.core.ext.TreeLogger; 8 import com.google.gwt.core.ext.UnableToCompleteException; 9 10 public class WelcomeGenerator extends Generator { 11 12 @Override 13 public String generate(TreeLogger logger, GeneratorContext context, 14 String typeName) throws UnableToCompleteException { 15 try { 16 if (typeName.equals("chapter2.client.WelcomeImpl")) { 17 PropertyOracle propOracle = context.getPropertyOracle(); 18 String userAgent = propOracle.getPropertyValue(logger, "user.agent"); 19 if (userAgent.equals("ie6")) { 20 return "chapter2.client.WelcomeImplIE"; 21 } 22 else if (userAgent.equals("gecko") || userAgent.equals("gecko1_8")) { 23 return "chapter2.client.WelcomeImplFirefox"; 24 } 25 else { 26 return "chapter2.client.WelcomeImpl"; 27 } 28 } 29 else { 30 throw new UnableToCompleteException(); 31 } 32 } catch (BadPropertyValueException e) { 33 e.printStackTrace(); 34 } 35 return null; 36 } 37 38 }
第16行进行前置条件验证,判断需要转换的类是否是chapter2.client.WelcomeImpl。第17~18行获取GWT的user.agent属性值。第19~28行根据不同的属性值返回不同的延迟绑定类的类名。
然后修改HelloGWT模块定义文件,修改后的HelloGWT.gwt.xml内容如代码清单3-14所示。
代码清单3-14 使用Generator类进行延迟绑定的模块定义文件
27 <super-source path="jre" /> 28 29 <generate-with class="chapter2.WelcomeGenerator"> 30 <when-type-assignable class="chapter2.client.WelcomeImpl"/> 31 </generate-with> 32 33 <!-- 34 <replace-with class="chapter2.client.WelcomeImpl"> 35 <when-type-is class="chapter2.client.WelcomeImpl" /> 36 </replace-with> 37 38 <replace-with class="chapter2.client.WelcomeImplIE"> 39 <when-type-is class="chapter2.client.WelcomeImpl"/> 40 <when-property-is name="user.agent" value="ie6" /> 41 </replace-with> 42 43 <replace-with class="chapter2.client.WelcomeImplFirefox"> 44 <when-type-is class="chapter2.client.WelcomeImpl" /> 45 <any> 46 <when-property-is name="user.agent" value="gecko"/> 47 <when-property-is name="user.agent" value="gecko1_8" /> 48 </any> 49 </replace-with> 50 -->
将前面添加的三个延迟绑定项注释掉。添加generate-with元素,它的class属性值为延迟绑定生成类,程序运行后效果与图3-7相同。
GWT编译器通过Generator类进行扩展,除了延迟绑定外,还可以用于创建动态类等进一步扩展。创建动态类的步骤是:首先创建ClassSourceFileComposerFactory源代码工厂类,由源代码工厂类创建出SourceWriter类实例,将源代码写入SourceWriter对象,最后通过GeneratorContext.commit函数将代码提交给GWT编译器。
3.7 发布JAR
使用GWT我们可以方便地将我们做的功能模块打包发布。比如我们在一个项目中做了一个漂亮的下拉树控件,或许在其他项目中也需要这样一个控件。这时就可以把模块以JAR的格式打包发布给第三方的开发人员。在一个JAR中可以打包一个或多个GWT模块,gwt-user.jar就是多个模块打包成的JAR文件。发布JAR的过程和普通类库JAR发布过程一样。唯一需要注意的就是,要把源代码也添加到JAR文件中,因为GWT编译的时候需要所有用到的源代码。换言之,如果你要用GWT开发类库,那就只能是开源的。
发布JAR有两种方式:在Eclipse中发布和用ANT发布。
在Eclipse中,右键单击项目文件夹,选择“Export...”菜单项,打开“JAR Export”对话框。在“Select an export destination”中输入jar file过滤导出类型。然后选中下面树控件中的“JAR file”项,单击“Next”按钮,转到“JAR File Specification”面板,选中要导出的模块所在的包及其子包。如果用到延迟绑定的Generator类,也需要把Generator类以及Generator类的依赖类选中。记得去除eclipse项目配置文件.classpath、.project以及war项目输出文件夹。勾选“Export generated class files and resources”和“Export Java source files and resources”,这两个选项指示Eclipse在导出JAR时,同时导出class文件和源代码。接着在“JAR file”中输入导出JAR的文件名,如图3-8所示。单击“Finish”按钮,执行导出动作。
图3-8 Eclipse导出JAR设置
使用ANT自动构建也可以方便地导出库文件。导出HelloGWT模块的ANT文件,如代码清单3-15所示。读者可以以此文件作为模版,略加修改后,合并到完整项目的ANT构建文件中,或者用AntCall调用。
代码清单3-15 导出HelloGWT模块的ANT构建文件
1 <?xml version="1.0" encoding="utf-8" ?> 2 <project name="Hellogwt" default="export-lib" basedir="."> 3 <property file="build.properties" /> 4 <target name="export-lib"> 5 <javac srcdir="src" destdir="war/WEB-INF/classes"> 6 <classpath> 7 <pathelement location="${gwt.sdk}/gwt-user.jar"/> 8 <fileset dir="${gwt.sdk}" includes="gwt-dev*.jar"/> 9 </classpath> 10 </javac> 11 <jar destfile="hellogwt.jar"> 12 <fileset dir="war/WEB-INF/classes"> 13 <include name="**/*.class" /> 14 </fileset> 15 <fileset dir="src"> 16 <include name="**/*.java" /> 17 <include name="**/*.gwt.xml" /> 18 </fileset> 19 </jar> 20 </target> 21 </project>
上面代码在第5~10行用javac编译源代码,将编译结果输出到war/WEB-INF/classes文件夹中。第11~19行用jar将java源代码文件、模块定义文件和编译生成的class文件打包成hellogwt.jar文件。
使用第三方模块库的项目只需要在Eclipse的项目属性对话框的“Java Build Path”配置项中将JAR文件作为外部JAR导入即可。
3.8 深入研究
GWT编译器的默认配置会将编译出的JavaScript进行混淆。在深入研究GWT之前,需要关闭编译混淆。单击“GWT Compile Project”按钮,打开“GWT Compile”对话框,可以通过“Output Style”下拉框设置GWT编译后输出文件是否进行混淆。有三个选项可以选择:Obfuscated、Pretty、Detailed。将HelloGWT.onModuleLoad函数分别通过三种编译输出选项进行编译,编译输出的代码如代码清单3-16、代码清单3-17、代码清单3-18所示。
代码清单3-16 Obfuscated输出
function km(){var a;!!$stats&&$stats({moduleName:$moduleName,subSystem:lb,evtGroup :mb,millis:(new Date()).getTime(),type:nb,className:ob});$wnd.alert(pb);$wnd. alert(qb);a=rj(new mj(),rb);ml(a,new Db(),(pd(),pd(),qd));ij((qk(),tk(null)),a)}
代码清单3-17 Pretty输出
function init(){ var welcomeButton; !!$stats && $stats({moduleName:$moduleName, subSystem:'startup', evtGroup:'moduleStartup', millis:(new Date()).getTime(), type:'onModuleLoadStart', className:'chapter2.client.HelloGWT'}); $wnd.alert('pseudo uuid string'); $wnd.alert('alert'); welcomeButton = $Button_0(new Button(), '\u6B22\u8FCE'); $addDomHandler(welcomeButton, new HelloGWT$1(), ($clinit_10() , $clinit_10() , TYPE)); $add(($clinit_77() , get_0(null)), welcomeButton); } 代码清单3-18 Detailed输出 function init(){ var chapter2_client_HelloGWT_$onModuleLoad__Lchapter2_client_HelloGWT_2_welcome Button_0; !!$stats && $stats({moduleName:$moduleName, subSystem:$intern_35, evtGroup: $intern_36, millis:(new Date()).getTime(), type:$intern_37, className:$intern_38}); $wnd.alert($intern_39); $wnd.alert($intern_40); chapter2_client_HelloGWT_$onModuleLoad__Lchapter2_client_HelloGWT_2_welcome Button_0 = com_google_gwt_user_client_ui_Button_$Button__Lcom_google_gwt_user_ client_ui_Button_2Ljava_lang_String_2(new com_google_gwt_user_client_ui_Button(), $intern_41); com_google_gwt_user_client_ui_Widget_$addDomHandler__Lcom_google_gwt_user_ client_ui_Widget_2Lcom_google_gwt_event_shared_EventHandler_2Lcom_google_ gwt_event_dom_client_DomEvent$Type_2(chapter2_client_HelloGWT_$onModuleLoad_ _Lchapter2_client_HelloGWT_2_welcomeButton_0, new chapter2_client_HelloGWT$1(), (com_google_gwt_event_dom_client_ClickEvent_$clinit__() , com_google_gwt_event_ dom_client_ClickEvent_$clinit__() , com_google_gwt_event_dom_client_Click Event_TYPE)); com_google_gwt_user_client_ui_AbsolutePanel_$add__Lcom_google_gwt_user_client_ ui_AbsolutePanel_2Lcom_google_gwt_user_client_ui_Widget_2((com_google_gwt_user_ client_ui_RootPanel_$clinit__() , com_google_gwt_user_client_ui_RootPanel_ get__Ljava_lang_String_2(null)), chapter2_client_HelloGWT_$onModuleLoad__ Lchapter2_client_HelloGWT_2_welcomeButton_0); }
“Obfuscated”是默认输出选项,执行混淆操作。通过代码清单3-16可以看到变量都被混淆成km、a、pd、qd这样的名称。混淆一方面可以保护代码,使其难以被分析,另一方面可以压缩生成的JavaScript文件的尺寸,加快页面的加载速度。“Pretty”选项不进行混淆,输出格式化的JavaScript,与我们日常手工编写的JavaScript比较相近。“Detailed”也不进行混淆,输出的变量名和类名都附带原始Java类的详细信息,通过Detailed可以清楚地了解到JavaScript中的变量对应于Java源代码中的哪个变量。
仔细研究代码清单3-17,可以发现在Java源代码中的ShowUUID.show()和MyAlert.alert()函数调用都被优化成了内联函数调用(inline)。GWT编译器会对生成的JavaScript高度优化,只被调用一次的函数,或只被调用一次且函数体内有效代码少于3行(包括3行)的函数会被内联到调用者函数体中。GWT延迟绑定工厂函数GWT.create也被替换,直接用new操作符创建出对应的对象,这就是延迟绑定没有运行时性能消耗的奥秘。延迟绑定不会在调用时才去做判断,而是在编译时就完成判断和生成相应的对象创建代码。顾名思义,延迟绑定的意思就是延迟到编译时再绑定。对于没有成员变量的简单类,GWT不会用new操作符创建新对象,而是直接用调用静态函数的方式调用其成员函数。如果函数符合内联规则,则会被优化成内联调用。同时,GWT只会将使用到的函数编译成JavaScript,没有使用到的函数都会被进行优化。目前的JavaScript解释引擎对对象的循环引用会引发内存泄漏。GWT也充分考虑了这一情况,在编译时生成的JavaScript代码都会解除对象的循环引用。我们可以充分相信GWT的编译器,为我们生成最优化、最稳定的代码。
打开war项目输出文件夹中hellogwt模块输出文件夹,可以看到部分文件名是由一系列无规律的数字和字母组成的。这些文件名并非没有规律,其实是文件内容的MD5值。GWT使用这种命名规则可以避免浏览器端错误的缓存文件。当文件内容一旦发生改变后,文件名也随着MD5值而改变。仔细辨别后可以发现模块输出目录中有这样几类文件:.nocache.js、hosted.html、GWT生成的脚本、GWT 生成的图片、clear.cache.gif、模块资源文件。
.nocache.js:gwt模块的初始化文件,使用GWT模块的HTML页面需要引用这个脚本。我们在第2章中介绍HelloGWT.html页面时,就提到要引用hellogwt.nocache.js,请参考代码清单2-2。
.nocache.js完成以下功能:
* 为每个GWT模块创建一个单独的iframe,把模块编译后的JavaScript写入iframe中。
* 判断GWT是否在受控模式下运行。在受控模式下运行则加载hosted.html,如果在浏览器中运行,则根据不同的浏览器加载针对特定浏览器编译出的JavaScript。
* 为页面注入模块中配置的CSS和JavaScript,见3.4节。
* 调用GWT模块的入口点,见3.2节。
每个GWT模块都在单独的iframe中运行,这样可以避免当页面使用多个GWT模块时,同名的变量或函数相互冲突。用IE Developer Toolbar查看HelloGWT运行时的DOM状态,可以看到BODY下面除了BUTTON元素外,还有一个名为hellogwt的IFRAME。HelloGWT模块就被加载到这个同名的IFRAME中,如图3-9所示。
图3-9 hellogwt被加载在同名的iframe中
hosted.html:GWT托管环境的运行拦截文件。GWT运行在托管环境下,hosted.html将代替编译出JavaScript,以使调试器能够介入。
GWT生成的脚本:一共有五个文件,打开hellogwt.nocache.js可以看到以下的语句:
unflattenKeylistIntoAnswers(['gecko1_8'],'7A5F577679969D19D6B5F6FB3968291D.cache.html'); unflattenKeylistIntoAnswers(['opera'], 'B515C54D6D0EF7B4198DE29A1FB203C4. cache.html'); unflattenKeylistIntoAnswers(['safari'], '8C65746A527B9C876422AB2EC8388D48. cache.html'); unflattenKeylistIntoAnswers(['gecko'], 'EA4AB258130A160F6A28327055751740. cache.html'); unflattenKeylistIntoAnswers(['ie6'], '26E71BDB0AD700CBCD531EAC52E23E71. cache.html'); strongName = answers[computePropValue('user.agent')];
这5个文件就是针对5种不同浏览器生成的脚本文件。分别对应gecko1_8、opera、safari、gecko、ie6。hellogwt.nocache.js在模块初始化时,根据用户的user.agent值,加载不同的脚本文件。
GWT默认生成所有浏览器平台的源代码。当项目规模比较大时,GWT编译会花费很长时间。在Intranet环境中,如果能确定客户只使用某种特定浏览器。那么可以在模块定义文件中添加以下设置:
<set-property name="user.agent" value="ie6" />
使用set-property指示user.agent只有ie6一个值。这样GWT就只编译出IE浏览器平台的JavaScript脚本。编译用时缩短至约为原先的1/5。
GWT生成的图片:为了提高图片的加载速度,GWT将多个图片合并成一个大图片,这样就不需要频繁地下载图片。显示的时候,使用CSS屏蔽不需要的图片部分。这项技术称为CSS Sprite,GWT编译器自动完成图片合并、显示图片屏蔽等复杂繁琐的工作。GWT使用CSS Sprite将会在GWT高级篇中介绍。
clear.cache.gif:CSS Sprite的占位文件,这是一个1x1的空白图片文件。
模块资源文件:GWT模块的public文件夹中的文件。GWT编译后将public目录中的文件复制到gwt模块输出目录,见3.5节。
3.9 小结
本章讲解了GWT模块的基础知识和内部运行原理,包括模块定义文件、入口点、模块继承、附加CSS和脚本文件、延迟绑定、路径配置、发布JAR文件等。模块是GWT的核心,这些知识点是以后各章节的基础;同时也是日常开发GWT程序所必需的。希望读者能够仔细阅读本章内容,彻底掌握本章中的知识点。