2010年8月26日 星期四

對戰吧~GWT 踩地雷! [下]

跟電腦(web server)對戰的故事大概是這樣的:
  1. 跟 server 要求開始一場遊戲
  2. 跟 server 取得遊戲資訊
  3. 根據遊戲資訊繪製畫面
  4. 將玩家踩的地點傳送給 server
    1. 命中地雷→更新遊戲資訊
    2. 沒有命中→輪到 AI 踩地雷→更新遊戲資訊
  5. 檢查是否有某方獲勝?
    1. 有→結束。
    2. 沒有→回到步驟 3
粗體的部份是跟 server 有關的部份。
那麼,以 RPC、或說以程式的講法
server 只需要提供兩個 method
  • startGame():負責開遊戲 
    • 回傳:該場 game 的 uid
  • shoot():傳送玩家踩的地點
    • 參數:uid, x, y
    • 回傳:GameInfo 物件
因為懶得多設計一堆有的沒的
所以開場之後讓 client 端程式自動踩 (-1,-1) 這個非法位置
就會進入到步驟 2~4 的循環當中 [逃]

好了,終於要進入到 GWT RPC 的部份了。
這裡打算跳過理論、架構的部份
直接以程式碼來說明一切...

首先,是要有一個 MineService 的 interface
繼承自 com.google.gwt.user.client.rpc.RemoteService
裡頭就是宣告上頭說得那兩個 method
@RemoteServiceRelativePath("mineRPC")
public interface MineService extends RemoteService {
  public String startGame();
  public GameInfo shoot(String id, int x, int y) throws Exception;
}
至於那個 annotation 先跳過,後頭會解釋

再來是一個對應的 interface,名稱通常是後頭補 Async
public interface MineServiceAsync {
  void startGame(AsyncCallback<String> callback);
  void shoot(String id, int x, int y, AsyncCallback<GameInfo> callback);
}

這兩個 class 必須放在 gwt compiler 會處理的目錄下(例如 client)

有 interface 自然有實做的 class
MineService 對應實做 class 通常叫做 MineServiceImpl
會長成這樣子:
public class MineServiceImpl extends RemoteServiceServlet implements MineService {
  private AI_Interface ai = new RandomAI();  //FIXME change your ai here!
  
  @Override
  public String startGame() {
    String id = UUID.randomUUID().toString();
    MineGM setting = new MineGM();
    setServer(id, setting);
    return id+",Random";
  }
    
  private void setServer(String id, MineGM setting){
    this.getThreadLocalRequest().getSession().setAttribute(id+"ID", setting);
  }
  
  private MineGM getServer(String id) throws Exception{
    String name = id+"ID";
    if(this.getThreadLocalRequest().getSession().getAttribute(name)!=null){
      return (MineGM) this.getThreadLocalRequest().getSession().getAttribute(name);
    }else{
      throw new Exception("還沒開局");
    }
  }
  
  @Override
  public GameInfo shoot(String id, int x, int y) throws Exception{
    MineGM server = getServer(id);
    if(x==-1 || y==-1 || server.getMap()[x][y]!=-1){
      return MineGM.toGameInfo(server);
    }
    
    if(!server.shoot(x, y, MineGM.USER)){
      int[] xy = new int[2];
      do{
        ai.guess(MineGM.toGameInfo(server), xy);
      }while(server.shoot(xy[0], xy[1], MineGM.AI));
    }
    
    setServer(id, server);
    return MineGM.toGameInfo(server);
  }
}

這個 class 還會繼承 RemoteServiceServlet
往上追溯,parent 是 HttpServlet
也就是說,MineServiceImpl 也是一個 HttpServlet
雖然 GWT RPC 很神奇地包裝好許多東西
但終究還是 base on JSP
所以寫這個 class 時,就不用管 GWT 的重重限制
只要 web.xml 有設定正確就好

講到 web.xml,回頭講一下 MineService 的 annotation
用 RemoteServiceRelativePath 設定 servlet-mapping 會比較方便
<!-- in web.xml -->
<servlet-mapping>
 <servlet-name>mineRPC</servlet-name>
 <url-pattern>/_mine/mineRPC</url-pattern>
