GWT初体验

GWT初体验

最近体验了一下GWT(Google Web Toolkit),其实这个技术老早就有了,写Java代码,代码很像AWT或者Swing,但是最后编译成一个war包,也就是说,没有啰嗦的JavaScript、HTML和模板语言,Java从前到后通吃,常用的模块都被封装成组件了。虽说写起来代码还挺啰嗦的(写法上面居然不支持链式调用,这确实让我看不懂),而且也没有传统Web开发方式来得直观,但也算一种很有意思的开发方式,值得体验一下。网上有足够多的教程,要系统地学习,官方文档是最好的材料,非常详尽。而我的方式,则更具个人风格一点,比较+吐槽,这可不是教程。

工程结构

我是用Eclipse+Google的全套插件建立起GWT工程的,这个过程很容易做到。Eclipse里面选择“Install New Software”,然后输入Google Update Site for Eclipse 4.3的地址:https://dl.google.com/eclipse/plugin/4.3

GWT初体验

我建立了一个GWT工程,取名为GWTToy,它的结构(上面的BrowserHistoryExample.java是我临时建立起来的,并不是工程自动生成的代码)包括:

1.

GWTToy.gwt.xml,这个是GWT的统一配置文件,模块都是使用inherits标签引入进来的:

  • 比如核心Web Toolkit:<inherits name='com.google.gwt.user.User'/>,
  • 比如XML文件解析:<inherits name ="com.google.gwt.xml.XML"/>,
  • 再比如多语言支持:<inherits name ="com.google.gwt.i18n.I18N"/>。

接着是程序的入口点:<entry-point class='com.toy.client.GWTToy' />。

下面是client和shared源码路径(相对于此xml文件)的配置,client部分的代码最终是要编译到客户端去执行的,shared部分是服务端和客户端都可以用的,这两部分需要在此声明一下是因为这两部分Java代码需要GWT编译器编译成JavaScript,因此,服务端的代码就不用声明了:

  • <source path='client'/>
  • <source path='shared'/>

2.

客户端代码:

GreetingService,这是远程方法和本地实现共用的接口定义,如果你使用过RPC的话这套东西应该很熟悉:

@RemoteServiceRelativePath("greet")
public interface GreetingService extends RemoteService {
	String greetServer(String name) throws IllegalArgumentException;
}

GreetingServiceAsync是GreetingService的副本:

public interface GreetingServiceAsync {
	void greetServer(String input, AsyncCallback callback)
			throws IllegalArgumentException;
}

在入口的GWTToy类里面,这样来关联起上面这两个接口:

private final GreetingServiceAsync greetingService = GWT.create(GreetingService.class);

你可以再比较一下GreetingService和副本GreetingServiceAsync接口的异同,前者像是在服务端等待被调用的定义方式,有一个RemoteServiceRelativePath的注解,实现自RemoteService,方法返回的是给客户端的消息字符串;后者满足客户端调用方式的定义,同名方法,方法参数里面有一个回调逻辑callback。

3.

抽象层面的关联关系理清楚以后,再来看实现,在server端GreetingServiceImpl实现了GreetingService接口;而在客户端GWTToy里面,拿着生成的greetingService远程调用:

