用Dojo實現Ajax請求:XHR、跨域、及其他

來源: InfoQ  發布時間: 2011-01-21 00:16  閱讀: 4043 次  推薦: 0   原文鏈接   [收藏]  
摘要:本文全面介紹了在 Dojo中如何實現Ajax請求,除了基本的異步請求之外,還介紹了諸如上傳文件,用JSONP實現跨域以及RPC請求這樣的高級使用場景。通過本文,將能夠熟練使用Ajax進行基于Dojo的RIA應用開發,并進一步了解Dojo風格的RIA架構。

  在任何瀏覽器上方便地實現Ajax請求是每一個Ajax框架的初衷。Dojo在這方面無疑提供了非常豐富的支持。除了XMLHttpRequest之外,動態script、iframe、RPC也應有盡有,并且接口統一,使用方便,大多數情況下都只需要一句話就能達到目的,從而免除重復造輪子的麻煩。而且,Dojo一貫追求的概念完整性也在這里有所體現,換句話說,在使用Dojo的Ajax工具的過程中不會感到任何的不自然,相反更容易有觸類旁通的感覺,因為API的模式是統一的,而且這里涉及到的某些概念(如Deferred對象)也貫穿在整個Dojo之中。

  Dojo的XHR函數

  Dojo的XMLHttpRequest函數就叫dojo.xhr,除了把自己取名美元符號之外,這好像是最直接的辦法了。它定義在Dojo基本庫里,所以不需要額外的require就能使用。它可以實現任何同域內的http請求。不過更常用的是dojo.xhrGet和dojo.xhrPost,它們只不過是對dojo.xhr函數的簡單封裝;當然根據REST風格,還有dojo.xhrPut和dojo.xhrDelete。

  這些函數的參數都很統一。除了dojo.xhr的第一個參數是http方法名之外,所有的dojo.xhr*系列函數都接受同一種散列式的參數,其中包含請求的細節,例如url、是否同步、要傳給服務器的內容(可以是普通對象、表單、或者純文本)、超時設定、返回結果的類型(非常豐富且可擴展)、以及請求成功和失敗時的回調。所有dojo.xhr*函數(實際上是所有IO函數)返回值也都一樣,都是一個Deferred對象,顧名思義,它能讓一些事情延遲發生,從而讓API用起來更靈活。

  下面的兩個例子可能會帶來一點直觀感受:

 
dojo.xhrGet({
url:
"something.html",
load: function(response, ioArgs){

//用response干一些事
console.log("xhr get success:", response)
return response //必須返回response
},
error: function(response, ioArgs){
console.log(
"xhr get failed:", response)
return response //必須返回response
}
})

//Deferred對象允許用同步調用的寫法寫異步調用
var deferredResult = dojo.xhrPost({
url:
"something.html",
form: formNode,
//Dojo會自動將form轉成object
timeout: 3000, //Dojo會保證超時設定的有效性
handleAs: "json" //得到的response將被認為是JSON,并自動轉為object
})
//當響應結果可用時再調用回調函數
deferredResult.then(function(response){
console.log(
"xhr get success:", response)
return response //必須返回response
})

  首先解釋一下timeout。除了IE8之外,目前大多數XMLHttpRequest對象都沒有內置的timeout功能,因此必須用 setTimeout。當同時存在大量請求時,需要為每一個請求設置單獨的定時器,這在某些瀏覽器(主要是IE)會造成嚴重的性能問題。dojo的做法是只用一個單獨的setInterval,定時輪詢(間隔50ms)所有還未結束的請求的狀態,這樣就高效地解決了一切遠程請求(包括JSONP和 iframe)的超時問題。

  值得一提的還有handleAs參數,通過設置這個參數,可以自動識別服務器的響應內容格式并轉換成對象或文本等方便使用的形式。根據文檔,它接受如下值:text (默認), json, json-comment-optional, json-comment-filtered, javascript, xml。

  而且它還是可擴展的。其實handleAs只是告訴xhr函數去調用哪個格式轉換插件,即dojo.contentHandlers對象里的一個方法。例如 dojo.contentHandlers.json就是處理JSON格式的插件。你可以方便地定制自己所需要的格式轉換插件,當然,你也可修改現有插件的行為:

 