</servlet-mapping>

url-pattern 的值,前半段 _mine 是在 gwt.xml 中
設定 <module rename-to='_mine' >
後半段 mineRPC 就是在 MineService 設定的值
(沒有用這個 annotation,得要多好幾行煩死人的 code)

至於遊戲資訊,我選擇塞在 session 當中
在 RemoteServiceServlet 要取得 session 比較囉唆一點
得要這樣才能取得 session
this.getThreadLocalRequest().getSession()
其餘的程式碼... 嗯... 不在 GWT 的範圍當中,跳過 XD

server 端的程式碼解決了,現在來看 client 端
client 端必須透過 MineServiceAsync 來呼叫 RPC
不過用法有點奇怪...... Orz

首先要先這樣寫,取得一個 MineServiceAsync 的 object
MineServiceAsync msa = GWT.create(MineService.class);
然後就可以用 msa.shoot() 來告訴 server 要踩哪個位置
但是事情還沒完,除了標準的 parameter
得要傳一個為 AsyncCallback 的 parameter
這是讓 server 端處理完畢後可以 callback 的一個手段
初期通常都會用 anonymous class 來解決
所以程式碼會長得像這樣:
msa.shoot(this.gameID, hitX, hitY, new AsyncCallback<GameInfo>(){
      @Override
      public void onFailure(Throwable caught) {
        Window.alert("shoot : "+caught.getLocalizedMessage());
      }

      @Override
      public void onSuccess(GameInfo result) {
        setShootResult(result);
      }
    });

這邊要注意兩件事情

其一,RPC 傳遞/回傳的 class(及其 field)
除了 primitive data type 外
一定得 implements IsSerializable
還必須是 GWT 允許的 class
此外,自訂的 class 還必須讓在 GWT compiler 會處理的目錄下
不然實際跑起來就會有(很難看懂的)錯誤訊息

其二,呼叫完 msa.shoot() 之後
下一步並不會執行 onSuccess()/onFailure()
更正確來說,在寫 client 端程式碼的時候
並不會知道 onSuccess()/onFailure() 什麼時候會呼叫到
端看 server 處理以及網路傳輸的速度... etc
這是 callback 的特性,在踩地雷的 case 當中並不會造成困擾
但在其他實務上,如果發現怎麼 RPC 回傳值都不正確
那大概就是忘記這個性質所導致的...... Orz

喔對... 都忘記講一個大前提了
要用 GWT RPC,server 必須是 JSP container

同時也來講講 GWT RPC 的好處 \囧/
一言以蔽之就是「通通傳便便
client 不用組 query string 或 post 內容
server 端不用準備對應的 url
client/server 都不需要剖析傳遞的資料
甚至可以傳遞(客製化的) exception
在 client 端的 onFailure() 可以分門別類處理......
程式碼看起來、寫起來都很 OO、都很 Java
以一個 Java Programmer 來說,有什麼比這個更快樂的事情呢? XD
(連 xml 都沒有用到呢! [握拳])

好了,「對抗電腦版的踩地雷」拆解到這裡
只剩下地雷區要設定 click 的 handler
收到 GameInfo 之後要更新畫面
以及電腦 AI 設計這些功能
相信你一定可以自己寫的很開心的,就跳過不細談了

如果你想偷懶想拿寫好的程式碼來執行看看,
可以到這裡下載。也歡迎投稿你的 AI 設計

Enjoy GWT and have fun! \囧/

2010年8月23日 星期一

對戰吧~GWT 踩地雷! [上]

上一次教完如何寫一個生命遊戲(好久以前啊 [遠目])
這次的題目同樣是陣列系的踩地雷
不過,如果單機自己玩也太無聊了點
所以呢... 這次的目標是「對抗電腦版的踩地雷」!
這個題目有點大,讓我們一步一步慢慢來......

首先是先弄出一個 MineGM 的物件
來負責創造地雷世界、運作規則邏輯
所以 MineGM 必須要有這些 field
public static final int UNKNOW = -1;

private int x;
private int y;
private int total;  //總共幾個地雷
private int remainder;  //剩下幾個地雷
private boolean[][] answer; //地雷分佈圖
private int[][] map;  //玩家看到的地圖
private int[] playerHit = new int[2]; //分別踩了幾個