greetingService.greetServer(textToServer,
	new AsyncCallback() {
		public void onFailure(Throwable caught) {
			// Show the RPC error message to the user
			dialogBox.setText("Remote Procedure Call - Failure");
			serverResponseLabel.addStyleName("serverResponseLabelError");
			serverResponseLabel.setHTML(SERVER_ERROR);
			dialogBox.center();
			closeButton.setFocus(true);
		}

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

这玩意儿不就是JavaScript翻译成Java以后的写法么?

4.

接着来看看onModuleLoad这个方法,用来加载模块绘制到界面上去:

创建一堆button、field和label之类的东西,放到RootPanel上去:

final Button sendButton = new Button("Send");
final TextBox nameField = new TextBox();
nameField.setText("GWT User");
final Label errorLabel = new Label();

// We can add style names to widgets
sendButton.addStyleName("sendButton");

// Add the nameField and sendButton to the RootPanel
// Use RootPanel.get() to get the entire body element
RootPanel.get("nameFieldContainer").add(nameField);
RootPanel.get("sendButtonContainer").add(sendButton);
RootPanel.get("errorLabelContainer").add(errorLabel);

事实上,在GWTToy.html里面,你很容易就可以看到nameFieldContainer、sendButtonContainer和errorLabelContainer这样的DOM对象,所以,归根到底这些布局操作,最后还是要通过编译后的JavaScript的方式,放到GWTToy.html里面去的。

另一方面,css文件你也可以找到,想因为使用GWT就免去css之苦可没门。

熟悉Swing或者AWT的工程师对这部分和下面这部分都会很熟悉:

VerticalPanel dialogVPanel = new VerticalPanel();
dialogVPanel.addStyleName("dialogVPanel");
dialogVPanel.add(new HTML("<b>Sending name to the server:</b>"));
dialogVPanel.add(textToServerLabel);
dialogVPanel.add(new HTML("<br><b>Server replies:</b>"));
dialogVPanel.add(serverResponseLabel);
dialogVPanel.setHorizontalAlignment(VerticalPanel.ALIGN_RIGHT);
dialogVPanel.add(closeButton);
dialogBox.setWidget(dialogVPanel);

这个Panel是属于垂直布局的方式,而且还不可避免的,HTML标签直接丑陋地嵌入Java代码里面去了,看起来确实有些掉价啊。

再看一段事件绑定:

closeButton.addClickHandler(new ClickHandler() {
	public void onClick(ClickEvent event) {
		dialogBox.hide();
		sendButton.setEnabled(true);
		sendButton.setFocus(true);
	}
});

你说,这种方式和JavaScript有啥区别?

5.

GWT初体验

对于Ajax交互,我使用FireBug抓了个包,发现使用dev模式启动应用,它实际是在服务端启动了一个Jetty服务器,response header包括:

Server: Jetty(8.y.z-SNAPSHOT)

这个Ajax的request是:

7|0|6|http://127.0.0.1:8888/gwttoy/|8E754888134EB175906676C7234FAD67|com.toy.client.GreetingService|greetServer|java.lang.String/2004016611|GWT User|1|2|3|4|1|5|6|

response:

//OK[1,["Hello, GWT User!\x3Cbr\x3E\x3Cbr\x3EI am running jetty/8.y.z-SNAPSHOT.\x3Cbr\x3E\x3Cbr\x3EIt looks like you are using:\x3Cbr\x3EMozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:17.0) Gecko/20100101 Firefox/17.0"],0,7]

可以从中看出GWT消息交互的格式,官方文档上也有详细说明,GWT对XML和JSON支持都很完善。

另外,在学习的过程中,这里有很多例子可以参考,特别是这个

使用感受

最后,在体验完毕之后,我阅读了一下这篇文章,列举了一些GWT的优劣,我在此摘录我觉得特别有道理的几条,并且也补充了许多我的看法:

1.

如果你以前使用JAVA开发Swing or AWT应用, 那么选择GWT是最自然的. 对这样的开发人员来说,学习曲线是最平缓的。(评论中被质疑。认为不懂得JAVASCRIPT就无法真正DEBUG使用GWT中遇到的问题)

不只是JavaScript的debug,还有布局、样式等等传统Web开发中遇到的问题,在这里其实依然可能遇到,如果不理解传统Web开发,但是非常熟悉Java,想走捷径,GWT并不是一个好的选择。关于GWT的运行方式,包含了Hosted模式和Web模式,在Hosted模式下,其实Java代码并没有真正被编译成JavaScript,因此开发效率很高,也才有调试方便的优势。

2.

集成的跟踪查错是开发人员梦寐以求的功能. 集成在JAVA IDE中的优秀的跟踪查错功能可以让任何人钟情于GWT。

能够前端后端统一到一起debug(包括语言层面上的统一,也包括IDE上面的统一)可以说是很多Web开发者的梦想,GWT在先,Node.js在后来也尝试去实现了这一点;另外,对于Web开发的模块化和组件化,GWT开了一个很好的头,Bootstrap之类的框架在后来也去做了这件事。所以说,GWT在很多方面都走在了前面。对于Ajax开发来说,对于one-page的应用来说,GWT调试过程改进的好处尤其明显。

3.

你可以使用GWT自己的协议在客户端和服务器端交换数据,这样就不用关心数据打包和传输的细节。如果你需要更多的控制,你可以使用XML, JSON或者其他任意的格式。在这种情况下使用JSON,你仍旧可以抛弃难用的JAVA的JSON类库。你可以直接使用JSNI去执行直接的JAVASCRIPT。

其实GWT对开发人员隐藏的细节又何止传输、浏览器兼容性和数据打包等等细节,仿佛降低了学习曲线,但是令人遗憾的是,真的不了解这些事情的开发人员,也难以很好地定位开发过程中的许多问题。所以实际学习曲线没有降低,反而提高了;当然,GWT因为绝大部分依赖于Java代码,成熟的代码规约和IDE等等使得代码容易控制,不容易出现那些破坏力过大的代码。

关于JSNI,全名是JavaScript Native Interface,很像JNI(Java Native Interface)对不对?正如对比Java跨平台一样,JNI的存在像一个通往hack之路的歪门,JNI就抛弃了跨平台的特性,却带来了实现上更大的可能;而JSNI也一样,失去了浏览器兼容性的保证,但是你可以生写JavaScript,做你曾经熟悉做的任何事情。所以说,这套东西还真是一堆对于Java极度痴迷的人弄出来的。在JSNI中声明一个本地方法时,使用Java的标准native关键字,而本地JavaScript代码用一种特殊的注释格式直接嵌入到Java源代码中:

public static native void alert(String msg) /*-{
  $wnd.alert(msg);
}-*/;

采用这种格式有两个原因:

  • 保证对Java的语法和IDE的兼容;
  • GWT编译器把客户端部分的上述Java程序转换成JavaScript。

所以最后的结果是看起来有点hack,想想看,看似注释、实际是代码的例子还真不少,比如HTML中为了兼容IE的某个(某些)版本经常需要这样写:

<!--[if lt IE 9]>
    ...
<![endif]-->

一样的道理,看多了确实有些不适。而且原来牛逼哄哄的debug代码大统一在这里也只能断了链了。

另一方面,想想Java直接调用JNI方法,直接调用就好了,这里调用JSNI方法也一样;但是如果反过来,想想JNI怎么调用Java?先要获取对象的类,然后查找到那个方法,再调用,用法基本上就和反射一致;而JSNI调用Java里面定义的方法,需要知道GWT编译器最后会根据什么样的规则来编译Java为JavaScript的:

  • Java代码中属性的变成了:obj.@class::field
  • Java代码中的方法变成了:obj.@class::method(sig)(args),其中sig表示内部的Java方法签名,这和学JVM的时候,方法签名的定义是基本一致的,比如Z表示boolean、B表示byte等等
4.

GWT使用AJAX并集成浏览器BACK的支持。如果你是一个AJAX程序员,你可以减少很多的工作量。

注意demo里面的html页面,有这样一句:

<iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>

其中的原理请参见The GWT History Mechanism这一节,有详细解释。它提供了不重新刷新页面的情况下,支持浏览器后退按钮的特性,其原理和Really Simple History类似(关于这个东西,有一个demo页面,满是程序员的体验字符串,你也可以去试试效果,蛮有趣的,链接在这里),就是说,把应用的内部状态放在url fragment identifier里面,其中这个fragment identifier其实就是我们经常看到的URL中“#”(这个井号被称为hash mark)后面的东西;而在更新这个fragment identifier的时候,并不会造成页面重新加载,但是浏览器却认为已经到达了新页面(或者回退到了原页面)。

这部分原理清楚了,但是那个iframe标签呢?它是干嘛的?我把它去掉了,对这个功能依然没有影响啊。

其实,这涉及到另一种实现形式,在GWT中是用来兼容IE低版本用的(IE6、IE7和IE8的compat模式),它们对HTML5的onhashchange方法支持不好,所以这个东西相当于一个workaround,加上一个不可见的iframe以后,它的history发生变化时,点回退接钮时会对这个iframe回退,而不会引起这个页面URL的变化——这也是前面说的Really Simple History的实现原理,这种实现方式可以保证用户所见的URL不发生任何变化,连fragment identifier都不变。

5.

在GWT 1.X中,表现层代码和逻辑代码是搅合在一起的。引入UI Binder之后,这个问题应该解决了。但是学习一门新的XML语言也是让人不爽的。

UI Binder可以看作是GWT发展的过程中在向传统Web开发方式的兼容和妥协,官方文档上面就说“makes it easier to collaborate with UI designers who are more comfortable with XML, HTML and CSS than Java source code”,还有“provides a gradual transition during development from HTML mocks to real, interactive UI”……同时,它也是帮助解耦的一种手段——这一步是必须的,因为单单从Java代码上,根本就读不懂DOM的结构啊,想想标记语言、表述语言真是有它不可替代的价值,比编程语言要直观和形象。可是呢,看看UI Binder使用的时候,写出这样的东西,和传统的页面模板+标签嵌套又有什么区别?

<g:DockLayoutPanel unit='EM'>
  <g:north size='5'>
    <g:Label>Top</g:Label>
  </g:north>
  <g:center>
    <g:Label>Body</g:Label>
  </g:center>
  <g:west size='10'>
    <g:HTML>
      <ul>
         <li>Sidebar</li>
          <li>Sidebar</li>
           <li>Sidebar</li>
       </ul>
     </g:HTML>
   </g:west>
 </g:DockLayoutPanel>

再一次可以得出这样的结论,GWT并不能降低开发学习的难度,还是只有传统Web开发能做好的人,才能做好GWT开发。

6.

关于GWT的I18N,这种实现形式是第一次见到:

  1. 建立一个继承自Constants的常量接口;
  2. 定义跟接口同名的properties文件;
  3. 获取文件中的资源字符串。
比如,接口定义:
public interface MyConstants {
	String welcome();
}

然后是资源文件:

welcome = Welcome to my site {0}!

接着在代码里面使用:

MyConstants cons = (MyConstants)GWT.create(MyConstants.class);
String res = cons.welcome();

所以使用一个资源要改三处地方,真是够啰嗦的,难道不能用一个资源Map之类的东西搞定吗?或者用注解指定资源key?

总而言之,这算是一次非常有趣的体验,开阔视野而且印象深刻,但是实际开发当中,我应该不会使用它。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:

One comment

  1. PsMonkey 说道:

    * 你這篇寫在 2014 年,就算以 2014 年的發展來說,有些吐槽點已經不存在、或改變了。
    * 我不太確定純 JS lib 在開發 one page app 會變成什麼樣子,但是 GWT 是針對 one page app 而設計的,而且可能也只適合開發這種類型、而不是適用於所有 browser 上頭需要 JS 的場合。
    * 承上,拜 Java 的囉唆所賜,即使開發複雜的 app、在管理以及程式碼重用方面都十分優良。反過來說,這是嘗試、跑跑 hello world 階段很難體認到的優點
    * GWT 原生的 widget,只能說示範性質遠大於實用度。用個 GXT 可能才真正能顯示這種開發方式的優良面。我改用 GXT 之後,大抵上沒再寫過 HTML 跟 CSS──當然,這有一半是因為我寫的系統都不太在意美觀性。
     

发表评论

电子邮件地址不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


Preview on Feedage: