一、 引言
在第一部分中,我們讨論了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"的每一個執行個體被不斷地重複建立。
是以,既然基于原型建構對象速度更快并且消費更少些的記憶體,那麼我們在最強調性能的場所特别支援使用公共的變量。請注意,你可以使用命名慣例來避免名稱沖突-例如,在公共的變量的前面加上該類名。
一、 引言
在第一部分中,我們讨論了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
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiZpdmLlxWYtV2LcR3btV2LcNXZnFWbp9CXwxWZo9CXuNmL1RWZuUHdqNnLk9mdvw1LcpDc0RHaiojIsJye.gif)
",等等。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,我們又在<PUBLIC:COMPONENT>部分加上一行:
<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浏覽器的可擴充的綁定執行個體。