一開始就用亂數把 answer 準備好
至於 map 的內容一開始都是 UNKNOWN,表示還不知道是啥狀況
map 當中還可能出現:
  • 0~8:九宮格內出現的地雷數
  • 9:玩家踩到的地雷
  • -9:電腦踩到的地雷
而 MineGM 還需要有一個 public 的 method「shoot()」
負責接受玩家的輸入、然後回報是否命中
public boolean shoot(int hitX, int hitY, boolean who){
  map[hitX][hitY] = count(hitX, hitY);

  //踩到空地的連鎖反應
  if(map[hitX][hitY]==0){
    for(int i=-1; i<2; i++){
      if(hitX+i==x || hitX+i<0){continue;}
      for(int j=-1; j<2; j++){
        if(hitY+j==y || hitY+j<0){continue;}
        if(map[hitX+i][hitY+j] != -1){
          continue;
        }else{
          shoot(hitX+i, hitY+j, who);
        }
      }
    }
  }

  //不同人踩到地雷要給不同值
  if(map[hitX][hitY]==9){
    remainder--;
    if(who){
      playerHit[0]++;
    }else{
      map[hitX][hitY]=-9;
      playerHit[1]++;
    }
  }
  
  return Math.abs(map[hitX][hitY])==9;      
}

count() 會計算周圍九宮格有幾個地雷
把回傳質設定到對應的 map 上
另外,因為我採取「answer 的周圍多一格空地」的作法
所以「踩到空地的連鎖反應」那段的迴圈可以比較好看一點

雖然已經有 MineGM 建立、維護地雷世界了
但是,我們還是需要另外一個 GameInfo 來包裝給玩家的資訊
不然如果玩家 or 電腦直接拿 MineGM 的 answer 來作弊怎麼辦? Orz
所以在 MineGM 當中弄了一個 static method 來轉換成 GameInfo
public static GameInfo toGameInfo(MineGM server) {
  GameInfo result = new GameInfo();
  result.setMap(server.getMap());
  result.setRemainder(server.remainder);
  result.setTotal(server.total);
  result.setPlayerHit(server.playerHit);
  return result;
}

至於其他的細節就留給大家慢慢寫了......

接下來處理 UI 的部份
這次使用 GWT 2.0 的 UiBinder 來處理排版
使用方法可以看官方文件痞子版的中文翻譯

預計的遊戲畫面長這樣:

上方是數據區
左右兩側是雙方的名字與分數,包了一個 PlayerInfo 來處理
其實很簡單,剛好適合拿來了解 UiBinder

PlayerInfo.ui.xml:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
  xmlns:g="urn:import:com.google.gwt.user.client.ui">
  <ui:style>
  .title{
    padding-left: 5px;  
  }
  </ui:style>
  <g:FlowPanel>
    <g:InlineLabel ui:field="title" styleName="{style.title}"></g:InlineLabel>
    <g:InlineLabel ui:field="hitCount"></g:InlineLabel>
  </g:FlowPanel>
</ui:UiBinder> 

PlayerInfo.java
public class PlayerInfo extends Composite {

  private static PlayerInfoUiBinder uiBinder = GWT.create(PlayerInfoUiBinder.class);

  interface PlayerInfoUiBinder extends UiBinder<Widget, PlayerInfo> {
  }

  @UiField Label title;
  @UiField Label hitCount;
  
  public PlayerInfo() {
    initWidget(uiBinder.createAndBindUi(this));
    setHitCount(0);
  }

  public void setHitCount(int i) {
    hitCount.setText(""+i);
  }

  public void setName(String name){
    title.setText(name+":");
  }
}