dojo.contentHandlers.json = (function(old){
return function(xhr){
var json
= old(xhr)
if(json.someSignalFormServer){
doSomthing(json)
delete json.someSignalFormServer
}

return json
}
})(dojo.contentHandlers.json)
//一個小技巧,利用傳參得到原方法

  如果要了解每個參數的細節,可以參考Dojo的文檔

  虛擬的參數類

  這里特別提一下Dojo在API設計上的兩個特點。其一是虛擬的參數類概念:通過利用javascript對象可以靈活擴展的特點,強行規定一個散列參數屬于某個類。例如dojo.xhr*系列函數所接受的參數就稱為dojo.__XhrArgs。這個類并不存在于實際代碼中(不要試圖用 instanceof驗證它),只停留在概念上,比抽象類還抽象,因此給它加上雙下劃線前綴(Dojo習慣為抽象類加單下劃線前綴)。

  這樣做看起來沒什么意思,但實際上簡化了API,因為它使API之間產生了聯系,更容易記憶也就更易于使用。這一點在對這種類做繼承時更明顯。例如 dojo.__XhrArgs繼承自dojo.__IoArgs,這是所有IO函數所必須支持的參數集合,同樣繼承自dojo.__IoArgs的還有 dojo.io.script.__ioArgs和dojo.io.iframe.__ioArgs,分別用于動態腳本請求和iframe請求。子類只向父類添加少量的屬性,這樣繁多的參數就具有了樹形類結構。原本散列式參數是用精確的參數名代替了固定的參數順序,在增加靈活性和可擴展性的同時,實際上增加了記憶量(畢竟參數名不能拼錯),使得API都不像看起來那么好用,有了參數類的設計就緩解了這個問題。

  這種參數類的做法在Dojo里隨處可見,讀源碼的話就會發現它們都是被正兒八經地以正常代碼形式聲明在一種特殊注釋格式里的,像這樣:

 
/*=====
dojo.declare("dojo.__XhrArgs", dojo.__IoArgs, {
constructor: function(){
//summary:
//...
//handleAs:
//...
//......
}
})
=====
*/

  這種格式可以被jsDoc工具自動提取成文檔,在文檔里這些虛擬出來的類就像真的類一樣五臟俱全了。

  Deferred對象

  另一個API設計特點就是Deferred對象的廣泛使用。Dojo里的Deferred是基于MochiKit實現稍加改進而成的,而后者則是受到 python的事件驅動網絡工具包Twisted里同名概念的啟發。概括來說的話,這個對象的作用就是將異步IO中回調函數的聲明位置與調用位置分離,這樣在一個異步IO最終完成的地方,開發人員可以簡單地說貨已經到了,想用的可以來拿了,而不用具體地指出到底該調用哪些回調函數。這樣做的好處是讓異步IO的寫法和同步IO一樣(對數據的處理總是在取數據函數的外面,而不是里面),從而簡化異步編程。

  具體做法是,異步函數總是同步地返回一個代理對象(這就是Deferred對象),可以將它看做你想要的數據的代表,它提供一些方法以添加回調函數,當數據可用時,這些回調函數(可以由很多個)便會按照添加順序依次執行。如果在取數據過程中出現錯誤,就會調用所提供的錯誤處理函數(也可以有很多個);如果想要取消這個異步請求,也可通過Deferred對象的cancel方法完成。

 
dojo.Deferred的核心方法如下:

then(callback, errback)
//添加回調函數
callback(result) //表示異步調用成功完成,觸發回調函數
errback(error) //表示異步調用中產生錯誤,觸發錯誤處理函數
cancel() //取消異步調用

  Dojo還提供了一個when方法,使同步的值和異步的Deferred對象在使用時寫法一樣。例如:

 
//某個工具函數的實現
var obj = {
getItem: function(){

if(this.item){
return this.item //這里同步地返回數據
}else{
return dojo.xhrGet({ //這里返回的是Deferred對象
url: "toGetItem.html",
load: dojo.hitch(
this, function(response){
this.item = response
return response
})
})
}
}
}

//用戶代碼
dojo.when(obj.getItem(), function(item){
//無論同步異步,使用工具函數getItem的方式都一樣
})

  在函數閉包的幫助下,Deferred對象的創建和使用變得更為簡單,你可以輕易寫出一個創建Deferred對象的函數,以同步的寫法做異步的事。例如寫一個使用store獲取數據的函數:

 
var store = new dojo.data.QueryReadStore({...})
function getData(start, count){
var d
= new dojo.Deferred() //初始化一個Deferred對象
store.fetch({
start: start,
count: count,
onComplete: function(items){

//直接取用上層閉包里的Deferred對象
d.callback(items)
}
})

return d //把它當做結果返回
}

  用dojo.io.script跨域

  dojo.xhr* 只是XmlHttpRequest對象的封裝,由于同源策略限制,它不能發跨域請求,要跨域還是需要動態創建script標簽。Dojo 沒有像JQuery一樣把所有東西都封裝在一起(JQuery的ajax()方法可以跨域,當然用的是JSONP,所以它不敢把自己稱為xhr),而是堅持一個API只干一件事情。畢竟在大部分應用中,同域請求比跨域請求多得多,如果一個應用不需要跨域,就沒必要加載相關代碼。因此與xhr不同,dojo 的跨域請求組件不在基本庫,而在核心庫,需要require一下才能使用:

 
