用AJAX開發智能Web應用程序之高級篇作者:Yulong 日期:2005-12-30
字體大小: 小 中 大?
一、 引言
在第一部分中,我們討論了AJAX基礎——建立從腳本到服務器的通訊的能力,這正是使HTML頁面具有動態能力的原因所在。然而,這就意味著我們已準備好拋棄我們自己版本的Yahoo郵件嗎?不,還沒有。原因在于:AJAX是一個混合的祝福。一方面,它使我們能夠在Web上創建豐富的桌面級的應用程序;另一方面,如果我們把"翻頁面式"的Web應用程序與客戶端/服務器或Swing版本的程序進行比較,那么會看到其開發實踐并不很相同。我們將需要習慣于這樣的事實:構建一個豐富的UI需要時間。須知,允許用戶實現更大的靈活性也就相應地需要付出更多的時間為代價。
最后的答案當然要依賴于大量的組件庫、框架以及具有工業力量的開發工具。且不考慮工具,本文集中于討論在今天對于AJAX熱心者有哪些技術是可用的。在強調需要構建可重用的商業組件的同時,本文將重點分析"隱含的"JavaScript中的面向對象的力量。另外,在強調需要構建定制的UI組件的同時,本文將介紹一個簡便的方法——用定制的客戶端HTML標簽來封裝描述邏輯。
二、 AJAX語言——對象面向的JavaScript
由定義來看,JavaScript是典型的AJAX語言。不同于Java,JavaScript并不強調OO風格的編碼。然而,令人吃驚的是JavaScript居然全面支持所有的OO語言的主要屬性:封裝、繼承和多態性。Douglas Crockford甚至稱JavaScript是"世界上最易被誤解的編程語言"。讓我們回顧一下JavaScript的面向對象的地方吧。
數據類型
在Java中,一個類定義了一個數據和它的相關行為的組合。盡管JavaScript保留了class關鍵字,但是它不支持與常規OOP語言一樣的語義。
這聽起來可能覺得奇怪,但是在JavaScript中,對象是用函數來定義的。事實上,通過在下面的示例中定義一個函數,你就定義了一個簡單的空類Calculator:
function Calculator() {}
一個新的實例的創建與在Java中相同-使用new操作符:
var myCalculator = new Calculator();
上面這個函數不僅定義一個類,而且還擔當了一個構造器。在此,操作符new實現了這一魔術-實例化一個類Calculator的對象并且返回一個對象參考而不是只調用該函數。
創建這樣的空類是沒錯,但在實際中并沒有多大用處。下面,我們準備使用一個Java-腳本原型結構來填充類定義。JavaScript使用原型當作創建對象的模板。所有的原型屬性和方法被參考引用地復制到一個類的每個對象中,所以它們都具有相同的值。你可以改變一個對象中的原型屬性的值,并且該新值會覆蓋從原型中復制過來的缺省值,但是這僅對于在一個實例中。下列語句將把一個新屬性添加到Calculator對象的原型上:
Calculator.prototype._prop = 0;
既然JavaScript并沒有提供一個方法來從句法上表示一個類定義,那么我們將使用with語句來標記該類的定義邊界。這也將使得示例代碼更為短小,因為該with語句被允許在一個指定的對象上執行一系列的語句而不需要限制屬性。
function Calculator() {};
with (Calculator) {
prototype._prop = 0;
prototype.setProp = function(p) {_prop = p};
prototype.getProp = function() {return _prop};
}
到目前為止,我們定義了并且初始化了公共變量_prop,并且為它提供了getter和setter方法。
需要定義一個靜態變量?你可以把靜態變量當作是為類所擁有的一個變量。因為在JavaScript中的類用函數對象來描述,所以我們只需要把一個新屬性添加到該函數上:
Calculator.iCount=0;
現在,既然這個iCount變量是一個Calculator對象的屬性,那么它將會被類Calculator的所有實例所共享。
function Calculator() {Calculator.iCount++;};
上面的代碼計算類Calculator的所有實例的個數。
封裝
通過使用如上面所定義的"Calculator",我們可以存取所有的"class"數據;然而,這增加了派生類中命名沖突的危險性。我們明顯地需要封裝以把對象看作自包含的實體。
數據封裝的一種標準語言機制是使用私有變量。并且一個常用的仿效一個私有變量的JavaScript技術是在構造器中定義一個局部變量;這樣以來,該局部變量的存取只能經由getter和setter來實現-它們是該構造器中的內部函數。在下列實例中,_prop變量在Calculator函數中定義并且在函數范圍外不可見。其中有兩個匿名的內部函數(分別被賦予setProp和getProp屬性)讓我們存取"私有"變量。另外,請注意,這里this的使用-十分相似于在Java中的用法:
function Calculator() {
var _prop = 0;
this.setProp = function (p){_prop = p};
this.getProp = function() {return _prop};
};
常常被忽視的是在JavaScript中作如此封裝所付出的代價。須知,這種代價可能是巨大的,因為內部函數對象對于該"class"的每一個實例被不斷地重復創建。
因此,既然基于原型構建對象速度更快并且消費更少些的內存,那么我們在最強調性能的場所特別支持使用公共的變量。請注意,你可以使用命名慣例來避免名稱沖突-例如,在公共的變量的前面加上該類名。
繼承
乍看之下,JavaScript缺乏對類層次的支持,這很相似于面向對象語言的程序員對于現代語言的期盼。然而,盡管JavaScript句法沒有象Java一樣支持類繼承,但是我們仍然能夠在JavaScript中實現繼承-通過把已定義類的一個實例拷貝到其派生類的原型當中。
在我們提供舉例之前,我們需要介紹一個constructor屬性。JavaScript保證每一個原型中包含constructor-它擁有到該構造器函數的一個參考。換句話說,Calculator.prototype.constructor包含一個到Calculator()的參考。
現在,下面的代碼顯示了怎樣從基類Calculator派生類ArithmeticCalculator。其中,"第一行"取得類Calculator的所有的屬性,而"第二行"把原型constructor的值恢復成ArithmeticCalculator:
function ArithmeticCalculator() { };
with (ArithmeticCalculator) {
ArithmeticCalculator .prototype = new Calculator();//第一行
prototype.constructor = ArithmeticCalculator;//第二行
}
就算上面的實例看起來象一個合成體而不象是繼承,但是JavaScript引擎還是清楚這個原型鏈的。特別是,instanceof操作符會正確地適用于基類和派生類。假定你創建類ArithmeticCalculator的一個新實例:
var c = new ArithmeticCalculator;
表達式c instanceof Calculator和c instanceof ArithmeticCalculator都會成立。
注意,在上面示例中的基類的constructor是在初始化ArithmeticCalculator原型時被調用的,而在創建派生類的實例時是不被調用的。這可能會帶來不想要的負面影響,而且為了實現初始化你應該考慮創建一個獨立的函數。由于該構造器并不是一個成員函數,所以它無法通過this參考引用調用。我們將需要一個能調用超類的"Calculator"成員函數:
function Calculator(ops) { ...};
with (Calculator) { prototype.Calculator=Calculator;}
現在,我們可以寫一個繼承類-它顯式地調用基類的構造器:
function ArithmeticCalculator(ops) { this.Calculator(ops);};
with (ArithmeticCalculator) {
ArithmeticCalculator .prototype = new Calculator;
prototype.constructor = ArithmeticCalculator;
prototype.ArithmeticCalculator = ArithmeticCalculator;
}
多態性
JavaScript是一種非類型化的語言-在此,一切都是對象。因此,如果有兩個類A和B,它們都定義一個foo(),那么JavaScript將允許在A和B的實例上多態地調用foo()-即使不存在層次關系(雖然是可實現的)。從這一角度來看,JavaScript提供一個比Java更寬的多態性。這種靈活性,象往常一樣,也要付出代價。在這種情況中,代價是把類型檢查工作代理到應用程序代碼。具體地說,如果需要檢查一個參考確實指向一個所希望的基類,那么這可以通過instanceof操作符來實現。
另一方面,JavaScript并不檢查函數調用中的參數-這可以防止用一樣的命名和不同的參數來定義多態函數(并且讓編譯器選擇正確的簽名)。代之的是,JavaScript提供了一個Java 5風格的函數范圍內的argument對象-它允許你根據參數的類型和數量的不同而實現一個不同的行為。
三、 示例展示
本文所附源碼列表1實現了一個計算器-它可以計算以一個逆向波蘭式標志的表達式。該示例展示了本文中所介紹的主要技術并且也介紹了一些獨特的JavaScript特性的用法,例如在一個動態函數調用中以一個數組元素的方式訪問對象屬性。
為了使列表1工作,我們需要另外準備一些代碼-它們用于實例化該計算器對象并且調用evaluate方法:
var e = new ArithmeticCalcuator([2,2,5,"add","mul"]);
alert(e.evaluate());
四、 AJAX組件授權
所有的AJAX組件授權方案在今天被邏輯地分成兩組。具體地說,第一組用于與基于HTML的UI定義的無縫集成。第二組把HTML當作一個UI定義語言以支持某種XML。在本文中,我們從第一組中來展示一種方法-雖然它存在于瀏覽器之中卻是類似于JSP標簽。這些瀏覽器特定的組件授權擴展在IE情形下稱作元素行為,而在最近版本的Firefox,Mozilla和Netscape 8情形下稱作可擴展的綁定。
五、 定制標簽
Internet Explorer,從版本5.5開始,支持定制的客戶端HTML元素的JavaScript授權。不象JSP標簽,這些對象并沒有在服務器端被預處理到HTML中。而是,它們成為一標準HTML對象模型的合法擴展,并且包括構造控件在內的一切事情,都是動態地發生在客戶端的。同樣,基于Gecko-引擎的瀏覽器能夠用一個可重用功能動態地裝飾任何現有的HTML元素。
因此,我們有可能用具有HTML語法的方法、事件和屬性來構建一個具有豐富的UI組件的庫。這樣的組件可以被自由地混合于標準HTML中。內部地,這些組件將會與應用程序服務器進行通訊-以AJAX風格。換句話說,你有可能(并且相對簡單地)構建自己的AJAX對象模型。
這種IE風味的方法被稱為HTC或HTML組件;其Gecko版本被稱為XBL-可擴展的綁定語言(eXtensible Bindings Language)。為了實現本文目的,我們集中于討論IE。
六、 輸入HTML組件-HTC
HTC或HTML組件也被稱作行為。它們被劃分為兩種類型:一種是依附的行為-用一組屬性、事件和方法裝飾任何現有的HTML元素;另一種是元素行為-看上去象宿主頁面的定制的HTML標簽的一個擴展集合。依附的行為和元素行為一起提供了開發組件和應用程序的一種簡單方案。在此,我們將展示一下最為綜合的情形-元素行為。
數據綁定復選框控件
為了展示元素行為,我們將構建一個定制的數據綁定復選框。構建這樣一個控件背后的基本原因在于,一個標準HTML復選框具有下面若干顯著的缺點:
·需要應用程序編碼來把"checked"屬性的值映射到商業域值,例如"Y[es]"/"N[o]","M[ale]"/"F[emale]",等等。HTML復選框使用"checked"屬性,而許多其它HTML控件使用的則是"value"屬性。
·需要應用程序編碼來維持該控件的狀態(修改過的/未修改過的)。這實際上是在所有的HTML控件普遍存在的一個問題。
·需要應用程序編碼才能創建一個關聯標簽-它應該接受鼠標點擊并相應地改變該復選框的狀態。
·標準HTML復選框不支持"校驗"事件以允許取消一個GUI行為,而這種要求可能存在于某些應用程序中。
現在,讓我們看一個正在構建的該控件的用法示例,它的用法可能如下情形:
<checkbox id="cbx_1" value="N" labelonleft="true"
label="Show Details:" onValue="Y" offValue="N"/>
另外,我們的控件將支持可取消的事件onItemChanging和通知事件onItemChanged。
定義定制標簽
從結構上講,一個定制標簽是一個具有一個HTC擴展名的文件-它在<PUBLIC:COMPONENT>和</PUBLIC:COMPONENT>標志之間對它的屬性,方法和事件加以描述。
為了定義一個定制CHECKBOX標簽,我們創建一個如下列代碼片斷中的文件checkbox.htc-其中,第一行負責設置該組件的標簽名:
<PUBLIC:COMPONENT NAME="cbx" tagName="CHECKBOX">
<PROPERTY NAME="value" GET="getValue" PUT="putValue" />
//我們把組件的所有另外的屬性放在這里
<METHOD NAME="show" />
//我們把組件的所有另外的方法放在這里
<EVENT NAME="onItemChanging" ID="onItemChanging"/>
//我們把組件將向應用程序激活的所有另外的事件放在這里
<ATTACH EVENT="oncontentready" HANDLER="constructor" />
//我們把組件自己處理的另外的事件放在這里
<SCRIPT>
//我們把所有的方法,屬性getters和setters和事件處理器放在這里
</SCRIPT>
</PUBLIC:COMPONENT>
使用定制標簽
盡管HTC文件的內容比較重要,但是這與其文件名是什么無關。值得注意的是,指向該HTC文件的URL需要被使用IMPORT指令指定-這必須在相應的定制標簽第一次出現之前(在頁面上)完成。下面是最簡單的可能的頁面使用一個定制的復選框可能看上去的樣子-假定該頁面和HTC文件處理同一個文件夾下:
<HTML xmlns:myns>
<?IMPORT namespace="myns" implementation="checkbox.htc" >
<BODY>
<myns:checkbox id='cbx_1' label='Hello'/>
</BODY>
</HTML>
請注意,定制CHECKBOX是怎樣在打開的HTML標簽中被映射到一個非缺省的命名空間"myns"的。這個IMPORT指令實現把HTC同步加載到瀏覽器的內存并且還指示瀏覽器怎樣為適當的命名空間實現名稱確定的(HTC到命名空間的關聯可能是多對一的)。
定制標簽的構造器
最好的初始化HTC的方法是,一旦它被裝載就處理oncontentready事件。因此,我們可以定義處理器函數-為了概念清晰起見,我們稱之為構造器:
<ATTACH EVENT="oncontentready" HANDLER="constructor" />
constructor()的邏輯是簡單的:根據屬性labelonleft的值(見下面的屬性定義)按順序連接一個常規HTML復選框和HTML標簽:
function constructor() {
//我們將把一個HTML復選框和標簽添加到元素體
//詳細情形見列表2
}
定義定制標簽屬性
為了定義屬性labelonleft,我們又在部分加上一行:
<PROPERTY NAME="labelonleft" VALUE="true"/>
請注意,這個屬性并沒有包含getter和/或setter方法。屬性onValue和offValue不僅提供了從復選框狀態到一個商業值域的映射而且不需要getters和setters:
<PROPERTY NAME="onValue" VALUE="true"/>
<PROPERTY NAME="offValue" VALUE="false" />
然而,屬性checked是用兩個getter和setter定義的:
<PROPERTY NAME="checked" GET="getChecked" PUT="putChecked" />
因此,我們在<SCRIPT>部分建立了上面兩個方法的定義。正如你所見,setter putChecked()-將在每次復選框狀態改變時激發-把value屬性設置為下面兩個變體之一:onValue或OffValue。請注意,putChecked()將不僅可由在復選框-宿主頁面中的腳本觸發,而且也能通過在checkbox.htc中的相應的任何賦值操作觸發。
var _value;
function putChecked( newValue ) {
value = (newValue?onValue:offValue);
}
function getChecked(){
return ( _value == onValue);
}
七、 為定制標簽定義事件
讓我們看一下onItemChanging和onItemChanged事件的定義以及這些事件是怎樣在value屬性的setter內部被激發和處理的(見所附源碼中的列表2)。方法putValue()有幾個讓人感興趣的地方。首先,在分析CHECKBOX標簽期間,可以調用這個方法-只要指定這個HTMLvalue屬性。這正解釋了為什么我們為非構造對象建立一個單獨的邏輯分支-為把構造過程與一個對用戶擊鍵的反應區別開來。其次,在此我們展示了定制事件onItemChanging的創建和處理-它允許應用程序取消行為。請注意,通過這種方式,無論是擊鍵還是通過編程方式實現賦值都能達到取消的目的。
事件取消
為了取消事件,一個應用程序應該攔截該事件并且把event.returnValue設置為false。下面的代碼片斷展示了應用程序是怎樣實現取消事件過程的:
cbx_1::onItemChanging() {
. . . . .
if (canNotBeAllowed) {
event.returnValue=false;
. . . . .
}
如果事件沒被取消,putValue()把內部的普通HTML復選框的checked屬性設置為每個相應的當前值-如果它等于onValue,這個內部復選框將被選中;如果它等于offValue(不存在第三種選擇),復選框不被選中(完整的列表見本文所附源碼中的列表2)。
復選框的HTML內幕
我們控件的繪制是通過助理函數addLabel()和addCheckBox()來實現的并且從一個constructor()內部調用。這些函數把HTML注入進元素的innerHTML。這種注入式HTML的一種簡化形式如下所示:
<LABEL for=cb_{uniqueID}>Show Details:</LABEL>
<INPUT id=cb_{uniqueID} type=checkbox />?
在此,uniqueID是一個由IE所生成的唯一的(在一個頁面內)字符串-它用來識別HTC的實例。
八、 再封裝
在我們的CHECKBOX中有一個缺點。按照我們建立它的方式,在constructor()期間被注入的HTML將隸屬于宿主該HTC的頁面的DOM。而且,全局的JavaScript變量like_value屬于它們所在的文檔的全局范圍。這是危險的,因為我們偶然會遇到命名沖突的可能性:最明顯的情形是使用同一個組件的多個實例。另外這還會導致一個可能性-我們的控件可能會偶然地用相同的名稱參考其它對象,反之也如此。
為簡化起見,需要建立一種專門的機制來為對象授權啟動一個真正模塊化方法。幸好,HTC技術支持一種智能答案-viewLink。
把一個控件聲明為封裝的最容易的方法是把一個額外聲明放到打開和關閉的PUBLIC:COMPONENT標簽之間:
<PUBLIC:DEFAULTS viewLinkContent/>?
該控件立即就變成封裝性的;而且它有自己的HTML文檔樹-成為主文檔的原子組件。該對象的每個實例有它自己的實例值的集合并且只有公共方法和屬性能夠從外界代碼中加以存取。換句話說,該viewLink機制充分地啟動了復雜的Web應用程序的設計和實現-通過使用一種真正的OO的基于組件的方法。
特別地,我們可以簡化代碼-通過從內部復選框和HTML標簽的定義中刪除uniqueID后綴,因為我們不再擔心命名沖突。因此,我們可以替換下面這一行:
eval( 'cb_'+uniqueID).checked = ( _value == onValue );?
用
cb.checked = ( _value == onValue );
并相應地改變addCheckbox()和addLabel()。
九、 結論
既然AJAX競賽剛剛開始,那么就不存在什么AJAX標準并且沒有現成的你可以依賴以構建你的應用程序的可廣為接受的RAD工具。雖然軟件供應商們可能還需要較長一段時間來創建這種強健的開發平臺,AJAX熱心者已經開始著手準備-通過一些良好定義的API把可重用的代碼塊封裝為商業組件。
以這種方向導航,本文概括了AJAX語言的OO"力量"-JavaScript。另外,還展示了一種可用的組件-授權策略-客戶端定制標簽技術。我們在僅描述IE特定的定制標簽的同時,還另外提供了一個可下載的實例-適于Mozilla瀏覽器的可擴展的綁定實例。