中間是還剩下多少地雷,用一個 Label 解決
下方的地雷區則是用 FlexTable 處理
這些東西都放在 MineMain 這個 class 當中
所以 MineMain.ui.xml 會長成這樣:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
  xmlns:g="urn:import:com.google.gwt.user.client.ui" xmlns:m="urn:import:org.psmonkey.product.client.mine">
  <ui:style>
  .playerInfo{
    width: 480px;
  }
  .cpu{
    width: 220px;
    background-color: red;
  }
  .player{
    width: 220px;
    background-color: #64A0C8;
  }
  .remainder{
    width: 40px;
    color: white;
    background-color: gray;
    text-align: center;
  }
  </ui:style>
  <g:VerticalPanel>
    <g:HorizontalPanel styleName="{style.playerInfo}">
      <m:PlayerInfo ui:field="cpu" styleName="{style.cpu}"></m:PlayerInfo>
      <g:Label ui:field="remainder" styleName="{style.remainder}"></g:Label>
      <m:PlayerInfo ui:field="player" styleName="{style.player}"></m:PlayerInfo>
    </g:HorizontalPanel>
    <g:FlexTable ui:field="map"></g:FlexTable>
  </g:VerticalPanel>
</ui:UiBinder> 

很懶惰地用 VerticalPanel 跟 HorizontalPanel 解決 XD

接下來,就要處理跟 web server 之間的溝通了! [待續]

2010年5月20日 星期四

GWT 2.1 M1 登場!

原文網址:http://googlewebtoolkit.blogspot.com/2010/05/gwt-21-milestone-1-is-now-available.html

GWT 2.1 M1 版當中,你可以更進一步地以雲端方式建立 business application 但卻如以往一樣容易。使用 GWT 新的 data presentation widget 以及 MVP framework,你可以弄出很好看的 web application、可是執行起來卻很快——無論你是要操作 25 筆資料還是 2500 萬筆資料。我們將這個 widget 設計的非常 lightweight 來達到這個目標,不用 compostie 跟 widget 改使用 DIV 跟 HTML;而 framework 讓你可以很容易地僅在需要的時候只抓取你所要的資料。

為了讓你建立 web application 更快一點,現在你可以使用 VMware 的 Spring Roo ,僅僅使用幾個指令就可以建造一個功能性十足的 application。這些 tools 都已經在  SpringSource Tool Suite (STS) 上可以取得了,包括 Google Plugin for Eclipse、App Engine SDK——提供你一個整合性的開發環境。

最後,當你使用 GWT 開發時,做出來的 application 就是一個 HTML5 的 application 了。這表示已經準備好可以開始進入雲端計算的領域了。

想開始行動了嗎? 看一下我們的商業版 GWT,以及 2.1 版將會有哪些東西?兩個 GWT codesite 上的章節吧!

2010年3月26日 星期五

App Engine SDK 1.3.2 發佈

今天,我們很高興宣佈 App Engine 1.3.2 版(Java 與 Python 版)正式發佈。1.3.2 版包含數個改變以及 bug 修正。

在這個版本當中,我們全神貫注在解除許多已經影響到開發者的限制:

  • Blobstore API—提供一個新的 method(Python 用 fetch_data,Java 用 fetchData)使你的 application 可以用程式讀取 Blob 的內容。
  • URLFetch API—你可以存取的 port 增加了。現在可以存取的 port 為 80~90、440~450、1024~65535。
  • Mail API—我們放寬了郵件附加檔案的種類,包含了常見的文件種類:.doc、.ppt、.xls。
  • Task Queue API—我們把 Task Queue 的 refill 率增加到每秒 50 次。

我們也很高興地宣佈,根據你們的回饋意見,App Engine 有了一個新的 DoS 阻斷系統。這個系統可以讓你把特定的 IP 存取你的 application 時列入黑名單當中,以防止他們浪費你的錢或是佔用你的 quota。你還可以在 Admin Console 看到最常存取你的 application 的 IP,以幫助你找出你想要阻擋的 IP。 關於這個功能的更多資訊,請參閱下列文件:Python 版、Java 版 。

這個版本還有很多改變跟修正,包含新 Java 版的 Appstat profiling tool。詳細的資料請閱讀版本更新紀錄(Python 版Java 版),並下載新版的 SDK

2010年2月12日 星期五

徹底瞭解 GWT Part 2:JavaScript 的 overlay type

原文:http://googlewebtoolkit.blogspot.com/2008/08/getting-to-really-know-gwt-part-2.html

技術校正、審閱:tkcn