dojo.require("dojo.io.script")

  這個包里面基本上只需要用到一個函數:dojo.io.script.get()。它也返回Deferred對象,并接受類型為 dojo.io.script.__ioArgs的散列參數。受益于虛擬參數類,我們不用從頭開始學習這個參數,它繼承了dojo.__IoArgs,因此和dojo.xhr*系列的參數大同小異。唯一需要注意的是handleAs在這里無效了,代之以jsonp或者checkString。

  前者用于實現JSONP協議,其值由服務器端指定,當script標簽加載后就按照JSONP協議執行這個函數,然后Dojo會自動介入,負責把真正的數據傳給load函數。需要指出的是在Dojo1.4以前,這個參數叫callbackParamName,冗長但意義明確。畢竟Dojo太早了,它成型的時候(2005)JSONP這個詞才剛出現不久。現在callbackParamName還是可用的(為了向后兼容),不過處于deprecated狀態。

  下面的例子從flickr獲取feed數據:

 
dojo.io.script.get({
url:
"http://www.flickr.com/services/feeds/photos_public.gne",
jsonp:
"jsoncallback", //由flickr指定
content: {format: "json"},
load: function(response){
console.log(response)

return response
},
error: function(response){
console.log(response)

return response
}
})

  與jsonp不同,checkString參數專門用于跨域獲取javascript代碼,它其實是那段跨域腳本里的一個有定義的變量的名字,Dojo會用它來判斷跨域代碼是否加載完畢,配合前面提到的timeout機制就能實現有效的超時處理。

 
dojo.io.script.get({
url:
"http://......", //某個提供腳本的URL
checkString: "obj",
load: function(response){

//腳本加載完畢,可以直接使用其中的對象了,如obj。
Return response
}
})

  用dojo.io.iframe傳數據

  dojo.io 包里還有一個工具就是iframe,常用于以不刷新頁面的方式上傳或下載文件。這個很經典的Ajax技巧在Dojo里就是一句 dojo.io.iframe.send({...})。這個函數接受dojo.io.iframe.__ioArgs,相比 dojo.__IoArgs,它只多了一個method參數,用于指定是用GET還是POST(默認)方法發送請求。下面的例子就實現了通過無刷新提交表單來上傳文件:

 
dojo.io.iframe.send({
form:
"formNodeId", //某個form元素包含本地文件路徑
handleAs: "html", //服務器將返回html頁面
load: onSubmitted, //提交成功
error: onSubmitError //提交失敗
})

  目前send函數的handleAs參數支持html, xml, text, json, 和javascript五種響應格式。除了html和xml之外,使用其他格式有一個比較特別的要求,就是服務端返回的響應必須具有以下格式:

 
html
head/head
body
textarea真正的響應內容/textarea
/body
/html

  這是因為服務器返回的東西是加載在iframe里的,而只有html頁面才能在任何瀏覽器里保證成功加載(有個DOM在,以后取數據也方便)。加一個textarea則可以盡量忠實于原始文本數據的格式,而不會受到html的影響。

  試試RPC(遠程過程調用)

  如果dojo.xhr*函數以及Deferred機制仍然無法避免代碼的混亂,那RPC可能就是唯一的選擇了。dojo.rpc包提供了基于簡單方法描述語言(SMD)的RPC實現。SMD的原理類似于WSDL(Web服務描述語言),不過是基于JSON的,它定義了遠程方法的名稱、參數等屬性,讓 Dojo能創建出代理方法以供調用。

  Dojo提供了兩種方式實現rpc:XHR和JSONP,分別對應dojo.rpc.JsonService類和dojo.rpc.JsonpService類,用戶可以根據是否需要跨域各取所需。

一個簡單的例子:

 
var smdObj = {
serviceType:
"JSON-RPC",
serviceURL:
"http://...."
methods: [
{name:
"myFunc", parameters: []}
]
}
var rpc
= new dojo.rpc.JsonpService(smdObj) //傳入SMD
var result = rpc.myFunc() //直接調用遠程方法,返回Deferred對象
dojo.when(result, function(result){
//得到結果
})

  SMD還沒有一個被廣泛認可的官方標準,一直以來都是Dojo社區領導著它的發展,以后這個模塊也有可能發生改變,但整個RPC基本的框架會保持穩定。

  結語

  Ajax請求這個主題太大,本文只能掛一漏萬地介紹一點dojo在這方面設計和實現的皮毛,包括基本XHR請求、動態script、iframe請求、以及RPC,并特別強調了幾個有Dojo特色的設計,如timeout機制、虛擬參數類、Deferred對象等。

  Dojo由Ajax領域的先驅們寫就,相信從它的代碼中我們一定能學到更多的東西。

0
0
 
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()