最近体验了一下 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!\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 等等
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?
总而言之,这算是一次非常有趣的体验,开阔视野而且印象深刻,但是实际开发当中,我应该不会使用它。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》
還是不知道怎麼使用 GWT 呀,有沒有好的教程推薦一下呢,雖然 2018 年了,,還要學 GWT 呀。。
* 你這篇寫在 2014 年,就算以 2014 年的發展來說,有些吐槽點已經不存在、或改變了。
* 我不太確定純 JS lib 在開發 one page app 會變成什麼樣子,但是 GWT 是針對 one page app 而設計的,而且可能也只適合開發這種類型、而不是適用於所有 browser 上頭需要 JS 的場合。
* 承上,拜 Java 的囉唆所賜,即使開發複雜的 app、在管理以及程式碼重用方面都十分優良。反過來說,這是嘗試、跑跑 hello world 階段很難體認到的優點
* GWT 原生的 widget,只能說示範性質遠大於實用度。用個 GXT 可能才真正能顯示這種開發方式的優良面。我改用 GXT 之後,大抵上沒再寫過 HTML 跟 CSS──當然,這有一半是因為我寫的系統都不太在意美觀性。