假設你已經在 GWT module 當中,愉快地使用 JSNI 來呼叫某些手寫的 JavaScript。一切運作正常,但是 JSNI 只能在獨立的 method 下運作。某些整合性狀況需要你徹底地把 JavaScript 跟 Java 的 object 綁在一起——寫 DOM 跟 JSON 就是兩個好例子——所以我們十分需要可以從 Java 程式碼直接與 JavaScript object 互動的方法。換句話說,我們想要 JavaScript 的 object 看起來就像我們寫的 Java object。

GWT 1.5 引入了 JavaScript overlay type,這讓 GWT 程式整合各種 JavaScript object 變得容易許多。這個技術有很多好處,像是讓你能用 Java IDE 的 code completion 跟 refactoring 功能,即使你寫的是 untype 的 JavaScript object。

範例:簡單、有效率的 JSON
用一個範例來瞭解 overlay type 是最簡單的方法。假設我們要存取一組「customer」數據,底層是用 JSON object。在 JavaScript 中的資料結構可能像這樣:
void jsonData = [
{ "FirstName" : "Ps", "LastName" : "Monkey" },
{ "FirstName" : "痞子", "LastName" : "猴" },
{ "FirstName" : "Pt2", "LastName" : "Club" },
{ "FirstName" : "STO", "LastName" : "Orz" },
];

要把一個 Java type 加到上述的資料結構,要從建立一個 JavaScriptObject 的 subclass 開始,這在 GWT 表示是一個 JavaScript 的 object。接著增加一些 getter。
// An overlay type
class Customer extends JavaScriptObject {
// Overlay types always have protected, zero-arg ctors
protected Customer() { }

// Typically, methods on overlay types are JSNI
public final native String getFirstName() /*-{ return this.FirstName; }-*/
public final native String getLastName() /*-{ return this.LastName; }-*/

// Note, though, that methods aren't required to be JSNI
public final String getFullName() {
return getFirstName() + " " + getLastName();
}
}

如此一來,GWT 就會瞭解所有 Customer 的 instance 實際上是來自 GWT module 以外的 JavaScript object。這包含了很多意義。舉例來說,看到 getFirstName()getLastName() 裡頭的 this reference。它實質上是代表一個 JavaScript object,所以你操作這個 this 就像在 JavaScript 裡頭一樣。在這個例子中,我們可以直接存取 JSON 中那些我們已知的 field:this.FirstNamethis.LastName

那麼,你要如何才能真正得到一個被包裝成 Java type 的 JavaScript object 呢?你不能用 new Customer() 來建構它,因為重點是把一個既有的 JavaScript object 包裝成 Java type。因此,我們必須使用 JSNI 來得到這樣一個 object:
class MyModuleEntryPoint implements EntryPoint {
public void onModuleLoad() {
Customer c = getFirstCustomer();
// Yay! Now I have a JS object that appears to be a Customer
Window.alert("Hello, " + c.getFirstName());
}

// Use JSNI to grab the JSON object we care about
// The JSON object gets its Java type implicitly
// based on the method's return type
private native Customer getFirstCustomer() /*-{
// Get a reference to the first customer in the JSON array from earlier
return $wnd.jsonData[0];
}-*/;
}

現在來搞清楚我們做了啥。我們拿到了一個 plain old JSON object(譯註:源自於 POJO)並且建立一個看起來很正常的 Java type,讓 GWT 程式碼中能夠使用它。於是你就有了 code completion、refactoring、compile 階段的檢查——這些寫 Java 時所擁有的好處。然而,你還是可以靈活地操作任何 JavaScript object,這使得存取 JSON service(使用 RequestBuilder)變得很輕而易舉。

為一些 compiler 強者岔題一下。overlay type 另一個美妙的事情是你可以增加 Java 的 type,但是卻不用影響底層的 JavaScript object。注意到上面例子中,我們加入的 getFullName() 這個 method。它是純粹的 Java 程式碼(並不存在於底層的 JavaScript object),但卻是依照底層 JavaScript object 所寫的。也就是說,處理同一個 JavaScript object,以 Java 的角度會比用 JavaScript 功能豐富得多;而且不用動到底層的 JavaScript object——無論是 instance 或是 prototype

