最近体验了一下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工程,取名为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.
对于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!x3Cbrx3Ex3Cbrx3EI am running jetty/8.y.z-SNAPSHOT.x3Cbrx3Ex3Cbrx3EIt looks like you are using:x3Cbrx3EMozilla/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等等
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,这种实现形式是第一次见到:
- 建立一个继承自Constants的常量接口;
- 定义跟接口同名的properties文件;
- 获取文件中的资源字符串。
public interface MyConstants { String welcome(); }
然后是资源文件:
welcome = Welcome to my site {0}!
接着在代码里面使用:
MyConstants cons = (MyConstants)GWT.create(MyConstants.class); String res = cons.welcome();
所以使用一个资源要改三处地方,真是够啰嗦的,难道不能用一个资源Map之类的东西搞定吗?或者用注解指定资源key?
总而言之,这算是一次非常有趣的体验,开阔视野而且印象深刻,但是实际开发当中,我应该不会使用它。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接
网友评论已有0条评论, 我也要评论