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

沒有留言:

張貼留言