(接續上一段的題外話)在 overlay type 增加 method 這個很酷的怪招是可行的,因為 overlay type 的設計規則是不允許 polymorphic 呼叫,所有的 method 必須是 final 且/或 private。因此,compiler 是靜態地解讀每一個 overlay type 的 method,所以不需要在 runtime 的時候動態 dispatch。這是為甚麼我們不用拘泥在 object 的 function pointer;compiler 可以直接對 method 呼叫,就好像是 global function、獨立於 object 之外。很明顯的,直接呼叫 function 會比間接快得多。更棒的是,因為呼叫 overlay type 的 method 是靜態解讀的,這些動作會嘗試自動 inline;這在為了 script 語言的效率而奮戰時,是非常強大的火力支援。接下來我們會重新來一遍,展示給你看這個方法有多成功。

範例:lightweight collection

我們在上面的例子當中掩蓋了某些事情。getFirstCustomer() 這個 method 是非常不切實際的。你一定會希望存取全部的 customer 陣列。所以,我們需要一個 overlay type 來表示這個 JavaScript 陣列。幸運的是,這很簡單:
//泛型在 overlay type 裡頭也運作正常!
class JsArray<E extends JavaScriptObject> extends JavaScriptObject {
protected JsArray() { }
public final native int length() /*-{ return this.length; }-*/;
public final native E get(int i) /*-{ return this[i]; }-*/;
}

現在我們可以寫出更有趣的程式了:
class MyModuleEntryPoint implements EntryPoint {
public void onModuleLoad() {
JsArray<Customer> cs = getCustomers();
for (int i = 0, n = cs.length(); i < n; ++i) {
Window.alert("Hello, " + cs.get(i).getFullName());
}
}

// Return the whole JSON array, as is 
private final native JsArray<Customer> getCustomers() /*-{
return $wnd.jsonData;
}-*/;
}

這是一個很乾淨的程式碼,尤其是以建立靈活配置的角度來看。正如上頭提到的,compiler 可以作一些十分 fancy 的事情,讓它相當有效率。看一下 onModuleLoad() 這個 method 在沒有 obfuscate 的 compile 結果:
function $onModuleLoad(){
var cs, i, n;
cs = $wnd.jsonData;
for (i = 0, n = cs.length; i < n; ++i) {
$wnd.alert('Hello, ' + (cs[i].FirstName + ' ' + cs[i].LastName));
}
}

這個最佳化真的是 xx 的好。即使是 getFullName() 這個 method 的 overhead 也沒了。事實上, 所有 Java method 的呼叫動作都不見了。當我們說:「GWT 給你可負擔的 abstraction」,這就是其中之一。不僅 inline 的程式碼執行速度明顯變快、我們不再需要定義 function 的內容、也因而得以將 script 簡短化(雖然持平而論,inline 的方式也很容易讓 script 量變多,所以我們小心地在速度與程式碼大小之間取得平衡)。現在回顧上頭原始的 Java 程式碼是十分有趣的,而試著推導 compiler 最佳化的步驟就展示到這邊。 不過,我們還是忍不住要 show 一下對應、obfuscate 過的程式碼:
function B(){var a,b,c;a=$wnd.jsonData;for(b=0,c=a.length;b<c;++b){  $wnd.alert(l+(a[b].FirstName+m+a[b].LastName))}}

注意在這個版本當中,唯一沒有 obfuscate 的是 JavaScript 當中的識別字,例如 FirstNameLastNamejsonData 等。這是為甚麼即使 GWT 努力讓大量 JavaScript 交互溝通的操作變得容易,但我們還是努力說服別人盡量用純 Java 來寫程式、而不是混著 JavaScript 寫。希望你聽到我們講這些之後,你會明白我們不是要打擊 JavaScript——只是我們不能對它們最佳化,這會讓我們很沮喪。

摻在一起作撒尿牛丸 
overlay type 是 GWT 1.5 的重要特性。這個技術讓直接與 JavaScript library 互相溝通變得相當容易。希望在讀完這篇文章之後,你可以想像如何以一組 Java type 直接導入任何 JavaScript library 到 GWT 裡頭,進而使用 Java IDE 來進行高效率的開發跟 debug,卻不會因為任何類型的 GWT overhead 而影響程式碼大小或執行速度。同時,overlay type 作為一個強大的 abstraction 工具,提供更優雅的低階 API,例如新的 GWT DOM package

2010年2月5日 星期五

徹底瞭解 GWT Part 1:JSNI

原文:http://googlewebtoolkit.blogspot.com/2008/07/getting-to-really-know-gwt-part-1-jsni.html

技術校正、審閱:tkcn

[前面略過一段很口語、很難翻譯、但是不太重要的一段 XD]

徹底瞭解 GWT,第一步:JSNI
如果你是 GWT 新手,你可能覺得奇怪:到底有什麼好激動的?GWT 跟其他 framework 有什麼不同?GWT 是一個基礎技術、串起許多工具,而不只是特定 application 的 framework。因此,雖然 GWT「有」很多 library,覺得有用的話,要用多用少都可以。不喜歡 GWT 的 UI?你可以用 DOM 建立自己的 UI。不想用 RPC 而想要用 JSON?那也沒問題。事實上,要用 GWT 重頭開始打造自己的  framework 是絕對沒問題的,也能保留所有 GWT debug 跟 compile 方面的優點。

所以,用一個宏觀的角度來思考:為什麼你要考慮使用 GWT 來開發下一個大的 web application 呢?
  • 相較於傳統 server-centric 的方式,如果 AJAX 設計得好,因為本身的特性,在許多種類的 application 上會提供更好的使用經驗。你都已經在讀這個 blog 了,可能早就知道啦。
  • GWT compiler 產生的 JavaScript 碼通常會比你自己撰寫的程式碼還要小、還要快。
  • 用 GWT 開發可以讓你更有產能、而且產生的 JavaScript 也更加可靠。
  • You get the opportunity to say "Gwit" a lot.

我們擔心你是那種容易被一些主張或是行銷說詞給影響了,所以我們在接下來幾篇 blog 文章中,會看到 GWT 如何實現性能的提昇、以及如何讓 JavaScript 使用起來更容易。

在深入技術細節之前,還有一件事。我們有時會問兩個跟 GWT 本質有關的問題:
  1. 為什麼 GWT 以 Java 語言和工具為核心?
  2. 跟手寫的 JavaScript 比起來,GWT 是不是比較多限制、也比較 heavyweight?

先回答第一個問題,請瞭解我們的目標跟滿腔熱血,是為了徹底改善網頁使用者的使用者經驗,這表示 GWT 產生的 JavaScript 必須有最好的效能跟可靠度。為了做到這一點,我們自然想加入一堆程式碼的最佳化,並且盡可能的提早抓出 bug。這兩個目標都容易在 Java 的 static type、以及既有的 Java IDE 來達成。這是為什麼我們冷靜地選擇了 Java 技術作為 GWT 的核心。就是這樣—沒啥語言聖戰的梗。

再來回答第二個問題,有些開發人員在第一次聽到 GWT 時,會假設它是一個 abstraction 的「Walled Garden」,讓你永遠只能用 Java 寫程式而不讓你使用或是整合手寫的 JavaScript。這實在錯的很離譜......

GWT 中的 JavaScript Native Interface(JSNI)
你可以輕鬆地把手寫的 JavaScript 直接嵌入 GWT 程式碼當中。反正最後都會變成 JavaScript,那為甚麼不提供 GWT 的開發人員一個有用的方法,讓他們能夠混合這兩個東西?這方法就叫做 JSNI 啦。它的名字跟 JNI(Java Native Interface)很像,因為他們的基本想法也一樣:把一個 Java method 宣告成「native」然後用另一個語言來實做內容。在 JSNI 當中,就是用 JavaScript 來實做內容。

用 JavaScript 寫 Java Method
JSNI 在創造功能最底層的 abstraction 是非常有用的,它很自然地使用 JavaScript、而不是 Java 語法。例如 Regular Expression 在 JavaScript 是相當簡潔的,所以你可以透過 JSNI 在 GWT 程式碼當中直接使用它們。假設你想把某人的名字(Ps Monkey)轉過來,讓姓在後面、名字在前面(Monkey, Ps)(當然,這在 I18N 當中會是一場惡夢)。你可以建立一個很簡短的 JSNI method 來做到:
// Java method declaration...
native String flipName(String name) /*-{
// ...implemented with JavaScript
var re = /(\w+)\s(\w+)/;
return name.replace(re, '$2, $1');
}-*/;
請注意,method 的內容整個被特殊標記「/*-」、「-*/」給包圍起來,變成一段 Java 的註解。

在 JSNI 中呼叫 Java method
你也可以反過來,在 JavaScript 呼叫一個 Java 的 method。假設我們修改上面的例子,改成這樣:
package org.example.foo;
public class Flipper {
public native void flipName(String name) /*-{
var re = /(\w+)\s(\w+)/;
var s = name.replace(re, '$2, $1');
this.@org.example.foo.Flipper::onFlip(Ljava/lang/String;)(s);
}-*/;

private void onFlip(String flippedName) {
// do something useful with the flipped name
}
}

用 JSNI 存取外部的 JavaScript 程式碼
當然,你可以在 GWT module 存取任何外部的 JavaScript 程式碼。舉例來說,如果你的 HTML 頁面看起來像這樣:
<html>
<head>
<SCRIPT>
function sayHello(name) {
alert("Hello from JavaScript, " + name);
}
</script>
<-- Include the GWT module called "Spiffy" -->
<script src="org.example.yourcode.Spiffy.nocache.js"></script>
</head>
...
在 Java 程式碼,您可以用 JSNI 去呼叫 JavaScript 的 sayHello() function:
// A Java method using JSNI
native void sayHelloInJava(String name) /*-{
$wnd.sayHello(name); // $wnd is a JSNI synonym for 'window'
}-*/;
GWT compiler 把外部的 method 呼叫給內嵌進來,所以在 Java 當中呼叫 sayHelloInJava() 的效率並不會比直接在 JavaScript 當中呼叫來的差。

用 GWT 建立 JavaScript library
你甚至可以用 GWT 創造一個 JavaScript 可以呼叫的 library。這是一個非常棒的技巧:
package org.example.yourcode.format.client;
public class DateFormatterLib implements EntryPoint {

// Expose the following method into JavaScript.
private static String formatAsCurrency(double x) {
return NumberFormat.getCurrencyFormat().format(x);
}
// Set up the JS-callable signature as a global JS function.
private native void publish() /*-{
$ wnd.formatAsCurrency =
@org.example.yourcode.format.client.DateFormatterLib::formatAsCurrency(D);
}-*/;

// Auto-publish the method into JS when the GWT module loads.
public void onModuleLoad() {
publish();
}
}

接著,你可以在任何 HTML 或是 JavaScript library 當中存取這個 GWT 做出來的功能:
<html>
<head>

<-- Include the GWT module that publishes the JS API -->
<script src="org.example.yourcode.FormatLib.nocache.js"></script>

<-- Write some JS that uses that GWT code -->
<SCRIPT>
function doStuff() {
alert(formatAsCurrency(1530281));
}
</script>
</head>
...

Ray Cromwell(最有名的是 GWT Extreme!)在他的 GWT Exporter 淋漓盡致地採用了上述的 JavaScript 發佈技術。它用 GWT compile 期的 code 產生器來自動創造所有發佈出的程式碼。(「compile 階段的程式碼產生器」聽起來很酷吧?如果你也這樣覺得,那請期待這個系列的後續文章。)

摻在一起作撒尿牛丸
用 JSNI,你可以用你想要的方式,自由地混和手工撰寫的 JavaScript、外部的 JavaScript library、以及 Java 程式碼。這就是我們說 GWT 並不是一個 abstraction 的「walled garden」,因為你可以逐步地將 GWT 融入到既有的 web application 當中。更棒的是,你可以用你喜歡的 Java debugger 來 debug 所有 Java 程式碼。

想要了解更多的話,請看 GWT Developer Guide 的 JavaScript Native Interface。或著,讓 GWT compiler 的設計師 Scott Blum 解釋給你聽也不錯喔。